mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
feat: new setting modal (#2834)
Co-authored-by: Alex Yang <himself65@outlook.com>
This commit is contained in:
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -78,6 +78,7 @@ jobs:
|
||||
ENABLE_ALL_PAGE_FILTER: true
|
||||
ENABLE_LEGACY_PROVIDER: true
|
||||
ENABLE_PRELOADING: false
|
||||
ENABLE_NEW_SETTING_MODAL: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -103,6 +104,7 @@ jobs:
|
||||
ENABLE_ALL_PAGE_FILTER: true
|
||||
ENABLE_LEGACY_PROVIDER: false
|
||||
ENABLE_PRELOADING: false
|
||||
ENABLE_NEW_SETTING_MODAL: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"@blocksuite/blocks": "0.0.0-20230607055421-9b20fcaf-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230607055421-9b20fcaf-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230607055421-9b20fcaf-nightly",
|
||||
"@blocksuite/icons": "^2.1.19",
|
||||
"@blocksuite/icons": "^2.1.21",
|
||||
"@blocksuite/lit": "0.0.0-20230607055421-9b20fcaf-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230607055421-9b20fcaf-nightly",
|
||||
"@dnd-kit/core": "^6.0.8",
|
||||
|
||||
@@ -46,4 +46,8 @@ export const buildFlags = {
|
||||
process.env.ENABLE_PRELOADING === undefined
|
||||
? true
|
||||
: process.env.ENABLE_PRELOADING === 'true',
|
||||
enableNewSettingModal:
|
||||
process.env.ENABLE_NEW_SETTING_MODAL === undefined
|
||||
? true
|
||||
: process.env.ENABLE_PRELOADING === 'true',
|
||||
};
|
||||
|
||||
@@ -44,6 +44,7 @@ import { BlockSuiteWorkspace } from '../../shared';
|
||||
import { toast } from '../../utils';
|
||||
import {
|
||||
BlockSuitePageList,
|
||||
NewWorkspaceSettingDetail,
|
||||
PageDetailEditor,
|
||||
WorkspaceHeader,
|
||||
WorkspaceSettingDetail,
|
||||
@@ -364,5 +365,18 @@ export const AffineAdapter: WorkspaceAdapter<WorkspaceFlavour.AFFINE> = {
|
||||
/>
|
||||
);
|
||||
},
|
||||
NewSettingsDetail: ({
|
||||
currentWorkspace,
|
||||
onDeleteWorkspace,
|
||||
onTransformWorkspace,
|
||||
}) => {
|
||||
return (
|
||||
<NewWorkspaceSettingDetail
|
||||
onDeleteWorkspace={onDeleteWorkspace}
|
||||
workspace={currentWorkspace}
|
||||
onTransferWorkspace={onTransformWorkspace}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ import { nanoid } from '@blocksuite/store';
|
||||
|
||||
import {
|
||||
BlockSuitePageList,
|
||||
NewWorkspaceSettingDetail,
|
||||
PageDetailEditor,
|
||||
WorkspaceHeader,
|
||||
WorkspaceSettingDetail,
|
||||
@@ -115,5 +116,18 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
|
||||
/>
|
||||
);
|
||||
},
|
||||
NewSettingsDetail: ({
|
||||
currentWorkspace,
|
||||
onDeleteWorkspace,
|
||||
onTransformWorkspace,
|
||||
}) => {
|
||||
return (
|
||||
<NewWorkspaceSettingDetail
|
||||
onDeleteWorkspace={onDeleteWorkspace}
|
||||
workspace={currentWorkspace}
|
||||
onTransferWorkspace={onTransformWorkspace}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { lazy } from 'react';
|
||||
|
||||
// export { WorkspaceSettingDetail as NewWorkspaceSettingDetail } from '../components/affine/new-workspace-setting-detail';
|
||||
export const WorkspaceSettingDetail = lazy(() =>
|
||||
import('../components/affine/workspace-setting-detail').then(
|
||||
({ WorkspaceSettingDetail }) => ({
|
||||
@@ -7,6 +7,13 @@ export const WorkspaceSettingDetail = lazy(() =>
|
||||
})
|
||||
)
|
||||
);
|
||||
export const NewWorkspaceSettingDetail = lazy(() =>
|
||||
import('../components/affine/new-workspace-setting-detail').then(
|
||||
({ WorkspaceSettingDetail }) => ({
|
||||
default: WorkspaceSettingDetail,
|
||||
})
|
||||
)
|
||||
);
|
||||
export const BlockSuitePageList = lazy(() =>
|
||||
import('../components/blocksuite/block-suite-page-list').then(
|
||||
({ BlockSuitePageList }) => ({
|
||||
|
||||
@@ -36,6 +36,7 @@ export const WorkspaceAdapters = {
|
||||
PageDetail: unimplemented,
|
||||
PageList: unimplemented,
|
||||
SettingsDetail: unimplemented,
|
||||
NewSettingsDetail: unimplemented,
|
||||
},
|
||||
},
|
||||
[WorkspaceFlavour.PUBLIC]: {
|
||||
@@ -57,6 +58,7 @@ export const WorkspaceAdapters = {
|
||||
PageDetail: unimplemented,
|
||||
PageList: unimplemented,
|
||||
SettingsDetail: unimplemented,
|
||||
NewSettingsDetail: unimplemented,
|
||||
},
|
||||
},
|
||||
} satisfies {
|
||||
|
||||
@@ -80,6 +80,7 @@ export const openWorkspacesModalAtom = atom(false);
|
||||
export const openCreateWorkspaceModalAtom = atom<CreateWorkspaceMode>(false);
|
||||
export const openQuickSearchModalAtom = atom(false);
|
||||
export const openOnboardingModalAtom = atom(false);
|
||||
export const openSettingModalAtom = atom(false);
|
||||
|
||||
export const openDisableCloudAlertModalAtom = atom(false);
|
||||
|
||||
|
||||
67
apps/web/src/atoms/settings.ts
Normal file
67
apps/web/src/atoms/settings.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { useAtom } from 'jotai';
|
||||
import { atomWithStorage } from 'jotai/utils';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export type DateFormats =
|
||||
| 'MM/dd/YYYY'
|
||||
| 'dd/MM/YYYY'
|
||||
| 'YYYY-MM-dd'
|
||||
| 'YYYY.MM.dd'
|
||||
| 'YYYY/MM/dd'
|
||||
| 'dd-MMM-YYYY'
|
||||
| 'dd MMMM YYYY';
|
||||
|
||||
export type AppSetting = {
|
||||
clientBorder: boolean;
|
||||
fullWidthLayout: boolean;
|
||||
windowFrameStyle: 'frameless' | 'NativeTitleBar';
|
||||
dateFormat: DateFormats;
|
||||
startWeekOnMonday: boolean;
|
||||
disableBlurBackground: boolean;
|
||||
disableNoisyBackground: boolean;
|
||||
autoCheckUpdate: boolean;
|
||||
autoDownloadUpdate: boolean;
|
||||
};
|
||||
export const windowFrameStyleOptions: AppSetting['windowFrameStyle'][] = [
|
||||
'frameless',
|
||||
'NativeTitleBar',
|
||||
];
|
||||
|
||||
export const dateFormatOptions: DateFormats[] = [
|
||||
'MM/dd/YYYY',
|
||||
'dd/MM/YYYY',
|
||||
'YYYY-MM-dd',
|
||||
'YYYY.MM.dd',
|
||||
'YYYY/MM/dd',
|
||||
'dd-MMM-YYYY',
|
||||
'dd MMMM YYYY',
|
||||
];
|
||||
|
||||
export const AppSettingAtom = atomWithStorage<AppSetting>('AFFiNE settings', {
|
||||
clientBorder: false,
|
||||
fullWidthLayout: false,
|
||||
windowFrameStyle: 'frameless',
|
||||
dateFormat: dateFormatOptions[0],
|
||||
startWeekOnMonday: false,
|
||||
disableBlurBackground: false,
|
||||
disableNoisyBackground: false,
|
||||
autoCheckUpdate: true,
|
||||
autoDownloadUpdate: true,
|
||||
});
|
||||
|
||||
export const useAppSetting = () => {
|
||||
const [settings, setSettings] = useAtom(AppSettingAtom);
|
||||
|
||||
return [
|
||||
settings,
|
||||
useCallback(
|
||||
(patch: Partial<AppSetting>) => {
|
||||
setSettings((prev: AppSetting) => ({
|
||||
...prev,
|
||||
...patch,
|
||||
}));
|
||||
},
|
||||
[setSettings]
|
||||
),
|
||||
] as const;
|
||||
};
|
||||
18
apps/web/src/components/affine/app-container.tsx
Normal file
18
apps/web/src/components/affine/app-container.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import {
|
||||
AppContainer as AppContainerWithoutSettings,
|
||||
type WorkspaceRootProps,
|
||||
} from '@affine/component/workspace';
|
||||
|
||||
import { useAppSetting } from '../../atoms/settings';
|
||||
|
||||
export const AppContainer = (props: WorkspaceRootProps) => {
|
||||
const [appSettings] = useAppSetting();
|
||||
|
||||
return (
|
||||
<AppContainerWithoutSettings
|
||||
useNoisyBackground={!appSettings.disableNoisyBackground}
|
||||
useBlurBackground={!appSettings.disableBlurBackground}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
70
apps/web/src/components/affine/language-menu/index.tsx
Normal file
70
apps/web/src/components/affine/language-menu/index.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { Menu, MenuItem, MenuTrigger, styled } from '@affine/component';
|
||||
import { LOCALES } from '@affine/i18n';
|
||||
import { useI18N } from '@affine/i18n';
|
||||
import type { FC, ReactElement } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const StyledListItem = styled(MenuItem)(() => ({
|
||||
width: '132px',
|
||||
height: '38px',
|
||||
textTransform: 'capitalize',
|
||||
}));
|
||||
|
||||
const LanguageMenuContent: FC<{
|
||||
currentLanguage?: string;
|
||||
}> = ({ currentLanguage }) => {
|
||||
const i18n = useI18N();
|
||||
const changeLanguage = useCallback(
|
||||
(event: string) => {
|
||||
return i18n.changeLanguage(event);
|
||||
},
|
||||
[i18n]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{LOCALES.map(option => {
|
||||
return (
|
||||
<StyledListItem
|
||||
key={option.name}
|
||||
active={currentLanguage === option.originalName}
|
||||
title={option.name}
|
||||
onClick={() => {
|
||||
changeLanguage(option.tag).catch(err => {
|
||||
throw new Error('Failed to change language', err);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{option.originalName}
|
||||
</StyledListItem>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export const LanguageMenu: FC = () => {
|
||||
const i18n = useI18N();
|
||||
|
||||
const currentLanguage = LOCALES.find(item => item.tag === i18n.language);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
content={
|
||||
(
|
||||
<LanguageMenuContent
|
||||
currentLanguage={currentLanguage?.originalName}
|
||||
/>
|
||||
) as ReactElement
|
||||
}
|
||||
placement="bottom-end"
|
||||
trigger="click"
|
||||
disablePortal={true}
|
||||
>
|
||||
<MenuTrigger
|
||||
data-testid="language-menu-button"
|
||||
style={{ textTransform: 'capitalize' }}
|
||||
>
|
||||
{currentLanguage?.originalName}
|
||||
</MenuTrigger>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import type {
|
||||
WorkspaceFlavour,
|
||||
WorkspaceRegistry,
|
||||
} from '@affine/env/workspace';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
||||
|
||||
export type WorkspaceSettingDetailProps = {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
onDeleteWorkspace: () => Promise<void>;
|
||||
onTransferWorkspace: <
|
||||
From extends WorkspaceFlavour,
|
||||
To extends WorkspaceFlavour
|
||||
>(
|
||||
from: From,
|
||||
to: To,
|
||||
workspace: WorkspaceRegistry[From]
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const WorkspaceSettingDetail: FC<WorkspaceSettingDetailProps> = ({
|
||||
workspace,
|
||||
}) => {
|
||||
const [workspaceName] = useBlockSuiteWorkspaceName(
|
||||
workspace.blockSuiteWorkspace ?? null
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<h2>New Workspace Setting Coming Soon!</h2>
|
||||
|
||||
{workspaceName}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export const AccountSetting = () => {
|
||||
return <div>AccountSetting</div>;
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { settingHeader } from './share.css';
|
||||
export const SettingHeader: FC<{ title: string; subtitle?: string }> = ({
|
||||
title,
|
||||
subtitle,
|
||||
}) => {
|
||||
return (
|
||||
<div className={settingHeader}>
|
||||
<div className="title">{title}</div>
|
||||
<div className="subtitle">{subtitle}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { CSSProperties, FC, PropsWithChildren, ReactElement } from 'react';
|
||||
|
||||
import { settingRow } from './share.css';
|
||||
|
||||
export const SettingRow: FC<
|
||||
PropsWithChildren<{
|
||||
name: string;
|
||||
desc: string | ReactElement;
|
||||
style?: CSSProperties;
|
||||
onClick?: () => void;
|
||||
}>
|
||||
> = ({ name, desc, children, onClick, style }) => {
|
||||
return (
|
||||
<div className={settingRow} style={style} onClick={onClick}>
|
||||
<div className="left-col">
|
||||
<div className="name">{name}</div>
|
||||
<div className="desc">{desc}</div>
|
||||
</div>
|
||||
<div className="right-col">{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const settingHeader = style({
|
||||
height: '68px',
|
||||
borderBottom: '1px solid var(--affine-border-color)',
|
||||
marginBottom: '24px',
|
||||
});
|
||||
|
||||
globalStyle(`${settingHeader} .title`, {
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
fontWeight: 600,
|
||||
lineHeight: '24px',
|
||||
marginBottom: '4px',
|
||||
});
|
||||
|
||||
globalStyle(`${settingHeader} .subtitle`, {
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
lineHeight: '16px',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
});
|
||||
|
||||
export const wrapper = style({
|
||||
borderBottom: '1px solid var(--affine-border-color)',
|
||||
paddingBottom: '24px',
|
||||
marginBottom: '24px',
|
||||
selectors: {
|
||||
'&:last-of-type': {
|
||||
borderBottom: 'none',
|
||||
paddingBottom: '0',
|
||||
marginBottom: '0',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
globalStyle(`${wrapper} .title`, {
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
fontWeight: 600,
|
||||
lineHeight: '18px',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
marginBottom: '16px',
|
||||
});
|
||||
|
||||
export const settingRow = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: '25px',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
});
|
||||
|
||||
globalStyle(`${settingRow} .left-col`, {
|
||||
flexShrink: 0,
|
||||
maxWidth: '80%',
|
||||
});
|
||||
globalStyle(`${settingRow} .name`, {
|
||||
marginBottom: '2px',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
fontWeight: 600,
|
||||
});
|
||||
globalStyle(`${settingRow} .desc`, {
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
});
|
||||
globalStyle(`${settingRow} .right-col`, {
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
paddingLeft: '15px',
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { FC, PropsWithChildren } from 'react';
|
||||
|
||||
import { wrapper } from './share.css';
|
||||
export const Wrapper: FC<PropsWithChildren<{ title?: string }>> = ({
|
||||
title,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<div className={wrapper}>
|
||||
{title ? <div className="title">{title}</div> : null}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
2
apps/web/src/components/affine/setting-modal/config.ts
Normal file
2
apps/web/src/components/affine/setting-modal/config.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// Some settings are not implemented yet, but need to show in the setting modal when boss is watching.
|
||||
export const IS_EXHIBITION = false;
|
||||
@@ -0,0 +1,110 @@
|
||||
import { Switch } from '@affine/component';
|
||||
import { relatedLinks } from '@affine/component/contact-modal';
|
||||
import { env } from '@affine/env';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ArrowRightSmallIcon, OpenInNewIcon } from '@blocksuite/icons';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { type AppSetting, useAppSetting } from '../../../../../atoms/settings';
|
||||
import { SettingHeader } from '../../common/setting-header';
|
||||
import { SettingRow } from '../../common/setting-row';
|
||||
import { Wrapper } from '../../common/wrapper';
|
||||
import { IS_EXHIBITION } from '../../config';
|
||||
import { communityItem, communityWrapper, link } from './style.css';
|
||||
|
||||
export const AboutAffine = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [appSettings, setAppSettings] = useAppSetting();
|
||||
const changeSwitch = useCallback(
|
||||
(key: keyof AppSetting, checked: boolean) => {
|
||||
setAppSettings({ [key]: checked });
|
||||
},
|
||||
[setAppSettings]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<SettingHeader title={t['About AFFiNE']()} subtitle={t['None yet']()} />
|
||||
{IS_EXHIBITION && env.isDesktop ? (
|
||||
<Wrapper title={t['Version']()}>
|
||||
<SettingRow
|
||||
name={t['Check for updates']()}
|
||||
desc={t['New version is ready']()}
|
||||
></SettingRow>
|
||||
<SettingRow
|
||||
name={t['Check for updates automatically']()}
|
||||
desc={t[
|
||||
'If enabled, it will automatically check for new versions at regular intervals.'
|
||||
]()}
|
||||
>
|
||||
<Switch
|
||||
checked={appSettings.autoCheckUpdate}
|
||||
onChange={checked => changeSwitch('autoCheckUpdate', checked)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t['Download updates automatically']()}
|
||||
desc={t[
|
||||
'If enabled, new versions will be automatically downloaded to the current device.'
|
||||
]()}
|
||||
>
|
||||
<Switch
|
||||
checked={appSettings.autoCheckUpdate}
|
||||
onChange={checked => changeSwitch('autoCheckUpdate', checked)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[`Discover what's new`]()}
|
||||
desc={t['View the AFFiNE Changelog.']()}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
window.open(
|
||||
'https://github.com/toeverything/AFFiNE/releases',
|
||||
'_blank'
|
||||
);
|
||||
}}
|
||||
>
|
||||
<ArrowRightSmallIcon />
|
||||
</SettingRow>
|
||||
</Wrapper>
|
||||
) : null}
|
||||
<Wrapper title={t['Contact with us']()}>
|
||||
<a className={link} href="https://affine.pro" target="_blank">
|
||||
{t['Official Website']()}
|
||||
<OpenInNewIcon className="icon" />
|
||||
</a>
|
||||
<a className={link} href="https://community.affine.pro" target="_blank">
|
||||
{t['AFFiNE Community']()}
|
||||
<OpenInNewIcon className="icon" />
|
||||
</a>
|
||||
</Wrapper>
|
||||
<Wrapper title={t['Communities']()}>
|
||||
<div className={communityWrapper}>
|
||||
{relatedLinks.map(({ icon, title, link }) => {
|
||||
return (
|
||||
<div
|
||||
className={communityItem}
|
||||
onClick={() => {
|
||||
window.open(link, '_blank');
|
||||
}}
|
||||
key={title}
|
||||
>
|
||||
{icon}
|
||||
<p>{title}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Wrapper>
|
||||
<Wrapper title={t['Info of legal']()}>
|
||||
<a className={link} href="https://affine.pro/privacy" target="_blank">
|
||||
{t['Privacy']()}
|
||||
<OpenInNewIcon className="icon" />
|
||||
</a>
|
||||
<a className={link} href="https://affine.pro/terms" target="_blank">
|
||||
{t['Terms of Use']()}
|
||||
<OpenInNewIcon className="icon" />
|
||||
</a>
|
||||
</Wrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const link = style({
|
||||
height: '18px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
fontWeight: 600,
|
||||
marginBottom: '12px',
|
||||
selectors: {
|
||||
'&:last-of-type': {
|
||||
marginBottom: '0',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
globalStyle(`${link} .icon`, {
|
||||
color: 'var(--affine-icon-color)',
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
marginLeft: '5px',
|
||||
});
|
||||
|
||||
export const communityWrapper = style({
|
||||
display: 'grid',
|
||||
justifyContent: 'space-between',
|
||||
gridTemplateColumns: 'repeat(auto-fill, 84px)',
|
||||
gridGap: '6px',
|
||||
});
|
||||
export const communityItem = style({
|
||||
width: '84px',
|
||||
height: '58px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid var(--affine-border-color)',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
cursor: 'pointer',
|
||||
});
|
||||
globalStyle(`${communityItem} svg`, {
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
display: 'block',
|
||||
margin: '8px auto 4px',
|
||||
});
|
||||
globalStyle(`${communityItem} p`, {
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
textAlign: 'center',
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Menu, MenuItem, MenuTrigger } from '@affine/component';
|
||||
import dayjs from 'dayjs';
|
||||
import { type FC, useCallback } from 'react';
|
||||
|
||||
import {
|
||||
dateFormatOptions,
|
||||
type DateFormats,
|
||||
useAppSetting,
|
||||
} from '../../../../../atoms/settings';
|
||||
|
||||
const DateFormatMenuContent: FC<{
|
||||
currentOption: DateFormats;
|
||||
onSelect: (option: DateFormats) => void;
|
||||
}> = ({ onSelect, currentOption }) => {
|
||||
return (
|
||||
<>
|
||||
{dateFormatOptions.map(option => {
|
||||
return (
|
||||
<MenuItem
|
||||
key={option}
|
||||
active={currentOption === option}
|
||||
onClick={() => {
|
||||
onSelect(option);
|
||||
}}
|
||||
>
|
||||
{dayjs(new Date()).format(option)}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export const DateFormatSetting = () => {
|
||||
const [appearanceSettings, setAppSettings] = useAppSetting();
|
||||
const handleSelect = useCallback(
|
||||
(option: DateFormats) => {
|
||||
setAppSettings({ dateFormat: option });
|
||||
},
|
||||
[setAppSettings]
|
||||
);
|
||||
return (
|
||||
<Menu
|
||||
content={
|
||||
<DateFormatMenuContent
|
||||
onSelect={handleSelect}
|
||||
currentOption={appearanceSettings.dateFormat}
|
||||
/>
|
||||
}
|
||||
placement="bottom-end"
|
||||
trigger="click"
|
||||
disablePortal={true}
|
||||
>
|
||||
<MenuTrigger data-testid="date-format-menu-trigger">
|
||||
{dayjs(new Date()).format(appearanceSettings.dateFormat)}
|
||||
</MenuTrigger>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,168 @@
|
||||
import { RadioButton, RadioButtonGroup, Switch } from '@affine/component';
|
||||
import { env } from '@affine/env';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import {
|
||||
type AppSetting,
|
||||
useAppSetting,
|
||||
windowFrameStyleOptions,
|
||||
} from '../../../../../atoms/settings';
|
||||
import { LanguageMenu } from '../../../language-menu';
|
||||
import { SettingHeader } from '../../common/setting-header';
|
||||
import { SettingRow } from '../../common/setting-row';
|
||||
import { Wrapper } from '../../common/wrapper';
|
||||
import { IS_EXHIBITION } from '../../config';
|
||||
import { DateFormatSetting } from './date-format-setting';
|
||||
import { settingWrapper } from './style.css';
|
||||
|
||||
export const ThemeSettings = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const { setTheme, theme } = useTheme();
|
||||
|
||||
return (
|
||||
<RadioButtonGroup
|
||||
className={settingWrapper}
|
||||
defaultValue={theme}
|
||||
onValueChange={useCallback(
|
||||
(value: string) => {
|
||||
setTheme(value);
|
||||
},
|
||||
[setTheme]
|
||||
)}
|
||||
>
|
||||
<RadioButton value="system">{t['system']()}</RadioButton>
|
||||
<RadioButton value="light">{t['light']()}</RadioButton>
|
||||
<RadioButton value="dark">{t['dark']()}</RadioButton>
|
||||
</RadioButtonGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export const AppearanceSettings = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const [appSettings, setAppSettings] = useAppSetting();
|
||||
const changeSwitch = useCallback(
|
||||
(key: keyof AppSetting, checked: boolean) => {
|
||||
setAppSettings({ [key]: checked });
|
||||
},
|
||||
[setAppSettings]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<SettingHeader
|
||||
title={t['Appearance Settings']()}
|
||||
subtitle={t['Customize your AFFiNE Appearance']()}
|
||||
/>
|
||||
|
||||
<Wrapper title={t['Theme']()}>
|
||||
<SettingRow
|
||||
name={t['Color Scheme']()}
|
||||
desc={t['Choose your color scheme']()}
|
||||
>
|
||||
<ThemeSettings />
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t['Display Language']()}
|
||||
desc={t['Select the language for the interface.']()}
|
||||
>
|
||||
<div className={settingWrapper}>
|
||||
<LanguageMenu />
|
||||
</div>
|
||||
</SettingRow>
|
||||
{IS_EXHIBITION && env.isDesktop ? (
|
||||
<SettingRow
|
||||
name={t['Client Border Style']()}
|
||||
desc={t['Customize the appearance of the client.']()}
|
||||
>
|
||||
<Switch
|
||||
checked={appSettings.clientBorder}
|
||||
onChange={checked => changeSwitch('clientBorder', checked)}
|
||||
/>
|
||||
</SettingRow>
|
||||
) : null}
|
||||
|
||||
<SettingRow
|
||||
name={t['Full width Layout']()}
|
||||
desc={t['Maximum display of content within a page.']()}
|
||||
>
|
||||
<Switch
|
||||
checked={appSettings.fullWidthLayout}
|
||||
onChange={checked => changeSwitch('fullWidthLayout', checked)}
|
||||
/>
|
||||
</SettingRow>
|
||||
{IS_EXHIBITION && env.isDesktop ? (
|
||||
<SettingRow
|
||||
name={t['Window frame style']()}
|
||||
desc={t['Customize appearance of Windows Client.']()}
|
||||
>
|
||||
<RadioButtonGroup
|
||||
className={settingWrapper}
|
||||
defaultValue={appSettings.windowFrameStyle}
|
||||
onValueChange={(value: AppSetting['windowFrameStyle']) => {
|
||||
setAppSettings({ windowFrameStyle: value });
|
||||
}}
|
||||
>
|
||||
{windowFrameStyleOptions.map(option => {
|
||||
return (
|
||||
<RadioButton value={option} key={option}>
|
||||
{t[option]()}
|
||||
</RadioButton>
|
||||
);
|
||||
})}
|
||||
</RadioButtonGroup>
|
||||
</SettingRow>
|
||||
) : null}
|
||||
</Wrapper>
|
||||
{IS_EXHIBITION ? (
|
||||
<Wrapper title={t['Date']()}>
|
||||
<SettingRow
|
||||
name={t['Date Format']()}
|
||||
desc={t['Customize your date style.']()}
|
||||
>
|
||||
<div className={settingWrapper}>
|
||||
<DateFormatSetting />
|
||||
</div>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t['Start Week On Monday']()}
|
||||
desc={t['By default, the week starts on Sunday.']()}
|
||||
>
|
||||
<Switch
|
||||
checked={appSettings.startWeekOnMonday}
|
||||
onChange={checked => changeSwitch('startWeekOnMonday', checked)}
|
||||
/>
|
||||
</SettingRow>
|
||||
</Wrapper>
|
||||
) : null}
|
||||
|
||||
{env.isDesktop ? (
|
||||
<Wrapper title={t['Sidebar']()}>
|
||||
<SettingRow
|
||||
name={t['Disable the noise background on the sidebar']()}
|
||||
desc={t['None yet']()}
|
||||
>
|
||||
<Switch
|
||||
checked={appSettings.disableNoisyBackground}
|
||||
onChange={checked =>
|
||||
changeSwitch('disableNoisyBackground', checked)
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t['Disable the blur sidebar']()}
|
||||
desc={t['None yet']()}
|
||||
>
|
||||
<Switch
|
||||
checked={appSettings.disableBlurBackground}
|
||||
onChange={checked =>
|
||||
changeSwitch('disableBlurBackground', checked)
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
</Wrapper>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const settingWrapper = style({
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
width: '50%',
|
||||
minWidth: '150px',
|
||||
maxWidth: '250px',
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
AppearanceIcon,
|
||||
InformationIcon,
|
||||
KeyboardIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import type { FC, SVGProps } from 'react';
|
||||
|
||||
import { AboutAffine } from './about';
|
||||
import { AppearanceSettings } from './appearance';
|
||||
import { Shortcuts } from './shortcuts';
|
||||
|
||||
export type GeneralSettingKeys = 'shortcuts' | 'appearance' | 'about';
|
||||
|
||||
export type GeneralSettingList = {
|
||||
key: GeneralSettingKeys;
|
||||
title: string;
|
||||
icon: FC<SVGProps<SVGSVGElement>>;
|
||||
}[];
|
||||
|
||||
export const generalSettingList: GeneralSettingList = [
|
||||
{
|
||||
key: 'appearance',
|
||||
title: 'Appearance',
|
||||
icon: AppearanceIcon,
|
||||
},
|
||||
{
|
||||
key: 'shortcuts',
|
||||
title: 'Keyboard Shortcuts',
|
||||
icon: KeyboardIcon,
|
||||
},
|
||||
{
|
||||
key: 'about',
|
||||
title: 'About AFFiNE',
|
||||
icon: InformationIcon,
|
||||
},
|
||||
];
|
||||
|
||||
export const GeneralSetting = ({
|
||||
generalKey,
|
||||
}: {
|
||||
generalKey: GeneralSettingKeys;
|
||||
}) => {
|
||||
switch (generalKey) {
|
||||
case 'shortcuts':
|
||||
return <Shortcuts />;
|
||||
case 'appearance':
|
||||
return <AppearanceSettings />;
|
||||
case 'about':
|
||||
return <AboutAffine />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
|
||||
import {
|
||||
useEdgelessShortcuts,
|
||||
useGeneralShortcuts,
|
||||
useMarkdownShortcuts,
|
||||
usePageShortcuts,
|
||||
} from '../../../../../hooks/affine/use-shortcuts';
|
||||
import { SettingHeader } from '../../common/setting-header';
|
||||
import { Wrapper } from '../../common/wrapper';
|
||||
import { shortcutRow } from './style.css';
|
||||
|
||||
export const Shortcuts = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const markdownShortcuts = useMarkdownShortcuts();
|
||||
const pageShortcuts = usePageShortcuts();
|
||||
const edgelessShortcuts = useEdgelessShortcuts();
|
||||
const generalShortcuts = useGeneralShortcuts();
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingHeader
|
||||
title={t['Keyboard Shortcuts']()}
|
||||
subtitle={t['Check Keyboard Shortcuts quickly']()}
|
||||
/>
|
||||
<Wrapper title={t['General']()}>
|
||||
{Object.entries(generalShortcuts).map(([title, shortcuts]) => {
|
||||
return (
|
||||
<div key={title} className={shortcutRow}>
|
||||
<span>{title}</span>
|
||||
<span className="shortcut">{shortcuts}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Wrapper>
|
||||
<Wrapper title={t['Page']()}>
|
||||
{Object.entries(pageShortcuts).map(([title, shortcuts]) => {
|
||||
return (
|
||||
<div key={title} className={shortcutRow}>
|
||||
<span>{title}</span>
|
||||
<span className="shortcut">{shortcuts}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Wrapper>
|
||||
<Wrapper title={t['Edgeless']()}>
|
||||
{Object.entries(edgelessShortcuts).map(([title, shortcuts]) => {
|
||||
return (
|
||||
<div key={title} className={shortcutRow}>
|
||||
<span>{title}</span>
|
||||
<span className="shortcut">{shortcuts}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Wrapper>
|
||||
<Wrapper title={t['Markdown Syntax']()}>
|
||||
{Object.entries(markdownShortcuts).map(([title, shortcuts]) => {
|
||||
return (
|
||||
<div key={title} className={shortcutRow}>
|
||||
<span>{title}</span>
|
||||
<span className="shortcut">{shortcuts}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Wrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const shortcutRow = style({
|
||||
height: '32px',
|
||||
marginBottom: '12px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
selectors: {
|
||||
'&:last-of-type': {
|
||||
marginBottom: '0',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
globalStyle(`${shortcutRow} .shortcut`, {
|
||||
border: '1px solid var(--affine-border-color)',
|
||||
borderRadius: '8px',
|
||||
padding: '4px 18px',
|
||||
});
|
||||
140
apps/web/src/components/affine/setting-modal/index.tsx
Normal file
140
apps/web/src/components/affine/setting-modal/index.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import { Modal, ModalCloseButton, ModalWrapper } from '@affine/component';
|
||||
import type {
|
||||
AffineLegacyCloudWorkspace,
|
||||
LocalWorkspace,
|
||||
} from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ContactWithUsIcon } from '@blocksuite/icons';
|
||||
import type { NextRouter } from 'next/router';
|
||||
import type React from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
|
||||
import { useWorkspaces } from '../../../hooks/use-workspaces';
|
||||
import type { BlockSuiteWorkspace } from '../../../shared';
|
||||
import { AccountSetting } from './account-setting';
|
||||
import {
|
||||
GeneralSetting,
|
||||
type GeneralSettingKeys,
|
||||
generalSettingList,
|
||||
} from './general-setting';
|
||||
import { SettingSidebar } from './setting-sidebar';
|
||||
import { settingContent } from './style.css';
|
||||
import type { Workspace } from './type';
|
||||
import { WorkSpaceSetting } from './workspace-setting';
|
||||
|
||||
export type QuickSearchModalProps = {
|
||||
currentWorkspace?: BlockSuiteWorkspace;
|
||||
workspaceList?: BlockSuiteWorkspace[];
|
||||
open: boolean;
|
||||
setOpen: (value: boolean) => void;
|
||||
router: NextRouter;
|
||||
};
|
||||
|
||||
export const SettingModal: React.FC<QuickSearchModalProps> = ({
|
||||
open,
|
||||
setOpen,
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const workspaces = useWorkspaces();
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
|
||||
const workspaceList = useMemo(() => {
|
||||
return workspaces.filter(
|
||||
({ flavour }) => flavour !== WorkspaceFlavour.PUBLIC
|
||||
) as Workspace[];
|
||||
}, [workspaces]);
|
||||
|
||||
const [currentRef, setCurrentRef] = useState<{
|
||||
workspace: Workspace | null;
|
||||
generalKey: GeneralSettingKeys | null;
|
||||
isAccount: boolean;
|
||||
}>({
|
||||
workspace: null,
|
||||
generalKey: generalSettingList[0].key,
|
||||
isAccount: false,
|
||||
});
|
||||
const handleClose = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, [setOpen]);
|
||||
|
||||
const onGeneralSettingClick = useCallback((key: GeneralSettingKeys) => {
|
||||
setCurrentRef({
|
||||
workspace: null,
|
||||
generalKey: key,
|
||||
isAccount: false,
|
||||
});
|
||||
}, []);
|
||||
const onWorkspaceSettingClick = useCallback((workspace: Workspace) => {
|
||||
setCurrentRef({
|
||||
workspace: workspace,
|
||||
generalKey: null,
|
||||
isAccount: false,
|
||||
});
|
||||
}, []);
|
||||
const onAccountSettingClick = useCallback(() => {
|
||||
setCurrentRef({
|
||||
workspace: null,
|
||||
generalKey: null,
|
||||
isAccount: true,
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
wrapperPosition={['center', 'center']}
|
||||
data-testid="setting-modal"
|
||||
>
|
||||
<ModalWrapper
|
||||
width={1080}
|
||||
height={760}
|
||||
style={{
|
||||
maxHeight: '85vh',
|
||||
maxWidth: '70vw',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
<ModalCloseButton top={16} right={20} onClick={handleClose} />
|
||||
|
||||
<SettingSidebar
|
||||
generalSettingList={generalSettingList}
|
||||
onGeneralSettingClick={onGeneralSettingClick}
|
||||
currentWorkspace={
|
||||
currentWorkspace as AffineLegacyCloudWorkspace | LocalWorkspace
|
||||
}
|
||||
workspaceList={workspaceList}
|
||||
onWorkspaceSettingClick={onWorkspaceSettingClick}
|
||||
selectedGeneralKey={currentRef.generalKey}
|
||||
selectedWorkspace={currentRef.workspace}
|
||||
onAccountSettingClick={onAccountSettingClick}
|
||||
/>
|
||||
|
||||
<div className={settingContent}>
|
||||
<div className="wrapper">
|
||||
<div className="content">
|
||||
{currentRef.workspace ? (
|
||||
<WorkSpaceSetting workspace={currentRef.workspace} />
|
||||
) : null}
|
||||
{currentRef.generalKey ? (
|
||||
<GeneralSetting generalKey={currentRef.generalKey} />
|
||||
) : null}
|
||||
{currentRef.isAccount ? <AccountSetting /> : null}
|
||||
</div>
|
||||
<div className="footer">
|
||||
<ContactWithUsIcon />
|
||||
<a href="https://community.affine.pro/home" target="_blank">
|
||||
{t[
|
||||
'Need more customization options? You can suggest them to us in the community.'
|
||||
]()}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,126 @@
|
||||
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
||||
import type {
|
||||
AffineLegacyCloudWorkspace,
|
||||
LocalWorkspace,
|
||||
} from '@affine/env/workspace';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import type {
|
||||
GeneralSettingKeys,
|
||||
GeneralSettingList,
|
||||
} from '../general-setting';
|
||||
import type { Workspace } from '../type';
|
||||
import {
|
||||
accountButton,
|
||||
settingSlideBar,
|
||||
sidebarItemsWrapper,
|
||||
sidebarSelectItem,
|
||||
sidebarSubtitle,
|
||||
sidebarTitle,
|
||||
} from './style.css';
|
||||
|
||||
export const SettingSidebar = ({
|
||||
generalSettingList,
|
||||
onGeneralSettingClick,
|
||||
currentWorkspace,
|
||||
workspaceList,
|
||||
onWorkspaceSettingClick,
|
||||
selectedWorkspace,
|
||||
selectedGeneralKey,
|
||||
onAccountSettingClick,
|
||||
}: {
|
||||
generalSettingList: GeneralSettingList;
|
||||
onGeneralSettingClick: (key: GeneralSettingKeys) => void;
|
||||
currentWorkspace: Workspace;
|
||||
workspaceList: Workspace[];
|
||||
onWorkspaceSettingClick: (
|
||||
workspace: AffineLegacyCloudWorkspace | LocalWorkspace
|
||||
) => void;
|
||||
|
||||
selectedWorkspace: Workspace | null;
|
||||
selectedGeneralKey: string | null;
|
||||
onAccountSettingClick: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<div className={settingSlideBar}>
|
||||
<div className={sidebarTitle}>Settings</div>
|
||||
<div className={sidebarSubtitle}>General</div>
|
||||
<div className={sidebarItemsWrapper}>
|
||||
{generalSettingList.map(({ title, icon, key }) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(sidebarSelectItem, {
|
||||
active: key === selectedGeneralKey,
|
||||
})}
|
||||
key={key}
|
||||
title={title}
|
||||
onClick={() => {
|
||||
onGeneralSettingClick(key);
|
||||
}}
|
||||
>
|
||||
{icon({ className: 'icon' })}
|
||||
<span className="setting-name">{title}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className={sidebarSubtitle}>Workspace</div>
|
||||
<div className={clsx(sidebarItemsWrapper, 'scroll')}>
|
||||
{workspaceList.map(workspace => {
|
||||
return (
|
||||
<WorkspaceListItem
|
||||
key={workspace.id}
|
||||
workspace={workspace}
|
||||
onClick={() => {
|
||||
onWorkspaceSettingClick(workspace);
|
||||
}}
|
||||
isCurrent={workspace.id === currentWorkspace.id}
|
||||
isActive={workspace.id === selectedWorkspace?.id}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className={accountButton} onClick={onAccountSettingClick}>
|
||||
<div className="avatar"></div>
|
||||
<div className="content">
|
||||
<div className="name" title="xxx">
|
||||
Account NameAccount Name
|
||||
</div>
|
||||
<div className="email" title="xxx">
|
||||
xxxxxxxx@gmail.comxxxxxxxx@gmail.com
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const WorkspaceListItem = ({
|
||||
workspace,
|
||||
onClick,
|
||||
isCurrent,
|
||||
isActive,
|
||||
}: {
|
||||
workspace: AffineLegacyCloudWorkspace | LocalWorkspace;
|
||||
onClick: () => void;
|
||||
isCurrent: boolean;
|
||||
isActive: boolean;
|
||||
}) => {
|
||||
const [workspaceName] = useBlockSuiteWorkspaceName(
|
||||
workspace.blockSuiteWorkspace ?? null
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className={clsx(sidebarSelectItem, { active: isActive })}
|
||||
title={workspaceName}
|
||||
onClick={onClick}
|
||||
>
|
||||
<WorkspaceAvatar size={14} workspace={workspace} className="icon" />
|
||||
<span className="setting-name">{workspaceName}</span>
|
||||
{isCurrent ? <div className="current-label">Current</div> : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const settingSlideBar = style({
|
||||
width: '25%',
|
||||
maxWidth: '242px',
|
||||
// TODO: use color variable
|
||||
// background: 'var(--affine-background-secondary-color)',
|
||||
backgroundColor: '#F4F4F5',
|
||||
padding: '20px 16px',
|
||||
height: '100%',
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
});
|
||||
|
||||
export const sidebarTitle = style({
|
||||
fontSize: 'var(--affine-font-h-6)',
|
||||
fontWeight: '600',
|
||||
lineHeight: 'var(--affine-line-height)',
|
||||
paddingLeft: '8px',
|
||||
});
|
||||
|
||||
export const sidebarSubtitle = style({
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
lineHeight: 'var(--affine-line-height)',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
paddingLeft: '8px',
|
||||
marginTop: '20px',
|
||||
marginBottom: '4px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const sidebarItemsWrapper = style({
|
||||
selectors: {
|
||||
'&.scroll': {
|
||||
flexGrow: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const sidebarSelectItem = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0 8px',
|
||||
height: '30px',
|
||||
marginBottom: '4px',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
':hover': {
|
||||
background: 'var(--affine-hover-color)',
|
||||
},
|
||||
selectors: {
|
||||
'&.active': {
|
||||
background: 'var(--affine-hover-color)',
|
||||
},
|
||||
[`${sidebarItemsWrapper} &:last-of-type`]: {
|
||||
marginBottom: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
globalStyle(`${settingSlideBar} .icon`, {
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
marginRight: '10px',
|
||||
flexShrink: 0,
|
||||
});
|
||||
globalStyle(`${settingSlideBar} .setting-name`, {
|
||||
minWidth: 0,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
flexGrow: 1,
|
||||
});
|
||||
globalStyle(`${settingSlideBar} .current-label`, {
|
||||
height: '20px',
|
||||
borderRadius: '8px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: '0 5px',
|
||||
// TODO: use color variable
|
||||
background: '#1E96EB',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
fontWeight: '600',
|
||||
color: 'var(--affine-white)',
|
||||
marginLeft: '10px',
|
||||
flexShrink: 0,
|
||||
});
|
||||
|
||||
export const accountButton = style({
|
||||
height: '42px',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
':hover': {
|
||||
background: 'var(--affine-hover-color)',
|
||||
},
|
||||
});
|
||||
|
||||
globalStyle(`${accountButton} .avatar`, {
|
||||
width: '28px',
|
||||
height: '28px',
|
||||
border: '1px solid',
|
||||
borderColor: 'var(--affine-white)',
|
||||
borderRadius: '14px',
|
||||
flexShrink: '0',
|
||||
marginRight: '10px',
|
||||
background: 'red',
|
||||
});
|
||||
globalStyle(`${accountButton} .content`, {
|
||||
flexGrow: '1',
|
||||
minWidth: 0,
|
||||
});
|
||||
globalStyle(`${accountButton} .name`, {
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
fontWeight: 600,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
flexGrow: 1,
|
||||
});
|
||||
globalStyle(`${accountButton} .email`, {
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
flexGrow: 1,
|
||||
});
|
||||
38
apps/web/src/components/affine/setting-modal/style.css.ts
Normal file
38
apps/web/src/components/affine/setting-modal/style.css.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const settingContent = style({
|
||||
flexGrow: '1',
|
||||
height: '100%',
|
||||
padding: '40px 0',
|
||||
});
|
||||
|
||||
globalStyle(`${settingContent} .wrapper`, {
|
||||
width: '66%',
|
||||
minWidth: '450px',
|
||||
height: '100%',
|
||||
maxWidth: '560px',
|
||||
margin: '0 auto',
|
||||
overflowY: 'auto',
|
||||
});
|
||||
globalStyle(`${settingContent} .content`, {
|
||||
minHeight: '100%',
|
||||
paddingBottom: '80px',
|
||||
});
|
||||
globalStyle(`${settingContent} .footer`, {
|
||||
cursor: 'pointer',
|
||||
paddingTop: '40px',
|
||||
marginTop: '-80px',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
display: 'flex',
|
||||
});
|
||||
|
||||
globalStyle(`${settingContent} .footer a`, {
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
});
|
||||
|
||||
globalStyle(`${settingContent} .footer > svg`, {
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
color: 'var(--affine-icon-color)',
|
||||
marginRight: '12px',
|
||||
marginTop: '2px',
|
||||
});
|
||||
6
apps/web/src/components/affine/setting-modal/type.ts
Normal file
6
apps/web/src/components/affine/setting-modal/type.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type {
|
||||
AffineLegacyCloudWorkspace,
|
||||
LocalWorkspace,
|
||||
} from '@affine/env/workspace';
|
||||
|
||||
export type Workspace = AffineLegacyCloudWorkspace | LocalWorkspace;
|
||||
@@ -0,0 +1,29 @@
|
||||
import { assertExists } from '@blocksuite/store';
|
||||
import React, { Suspense, useCallback } from 'react';
|
||||
|
||||
import { getUIAdapter } from '../../../../adapters/workspace';
|
||||
import { useOnTransformWorkspace } from '../../../../hooks/root/use-on-transform-workspace';
|
||||
import { useAppHelper } from '../../../../hooks/use-workspaces';
|
||||
import type { Workspace } from '../type';
|
||||
|
||||
export const WorkSpaceSetting = ({ workspace }: { workspace: Workspace }) => {
|
||||
const helper = useAppHelper();
|
||||
const { NewSettingsDetail } = getUIAdapter(workspace.flavour);
|
||||
|
||||
const onDeleteWorkspace = useCallback(async () => {
|
||||
assertExists(currentWorkspace);
|
||||
const workspaceId = currentWorkspace.id;
|
||||
return helper.deleteWorkspace(workspaceId);
|
||||
}, [helper]);
|
||||
const onTransformWorkspace = useOnTransformWorkspace();
|
||||
|
||||
return (
|
||||
<Suspense fallback={<div>loading</div>}>
|
||||
<NewSettingsDetail
|
||||
onTransformWorkspace={onTransformWorkspace}
|
||||
onDeleteWorkspace={onDeleteWorkspace}
|
||||
currentWorkspace={workspace}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Menu, MenuItem } from '@affine/component';
|
||||
import { AffineIcon, SignOutIcon } from '@blocksuite/icons';
|
||||
import { AffineLogoSBlue2_1Icon, SignOutIcon } from '@blocksuite/icons';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
@@ -90,7 +90,7 @@ export const WorkspaceAvatar = forwardRef<HTMLDivElement, WorkspaceAvatarProps>(
|
||||
{props.name ? (
|
||||
props.name.substring(0, 1)
|
||||
) : (
|
||||
<AffineIcon fontSize={24} color={'#5438FF'} />
|
||||
<AffineLogoSBlue2_1Icon fontSize={24} color={'#5438FF'} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -4,3 +4,16 @@ export const pluginContainer = style({
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
});
|
||||
|
||||
export const editor = style({
|
||||
height: 'calc(100% - 52px)',
|
||||
|
||||
selectors: {
|
||||
'&.full-screen': {
|
||||
padding: '0 5%',
|
||||
vars: {
|
||||
'--affine-editor-width': '100%',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
PluginUIAdapter,
|
||||
} from '@toeverything/plugin-infra/type';
|
||||
import type { PluginBlockSuiteAdapter } from '@toeverything/plugin-infra/type';
|
||||
import clsx from 'clsx';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import Head from 'next/head';
|
||||
import type { FC, ReactElement } from 'react';
|
||||
@@ -32,8 +33,10 @@ import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
||||
|
||||
import { pageSettingFamily } from '../atoms';
|
||||
import { contentLayoutAtom } from '../atoms/layout';
|
||||
import { useAppSetting } from '../atoms/settings';
|
||||
import type { AffineOfficialWorkspace } from '../shared';
|
||||
import { BlockSuiteEditor as Editor } from './blocksuite/block-suite-editor';
|
||||
import { editor } from './page-detail-editor.css';
|
||||
import { pluginContainer } from './page-detail-editor.css';
|
||||
|
||||
export type PageDetailEditorProps = {
|
||||
@@ -71,12 +74,15 @@ const EditorWrapper = memo(function EditorWrapper({
|
||||
(DEFAULT_HELLO_WORLD_PAGE_ID === pageId ? 'edgeless' : 'page');
|
||||
|
||||
const setEditor = useSetAtom(rootCurrentEditorAtom);
|
||||
const [appSettings] = useAppSetting();
|
||||
|
||||
assertExists(meta);
|
||||
|
||||
return (
|
||||
<Editor
|
||||
style={{
|
||||
height: 'calc(100% - 52px)',
|
||||
}}
|
||||
className={clsx(editor, {
|
||||
'full-screen': appSettings?.fullWidthLayout,
|
||||
})}
|
||||
key={`${workspace.flavour}-${workspace.id}-${pageId}`}
|
||||
mode={isPublic ? 'page' : currentMode}
|
||||
page={page}
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
|
||||
interface ShortcutTip {
|
||||
[x: string]: string;
|
||||
}
|
||||
export const useMacKeyboardShortcuts = (): ShortcutTip => {
|
||||
const t = useAFFiNEI18N();
|
||||
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 = useAFFiNEI18N();
|
||||
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 = useAFFiNEI18N();
|
||||
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 = useAFFiNEI18N();
|
||||
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',
|
||||
};
|
||||
};
|
||||
@@ -3,16 +3,14 @@ import {
|
||||
MuiClickAwayListener,
|
||||
MuiSlide,
|
||||
} from '@affine/component';
|
||||
import { env } from '@affine/env';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
useMacKeyboardShortcuts,
|
||||
useMacMarkdownShortcuts,
|
||||
useWindowsKeyboardShortcuts,
|
||||
useWinMarkdownShortcuts,
|
||||
} from './config';
|
||||
useEdgelessShortcuts,
|
||||
useGeneralShortcuts,
|
||||
useMarkdownShortcuts,
|
||||
usePageShortcuts,
|
||||
} from '../../../hooks/affine/use-shortcuts';
|
||||
import { KeyboardIcon } from './icons';
|
||||
import {
|
||||
StyledListItem,
|
||||
@@ -26,25 +24,13 @@ type ModalProps = {
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const checkIsMac = () => {
|
||||
return env.isBrowser && env.isMacOs;
|
||||
};
|
||||
|
||||
export const ShortcutsModal = ({ open, onClose }: ModalProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const macMarkdownShortcuts = useMacMarkdownShortcuts();
|
||||
const winMarkdownShortcuts = useWinMarkdownShortcuts();
|
||||
const macKeyboardShortcuts = useMacKeyboardShortcuts();
|
||||
const windowsKeyboardShortcuts = useWindowsKeyboardShortcuts();
|
||||
const [isMac, setIsMac] = useState(false);
|
||||
const markdownShortcuts = isMac ? macMarkdownShortcuts : winMarkdownShortcuts;
|
||||
const keyboardShortcuts = isMac
|
||||
? macKeyboardShortcuts
|
||||
: windowsKeyboardShortcuts;
|
||||
|
||||
useEffect(() => {
|
||||
setIsMac(checkIsMac());
|
||||
}, []);
|
||||
const markdownShortcuts = useMarkdownShortcuts();
|
||||
const pageShortcuts = usePageShortcuts();
|
||||
const edgelessShortcuts = useEdgelessShortcuts();
|
||||
const generalShortcuts = useGeneralShortcuts();
|
||||
|
||||
return (
|
||||
<MuiSlide direction="left" in={open} mountOnEnter unmountOnExit>
|
||||
@@ -72,9 +58,27 @@ export const ShortcutsModal = ({ open, onClose }: ModalProps) => {
|
||||
/>
|
||||
</StyledModalHeader>
|
||||
<StyledSubTitle style={{ marginTop: 0 }}>
|
||||
{t['Keyboard Shortcuts']()}
|
||||
{t['General']()}
|
||||
</StyledSubTitle>
|
||||
{Object.entries(keyboardShortcuts).map(([title, shortcuts]) => {
|
||||
{Object.entries(generalShortcuts).map(([title, shortcuts]) => {
|
||||
return (
|
||||
<StyledListItem key={title}>
|
||||
<span>{title}</span>
|
||||
<span>{shortcuts}</span>
|
||||
</StyledListItem>
|
||||
);
|
||||
})}
|
||||
<StyledSubTitle>{t['Page']()}</StyledSubTitle>
|
||||
{Object.entries(pageShortcuts).map(([title, shortcuts]) => {
|
||||
return (
|
||||
<StyledListItem key={title}>
|
||||
<span>{title}</span>
|
||||
<span>{shortcuts}</span>
|
||||
</StyledListItem>
|
||||
);
|
||||
})}
|
||||
<StyledSubTitle>{t['Edgeless']()}</StyledSubTitle>
|
||||
{Object.entries(edgelessShortcuts).map(([title, shortcuts]) => {
|
||||
return (
|
||||
<StyledListItem key={title}>
|
||||
<span>{title}</span>
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
appSidebarOpenAtom,
|
||||
AppUpdaterButton,
|
||||
CategoryDivider,
|
||||
MenuItem,
|
||||
MenuLinkItem,
|
||||
QuickSearchInput,
|
||||
SidebarContainer,
|
||||
@@ -25,6 +26,7 @@ import type { ReactElement } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
import { useHistoryAtom } from '../../atoms/history';
|
||||
import { useAppSetting } from '../../atoms/settings';
|
||||
import type { AllWorkspace } from '../../shared';
|
||||
import FavoriteList from '../pure/workspace-slider-bar/favorite/favorite-list';
|
||||
import { WorkspaceSelector } from '../pure/workspace-slider-bar/WorkspaceSelector';
|
||||
@@ -32,6 +34,7 @@ import { WorkspaceSelector } from '../pure/workspace-slider-bar/WorkspaceSelecto
|
||||
export type RootAppSidebarProps = {
|
||||
isPublicWorkspace: boolean;
|
||||
onOpenQuickSearchModal: () => void;
|
||||
onOpenSettingModal: () => void;
|
||||
onOpenWorkspaceListModal: () => void;
|
||||
currentWorkspace: AllWorkspace | null;
|
||||
openPage: (pageId: string) => void;
|
||||
@@ -88,8 +91,11 @@ export const RootAppSidebar = ({
|
||||
paths,
|
||||
onOpenQuickSearchModal,
|
||||
onOpenWorkspaceListModal,
|
||||
onOpenSettingModal,
|
||||
}: RootAppSidebarProps): ReactElement => {
|
||||
const currentWorkspaceId = currentWorkspace?.id || null;
|
||||
const [appSettings] = useAppSetting();
|
||||
|
||||
const blockSuiteWorkspace = currentWorkspace?.blockSuiteWorkspace;
|
||||
const t = useAFFiNEI18N();
|
||||
const onClickNewPage = useCallback(async () => {
|
||||
@@ -143,7 +149,10 @@ export const RootAppSidebar = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppSidebar router={router}>
|
||||
<AppSidebar
|
||||
router={router}
|
||||
hasBackground={!appSettings.disableBlurBackground}
|
||||
>
|
||||
<SidebarContainer>
|
||||
<WorkspaceSelector
|
||||
currentWorkspace={currentWorkspace}
|
||||
@@ -168,6 +177,25 @@ export const RootAppSidebar = ({
|
||||
>
|
||||
<span data-testid="settings">{t['Settings']()}</span>
|
||||
</RouteMenuLinkItem>
|
||||
{config.enableNewSettingModal ? (
|
||||
<MenuItem icon={<SettingsIcon />} onClick={onOpenSettingModal}>
|
||||
<span data-testid="new-settings">
|
||||
{t['Settings']()}
|
||||
<i
|
||||
style={{
|
||||
background: 'var(--affine-palette-line-blue)',
|
||||
borderRadius: '2px',
|
||||
fontSize: '8px',
|
||||
padding: '0 5px',
|
||||
color: 'var(--affine-white)',
|
||||
marginLeft: '15px',
|
||||
}}
|
||||
>
|
||||
NEW
|
||||
</i>
|
||||
</span>
|
||||
</MenuItem>
|
||||
) : null}
|
||||
</SidebarContainer>
|
||||
|
||||
<SidebarScrollableContainer>
|
||||
|
||||
237
apps/web/src/hooks/affine/use-shortcuts.ts
Normal file
237
apps/web/src/hooks/affine/use-shortcuts.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import { env } from '@affine/env';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
interface ShortcutTip {
|
||||
[x: string]: string;
|
||||
}
|
||||
|
||||
export const useWinGeneralKeyboardShortcuts = (): ShortcutTip => {
|
||||
const t = useAFFiNEI18N();
|
||||
return useMemo(
|
||||
() => ({
|
||||
[t['Cancel']()]: 'ESC',
|
||||
[t['Quick Search']()]: 'Ctrl + K',
|
||||
[t['New Page']()]: 'Ctrl + N',
|
||||
// not implement yet
|
||||
// [t['Append to Daily Note']()]: 'Ctrl + Alt + A',
|
||||
[t['Expand/Collapse Sidebar']()]: 'Ctrl + /',
|
||||
// not implement yet
|
||||
// [t['Go Back']()]: 'Ctrl + [',
|
||||
// [t['Go Forward']()]: 'Ctrl + ]',
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
};
|
||||
export const useMacGeneralKeyboardShortcuts = (): ShortcutTip => {
|
||||
const t = useAFFiNEI18N();
|
||||
return useMemo(
|
||||
() => ({
|
||||
[t['Cancel']()]: 'ESC',
|
||||
[t['Quick Search']()]: '⌘ + K',
|
||||
[t['New Page']()]: '⌘ + N',
|
||||
// not implement yet
|
||||
// [t['Append to Daily Note']()]: '⌘ + ⌥ + A',
|
||||
[t['Expand/Collapse Sidebar']()]: '⌘ + /',
|
||||
// not implement yet
|
||||
// [t['Go Back']()]: '⌘ + [',
|
||||
// [t['Go Forward']()]: '⌘ + ]',
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
};
|
||||
|
||||
export const useMacEdgelessKeyboardShortcuts = (): ShortcutTip => {
|
||||
const t = useAFFiNEI18N();
|
||||
return useMemo(
|
||||
() => ({
|
||||
[t['Select All']()]: '⌘ + A',
|
||||
[t['Undo']()]: '⌘ + Z',
|
||||
[t['Redo']()]: '⌘ + ⇧ + Z',
|
||||
[t['Zoom in']()]: '⌘ + +',
|
||||
[t['Zoom out']()]: '⌘ + -',
|
||||
[t['Zoom to 100%']()]: '⌘ + 0',
|
||||
[t['Zoom to fit']()]: '⌘ + 1',
|
||||
[t['Select']()]: 'V',
|
||||
[t['Text']()]: 'T',
|
||||
[t['Shape']()]: 'S',
|
||||
[t['Image']()]: 'I',
|
||||
[t['Straight Connector']()]: 'L',
|
||||
[t['Elbowed Connector']()]: 'X',
|
||||
// not implement yet
|
||||
// [t['Curve Connector']()]: 'C',
|
||||
[t['Pen']()]: 'P',
|
||||
[t['Hand']()]: 'H',
|
||||
[t['Note']()]: 'N',
|
||||
// not implement yet
|
||||
// [t['Group']()]: '⌘ + G',
|
||||
// [t['Ungroup']()]: '⌘ + ⇧ + G',
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
};
|
||||
export const useWinEdgelessKeyboardShortcuts = (): ShortcutTip => {
|
||||
const t = useAFFiNEI18N();
|
||||
return useMemo(
|
||||
() => ({
|
||||
[t['Select All']()]: 'Ctrl + A',
|
||||
[t['Undo']()]: 'Ctrl + Z',
|
||||
[t['Redo']()]: 'Ctrl + Y/Ctrl + Shift + Z',
|
||||
[t['Zoom in']()]: 'Ctrl + +',
|
||||
[t['Zoom out']()]: 'Ctrl + -',
|
||||
[t['Zoom to 100%']()]: 'Ctrl + 0',
|
||||
[t['Zoom to fit']()]: 'Ctrl + 1',
|
||||
[t['Select']()]: 'V',
|
||||
[t['Text']()]: 'T',
|
||||
[t['Shape']()]: 'S',
|
||||
[t['Image']()]: 'I',
|
||||
[t['Straight Connector']()]: 'L',
|
||||
[t['Elbowed Connector']()]: 'X',
|
||||
// not implement yet
|
||||
// [t['Curve Connector']()]: 'C',
|
||||
[t['Pen']()]: 'P',
|
||||
[t['Hand']()]: 'H',
|
||||
[t['Note']()]: 'N',
|
||||
// not implement yet
|
||||
// [t['Group']()]: 'Ctrl + G',
|
||||
// [t['Ungroup']()]: 'Ctrl + Shift + G',
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
};
|
||||
export const useMacPageKeyboardShortcuts = (): ShortcutTip => {
|
||||
const t = useAFFiNEI18N();
|
||||
return useMemo(
|
||||
() => ({
|
||||
[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',
|
||||
[t['Group as Database']()]: '⌘ + G',
|
||||
// not implement yet
|
||||
// [t['Move Up']()]: '⌘ + ⌥ + ↑',
|
||||
// [t['Move Down']()]: '⌘ + ⌥ + ↓',
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
};
|
||||
|
||||
export const useMacMarkdownShortcuts = (): ShortcutTip => {
|
||||
const t = useAFFiNEI18N();
|
||||
return useMemo(
|
||||
() => ({
|
||||
[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',
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
};
|
||||
|
||||
export const useWinPageKeyboardShortcuts = (): ShortcutTip => {
|
||||
const t = useAFFiNEI18N();
|
||||
return useMemo(
|
||||
() => ({
|
||||
[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',
|
||||
[t['Group as Database']()]: 'Ctrl + G',
|
||||
// not implement yet
|
||||
// [t['Move Up']()]: 'Ctrl + Alt + ↑',
|
||||
// [t['Move Down']()]: 'Ctrl + Alt + ↓',
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
};
|
||||
export const useWinMarkdownShortcuts = (): ShortcutTip => {
|
||||
const t = useAFFiNEI18N();
|
||||
return useMemo(
|
||||
() => ({
|
||||
[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',
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
};
|
||||
|
||||
export const useMarkdownShortcuts = (): ShortcutTip => {
|
||||
const macMarkdownShortcuts = useMacMarkdownShortcuts();
|
||||
const winMarkdownShortcuts = useWinMarkdownShortcuts();
|
||||
const isMac = env.isBrowser && env.isMacOs;
|
||||
return isMac ? macMarkdownShortcuts : winMarkdownShortcuts;
|
||||
};
|
||||
|
||||
export const usePageShortcuts = (): ShortcutTip => {
|
||||
const macPageShortcuts = useMacPageKeyboardShortcuts();
|
||||
const winPageShortcuts = useWinPageKeyboardShortcuts();
|
||||
const isMac = env.isBrowser && env.isMacOs;
|
||||
return isMac ? macPageShortcuts : winPageShortcuts;
|
||||
};
|
||||
|
||||
export const useEdgelessShortcuts = (): ShortcutTip => {
|
||||
const macEdgelessShortcuts = useMacEdgelessKeyboardShortcuts();
|
||||
const winEdgelessShortcuts = useWinEdgelessKeyboardShortcuts();
|
||||
const isMac = env.isBrowser && env.isMacOs;
|
||||
|
||||
return isMac ? macEdgelessShortcuts : winEdgelessShortcuts;
|
||||
};
|
||||
|
||||
export const useGeneralShortcuts = (): ShortcutTip => {
|
||||
const macGeneralShortcuts = useMacGeneralKeyboardShortcuts();
|
||||
const winGeneralShortcuts = useWinGeneralKeyboardShortcuts();
|
||||
const isMac = env.isBrowser && env.isMacOs;
|
||||
|
||||
return isMac ? macGeneralShortcuts : winGeneralShortcuts;
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AppContainer, MainContainer } from '@affine/component/workspace';
|
||||
import { MainContainer } from '@affine/component/workspace';
|
||||
import type { AffinePublicWorkspace } from '@affine/env/workspace';
|
||||
import { useAtom } from 'jotai';
|
||||
import Head from 'next/head';
|
||||
@@ -7,6 +7,7 @@ import type React from 'react';
|
||||
import { lazy, Suspense } from 'react';
|
||||
|
||||
import { openQuickSearchModalAtom } from '../atoms';
|
||||
import { AppContainer } from '../components/affine/app-container';
|
||||
import { useRouterTitle } from '../hooks/use-router-title';
|
||||
|
||||
const QuickSearchModal = lazy(() =>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { appSidebarResizingAtom } from '@affine/component/app-sidebar';
|
||||
import type { DraggableTitleCellData } from '@affine/component/page-list';
|
||||
import { StyledTitleLink } from '@affine/component/page-list';
|
||||
import {
|
||||
AppContainer,
|
||||
MainContainer,
|
||||
ToolContainer,
|
||||
WorkspaceFallback,
|
||||
@@ -42,12 +41,17 @@ import type { FC, PropsWithChildren, ReactElement } from 'react';
|
||||
import { lazy, Suspense, useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
import { WorkspaceAdapters } from '../adapters/workspace';
|
||||
import { openQuickSearchModalAtom, openWorkspacesModalAtom } from '../atoms';
|
||||
import {
|
||||
openQuickSearchModalAtom,
|
||||
openSettingModalAtom,
|
||||
openWorkspacesModalAtom,
|
||||
} from '../atoms';
|
||||
import { useTrackRouterHistoryEffect } from '../atoms/history';
|
||||
import {
|
||||
publicWorkspaceAtom,
|
||||
publicWorkspaceIdAtom,
|
||||
} from '../atoms/public-workspace';
|
||||
import { AppContainer } from '../components/affine/app-container';
|
||||
import type { IslandItemNames } from '../components/pure/help-island';
|
||||
import { HelpIsland } from '../components/pure/help-island';
|
||||
import {
|
||||
@@ -71,6 +75,11 @@ const QuickSearchModal = lazy(() =>
|
||||
default: module.QuickSearchModal,
|
||||
}))
|
||||
);
|
||||
const SettingModal = lazy(() =>
|
||||
import('../components/affine/setting-modal').then(module => ({
|
||||
default: module.SettingModal,
|
||||
}))
|
||||
);
|
||||
|
||||
export const PublicQuickSearch: FC = () => {
|
||||
const publicWorkspace = useAtomValue(publicWorkspaceAtom);
|
||||
@@ -119,6 +128,25 @@ export const QuickSearch: FC = () => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
export const Setting: FC = () => {
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const router = useRouter();
|
||||
const [openSettingModal, setOpenSettingModalAtom] =
|
||||
useAtom(openSettingModalAtom);
|
||||
const blockSuiteWorkspace = currentWorkspace?.blockSuiteWorkspace;
|
||||
const isPublicWorkspace =
|
||||
router.pathname.split('/')[1] === 'public-workspace';
|
||||
if (!blockSuiteWorkspace || isPublicWorkspace) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<SettingModal
|
||||
open={openSettingModal}
|
||||
setOpen={setOpenSettingModalAtom}
|
||||
router={router}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const logger = new DebugLogger('workspace-layout');
|
||||
|
||||
@@ -390,6 +418,12 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
|
||||
setOpenQuickSearchModalAtom(true);
|
||||
}, [setOpenQuickSearchModalAtom]);
|
||||
|
||||
const [, setOpenSettingModalAtom] = useAtom(openSettingModalAtom);
|
||||
|
||||
const handleOpenSettingModal = useCallback(() => {
|
||||
setOpenSettingModalAtom(true);
|
||||
}, [setOpenSettingModalAtom]);
|
||||
|
||||
const resizing = useAtomValue(appSidebarResizingAtom);
|
||||
|
||||
const sensors = useSensors(
|
||||
@@ -444,6 +478,7 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
|
||||
<RootAppSidebar
|
||||
isPublicWorkspace={isPublicWorkspace}
|
||||
onOpenQuickSearchModal={handleOpenQuickSearchModal}
|
||||
onOpenSettingModal={handleOpenSettingModal}
|
||||
currentWorkspace={currentWorkspace}
|
||||
onOpenWorkspaceListModal={handleOpenWorkspaceListModal}
|
||||
openPage={useCallback(
|
||||
@@ -476,6 +511,7 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
|
||||
<PageListTitleCellDragOverlay />
|
||||
</DndContext>
|
||||
<QuickSearch />
|
||||
<Setting />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from '@affine/component';
|
||||
import { AppContainer, MainContainer } from '@affine/component/workspace';
|
||||
import { MainContainer } from '@affine/component/workspace';
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import type { BroadCastChannelProvider } from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
@@ -10,6 +10,7 @@ import { Typography } from '@mui/material';
|
||||
import type React from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { AppContainer } from '../../components/affine/app-container';
|
||||
import { BlockSuitePageList } from '../../components/blocksuite/block-suite-page-list';
|
||||
import { toast } from '../../utils';
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { AppContainer, MainContainer } from '@affine/component/workspace';
|
||||
import { MainContainer } from '@affine/component/workspace';
|
||||
import { useRouter } from 'next/router';
|
||||
import { lazy, Suspense } from 'react';
|
||||
|
||||
import { AppContainer } from '../../components/affine/app-container';
|
||||
import type { NextPageWithLayout } from '../../shared';
|
||||
|
||||
const Editor = lazy(() =>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from '@affine/component';
|
||||
import { AppContainer, MainContainer } from '@affine/component/workspace';
|
||||
import { MainContainer } from '@affine/component/workspace';
|
||||
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
|
||||
import {
|
||||
clearLoginStorage,
|
||||
@@ -14,6 +14,7 @@ import { useAtom } from 'jotai';
|
||||
import type { NextPage } from 'next';
|
||||
import { lazy, Suspense, useMemo } from 'react';
|
||||
|
||||
import { AppContainer } from '../../components/affine/app-container';
|
||||
import { toast } from '../../utils';
|
||||
|
||||
const Viewer = lazy(() =>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AppContainer, MainContainer } from '@affine/component/workspace';
|
||||
import { MainContainer } from '@affine/component/workspace';
|
||||
import { config } from '@affine/env';
|
||||
import { NoSsr } from '@mui/material';
|
||||
import { affinePluginsAtom } from '@toeverything/plugin-infra/manager';
|
||||
@@ -6,6 +6,8 @@ import { useAtomValue } from 'jotai';
|
||||
import type { ReactElement } from 'react';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
import { AppContainer } from '../components/affine/app-container';
|
||||
|
||||
const Plugins = () => {
|
||||
const plugins = useAtomValue(affinePluginsAtom);
|
||||
return (
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
"@blocksuite/blocks": "0.0.0-20230607055421-9b20fcaf-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230607055421-9b20fcaf-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230607055421-9b20fcaf-nightly",
|
||||
"@blocksuite/icons": "^2.1.19",
|
||||
"@blocksuite/icons": "^2.1.21",
|
||||
"@blocksuite/lit": "0.0.0-20230607055421-9b20fcaf-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230607055421-9b20fcaf-nightly",
|
||||
"@types/react": "^18.2.12",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { AffineLogoSimCBlue1_1Icon, CloseIcon } from '@blocksuite/icons';
|
||||
import { AffineLogoSBlue2_1Icon, CloseIcon } from '@blocksuite/icons';
|
||||
|
||||
import {
|
||||
downloadCloseButtonStyle,
|
||||
@@ -17,7 +17,7 @@ export const DownloadTips = ({ onClose }: { onClose: () => void }) => {
|
||||
data-testid="download-client-tip"
|
||||
>
|
||||
<div className={downloadTipStyle}>
|
||||
<AffineLogoSimCBlue1_1Icon className={downloadTipIconStyle} />
|
||||
<AffineLogoSBlue2_1Icon className={downloadTipIconStyle} />
|
||||
<div className={downloadMessageStyle}>
|
||||
<Trans i18nKey="com.affine.banner.content">
|
||||
Enjoying the demo?
|
||||
|
||||
@@ -16,6 +16,7 @@ export const navWrapperStyle = style({
|
||||
zIndex: 2,
|
||||
paddingBottom: '8px',
|
||||
backgroundColor: 'transparent',
|
||||
borderRight: '1px solid var(--affine-border-color)',
|
||||
'@media': {
|
||||
[`(max-width: ${floatingMaxWidth}px)`]: {
|
||||
position: 'absolute',
|
||||
@@ -39,6 +40,9 @@ export const navWrapperStyle = style({
|
||||
'&[data-enable-animation="true"]': {
|
||||
transition: 'margin-left .3s, width .3s',
|
||||
},
|
||||
'&.has-background': {
|
||||
backgroundColor: 'var(--affine-white-60)',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { env } from '@affine/env';
|
||||
import { Skeleton } from '@mui/material';
|
||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||
import clsx from 'clsx';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import type { PropsWithChildren, ReactElement } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
@@ -25,7 +26,11 @@ import { ResizeIndicator } from './resize-indicator';
|
||||
import type { SidebarHeaderProps } from './sidebar-header';
|
||||
import { SidebarHeader } from './sidebar-header';
|
||||
|
||||
export type AppSidebarProps = PropsWithChildren<SidebarHeaderProps>;
|
||||
export type AppSidebarProps = PropsWithChildren<
|
||||
SidebarHeaderProps & {
|
||||
hasBackground?: boolean;
|
||||
}
|
||||
>;
|
||||
|
||||
function useEnableAnimation() {
|
||||
const [enable, setEnable] = useState(false);
|
||||
@@ -91,7 +96,9 @@ export function AppSidebar(props: AppSidebarProps): ReactElement {
|
||||
style={assignInlineVars({
|
||||
[navWidthVar]: `${appSidebarWidth}px`,
|
||||
})}
|
||||
className={navWrapperStyle}
|
||||
className={clsx(navWrapperStyle, {
|
||||
'has-background': env.isDesktop && props.hasBackground,
|
||||
})}
|
||||
data-open={open}
|
||||
data-is-macos-electron={isMacosDesktop}
|
||||
data-enable-animation={enableAnimation && !isResizing}
|
||||
|
||||
@@ -23,6 +23,7 @@ export type EditorProps = {
|
||||
onInit: (page: Page, editor: Readonly<EditorContainer>) => void;
|
||||
onLoad?: (page: Page, editor: EditorContainer) => () => void;
|
||||
style?: CSSProperties;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export type ErrorBoundaryProps = {
|
||||
@@ -122,7 +123,9 @@ const BlockSuiteEditorImpl = (props: EditorProps): ReactElement => {
|
||||
}, [editor, page]);
|
||||
|
||||
// issue: https://github.com/toeverything/AFFiNE/issues/2004
|
||||
const className = `editor-wrapper ${editor.mode}-mode`;
|
||||
const className = `editor-wrapper ${editor.mode}-mode ${
|
||||
props.className || ''
|
||||
}`;
|
||||
return (
|
||||
<div
|
||||
data-testid={`editor-${page.id}`}
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
StyledSmallLink,
|
||||
StyledSubTitle,
|
||||
} from './style';
|
||||
const linkList = [
|
||||
export const relatedLinks = [
|
||||
{
|
||||
icon: <GithubIcon />,
|
||||
title: 'GitHub',
|
||||
@@ -107,7 +107,7 @@ export const ContactModal = ({
|
||||
{t['Get in touch! Join our communities']()}
|
||||
</StyledSubTitle>
|
||||
<FlexWrapper justifyContent="center">
|
||||
{linkList.map(({ icon, title, link }) => {
|
||||
{relatedLinks.map(({ icon, title, link }) => {
|
||||
return (
|
||||
<StyledSmallLink key={title} href={link} target="_blank">
|
||||
{icon}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// License on the MIT
|
||||
// https://github.com/emilkowalski/sonner/blob/5cb703edc108a23fd74979235c2f3c4005edd2a7/src/index.tsx
|
||||
|
||||
import { CloseIcon, InformationFillIcon } from '@blocksuite/icons';
|
||||
import { CloseIcon, InformationIcon } from '@blocksuite/icons';
|
||||
import * as Toast from '@radix-ui/react-toast';
|
||||
import clsx from 'clsx';
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||
@@ -303,7 +303,7 @@ function NotificationCard(props: NotificationCardProps): ReactElement {
|
||||
[styles.lightInfoIconStyle]: notification.theme !== 'dark',
|
||||
})}
|
||||
>
|
||||
<InformationFillIcon />
|
||||
<InformationIcon />
|
||||
</div>
|
||||
<div className={styles.notificationTitleContactStyle}>
|
||||
{notification.title}
|
||||
|
||||
@@ -14,7 +14,10 @@ export const appStyle = style({
|
||||
'&[data-is-resizing="true"]': {
|
||||
cursor: 'col-resize',
|
||||
},
|
||||
'&:before': {
|
||||
'&.blur-background': {
|
||||
backgroundColor: 'var(--affine-background-primary-color)',
|
||||
},
|
||||
'&.noisy-background::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
|
||||
@@ -1,22 +1,33 @@
|
||||
import { clsx } from 'clsx';
|
||||
import type { PropsWithChildren, ReactElement } from 'react';
|
||||
import type { FC, PropsWithChildren, ReactElement } from 'react';
|
||||
|
||||
import { AppSidebarFallback } from '../app-sidebar';
|
||||
import { appStyle, mainContainerStyle, toolStyle } from './index.css';
|
||||
|
||||
export type WorkspaceRootProps = PropsWithChildren<{
|
||||
resizing?: boolean;
|
||||
useNoisyBackground?: boolean;
|
||||
useBlurBackground?: boolean;
|
||||
}>;
|
||||
|
||||
export const AppContainer = (props: WorkspaceRootProps): ReactElement => {
|
||||
const noisyBackground = environment.isDesktop && environment.isMacOs;
|
||||
export const AppContainer: FC<WorkspaceRootProps> = ({
|
||||
resizing,
|
||||
useNoisyBackground,
|
||||
useBlurBackground,
|
||||
children,
|
||||
}) => {
|
||||
const noisyBackground =
|
||||
useNoisyBackground && environment.isDesktop && environment.isMacOs;
|
||||
return (
|
||||
<div
|
||||
className={appStyle}
|
||||
className={clsx(appStyle, {
|
||||
'noisy-background': noisyBackground,
|
||||
'blur-background': environment.isDesktop && useBlurBackground,
|
||||
})}
|
||||
data-noise-background={noisyBackground}
|
||||
data-is-resizing={props.resizing}
|
||||
data-is-resizing={resizing}
|
||||
>
|
||||
{props.children}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './menu';
|
||||
// export { StyledMenuItem as MenuItem } from './styles';
|
||||
export * from './menu-item';
|
||||
export * from './menu-trigger';
|
||||
export * from './pure-menu';
|
||||
|
||||
22
packages/component/src/ui/menu/menu-trigger.tsx
Normal file
22
packages/component/src/ui/menu/menu-trigger.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { ArrowDownSmallIcon } from '@blocksuite/icons';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import type { ButtonProps } from '../button';
|
||||
import { StyledButton } from './styles';
|
||||
|
||||
export const MenuTrigger = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ children, ...props }, ref) => {
|
||||
return (
|
||||
<StyledButton
|
||||
ref={ref}
|
||||
icon={<ArrowDownSmallIcon />}
|
||||
iconPosition="end"
|
||||
noBorder={true}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</StyledButton>
|
||||
);
|
||||
}
|
||||
);
|
||||
MenuTrigger.displayName = 'MenuTrigger';
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { CSSProperties } from 'react';
|
||||
|
||||
import { displayFlex, styled, textEllipsis } from '../../styles';
|
||||
import { Button } from '../button';
|
||||
import StyledPopperContainer from '../shared/container';
|
||||
|
||||
export const StyledMenuWrapper = styled(StyledPopperContainer, {
|
||||
@@ -100,3 +101,16 @@ export const StyledMenuItem = styled('button')<{
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export const StyledButton = styled(Button)(() => {
|
||||
return {
|
||||
width: '100%',
|
||||
height: '32px',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: 'transparent',
|
||||
...displayFlex('space-between', 'center'),
|
||||
border: `1px solid var(--affine-border-color)`,
|
||||
padding: '0 10px',
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
};
|
||||
});
|
||||
|
||||
1
packages/env/src/config.ts
vendored
1
packages/env/src/config.ts
vendored
@@ -48,6 +48,7 @@ export const buildFlagsSchema = z.object({
|
||||
enableLegacyCloud: z.boolean(),
|
||||
changelogUrl: z.string(),
|
||||
enablePreloading: z.boolean(),
|
||||
enableNewSettingModal: z.boolean(),
|
||||
});
|
||||
|
||||
export const blockSuiteFeatureFlags = z.object({
|
||||
|
||||
14
packages/env/src/workspace.ts
vendored
14
packages/env/src/workspace.ts
vendored
@@ -201,6 +201,19 @@ type SettingProps<Flavour extends keyof WorkspaceRegistry> =
|
||||
) => void;
|
||||
};
|
||||
|
||||
type NewSettingProps<Flavour extends keyof WorkspaceRegistry> =
|
||||
UIBaseProps<Flavour> & {
|
||||
onDeleteWorkspace: () => Promise<void>;
|
||||
onTransformWorkspace: <
|
||||
From extends keyof WorkspaceRegistry,
|
||||
To extends keyof WorkspaceRegistry
|
||||
>(
|
||||
from: From,
|
||||
to: To,
|
||||
workspace: WorkspaceRegistry[From]
|
||||
) => void;
|
||||
};
|
||||
|
||||
type PageDetailProps<Flavour extends keyof WorkspaceRegistry> =
|
||||
UIBaseProps<Flavour> & {
|
||||
currentPageId: string;
|
||||
@@ -218,6 +231,7 @@ export interface WorkspaceUISchema<Flavour extends keyof WorkspaceRegistry> {
|
||||
PageDetail: FC<PageDetailProps<Flavour>>;
|
||||
PageList: FC<PageListProps<Flavour>>;
|
||||
SettingsDetail: FC<SettingProps<Flavour>>;
|
||||
NewSettingsDetail: FC<NewSettingProps<Flavour>>;
|
||||
Provider: FC<PropsWithChildren>;
|
||||
}
|
||||
|
||||
|
||||
@@ -297,5 +297,67 @@
|
||||
"Update Available": "Update available",
|
||||
"dark": "Dark",
|
||||
"system": "System",
|
||||
"light": "Light"
|
||||
"light": "Light",
|
||||
"Need more customization options? You can suggest them to us in the community.": "Need more customization options? You can suggest them to us in the community.",
|
||||
"Check Keyboard Shortcuts quickly": "Check Keyboard Shortcuts quickly",
|
||||
"Quick Search": "Quick Search",
|
||||
"Append to Daily Note": "Append to Daily Note",
|
||||
"Expand/Collapse Sidebar": "Expand/Collapse Sidebar",
|
||||
"Go Back": "Go Back",
|
||||
"Go Forward": "Go Forward",
|
||||
"Select All": "Select All",
|
||||
"Zoom in": "Zoom in",
|
||||
"Zoom out": "Zoom out",
|
||||
"Zoom to 100%": "Zoom to 100%",
|
||||
"Zoom to fit": "Zoom to fit",
|
||||
"Image": "Image",
|
||||
"Straight Connector": "Straight Connector",
|
||||
"Elbowed Connector": "Elbowed Connector",
|
||||
"Curve Connector": "Curve Connector",
|
||||
"Hand": "Hand",
|
||||
"Note": "Note",
|
||||
"Group": "Group",
|
||||
"Ungroup": "Ungroup",
|
||||
"Group as Database": "Group as Database",
|
||||
"Move Up": "Move Up",
|
||||
"Move Down": "Move Down",
|
||||
"Appearance Settings": "Appearance Settings",
|
||||
"Customize your AFFiNE Appearance": "Customize your AFFiNE Appearance",
|
||||
"Theme": "Theme",
|
||||
"Date": "Date",
|
||||
"Sidebar": "Sidebar",
|
||||
"Color Scheme": "Color Scheme",
|
||||
"Choose your color scheme": "Choose your color scheme",
|
||||
"Display Language": "Display Language",
|
||||
"Select the language for the interface.": "Select the language for the interface.",
|
||||
"Client Border Style": "Client Border Style",
|
||||
"Customize the appearance of the client.": "Customize the appearance of the client.",
|
||||
"Full width Layout": "Full width Layout",
|
||||
"Maximum display of content within a page.": "Maximum display of content within a page.",
|
||||
"Window frame style": "Window frame style",
|
||||
"Customize appearance of Windows Client.": "Customize appearance of Windows Client.",
|
||||
"Date Format": "Date Format",
|
||||
"Customize your date style.": "Customize your date style.",
|
||||
"Start Week On Monday": "Start Week On Monday",
|
||||
"By default, the week starts on Sunday.": "By default, the week starts on Sunday.",
|
||||
"Disable the noise background on the sidebar": "Disable the noise background on the sidebar",
|
||||
"None yet": "None yet",
|
||||
"Disable the blur sidebar": "Disable the blur sidebar",
|
||||
"frameless": "Frameless",
|
||||
"NativeTitleBar": "Native Titlebar",
|
||||
"About AFFiNE": "About AFFiNE",
|
||||
"Version": "Version",
|
||||
"Contact with us": "Contact with us",
|
||||
"Communities": "Communities",
|
||||
"Info of legal": "Info of legal",
|
||||
"Check for updates": "Check for updates",
|
||||
"New version is ready": "New version is ready",
|
||||
"Check for updates automatically": "Check for updates automatically",
|
||||
"If enabled, it will automatically check for new versions at regular intervals.": "If enabled, it will automatically check for new versions at regular intervals.",
|
||||
"Download updates automatically": "Download updates automatically",
|
||||
"If enabled, new versions will be automatically downloaded to the current device.": " If enabled, new versions will be automatically downloaded to the current device.",
|
||||
"Discover what's new": "Discover what's new",
|
||||
"View the AFFiNE Changelog.": "View the AFFiNE Changelog.",
|
||||
"Privacy": "Privacy",
|
||||
"Terms of Use": "Terms of Use"
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"@blocksuite/blocks": "0.0.0-20230607055421-9b20fcaf-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230607055421-9b20fcaf-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230607055421-9b20fcaf-nightly",
|
||||
"@blocksuite/icons": "^2.1.19",
|
||||
"@blocksuite/icons": "^2.1.21",
|
||||
"@blocksuite/lit": "0.0.0-20230607055421-9b20fcaf-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230607055421-9b20fcaf-nightly",
|
||||
"react": "18.3.0-canary-16d053d59-20230506",
|
||||
|
||||
@@ -47,7 +47,7 @@ test('Drag resizer can resize sidebar', async ({ page }) => {
|
||||
});
|
||||
await page.mouse.up();
|
||||
const boundingBox = await page.getByTestId('app-sidebar').boundingBox();
|
||||
expect(boundingBox?.width).toBe(400);
|
||||
expect(boundingBox?.width).toBe(399);
|
||||
});
|
||||
|
||||
test('Sidebar in between sm & md breakpoint', async ({ page }) => {
|
||||
|
||||
@@ -16,5 +16,5 @@ test('Open shortcuts modal', async ({ page }) => {
|
||||
await shortcutsIcon.click();
|
||||
await page.waitForTimeout(1000);
|
||||
const shortcutsModal = page.locator('[data-testid=shortcuts-modal]');
|
||||
await expect(shortcutsModal).toContainText('Keyboard shortcuts');
|
||||
await expect(shortcutsModal).toContainText('Page');
|
||||
});
|
||||
|
||||
14
yarn.lock
14
yarn.lock
@@ -69,7 +69,7 @@ __metadata:
|
||||
"@blocksuite/blocks": 0.0.0-20230607055421-9b20fcaf-nightly
|
||||
"@blocksuite/editor": 0.0.0-20230607055421-9b20fcaf-nightly
|
||||
"@blocksuite/global": 0.0.0-20230607055421-9b20fcaf-nightly
|
||||
"@blocksuite/icons": ^2.1.19
|
||||
"@blocksuite/icons": ^2.1.21
|
||||
"@blocksuite/lit": 0.0.0-20230607055421-9b20fcaf-nightly
|
||||
"@blocksuite/store": 0.0.0-20230607055421-9b20fcaf-nightly
|
||||
"@dnd-kit/core": ^6.0.8
|
||||
@@ -398,7 +398,7 @@ __metadata:
|
||||
"@blocksuite/blocks": 0.0.0-20230607055421-9b20fcaf-nightly
|
||||
"@blocksuite/editor": 0.0.0-20230607055421-9b20fcaf-nightly
|
||||
"@blocksuite/global": 0.0.0-20230607055421-9b20fcaf-nightly
|
||||
"@blocksuite/icons": ^2.1.19
|
||||
"@blocksuite/icons": ^2.1.21
|
||||
"@blocksuite/lit": 0.0.0-20230607055421-9b20fcaf-nightly
|
||||
"@blocksuite/store": 0.0.0-20230607055421-9b20fcaf-nightly
|
||||
"@storybook/addon-actions": ^7.0.21
|
||||
@@ -455,7 +455,7 @@ __metadata:
|
||||
"@blocksuite/blocks": 0.0.0-20230607055421-9b20fcaf-nightly
|
||||
"@blocksuite/editor": 0.0.0-20230607055421-9b20fcaf-nightly
|
||||
"@blocksuite/global": 0.0.0-20230607055421-9b20fcaf-nightly
|
||||
"@blocksuite/icons": ^2.1.19
|
||||
"@blocksuite/icons": ^2.1.21
|
||||
"@blocksuite/lit": 0.0.0-20230607055421-9b20fcaf-nightly
|
||||
"@blocksuite/store": 0.0.0-20230607055421-9b20fcaf-nightly
|
||||
"@dnd-kit/core": ^6.0.8
|
||||
@@ -3984,13 +3984,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@blocksuite/icons@npm:^2.1.19":
|
||||
version: 2.1.19
|
||||
resolution: "@blocksuite/icons@npm:2.1.19"
|
||||
"@blocksuite/icons@npm:^2.1.21":
|
||||
version: 2.1.21
|
||||
resolution: "@blocksuite/icons@npm:2.1.21"
|
||||
peerDependencies:
|
||||
"@types/react": ^18.0.25
|
||||
react: ^18.2.0
|
||||
checksum: 6b4abbac571324241a1ada723553f8e7fd2e91c0efd57fda68ea24ffc301e4ebb2691ba59744593109b153039dc94299b139f674fc656be48037d0e29454f71b
|
||||
checksum: ade86c53243691da1aae2bf2abca88b0d9594590a59cf30ec361cba8cb4268737e7129fc0a61ad87e610d709e3eb3d10c8fea3bb76beeeebb334dd14f1001ea1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user