refactor: new project struct (#8199)

packages/frontend/web -> packages/frontend/apps/web
packages/frontend/mobile -> packages/frontend/apps/mobile
packages/frontend/electron -> packages/frontend/apps/electron
This commit is contained in:
EYHN
2024-09-12 07:42:57 +00:00
parent 7c4eab6cd3
commit cc5a6e6d40
291 changed files with 139 additions and 134 deletions
@@ -0,0 +1,3 @@
# mobile views
Maintain complex views that used for `../pages`, view can contain mobile-components in `../components`
@@ -0,0 +1,19 @@
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const headerContent = style({
display: 'flex',
alignItems: 'center',
gap: 8,
fontSize: 17,
fontWeight: 600,
lineHeight: '22px',
letterSpacing: -0.43,
color: cssVarV2('text/primary'),
});
export const headerIcon = style({
fontSize: 24,
color: cssVarV2('icon/primary'),
});
@@ -0,0 +1,54 @@
import { IconButton, MobileMenu } from '@affine/component';
import { EmptyCollectionDetail } from '@affine/core/components/affine/empty';
import { isEmptyCollection } from '@affine/core/pages/workspace/collection';
import type { Collection } from '@affine/env/filter';
import { MoreHorizontalIcon, ViewLayersIcon } from '@blocksuite/icons/rc';
import { PageHeader } from '../../../components';
import { AllDocList } from '../doc/list';
import { AllDocsMenu } from '../doc/menu';
import * as styles from './detail.css';
export const DetailHeader = ({ collection }: { collection: Collection }) => {
return (
<PageHeader
back
suffix={
<MobileMenu items={<AllDocsMenu />}>
<IconButton
size="24"
style={{ padding: 10 }}
icon={<MoreHorizontalIcon />}
/>
</MobileMenu>
}
>
<div className={styles.headerContent}>
<ViewLayersIcon className={styles.headerIcon} />
{collection.name}
</div>
</PageHeader>
);
};
export const CollectionDetail = ({
collection,
}: {
collection: Collection;
}) => {
if (isEmptyCollection(collection)) {
return (
<>
<DetailHeader collection={collection} />
<EmptyCollectionDetail collection={collection} absoluteCenter />
</>
);
}
return (
<>
<DetailHeader collection={collection} />
<AllDocList collection={collection} />
</>
);
};
@@ -0,0 +1,12 @@
import type { Collection } from '@affine/env/filter';
import { DetailHeader } from './detail';
export const EmptyCollection = ({ collection }: { collection: Collection }) => {
return (
<>
<DetailHeader collection={collection} />
Empty
</>
);
};
@@ -0,0 +1,3 @@
export { CollectionDetail } from './detail';
export * from './empty';
export * from './list';
@@ -0,0 +1,38 @@
import { IconButton } from '@affine/component';
import type { CollectionMeta } from '@affine/core/components/page-list';
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties';
import { WorkbenchLink } from '@affine/core/modules/workbench';
import { ViewLayersIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra';
import { type MouseEvent, useCallback } from 'react';
import { item, name, prefixIcon, suffixIcon } from './styles.css';
export const CollectionListItem = ({ meta }: { meta: CollectionMeta }) => {
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
const isFavorite = useLiveData(favAdapter.isFavorite$(meta.id, 'collection'));
const toggle = useCallback(
(e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
favAdapter.toggle(meta.id, 'collection');
},
[favAdapter, meta.id]
);
return (
<WorkbenchLink to={`/collection/${meta.id}`} className={item}>
<ViewLayersIcon className={prefixIcon} />
<span className={name}>{meta.title}</span>
<IconButton
className={suffixIcon}
onClick={toggle}
icon={<IsFavoriteIcon favorite={isFavorite} />}
size="24"
/>
</WorkbenchLink>
);
};
@@ -0,0 +1,34 @@
import { EmptyCollections } from '@affine/core/components/affine/empty';
import type { CollectionMeta } from '@affine/core/components/page-list';
import { CollectionService } from '@affine/core/modules/collection';
import { useLiveData, useService } from '@toeverything/infra';
import { useMemo } from 'react';
import { CollectionListItem } from './item';
import { list } from './styles.css';
export const CollectionList = () => {
const collectionService = useService(CollectionService);
const collections = useLiveData(collectionService.collections$);
const collectionMetas = useMemo(
() =>
collections.map(
collection =>
({ ...collection, title: collection.name }) satisfies CollectionMeta
),
[collections]
);
if (!collectionMetas.length) {
return <EmptyCollections absoluteCenter />;
}
return (
<ul className={list}>
{collectionMetas.map(meta => (
<CollectionListItem key={meta.id} meta={meta} />
))}
</ul>
);
};
@@ -0,0 +1,45 @@
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const list = style({});
export const item = style({
display: 'flex',
alignItems: 'center',
gap: 8,
padding: 16,
color: 'unset',
':visited': { color: 'unset' },
':hover': { color: 'unset' },
':active': { color: 'unset' },
':focus': { color: 'unset' },
selectors: {
'&:not(:last-child)': {
borderBottom: `0.5px solid ${cssVarV2('layer/insideBorder/border')}`,
},
},
});
export const icon = style({
width: 24,
height: 24,
color: cssVarV2('icon/primary'),
flexShrink: 0,
});
export const prefixIcon = style([icon]);
export const name = style({
width: 0,
flex: 1,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
color: cssVarV2('text/primary'),
fontSize: 17,
fontWeight: 600,
lineHeight: '24px',
letterSpacing: -0.43,
});
export const suffixIcon = style([icon]);
@@ -0,0 +1,3 @@
export * from './list';
export * from './masonry';
export * from './menu';
@@ -0,0 +1,32 @@
import { cssVarV2 } from '@toeverything/theme/v2';
import { globalStyle, style } from '@vanilla-extract/css';
export const groupTitle = style({
display: 'flex',
alignItems: 'center',
padding: '0px 16px',
width: '100%',
});
// to override style defined in `core`
globalStyle(`${groupTitle} > div`, {
marginRight: -4,
});
globalStyle(`${groupTitle} div[data-testid^='group-label']`, {
fontSize: `20px !important`,
color: `${cssVarV2('text/primary')} !important`,
lineHeight: '25px !important',
});
export const groupTitleIcon = style({
color: cssVarV2('icon/tertiary'),
transition: 'transform 0.2s',
selectors: {
'[data-state="closed"] &': {
transform: `rotate(-90deg)`,
},
},
});
export const groups = style({
display: 'flex',
flexDirection: 'column',
gap: 32,
});
@@ -0,0 +1,93 @@
import { EmptyDocs } from '@affine/core/components/affine/empty';
import {
type ItemGroupDefinition,
type ItemGroupProps,
useAllDocDisplayProperties,
useFilteredPageMetas,
usePageItemGroupDefinitions,
} from '@affine/core/components/page-list';
import { itemsToItemGroups } from '@affine/core/components/page-list/items-to-item-group';
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import type { Tag } from '@affine/core/modules/tag';
import type { Collection, Filter } from '@affine/env/filter';
import { ToggleExpandIcon } from '@blocksuite/icons/rc';
import type { DocMeta } from '@blocksuite/store';
import * as Collapsible from '@radix-ui/react-collapsible';
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
import { useMemo } from 'react';
import * as styles from './list.css';
import { MasonryDocs } from './masonry';
const DocGroup = ({ group }: { group: ItemGroupProps<DocMeta> }) => {
const [properties] = useAllDocDisplayProperties();
const showTags = properties.displayProperties.tags;
if (group.id === 'all') {
return <MasonryDocs items={group.items} showTags={showTags} />;
}
return (
<Collapsible.Root defaultOpen>
<Collapsible.Trigger className={styles.groupTitle}>
{group.label}
<ToggleExpandIcon className={styles.groupTitleIcon} />
</Collapsible.Trigger>
<Collapsible.Content>
<MasonryDocs items={group.items} showTags={showTags} />
</Collapsible.Content>
</Collapsible.Root>
);
};
export interface AllDocListProps {
collection?: Collection;
tag?: Tag;
filters?: Filter[];
trash?: boolean;
}
export const AllDocList = ({
trash,
collection,
tag,
filters = [],
}: AllDocListProps) => {
const workspace = useService(WorkspaceService).workspace;
const allPageMetas = useBlockSuiteDocMeta(workspace.docCollection);
const tagPageIds = useLiveData(tag?.pageIds$);
const filteredPageMetas = useFilteredPageMetas(allPageMetas, {
trash,
filters,
collection,
});
const finalPageMetas = useMemo(() => {
if (tag) {
const pageIdsSet = new Set(tagPageIds);
return filteredPageMetas.filter(page => pageIdsSet.has(page.id));
}
return filteredPageMetas;
}, [filteredPageMetas, tag, tagPageIds]);
const groupDefs =
usePageItemGroupDefinitions() as ItemGroupDefinition<DocMeta>[];
const groups = useMemo(() => {
return itemsToItemGroups(finalPageMetas ?? [], groupDefs);
}, [finalPageMetas, groupDefs]);
if (!groups.length) {
return <EmptyDocs absoluteCenter tagId={tag?.id} />;
}
return (
<div className={styles.groups}>
{groups.map(group => (
<DocGroup key={group.id} group={group} />
))}
</div>
);
};
@@ -0,0 +1,28 @@
import { style } from '@vanilla-extract/css';
export const invisibleWrapper = style({
position: 'absolute',
padding: 'inherit',
width: '100%',
height: 0,
overflow: 'hidden',
visibility: 'hidden',
pointerEvents: 'none',
});
export const invisibleList = style({
width: `calc(50% - 17px / 2)`,
});
export const stacks = style({
position: 'relative',
width: '100%',
display: 'flex',
gap: 17,
padding: 16,
});
export const stack = style({
width: 0,
flex: 1,
display: 'flex',
flexDirection: 'column',
gap: 10,
});
@@ -0,0 +1,37 @@
import type { DocMeta } from '@blocksuite/store';
import { useMemo } from 'react';
import { DocCard } from '../../../components';
import * as styles from './masonry.css';
// TODO(@CatsJuice): Large amount docs performance
export const MasonryDocs = ({
items,
showTags,
}: {
items: DocMeta[];
showTags?: boolean;
}) => {
// card preview is loaded lazily, it's meaningless to calculate height
const stacks = useMemo(() => {
return items.reduce(
(acc, item, i) => {
acc[i % 2].push(item);
return acc;
},
[[], []] as [DocMeta[], DocMeta[]]
);
}, [items]);
return (
<div className={styles.stacks}>
{stacks.map((stack, i) => (
<ul key={i} className={styles.stack}>
{stack.map(item => (
<DocCard showTags={showTags} key={item.id} meta={item} />
))}
</ul>
))}
</div>
);
};
@@ -0,0 +1,67 @@
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const root = style({
display: 'flex',
flexDirection: 'column',
gap: 10,
});
export const head = style({
padding: '10px 20px',
fontSize: 17,
fontWeight: 600,
lineHeight: '22px',
letterSpacing: -0.43,
color: cssVarV2('text/primary'),
});
export const item = style({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: 4,
height: 34,
padding: '0 20px',
fontSize: 17,
lineHeight: '22px',
fontWeight: 400,
letterSpacing: -0.43,
});
export const itemSuffix = style({
display: 'flex',
gap: 8,
alignItems: 'center',
});
export const itemSuffixText = style({
color: cssVarV2('text/secondary'),
});
export const itemSuffixIcon = style({
color: cssVarV2('icon/primary'),
fontSize: 20,
});
export const divider = style({
width: '100%',
height: 16,
display: 'flex',
alignItems: 'center',
':before': {
content: "''",
height: 0.5,
width: '100%',
backgroundColor: cssVarV2('layer/insideBorder/border'),
},
});
export const propertiesList = style({
padding: '4px 20px',
display: 'flex',
gap: 8,
});
export const propertyButton = style({
opacity: 0.4,
selectors: {
'&[data-selected="true"]': {
opacity: 1,
},
},
});
@@ -0,0 +1,73 @@
import { Button, MobileMenu, MobileMenuItem } from '@affine/component';
import {
getGroupOptions,
useAllDocDisplayProperties,
} from '@affine/core/components/page-list';
import { useI18n } from '@affine/i18n';
import { ArrowRightSmallIcon } from '@blocksuite/icons/rc';
import { useMemo } from 'react';
import * as styles from './menu.css';
export const AllDocsMenu = () => {
const t = useI18n();
const [properties, setProperties] = useAllDocDisplayProperties();
const groupOptions = useMemo(() => getGroupOptions(t), [t]);
const activeGroup = useMemo(
() => groupOptions.find(g => g.value === properties.groupBy),
[groupOptions, properties.groupBy]
);
return (
<div className={styles.root}>
<header className={styles.head}>Display Settings</header>
<MobileMenu
items={
<>
<div className={styles.divider} />
{groupOptions.map(group => (
<MobileMenuItem
onSelect={() => setProperties('groupBy', group.value)}
selected={properties.groupBy === group.value}
key={group.value}
>
{group.label}
</MobileMenuItem>
))}
</>
}
>
<div className={styles.item}>
<span>{t['com.affine.page.display.grouping']()}</span>
<div className={styles.itemSuffix}>
<span className={styles.itemSuffixText}>{activeGroup?.label}</span>
<ArrowRightSmallIcon className={styles.itemSuffixIcon} />
</div>
</div>
</MobileMenu>
<div className={styles.divider} />
<div className={styles.item}>
{t['com.affine.page.display.display-properties']()}
</div>
<div className={styles.propertiesList}>
<Button
size="large"
className={styles.propertyButton}
data-selected={properties.displayProperties.tags}
onClick={() =>
setProperties('displayProperties', {
...properties.displayProperties,
tags: !properties.displayProperties.tags,
})
}
>
Tag
</Button>
</div>
</div>
);
};
@@ -0,0 +1,31 @@
import { IconButton, MobileMenu, SafeArea } from '@affine/component';
import { MoreHorizontalIcon } from '@blocksuite/icons/rc';
import { header, headerContent, headerSpace } from './style.css';
import { AllDocsTabs } from './tabs';
export interface AllDocsHeaderProps {
operations?: React.ReactNode;
}
export const AllDocsHeader = ({ operations }: AllDocsHeaderProps) => {
return (
<>
<SafeArea top className={header}>
<header className={headerContent}>
<AllDocsTabs />
<div>
{operations ? (
<MobileMenu items={operations}>
<IconButton icon={<MoreHorizontalIcon />} />
</MobileMenu>
) : null}
</div>
</header>
</SafeArea>
<SafeArea top>
<div className={headerSpace} />
</SafeArea>
</>
);
};
@@ -0,0 +1,4 @@
export * from './collection';
export * from './doc/';
export * from './header';
export * from './tag';
@@ -0,0 +1,43 @@
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
const basicHeader = style({
width: '100%',
height: 56,
});
export const header = style({
width: '100%',
position: 'fixed',
top: 0,
backgroundColor: cssVarV2('layer/background/secondary'),
zIndex: 1,
});
export const headerSpace = style([basicHeader]);
export const headerContent = style([
basicHeader,
{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 16,
padding: `0px 16px`,
},
]);
export const tabs = style({
height: 56,
gap: 16,
display: 'flex',
alignItems: 'center',
});
export const tab = style({
fontSize: 20,
fontWeight: 600,
lineHeight: '28px',
color: cssVarV2('text/tertiary'),
selectors: {
'&[data-active="true"]': {
color: cssVarV2('text/primary'),
},
},
});
@@ -0,0 +1,50 @@
import {
WorkbenchLink,
WorkbenchService,
} from '@affine/core/modules/workbench';
import { useLiveData, useService } from '@toeverything/infra';
import * as styles from './style.css';
interface Tab {
to: string;
label: string;
}
const tabs: Tab[] = [
{
to: '/all',
label: 'Docs',
},
{
to: '/collection',
label: 'Collections',
},
{
to: '/tag',
label: 'Tags',
},
];
export const AllDocsTabs = () => {
const workbench = useService(WorkbenchService).workbench;
const location = useLiveData(workbench.location$);
return (
<ul className={styles.tabs}>
{tabs.map(tab => {
return (
<WorkbenchLink
data-active={location.pathname === tab.to}
replaceHistory
className={styles.tab}
key={tab.to}
to={tab.to}
>
{tab.label}
</WorkbenchLink>
);
})}
</ul>
);
};
@@ -0,0 +1,32 @@
import { IconButton, MobileMenu } from '@affine/component';
import type { Tag } from '@affine/core/modules/tag';
import { MoreHorizontalIcon } from '@blocksuite/icons/rc';
import { useLiveData } from '@toeverything/infra';
import { PageHeader } from '../../../components';
import { AllDocsMenu } from '../doc';
import * as styles from './detail.css';
export const TagDetailHeader = ({ tag }: { tag: Tag }) => {
const name = useLiveData(tag.value$);
const color = useLiveData(tag.color$);
return (
<PageHeader
back
suffix={
<MobileMenu items={<AllDocsMenu />}>
<IconButton
size="24"
style={{ padding: 10 }}
icon={<MoreHorizontalIcon />}
/>
</MobileMenu>
}
>
<div className={styles.headerContent}>
<div className={styles.headerIcon} style={{ color }} />
{name}
</div>
</PageHeader>
);
};
@@ -0,0 +1,29 @@
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const headerContent = style({
fontSize: 17,
fontWeight: 600,
lineHeight: '22px',
letterSpacing: -0.43,
color: cssVarV2('text/primary'),
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
overflow: 'hidden',
});
export const headerIcon = style({
width: 24,
height: 24,
marginRight: 8,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
':before': {
content: '""',
width: 12,
height: 12,
borderRadius: 6,
backgroundColor: 'currentColor',
},
});
@@ -0,0 +1,15 @@
import type { Tag } from '@affine/core/modules/tag';
import { AppTabs } from '../../../components';
import { AllDocList } from '../doc';
import { TagDetailHeader } from './detail-header';
export const TagDetail = ({ tag }: { tag: Tag }) => {
return (
<>
<TagDetailHeader tag={tag} />
<AllDocList tag={tag} />
<AppTabs />
</>
);
};
@@ -0,0 +1,12 @@
import type { Tag } from '@affine/core/modules/tag';
import { TagDetailHeader } from './detail-header';
export const TagEmpty = ({ tag }: { tag: Tag }) => {
return (
<>
<TagDetailHeader tag={tag} />
Empty
</>
);
};
@@ -0,0 +1,3 @@
export { TagDetail } from './detail';
export * from './item';
export * from './list';
@@ -0,0 +1,38 @@
import { IconButton } from '@affine/component';
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties';
import type { Tag } from '@affine/core/modules/tag';
import { WorkbenchLink } from '@affine/core/modules/workbench';
import { useLiveData, useService } from '@toeverything/infra';
import { type MouseEvent, useCallback } from 'react';
import { content, item, prefixIcon, suffixIcon } from './styles.css';
export const TagItem = ({ tag }: { tag: Tag }) => {
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
const isFavorite = useLiveData(favAdapter.isFavorite$(tag.id, 'tag'));
const color = useLiveData(tag.color$);
const name = useLiveData(tag.value$);
const toggle = useCallback(
(e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
favAdapter.toggle(tag.id, 'tag');
},
[favAdapter, tag.id]
);
return (
<WorkbenchLink to={`/tag/${tag.id}`} className={item}>
<div className={prefixIcon} style={{ color }} />
<span className={content}>{name}</span>
<IconButton
className={suffixIcon}
onClick={toggle}
icon={<IsFavoriteIcon favorite={isFavorite} />}
size="24"
/>
</WorkbenchLink>
);
};
@@ -0,0 +1,23 @@
import { EmptyTags } from '@affine/core/components/affine/empty';
import { TagService } from '@affine/core/modules/tag';
import { useLiveData, useService } from '@toeverything/infra';
import { TagItem } from './item';
import { list } from './styles.css';
export const TagList = () => {
const tagList = useService(TagService).tagList;
const tags = useLiveData(tagList.tags$);
if (!tags.length) {
return <EmptyTags absoluteCenter />;
}
return (
<ul className={list}>
{tags.map(tag => (
<TagItem key={tag.id} tag={tag} />
))}
</ul>
);
};
@@ -0,0 +1,54 @@
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const list = style({});
export const item = style({
display: 'flex',
alignItems: 'center',
gap: 8,
padding: 16,
':visited': { color: 'unset' },
':hover': { color: 'unset' },
':active': { color: 'unset' },
':focus': { color: 'unset' },
selectors: {
'&:not(:last-child)': {
borderBottom: `0.5px solid ${cssVarV2('layer/insideBorder/border')}`,
},
},
});
export const content = style({
width: 0,
flex: 1,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
color: cssVarV2('text/primary'),
fontSize: 17,
fontWeight: 600,
lineHeight: '24px',
letterSpacing: -0.43,
});
export const prefixIcon = style({
width: 24,
height: 24,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
':before': {
content: '""',
width: 12,
height: 12,
borderRadius: 6,
backgroundColor: 'currentColor',
},
});
export const suffixIcon = style({
padding: 0,
fontSize: 24,
});
@@ -0,0 +1,73 @@
import {
IconButton,
SafeArea,
startScopedViewTransition,
} from '@affine/component';
import { openSettingModalAtom } from '@affine/core/atoms';
import { WorkbenchService } from '@affine/core/modules/workbench';
import { useI18n } from '@affine/i18n';
import { SettingsIcon } from '@blocksuite/icons/rc';
import { useService } from '@toeverything/infra';
import clsx from 'clsx';
import { useSetAtom } from 'jotai';
import { useCallback, useState } from 'react';
import { SearchInput, WorkspaceSelector } from '../../components';
import { searchVTScope } from '../../components/search-input/style.css';
import { useGlobalEvent } from '../../hooks/use-global-events';
import * as styles from './styles.css';
/**
* Contains `Setting`, `Workspace Selector`, `Search`
* When scrolled:
* - combine Setting and Workspace Selector
* - hide Search
*/
export const HomeHeader = () => {
const t = useI18n();
const workbench = useService(WorkbenchService).workbench;
const openSetting = useSetAtom(openSettingModalAtom);
const [dense, setDense] = useState(false);
useGlobalEvent(
'scroll',
useCallback(() => {
setDense(window.scrollY > 114);
}, [])
);
const navSearch = useCallback(() => {
startScopedViewTransition(searchVTScope, () => {
workbench.open('/search');
});
}, [workbench]);
return (
<div className={clsx(styles.root, { dense })}>
<SafeArea top className={styles.float}>
<div className={styles.headerAndWsSelector}>
<div className={styles.wsSelectorWrapper}>
<WorkspaceSelector />
</div>
<div className={styles.settingWrapper}>
<IconButton
onClick={() => {
openSetting({ open: true, activeTab: 'appearance' });
}}
size="24"
style={{ padding: 10 }}
icon={<SettingsIcon />}
/>
</div>
</div>
<div className={styles.searchWrapper}>
<SearchInput placeholder={t['Quick search']()} onClick={navSearch} />
</div>
</SafeArea>
<SafeArea top>
<div className={styles.space} />
</SafeArea>
</div>
);
};
@@ -0,0 +1,84 @@
import { cssVarV2 } from '@toeverything/theme/v2';
import { createVar, style } from '@vanilla-extract/css';
const headerHeight = createVar('headerHeight');
const wsSelectorHeight = createVar('wsSelectorHeight');
const searchHeight = createVar('searchHeight');
const searchPadding = [10, 16, 15, 16];
export const root = style({
vars: {
[headerHeight]: '44px',
[wsSelectorHeight]: '48px',
[searchHeight]: '44px',
},
width: '100vw',
});
export const float = style({
// why not 'sticky'?
// when height change, will affect scroll behavior, causing shaking
position: 'fixed',
top: 0,
width: '100%',
background: cssVarV2('layer/background/secondary'),
zIndex: 1,
});
export const space = style({
height: `calc(${headerHeight} + ${wsSelectorHeight} + ${searchHeight} + ${searchPadding[0] + searchPadding[2]}px + 12px)`,
});
export const headerAndWsSelector = style({
display: 'flex',
gap: 10,
alignItems: 'end',
transition: 'height 0.2s',
height: `calc(${headerHeight} + ${wsSelectorHeight})`,
selectors: {
[`${root}.dense &`]: {
height: wsSelectorHeight,
},
},
});
export const wsSelectorWrapper = style({
width: 0,
flex: 1,
height: wsSelectorHeight,
padding: '0 10px 0 16px',
display: 'flex',
alignItems: 'center',
});
export const settingWrapper = style({
width: '44px',
height: headerHeight,
transition: 'height 0.2s',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
alignSelf: 'start',
selectors: {
[`${root}.dense &`]: {
height: wsSelectorHeight,
},
},
});
export const searchWrapper = style({
padding: searchPadding.map(v => `${v}px`).join(' '),
width: '100%',
height: 44 + searchPadding[0] + searchPadding[2],
transition: 'all 0.2s',
overflow: 'hidden',
selectors: {
[`${root}.dense &`]: {
height: 0,
paddingTop: 0,
paddingBottom: 0,
},
},
});
@@ -0,0 +1,4 @@
export * from './all-docs';
export * from './home-header';
export * from './recent-docs';
export * from './settings';
@@ -0,0 +1,43 @@
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import { CollapsibleSection } from '@affine/core/modules/explorer';
import { useService, WorkspaceService } from '@toeverything/infra';
import { useMemo } from 'react';
import { DocCard } from '../../components/doc-card';
import * as styles from './styles.css';
export const RecentDocs = ({ max = 5 }: { max?: number }) => {
const workspace = useService(WorkspaceService).workspace;
const allPageMetas = useBlockSuiteDocMeta(workspace.docCollection);
const cardMetas = useMemo(() => {
return [...allPageMetas]
.filter(meta => !meta.trash)
.sort((a, b) => (b.updatedDate ?? 0) - (a.updatedDate ?? 0))
.slice(0, max);
}, [allPageMetas, max]);
if (!cardMetas.length) {
return null;
}
return (
<CollapsibleSection
name="recent"
title="Recent"
headerClassName={styles.header}
className={styles.recentSection}
testId="recent-docs"
>
<div className={styles.scroll} data-testid="recent-docs-list">
<ul className={styles.list}>
{cardMetas.map((doc, index) => (
<li key={index} className={styles.cardWrapper}>
<DocCard meta={doc} />
</li>
))}
</ul>
</div>
</CollapsibleSection>
);
};
@@ -0,0 +1,39 @@
import { globalStyle, style } from '@vanilla-extract/css';
export const recentSection = style({
paddingBottom: 32,
selectors: {
'&[data-state="open"]': {
paddingBottom: 0,
},
},
});
export const scroll = style({
width: '100%',
paddingTop: 8,
paddingBottom: 32,
overflowX: 'auto',
});
export const list = style({
paddingLeft: 16,
paddingRight: 16,
display: 'flex',
gap: 10,
width: 'fit-content',
});
export const cardWrapper = style({
width: 172,
height: 210,
flexShrink: 0,
});
export const header = style({
margin: '0 8px',
});
globalStyle(`${cardWrapper} > *`, {
width: '100%',
height: '100%',
});
@@ -0,0 +1,87 @@
import { useI18n } from '@affine/i18n';
import { DocCard, type DocCardProps } from '../../components';
import {
UniversalSearchResultItem,
type UniversalSearchResultItemProps,
} from '../../components/search-result/universal-item';
import * as styles from './style.css';
export interface SearchResultsProps {
title: string;
docs?: DocCardProps['meta'][];
collections?: UniversalSearchResultItemProps['item'][];
tags?: UniversalSearchResultItemProps['item'][];
}
const Empty = () => {
const t = useI18n();
return (
<div className={styles.empty}>{t['com.affine.mobile.search.empty']()}</div>
);
};
export const SearchResults = ({
title,
docs,
collections,
tags,
}: SearchResultsProps) => {
return (
<>
<div className={styles.resTitle}>{title}</div>
{!docs?.length && !collections?.length && !tags?.length ? (
<Empty />
) : null}
{/* Doc Res */}
{docs?.length ? (
<div className={styles.resBlock} data-scroll>
<div className={styles.resBlockTitle}>Docs</div>
<div className={styles.resBlockScrollContent}>
<div className={styles.scrollDocsContent}>
{docs.map(doc => (
<DocCard meta={doc} key={doc.id} className={styles.docCard} />
))}
</div>
</div>
</div>
) : null}
{/* Collection Res */}
{collections?.length ? (
<div className={styles.resBlock}>
<div className={styles.resBlockTitle}>Collections</div>
<div className={styles.resBlockListContent}>
{collections.map(collection => (
<UniversalSearchResultItem
category="collection"
id={collection.payload.collectionId}
key={collection.id}
item={collection}
/>
))}
</div>
</div>
) : null}
{/* Tag Res */}
{tags?.length ? (
<div className={styles.resBlock}>
<div className={styles.resBlockTitle}>Tags</div>
<div className={styles.resBlockListContent}>
{tags.map(tag => (
<UniversalSearchResultItem
category="tag"
id={tag.payload.tagId}
key={tag.id}
item={tag}
/>
))}
</div>
</div>
) : null}
</>
);
};
@@ -0,0 +1,74 @@
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const searchHeader = style({
padding: 16,
});
export const resTitle = style({
padding: '6px 16px',
marginBottom: 8,
height: 30,
fontSize: 13,
lineHeight: '18px',
fontWeight: 400,
letterSpacing: -0.08,
color: cssVarV2('text/secondary'),
});
export const resBlock = style({
paddingBottom: 32,
selectors: {
'&[data-scroll]': {
paddingBottom: 0,
},
},
});
export const resBlockTitle = style({
padding: '0 16px',
fontSize: 20,
lineHeight: '25px',
fontWeight: 400,
letterSpacing: -0.45,
color: cssVarV2('text/primary'),
});
const resBlockContent = style({
padding: '12px 0px',
});
export const resBlockListContent = style([
resBlockContent,
{
paddingLeft: 16,
paddingRight: 16,
},
]);
export const resBlockScrollContent = style([
resBlockContent,
{
width: '100%',
overflowX: 'auto',
paddingBottom: 32,
},
]);
export const scrollDocsContent = style({
display: 'flex',
gap: 12,
padding: '0 16px',
width: 'fit-content',
});
export const docCard = style({
width: 170,
height: 210,
flexShrink: 0,
});
export const empty = style({
padding: '0 16px',
fontSize: 20,
fontWeight: 400,
lineHeight: '25px',
letterSpacing: -0.45,
color: cssVarV2('text/primary'),
});
@@ -0,0 +1,22 @@
import { useI18n } from '@affine/i18n';
import { SettingGroup } from '../group';
import { RowLayout } from '../row.layout';
const { appVersion, editorVersion } = runtimeConfig;
export const AboutGroup = () => {
const t = useI18n();
return (
<SettingGroup title={t['com.affine.mobile.setting.about.title']()}>
<RowLayout label={t['com.affine.mobile.setting.about.appVersion']()}>
{appVersion}
</RowLayout>
<RowLayout label={t['com.affine.mobile.setting.about.editorVersion']()}>
{editorVersion}
</RowLayout>
</SettingGroup>
);
};
@@ -0,0 +1,37 @@
import { getBaseFontStyleOptions } from '@affine/core/components/affine/setting-modal/general-setting/editor/general';
import {
EditorSettingService,
type FontFamily,
} from '@affine/core/modules/editor-settting';
import { useI18n } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra';
import { useCallback, useMemo } from 'react';
import { SettingDropdownSelect } from '../dropdown-select';
import { RowLayout } from '../row.layout';
export const FontStyleSetting = () => {
const t = useI18n();
const editorSetting = useService(EditorSettingService).editorSetting;
const fontFamily = useLiveData(
editorSetting.settings$.selector(s => s.fontFamily)
);
const options = useMemo(() => getBaseFontStyleOptions(t), [t]);
const handleEdit = useCallback(
(v: FontFamily) => {
editorSetting.set('fontFamily', v);
},
[editorSetting]
);
return (
<RowLayout label={t['com.affine.mobile.setting.appearance.font']()}>
<SettingDropdownSelect<FontFamily>
options={options}
value={fontFamily}
onChange={handleEdit}
/>
</RowLayout>
);
};
@@ -0,0 +1,17 @@
import { useI18n } from '@affine/i18n';
import { SettingGroup } from '../group';
import { FontStyleSetting } from './font';
import { LanguageSetting } from './language';
import { ThemeSetting } from './theme';
export const AppearanceGroup = () => {
const t = useI18n();
return (
<SettingGroup title={t['com.affine.mobile.setting.appearance.title']()}>
<ThemeSetting />
<FontStyleSetting />
<LanguageSetting />
</SettingGroup>
);
};
@@ -0,0 +1,39 @@
import { useLanguageHelper } from '@affine/core/hooks/affine/use-language-helper';
import { useI18n } from '@affine/i18n';
import { useMemo } from 'react';
import { SettingDropdownSelect } from '../dropdown-select';
import { RowLayout } from '../row.layout';
export const LanguageSetting = () => {
const t = useI18n();
const { currentLanguage, languagesList, onLanguageChange } =
useLanguageHelper();
const languageOptions = useMemo(
() =>
languagesList.map(language => ({
label: language.originalName,
value: language.tag,
})),
[languagesList]
);
return (
<RowLayout label={t['com.affine.mobile.setting.appearance.language']()}>
<SettingDropdownSelect
options={languageOptions}
value={currentLanguage?.tag}
onChange={onLanguageChange}
menuOptions={{
contentOptions: {
style: {
maxHeight: '60dvh',
overflowY: 'auto',
},
},
}}
/>
</RowLayout>
);
};
@@ -0,0 +1,24 @@
import { getThemeOptions } from '@affine/core/components/affine/setting-modal/general-setting/appearance';
import { useI18n } from '@affine/i18n';
import { useTheme } from 'next-themes';
import { useMemo } from 'react';
import { SettingDropdownSelect } from '../dropdown-select';
import { RowLayout } from '../row.layout';
export const ThemeSetting = () => {
const t = useI18n();
const options = useMemo(() => getThemeOptions(t), [t]);
const { setTheme, theme } = useTheme();
return (
<RowLayout label={t['com.affine.mobile.setting.appearance.theme']()}>
<SettingDropdownSelect
options={options}
value={theme}
onChange={setTheme}
/>
</RowLayout>
);
};
@@ -0,0 +1,19 @@
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const root = style({
display: 'flex',
alignItems: 'center',
gap: 8,
});
export const label = style({
fontSize: 17,
lineHeight: '22px',
fontWeight: 400,
letterSpacing: -0.43,
color: cssVarV2('text/placeholder'),
});
export const icon = style({
fontSize: 24,
color: cssVarV2('icon/primary'),
});
@@ -0,0 +1,74 @@
import { type MenuProps, MobileMenu, MobileMenuItem } from '@affine/component';
import { ArrowDownSmallIcon } from '@blocksuite/icons/rc';
import clsx from 'clsx';
import {
type CSSProperties,
type HTMLAttributes,
type ReactNode,
useMemo,
} from 'react';
import * as styles from './dropdown-select.css';
interface DropdownItem<V extends string> {
label?: ReactNode;
value: V;
testId?: string;
style?: CSSProperties;
[key: string]: any;
}
export interface SettingDropdownSelectProps<
V extends string,
E extends boolean | undefined,
> extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
options?: Array<DropdownItem<V>>;
value?: V;
onChange?: (
v: E extends true ? DropdownItem<V>['value'] : DropdownItem<V>
) => void;
emitValue?: E;
menuOptions?: Omit<MenuProps, 'items' | 'children'>;
}
export const SettingDropdownSelect = <
V extends string = string,
E extends boolean | undefined = true,
>({
options = [],
value,
emitValue = true,
onChange,
className,
menuOptions,
...attrs
}: SettingDropdownSelectProps<V, E>) => {
const selectedItem = useMemo(
() => options.find(opt => opt.value === value),
[options, value]
);
return (
<MobileMenu
items={options.map(opt => (
<MobileMenuItem
divide
key={opt.value}
selected={value === opt.value}
data-testid={opt.testId}
onSelect={() =>
emitValue ? onChange?.(opt.value as any) : onChange?.(opt as any)
}
style={opt.style}
>
{opt.label}
</MobileMenuItem>
))}
{...menuOptions}
>
<div className={clsx(styles.root, className)} {...attrs}>
<span className={styles.label}>{selectedItem?.label ?? ''}</span>
<ArrowDownSmallIcon className={styles.icon} />
</div>
</MobileMenu>
);
};
@@ -0,0 +1,27 @@
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const group = style({
display: 'flex',
flexDirection: 'column',
gap: 4,
width: '100%',
});
export const title = style({
padding: '0px 8px',
color: cssVarV2('text/tertiary'),
fontSize: 13,
lineHeight: '18px',
letterSpacing: -0.08,
fontWeight: 400,
});
export const content = style({
background: cssVarV2('layer/background/primary'),
borderRadius: 12,
padding: '10px 16px',
display: 'flex',
flexDirection: 'column',
gap: 8,
});
@@ -0,0 +1,35 @@
import clsx from 'clsx';
import {
type CSSProperties,
forwardRef,
type HTMLProps,
type ReactNode,
} from 'react';
import * as styles from './group.css';
export interface SettingGroupProps
extends Omit<HTMLProps<HTMLDivElement>, 'title'> {
title?: ReactNode;
contentClassName?: string;
contentStyle?: CSSProperties;
}
export const SettingGroup = forwardRef<HTMLDivElement, SettingGroupProps>(
function SettingGroup(
{ children, title, className, contentClassName, contentStyle, ...attrs },
ref
) {
return (
<div className={clsx(styles.group, className)} ref={ref} {...attrs}>
{title ? <h6 className={styles.title}>{title}</h6> : null}
<div
className={clsx(styles.content, contentClassName)}
style={contentStyle}
>
{children}
</div>
</div>
);
}
);
@@ -0,0 +1,70 @@
import { Modal } from '@affine/component';
import { openSettingModalAtom } from '@affine/core/atoms';
import { AuthService } from '@affine/core/modules/cloud';
import { useI18n } from '@affine/i18n';
import { useService } from '@toeverything/infra';
import { cssVarV2 } from '@toeverything/theme/v2';
import { useAtom } from 'jotai';
import { useCallback, useEffect } from 'react';
import { PageHeader } from '../../components';
import { AboutGroup } from './about';
import { AppearanceGroup } from './appearance';
import { OthersGroup } from './others';
import * as styles from './style.css';
import { UserProfile } from './user-profile';
import { UserUsage } from './user-usage';
export const MobileSettingModal = () => {
const [{ open }, setOpen] = useAtom(openSettingModalAtom);
const onOpenChange = useCallback(
(open: boolean) => setOpen(prev => ({ ...prev, open })),
[setOpen]
);
const closeModal = useCallback(() => onOpenChange(false), [onOpenChange]);
return (
<Modal
fullScreen
animation="slideBottom"
open={open}
onOpenChange={onOpenChange}
contentOptions={{
style: {
padding: 0,
overflowY: 'auto',
backgroundColor: cssVarV2('layer/background/secondary'),
},
}}
withoutCloseButton
>
<MobileSetting onClose={closeModal} />
</Modal>
);
};
const MobileSetting = ({ onClose }: { onClose: () => void }) => {
const t = useI18n();
const session = useService(AuthService).session;
useEffect(() => session.revalidate(), [session]);
return (
<>
<PageHeader back backAction={onClose}>
<span className={styles.pageTitle}>
{t['com.affine.mobile.setting.header-title']()}
</span>
</PageHeader>
<div className={styles.root}>
<UserProfile />
<UserUsage />
<AppearanceGroup />
<AboutGroup />
<OthersGroup />
</div>
</>
);
};
@@ -0,0 +1,32 @@
import { useI18n } from '@affine/i18n';
import { SettingGroup } from '../group';
import { RowLayout } from '../row.layout';
export const OthersGroup = () => {
const t = useI18n();
return (
<SettingGroup title={t['com.affine.mobile.setting.others.title']()}>
<RowLayout
label={t['com.affine.mobile.setting.others.github']()}
href="https://github.com/toeverything/AFFiNE"
/>
<RowLayout
label={t['com.affine.mobile.setting.others.website']()}
href="https://affine.pro/"
/>
<RowLayout
label={t['com.affine.mobile.setting.others.privacy']()}
href="https://affine.pro/privacy"
/>
<RowLayout
label={t['com.affine.mobile.setting.others.terms']()}
href="https://affine.pro/terms"
/>
</SettingGroup>
);
};
@@ -0,0 +1,28 @@
import { DualLinkIcon } from '@blocksuite/icons/rc';
import type { PropsWithChildren, ReactNode } from 'react';
import * as styles from './style.css';
export const RowLayout = ({
label,
children,
href,
}: PropsWithChildren<{ label: ReactNode; href?: string }>) => {
const content = (
<div className={styles.baseSettingItem}>
<div className={styles.baseSettingItemName}>{label}</div>
<div className={styles.baseSettingItemAction}>
{children ||
(href ? <DualLinkIcon className={styles.linkIcon} /> : null)}
</div>
</div>
);
return href ? (
<a target="_blank" href={href} rel="noreferrer">
{content}
</a>
) : (
content
);
};
@@ -0,0 +1,49 @@
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const pageTitle = style({
fontSize: 17,
lineHeight: '22px',
fontWeight: 600,
letterSpacing: -0.43,
});
export const root = style({
padding: '24px 16px',
display: 'flex',
flexDirection: 'column',
gap: 16,
});
export const baseSettingItem = style({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: 32,
padding: '8px 0',
});
export const baseSettingItemName = style({
fontSize: 17,
lineHeight: '22px',
letterSpacing: -0.43,
fontWeight: 400,
color: cssVarV2('text/primary'),
flexShrink: 0,
whiteSpace: 'nowrap',
});
export const baseSettingItemAction = style([
baseSettingItemName,
{
color: cssVarV2('text/placeholder'),
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
overflow: 'hidden',
flexShrink: 1,
},
]);
export const linkIcon = style({
fontSize: 24,
color: cssVarV2('icon/primary'),
});
@@ -0,0 +1,92 @@
import { Avatar } from '@affine/component';
import { authAtom } from '@affine/core/atoms';
import { useSignOut } from '@affine/core/hooks/affine/use-sign-out';
import { AuthService } from '@affine/core/modules/cloud';
import { ArrowRightSmallIcon } from '@blocksuite/icons/rc';
import {
useEnsureLiveData,
useLiveData,
useService,
} from '@toeverything/infra';
import { useSetAtom } from 'jotai';
import { type ReactNode } from 'react';
import { UserPlanTag } from '../../../components';
import { SettingGroup } from '../group';
import * as styles from './style.css';
export const UserProfile = () => {
const session = useService(AuthService).session;
const loginStatus = useLiveData(session.status$);
return loginStatus === 'authenticated' ? (
<AuthorizedUserProfile />
) : (
<UnauthorizedUserProfile />
);
};
const BaseLayout = ({
avatar,
title,
caption,
onClick,
}: {
avatar: ReactNode;
title: ReactNode;
caption: ReactNode;
onClick?: () => void;
}) => {
return (
<SettingGroup contentStyle={{ padding: '10px 8px 10px 10px' }}>
<div className={styles.profile} onClick={onClick}>
<div className={styles.avatarWrapper}>{avatar}</div>
<div className={styles.content}>
<div className={styles.title}>{title}</div>
<div className={styles.caption}>{caption}</div>
</div>
<ArrowRightSmallIcon className={styles.suffixIcon} />
</div>
</SettingGroup>
);
};
const AuthorizedUserProfile = () => {
const session = useService(AuthService).session;
const account = useEnsureLiveData(session.account$);
const confirmSignOut = useSignOut();
return (
<BaseLayout
avatar={
<Avatar
size={48}
rounded={4}
url={account.avatar}
name={account.label}
/>
}
caption={<span className={styles.emailInfo}>{account.email}</span>}
title={
<div className={styles.nameWithTag}>
<span className={styles.name}>{account.label}</span>
<UserPlanTag />
</div>
}
onClick={confirmSignOut}
/>
);
};
const UnauthorizedUserProfile = () => {
const setAuthModal = useSetAtom(authAtom);
return (
<BaseLayout
onClick={() => setAuthModal({ openModal: true, state: 'signIn' })}
avatar={<Avatar size={48} rounded={4} />}
title="Sign up / Sign in"
caption="Sync with AFFiNE Cloud"
/>
);
};
@@ -0,0 +1,58 @@
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const profile = style({
display: 'flex',
alignItems: 'center',
gap: 13,
});
export const avatarWrapper = style({
width: 48,
height: 48,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
});
export const content = style({
width: 0,
flex: 1,
display: 'flex',
flexDirection: 'column',
gap: 6,
});
const ellipsis = style({
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
});
export const title = style({
fontSize: 17,
lineHeight: '22px',
fontWeight: 400,
letterSpacing: -0.43,
color: cssVarV2('text/primary'),
});
export const caption = style({
fontSize: 15,
lineHeight: '20px',
fontWeight: 400,
letterSpacing: -0.23,
color: cssVarV2('text/secondary'),
});
export const suffixIcon = style({
fontSize: 30,
color: cssVarV2('icon/primary'),
});
export const emailInfo = style([ellipsis, { width: '100%' }]);
export const nameWithTag = style({
display: 'flex',
gap: 8,
});
export const name = style([ellipsis]);
@@ -0,0 +1,138 @@
import {
AuthService,
ServerConfigService,
UserCopilotQuotaService,
UserQuotaService,
} from '@affine/core/modules/cloud';
import { useLiveData, useService } from '@toeverything/infra';
import { cssVar } from '@toeverything/theme';
import { cssVarV2 } from '@toeverything/theme/v2';
import { useEffect } from 'react';
import { SettingGroup } from '../group';
import * as styles from './style.css';
export const UserUsage = () => {
const session = useService(AuthService).session;
const loginStatus = useLiveData(session.status$);
if (loginStatus !== 'authenticated') {
return null;
}
return <UsagePanel />;
};
const Progress = ({
name,
percent,
desc,
color,
}: {
name: string;
percent: number;
desc: string;
color: string | null;
}) => {
return (
<div className={styles.progressRoot}>
<div className={styles.progressInfoRow}>
<span className={styles.progressName}>{name}</span>
<span className={styles.progressDesc}>{desc}</span>
</div>
<div className={styles.progressTrack}>
<div
className={styles.progressBar}
style={{
width: `${percent}%`,
backgroundColor: color ?? cssVarV2('button/primary'),
}}
/>
</div>
</div>
);
};
const UsagePanel = () => {
const serverConfigService = useService(ServerConfigService);
const serverFeatures = useLiveData(
serverConfigService.serverConfig.features$
);
return (
<SettingGroup title="Storage">
<CloudUsage />
{serverFeatures?.copilot ? <AiUsage /> : null}
</SettingGroup>
);
};
const CloudUsage = () => {
const quota = useService(UserQuotaService).quota;
const color = useLiveData(quota.color$);
const usedFormatted = useLiveData(quota.usedFormatted$);
const maxFormatted = useLiveData(quota.maxFormatted$);
const percent = useLiveData(quota.percent$);
useEffect(() => {
// revalidate quota to get the latest status
quota.revalidate();
}, [quota]);
const loading = percent === null;
if (loading) return null;
return (
<Progress
name="Cloud"
percent={percent}
desc={`${usedFormatted}/${maxFormatted}`}
color={color}
/>
);
};
const AiUsage = () => {
const copilotQuotaService = useService(UserCopilotQuotaService);
const copilotActionLimit = useLiveData(
copilotQuotaService.copilotQuota.copilotActionLimit$
);
const copilotActionUsed = useLiveData(
copilotQuotaService.copilotQuota.copilotActionUsed$
);
const loading = copilotActionLimit === null || copilotActionUsed === null;
const loadError = useLiveData(copilotQuotaService.copilotQuota.error$);
useEffect(() => {
copilotQuotaService.copilotQuota.revalidate();
}, [copilotQuotaService]);
if (loading || loadError) {
return null;
}
if (copilotActionLimit === 'unlimited') {
return null;
}
const percent = Math.min(
100,
Math.max(
0.5,
Number(((copilotActionUsed / copilotActionLimit) * 100).toFixed(4))
)
);
const color = percent > 80 ? cssVar('errorColor') : cssVar('processingColor');
return (
<Progress
name="AI"
percent={percent}
desc={`${copilotActionUsed}/${copilotActionLimit}`}
color={color}
/>
);
};
@@ -0,0 +1,35 @@
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const progressRoot = style({
paddingBottom: 8,
});
export const progressInfoRow = style({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
paddingBottom: 4,
});
export const progressName = style({
fontSize: 17,
lineHeight: '22px',
letterSpacing: -0.43,
color: cssVarV2('text/primary'),
});
export const progressDesc = style({
fontSize: 12,
lineHeight: '16px',
color: cssVarV2('text/secondary'),
});
export const progressTrack = style({
width: '100%',
height: 10,
borderRadius: 5,
backgroundColor: cssVarV2('layer/background/hoverOverlay'),
overflow: 'hidden',
});
export const progressBar = style({
height: 'inherit',
borderTopRightRadius: 5,
borderBottomRightRadius: 5,
});
@@ -0,0 +1,114 @@
<svg viewBox="0 0 393 268" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_dddd_5_8)">
<path
d="M303.627 108.436C304.395 114.603 305.588 123.539 310.979 133.694C317.042 145.118 325.33 152.128 330.252 155.717C336.082 153.95 346.218 150.065 355.707 141.278C364.144 133.467 368.217 125.424 370.974 119.854C376.695 108.297 378.944 97.1588 379.689 88.2076C374.962 87.098 368.639 85.0971 361.818 81.322C354.081 77.0403 348.645 72.0796 345.072 68.3164C340.457 70.6912 333.69 73.5828 324.975 75.0751C317.286 76.39 310.661 76.1956 305.833 75.6854C303.582 84.3809 302.038 95.6392 303.629 108.436L303.627 108.436Z"
fill="#252525" />
</g>
<g filter="url(#filter1_ddddd_5_8)">
<path
d="M-20.3209 42.0156C-22.7192 34.8601 -18.8627 27.1151 -11.7072 24.7168L50.2776 3.94111C57.4331 1.54277 65.1781 5.39922 67.5764 12.5547L77.1078 40.992L-10.7894 70.4528L-20.3209 42.0156Z"
fill="#252525" />
<path
d="M29.6583 102.94L32.3749 96.6289L31.2069 85.3674L10.9017 92.1732C3.74617 94.5715 -3.99875 90.7151 -6.39709 83.5596L-9.07716 75.5635L78.8201 46.1027L81.5002 54.0987C83.8985 61.2542 80.042 68.9992 72.8865 71.3975L52.5828 78.2028L58.4375 87.8934L64.4084 91.2923L29.6583 102.94Z"
fill="#252525" />
</g>
<g filter="url(#filter2_dddd_5_8)">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M119.605 155.688C119.605 139.292 132.995 126 149.514 126C162.899 126 174.225 134.725 178.044 146.756C193.382 148.028 205.431 160.787 205.431 176.34C205.431 192.736 192.04 206.028 175.522 206.028H128.707C115.062 206.028 104 195.048 104 181.503C104 171.091 110.534 162.201 119.751 158.641C119.654 157.669 119.605 156.683 119.605 155.688Z"
fill="#252525" />
</g>
<defs>
<filter id="filter0_dddd_5_8" x="297.976" y="64.3164" width="129.712" height="152.401"
filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dx="2" dy="3" />
<feGaussianBlur stdDeviation="3.5" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_5_8" />
<feColorMatrix in="SourceAlpha" type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dx="7" dy="10" />
<feGaussianBlur stdDeviation="6" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.09 0" />
<feBlend mode="normal" in2="effect1_dropShadow_5_8" result="effect2_dropShadow_5_8" />
<feColorMatrix in="SourceAlpha" type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dx="15" dy="23" />
<feGaussianBlur stdDeviation="8.5" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" />
<feBlend mode="normal" in2="effect2_dropShadow_5_8" result="effect3_dropShadow_5_8" />
<feColorMatrix in="SourceAlpha" type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dx="28" dy="41" />
<feGaussianBlur stdDeviation="10" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.01 0" />
<feBlend mode="normal" in2="effect3_dropShadow_5_8" result="effect4_dropShadow_5_8" />
<feBlend mode="normal" in="SourceGraphic" in2="effect4_dropShadow_5_8" result="shape" />
</filter>
<filter id="filter1_ddddd_5_8" x="-24.5953" y="0.175569" width="147.011" height="147.548"
filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_5_8" />
<feColorMatrix in="SourceAlpha" type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dx="1.52672" dy="2.03562" />
<feGaussianBlur stdDeviation="2.54453" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" />
<feBlend mode="normal" in2="effect1_dropShadow_5_8" result="effect2_dropShadow_5_8" />
<feColorMatrix in="SourceAlpha" type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dx="6.10687" dy="7.12468" />
<feGaussianBlur stdDeviation="4.83461" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.09 0" />
<feBlend mode="normal" in2="effect2_dropShadow_5_8" result="effect3_dropShadow_5_8" />
<feColorMatrix in="SourceAlpha" type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dx="13.7405" dy="16.7939" />
<feGaussianBlur stdDeviation="6.61578" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" />
<feBlend mode="normal" in2="effect3_dropShadow_5_8" result="effect4_dropShadow_5_8" />
<feColorMatrix in="SourceAlpha" type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dx="24.9364" dy="29.5165" />
<feGaussianBlur stdDeviation="7.63359" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.01 0" />
<feBlend mode="normal" in2="effect4_dropShadow_5_8" result="effect5_dropShadow_5_8" />
<feBlend mode="normal" in="SourceGraphic" in2="effect5_dropShadow_5_8" result="shape" />
</filter>
<filter id="filter2_dddd_5_8" x="99" y="122" width="154.43" height="145.028"
filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dx="2" dy="3" />
<feGaussianBlur stdDeviation="3.5" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_5_8" />
<feColorMatrix in="SourceAlpha" type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dx="7" dy="10" />
<feGaussianBlur stdDeviation="6" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.09 0" />
<feBlend mode="normal" in2="effect1_dropShadow_5_8" result="effect2_dropShadow_5_8" />
<feColorMatrix in="SourceAlpha" type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dx="15" dy="23" />
<feGaussianBlur stdDeviation="8.5" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" />
<feBlend mode="normal" in2="effect2_dropShadow_5_8" result="effect3_dropShadow_5_8" />
<feColorMatrix in="SourceAlpha" type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dx="28" dy="41" />
<feGaussianBlur stdDeviation="10" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.01 0" />
<feBlend mode="normal" in2="effect3_dropShadow_5_8" result="effect4_dropShadow_5_8" />
<feBlend mode="normal" in="SourceGraphic" in2="effect4_dropShadow_5_8" result="shape" />
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 7.7 KiB

@@ -0,0 +1,114 @@
<svg viewBox="0 0 393 268" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_dddd_5_8)">
<path
d="M303.627 108.436C304.395 114.603 305.588 123.539 310.979 133.694C317.042 145.118 325.33 152.128 330.252 155.717C336.082 153.95 346.218 150.065 355.707 141.278C364.144 133.467 368.217 125.424 370.974 119.854C376.695 108.297 378.944 97.1588 379.689 88.2076C374.962 87.098 368.639 85.0971 361.818 81.322C354.081 77.0403 348.645 72.0796 345.072 68.3164C340.457 70.6912 333.69 73.5828 324.975 75.0751C317.286 76.39 310.661 76.1956 305.833 75.6854C303.582 84.3809 302.038 95.6392 303.629 108.436L303.627 108.436Z"
fill="white" />
</g>
<g filter="url(#filter1_ddddd_5_8)">
<path
d="M-20.3209 42.0156C-22.7192 34.8601 -18.8628 27.1151 -11.7072 24.7168L50.2776 3.94111C57.4331 1.54277 65.178 5.39922 67.5764 12.5547L77.1078 40.992L-10.7894 70.4528L-20.3209 42.0156Z"
fill="white" />
<path
d="M29.6583 102.94L32.3749 96.6289L31.2069 85.3674L10.9017 92.1732C3.74617 94.5715 -3.99876 90.7151 -6.3971 83.5596L-9.07716 75.5635L78.8201 46.1027L81.5001 54.0987C83.8985 61.2542 80.042 68.9992 72.8865 71.3975L52.5828 78.2028L58.4374 87.8934L64.4084 91.2923L29.6583 102.94Z"
fill="white" />
</g>
<g filter="url(#filter2_dddd_5_8)">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M119.605 155.688C119.605 139.292 132.995 126 149.514 126C162.899 126 174.225 134.725 178.044 146.756C193.382 148.028 205.431 160.787 205.431 176.34C205.431 192.736 192.04 206.028 175.522 206.028H128.707C115.062 206.028 104 195.048 104 181.503C104 171.091 110.534 162.201 119.751 158.641C119.654 157.669 119.605 156.683 119.605 155.688Z"
fill="white" />
</g>
<defs>
<filter id="filter0_dddd_5_8" x="297.976" y="64.3164" width="129.712" height="152.401"
filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dx="2" dy="3" />
<feGaussianBlur stdDeviation="3.5" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_5_8" />
<feColorMatrix in="SourceAlpha" type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dx="7" dy="10" />
<feGaussianBlur stdDeviation="6" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.09 0" />
<feBlend mode="normal" in2="effect1_dropShadow_5_8" result="effect2_dropShadow_5_8" />
<feColorMatrix in="SourceAlpha" type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dx="15" dy="23" />
<feGaussianBlur stdDeviation="8.5" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" />
<feBlend mode="normal" in2="effect2_dropShadow_5_8" result="effect3_dropShadow_5_8" />
<feColorMatrix in="SourceAlpha" type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dx="28" dy="41" />
<feGaussianBlur stdDeviation="10" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.01 0" />
<feBlend mode="normal" in2="effect3_dropShadow_5_8" result="effect4_dropShadow_5_8" />
<feBlend mode="normal" in="SourceGraphic" in2="effect4_dropShadow_5_8" result="shape" />
</filter>
<filter id="filter1_ddddd_5_8" x="-24.5953" y="0.175569" width="147.011" height="147.548"
filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_5_8" />
<feColorMatrix in="SourceAlpha" type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dx="1.52672" dy="2.03562" />
<feGaussianBlur stdDeviation="2.54453" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" />
<feBlend mode="normal" in2="effect1_dropShadow_5_8" result="effect2_dropShadow_5_8" />
<feColorMatrix in="SourceAlpha" type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dx="6.10687" dy="7.12468" />
<feGaussianBlur stdDeviation="4.83461" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.09 0" />
<feBlend mode="normal" in2="effect2_dropShadow_5_8" result="effect3_dropShadow_5_8" />
<feColorMatrix in="SourceAlpha" type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dx="13.7405" dy="16.7939" />
<feGaussianBlur stdDeviation="6.61578" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" />
<feBlend mode="normal" in2="effect3_dropShadow_5_8" result="effect4_dropShadow_5_8" />
<feColorMatrix in="SourceAlpha" type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dx="24.9364" dy="29.5165" />
<feGaussianBlur stdDeviation="7.63359" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.01 0" />
<feBlend mode="normal" in2="effect4_dropShadow_5_8" result="effect5_dropShadow_5_8" />
<feBlend mode="normal" in="SourceGraphic" in2="effect5_dropShadow_5_8" result="shape" />
</filter>
<filter id="filter2_dddd_5_8" x="99" y="122" width="154.431" height="145.028"
filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dx="2" dy="3" />
<feGaussianBlur stdDeviation="3.5" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_5_8" />
<feColorMatrix in="SourceAlpha" type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dx="7" dy="10" />
<feGaussianBlur stdDeviation="6" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.09 0" />
<feBlend mode="normal" in2="effect1_dropShadow_5_8" result="effect2_dropShadow_5_8" />
<feColorMatrix in="SourceAlpha" type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dx="15" dy="23" />
<feGaussianBlur stdDeviation="8.5" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" />
<feBlend mode="normal" in2="effect2_dropShadow_5_8" result="effect3_dropShadow_5_8" />
<feColorMatrix in="SourceAlpha" type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dx="28" dy="41" />
<feGaussianBlur stdDeviation="10" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.01 0" />
<feBlend mode="normal" in2="effect3_dropShadow_5_8" result="effect4_dropShadow_5_8" />
<feBlend mode="normal" in="SourceGraphic" in2="effect4_dropShadow_5_8" result="shape" />
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 7.7 KiB

@@ -0,0 +1,48 @@
import { createVar, globalStyle, style } from '@vanilla-extract/css';
const bgColor = createVar();
const dotColor = createVar();
export const root = style({
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
pointerEvents: 'none',
zIndex: -1,
});
export const dotBg = style({
height: '100%',
width: '100%',
position: 'absolute',
zIndex: -1,
top: 0,
left: 0,
backgroundImage: `linear-gradient(to bottom, transparent 0%, ${bgColor} 90%),
radial-gradient(${dotColor} 2px, transparent 2px),
radial-gradient(${dotColor} 2px, transparent 2px)`,
backgroundSize: '100% 100%, 20px 20px, 20px 20px',
});
export const arts = style({
paddingTop: 60,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
});
globalStyle(`[data-theme="light"] ${root}`, {
vars: {
[bgColor]: '#fff',
[dotColor]: '#d9d9d9',
},
});
globalStyle(`[data-theme="dark"] ${root}`, {
vars: {
[bgColor]: '#141414',
[dotColor]: '#393939',
},
});
@@ -0,0 +1,19 @@
import { useTheme } from 'next-themes';
import artsDark from './art-dark.inline.svg';
import artsLight from './art-light.inline.svg';
import * as styles from './background.css';
export const SignInBackground = () => {
const { resolvedTheme } = useTheme();
return (
<div className={styles.root}>
<div className={styles.dotBg} />
<img
className={styles.arts}
src={resolvedTheme === 'dark' ? artsDark : artsLight}
/>
</div>
);
};
@@ -0,0 +1,18 @@
import { style } from '@vanilla-extract/css';
export const root = style({
padding: '40px',
justifyContent: 'flex-end',
minHeight: '100dvh',
display: 'flex',
flexDirection: 'column',
position: 'relative',
zIndex: 0,
});
export const content = style({
display: 'flex',
flexDirection: 'column',
alignItems: 'stretch',
gap: 24,
});
@@ -0,0 +1,13 @@
import type { PropsWithChildren } from 'react';
import { SignInBackground } from './background';
import * as styles from './layout.css';
export const MobileSignInLayout = ({ children }: PropsWithChildren) => {
return (
<div className={styles.root}>
<SignInBackground />
<div className={styles.content}>{children}</div>
</div>
);
};
@@ -0,0 +1,11 @@
import { AuthPanel } from '@affine/core/components/affine/auth';
import { MobileSignInLayout } from './layout';
export const MobileSignIn = ({ onSkip }: { onSkip: () => void }) => {
return (
<MobileSignInLayout>
<AuthPanel onSkip={onSkip} />
</MobileSignInLayout>
);
};
@@ -0,0 +1,40 @@
import { Modal } from '@affine/component';
import { authAtom } from '@affine/core/atoms';
import { cssVarV2 } from '@toeverything/theme/v2';
import { useAtom } from 'jotai';
import { useCallback } from 'react';
import { MobileSignIn } from './mobile-sign-in';
export const MobileSignInModal = () => {
const [authAtomValue, setAuthAtom] = useAtom(authAtom);
const setOpen = useCallback(
(open: boolean) => {
setAuthAtom(prev => ({ ...prev, openModal: open }));
},
[setAuthAtom]
);
const closeModal = useCallback(() => {
setOpen(false);
}, [setOpen]);
return (
<Modal
fullScreen
animation="slideBottom"
open={authAtomValue.openModal}
onOpenChange={setOpen}
contentOptions={{
style: {
padding: 0,
overflowY: 'auto',
backgroundColor: cssVarV2('layer/background/secondary'),
},
}}
withoutCloseButton
>
<MobileSignIn onSkip={closeModal} />
</Modal>
);
};