fix(mobile): doc property styles (#8760)

fix AF-1582
fix AF-1671

- mobile doc info dialog styles
- added ConfigModal for editing property values in modal, including:
  - workspace properties: text, number, tags
  - db properties: text, number, label, link
This commit is contained in:
pengx17
2024-11-12 07:11:00 +00:00
parent 2ee2cbfe36
commit fa82842cd7
67 changed files with 1460 additions and 492 deletions

View File

@@ -1,6 +1,5 @@
export * from './app-tabs';
export * from './doc-card';
export * from './page-header';
export * from './rename';
export * from './search-input';
export * from './search-result';

View File

@@ -1,115 +0,0 @@
import { IconButton, SafeArea } from '@affine/component';
import { ArrowLeftSmallIcon } from '@blocksuite/icons/rc';
import clsx from 'clsx';
import {
forwardRef,
type HtmlHTMLAttributes,
type ReactNode,
useCallback,
} from 'react';
import * as styles from './styles.css';
export interface PageHeaderProps
extends Omit<HtmlHTMLAttributes<HTMLHeadElement>, 'prefix'> {
/**
* whether to show back button
*/
back?: boolean;
/**
* Override back button action
*/
backAction?: () => void;
/**
* prefix content, shown after back button(if exists)
*/
prefix?: ReactNode;
/**
* suffix content
*/
suffix?: ReactNode;
/**
* Weather to center the content
* @default true
*/
centerContent?: boolean;
prefixClassName?: string;
prefixStyle?: React.CSSProperties;
suffixClassName?: string;
suffixStyle?: React.CSSProperties;
}
export const PageHeader = forwardRef<HTMLDivElement, PageHeaderProps>(
function PageHeader(
{
back,
backAction,
prefix,
suffix,
children,
className,
centerContent = true,
prefixClassName,
prefixStyle,
suffixClassName,
suffixStyle,
...attrs
},
ref
) {
const handleRouteBack = useCallback(() => {
backAction ? backAction() : history.back();
}, [backAction]);
return (
<>
<SafeArea
top
ref={ref}
className={clsx(styles.root, className)}
data-testid="mobile-page-header"
{...attrs}
>
<header className={styles.inner}>
<section
className={clsx(styles.prefix, prefixClassName)}
style={prefixStyle}
>
{back ? (
<IconButton
size={24}
style={{ padding: 10 }}
onClick={handleRouteBack}
icon={<ArrowLeftSmallIcon />}
data-testid="page-header-back"
/>
) : null}
{prefix}
</section>
<section
className={clsx(styles.content, { center: centerContent })}
>
{children}
</section>
<section
className={clsx(styles.suffix, suffixClassName)}
style={suffixStyle}
>
{suffix}
</section>
</header>
</SafeArea>
{/* Spacer */}
<SafeArea top>
<div className={styles.headerSpacer} />
</SafeArea>
</>
);
}
);

View File

@@ -1,52 +0,0 @@
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const root = style({
width: '100%',
position: 'fixed',
top: 0,
zIndex: 1,
backgroundColor: cssVarV2('layer/background/secondary'),
});
export const headerSpacer = style({
height: 44,
});
export const inner = style({
height: 44,
padding: '0 6px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
});
export const content = style({
selectors: {
'&.center': {
position: 'absolute',
left: '50%',
transform: 'translateX(-50%)',
width: 'fit-content',
maxWidth: 'calc(100% - 12px - 88px - 16px)',
display: 'flex',
justifyContent: 'center',
pointerEvents: 'none',
},
'&:not(.center)': {
width: 0,
flex: 1,
},
},
});
export const spacer = style({
width: 0,
flex: 1,
});
export const prefix = style({
display: 'flex',
alignItems: 'center',
gap: 0,
});
export const suffix = style({
display: 'flex',
alignItems: 'center',
gap: 6,
});

View File

@@ -5,6 +5,7 @@ import {
Scrollable,
useThemeColorMeta,
} from '@affine/component';
import { PageHeader } from '@affine/core/components/mobile';
import { useI18n } from '@affine/i18n';
import { ArrowRightSmallIcon } from '@blocksuite/icons/rc';
import { cssVarV2 } from '@toeverything/theme/v2';
@@ -18,7 +19,6 @@ import {
useState,
} from 'react';
import { PageHeader } from '../../components/page-header';
import * as styles from './generic.css';
export interface GenericSelectorProps {

View File

@@ -1,5 +1,3 @@
import { footnoteRegular } from '@toeverything/theme/typography';
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const group = style({
@@ -8,20 +6,3 @@ export const group = style({
gap: 4,
width: '100%',
});
export const title = style([
footnoteRegular,
{
padding: '0px 8px',
color: cssVarV2('text/tertiary'),
},
]);
export const content = style({
background: cssVarV2('layer/background/primary'),
borderRadius: 12,
padding: '10px 16px',
display: 'flex',
flexDirection: 'column',
gap: 8,
});

View File

@@ -1,3 +1,4 @@
import { ConfigModal } from '@affine/core/components/mobile';
import clsx from 'clsx';
import {
type CSSProperties,
@@ -21,15 +22,16 @@ export const SettingGroup = forwardRef<HTMLDivElement, SettingGroupProps>(
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>
<ConfigModal.RowGroup
{...attrs}
ref={ref}
title={title}
className={clsx(styles.group, className)}
contentClassName={contentClassName}
contentStyle={contentStyle}
>
{children}
</ConfigModal.RowGroup>
);
}
);

View File

@@ -1,4 +1,4 @@
import { Modal } from '@affine/component';
import { ConfigModal } from '@affine/core/components/mobile';
import { AuthService } from '@affine/core/modules/cloud';
import type {
DialogComponentProps,
@@ -6,10 +6,8 @@ import type {
} from '@affine/core/modules/dialogs';
import { useI18n } from '@affine/i18n';
import { useService } from '@toeverything/infra';
import { cssVarV2 } from '@toeverything/theme/v2';
import { useEffect } from 'react';
import { PageHeader } from '../../components';
import { AboutGroup } from './about';
import { AppearanceGroup } from './appearance';
import { OthersGroup } from './others';
@@ -17,50 +15,34 @@ import * as styles from './style.css';
import { UserProfile } from './user-profile';
import { UserUsage } from './user-usage';
const MobileSetting = ({ onClose }: { onClose: () => void }) => {
const t = useI18n();
const MobileSetting = () => {
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>
</>
<div className={styles.root}>
<UserProfile />
<UserUsage />
<AppearanceGroup />
<AboutGroup />
<OthersGroup />
</div>
);
};
export const SettingDialog = ({
close,
}: DialogComponentProps<GLOBAL_DIALOG_SCHEMA['setting']>) => {
const t = useI18n();
return (
<Modal
fullScreen
animation="slideBottom"
<ConfigModal
title={t['com.affine.mobile.setting.header-title']()}
open
onOpenChange={() => close()}
contentOptions={{
style: {
padding: 0,
overflowY: 'auto',
backgroundColor: cssVarV2('layer/background/secondary'),
},
}}
withoutCloseButton
onBack={close}
>
<MobileSetting onClose={close} />
</Modal>
<MobileSetting />
</ConfigModal>
);
};

View File

@@ -1,3 +1,4 @@
import { ConfigModal } from '@affine/core/components/mobile';
import { DualLinkIcon } from '@blocksuite/icons/rc';
import type { PropsWithChildren, ReactNode } from 'react';
@@ -9,13 +10,13 @@ export const RowLayout = ({
href,
}: PropsWithChildren<{ label: ReactNode; href?: string }>) => {
const content = (
<div className={styles.baseSettingItem}>
<ConfigModal.Row className={styles.baseSettingItem}>
<div className={styles.baseSettingItemName}>{label}</div>
<div className={styles.baseSettingItemAction}>
{children ||
(href ? <DualLinkIcon className={styles.linkIcon} /> : null)}
</div>
</div>
</ConfigModal.Row>
);
return href ? (

View File

@@ -5,7 +5,6 @@ import { style } from '@vanilla-extract/css';
export const pageTitle = style([bodyEmphasized]);
export const root = style({
padding: '24px 16px',
display: 'flex',
flexDirection: 'column',
gap: 16,
@@ -16,8 +15,9 @@ export const baseSettingItem = style({
justifyContent: 'space-between',
alignItems: 'center',
gap: 32,
padding: '8px 0',
padding: 8,
});
export const baseSettingItemName = style([
bodyRegular,
{
@@ -26,6 +26,7 @@ export const baseSettingItemName = style([
whiteSpace: 'nowrap',
},
]);
export const baseSettingItemAction = style([
baseSettingItemName,
{

View File

@@ -77,7 +77,7 @@ const UsagePanel = () => {
);
return (
<SettingGroup title="Storage">
<SettingGroup title="Storage" contentStyle={{ padding: '10px 16px' }}>
<CloudUsage />
{serverFeatures?.copilot ? <AiUsage /> : null}
</SettingGroup>

View File

@@ -2,9 +2,7 @@ import { bodyRegular, caption1Regular } from '@toeverything/theme/typography';
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const progressRoot = style({
paddingBottom: 8,
});
export const progressRoot = style({});
export const progressInfoRow = style({
display: 'flex',
justifyContent: 'space-between',

View File

@@ -7,6 +7,7 @@ import { useDocMetaHelper } from '@affine/core/components/hooks/use-block-suite-
import { usePageDocumentTitle } from '@affine/core/components/hooks/use-global-state';
import { useJournalRouteHelper } from '@affine/core/components/hooks/use-journal';
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
import { PageHeader } from '@affine/core/components/mobile';
import { PageDetailEditor } from '@affine/core/components/page-detail-editor';
import { DetailPageWrapper } from '@affine/core/desktop/pages/workspace/detail-page/detail-page-wrapper';
import { EditorService } from '@affine/core/modules/editor';
@@ -42,7 +43,7 @@ import dayjs from 'dayjs';
import { useCallback, useEffect, useRef } from 'react';
import { useParams } from 'react-router-dom';
import { AppTabs, PageHeader } from '../../../components';
import { AppTabs } from '../../../components';
import { JournalDatePicker } from './journal-date-picker';
import * as styles from './mobile-detail-page.css';
import { PageHeaderMenuButton } from './page-header-more-button';
@@ -215,12 +216,12 @@ const notFound = (
</>
);
const JournalDetailPage = ({
const MobileDetailPage = ({
pageId,
date,
}: {
pageId: string;
date: string;
date?: string;
}) => {
const journalService = useService(JournalService);
const { openJournal } = useJournalRouteHelper();
@@ -250,40 +251,23 @@ const JournalDetailPage = ({
</>
}
>
<span className={bodyEmphasized}>
{i18nTime(dayjs(date), { absolute: { accuracy: 'month' } })}
</span>
{date ? (
<span className={bodyEmphasized}>
{i18nTime(dayjs(date), { absolute: { accuracy: 'month' } })}
</span>
) : null}
</PageHeader>
<JournalDatePicker
date={date}
onChange={handleDateChange}
withDotDates={allJournalDates}
/>
<DetailPageImpl />
<AppTabs background={cssVarV2('layer/background/primary')} />
</DetailPageWrapper>
</div>
);
};
const NormalDetailPage = ({ pageId }: { pageId: string }) => {
return (
<div className={styles.root}>
<DetailPageWrapper
skeleton={skeleton}
notFound={notFound}
pageId={pageId}
>
<PageHeader
back
className={styles.header}
suffix={
<>
<PageHeaderShareButton />
<PageHeaderMenuButton />
</>
}
/>
{date ? (
<JournalDatePicker
date={date}
onChange={handleDateChange}
withDotDates={allJournalDates}
/>
) : null}
<DetailPageImpl />
{date ? (
<AppTabs background={cssVarV2('layer/background/primary')} />
) : null}
</DetailPageWrapper>
</div>
);
@@ -300,9 +284,5 @@ export const Component = () => {
return null;
}
return journalDate ? (
<JournalDetailPage pageId={pageId} date={journalDate} />
) : (
<NormalDetailPage pageId={pageId} />
);
return <MobileDetailPage pageId={pageId} date={journalDate} />;
};

View File

@@ -1,6 +1,7 @@
import { IconButton, notify } from '@affine/component';
import {
MenuSeparator,
MenuSub,
MobileMenu,
MobileMenuItem,
} from '@affine/component/ui/menu';
@@ -42,6 +43,7 @@ export const PageHeaderMenuButton = () => {
editorService.editor.doc.meta$.map(meta => meta.trash)
);
const primaryMode = useLiveData(editorService.editor.doc.primaryMode$);
const title = useLiveData(editorService.editor.doc.title$);
const { favorite, toggleFavorite } = useFavorite(docId);
@@ -104,14 +106,16 @@ export const PageHeaderMenuButton = () => {
: t['com.affine.favoritePageOperation.add']()}
</MobileMenuItem>
<MenuSeparator />
<MobileMenu items={<DocInfoSheet docId={docId} />}>
<MobileMenuItem
prefixIcon={<InformationIcon />}
onClick={preventDefault}
>
<span>{t['com.affine.page-properties.page-info.view']()}</span>
</MobileMenuItem>
</MobileMenu>
<MenuSub
triggerOptions={{
prefixIcon: <InformationIcon />,
onClick: preventDefault,
}}
title={title ?? t['unnamed']()}
items={<DocInfoSheet docId={docId} />}
>
<span>{t['com.affine.page-properties.page-info.view']()}</span>
</MenuSub>
<MobileMenu
items={
<div className={styles.outlinePanel}>

View File

@@ -1,13 +1,49 @@
import { style } from '@vanilla-extract/css';
import { cssVar } from '@toeverything/theme';
import { cssVarV2 } from '@toeverything/theme/v2';
import { globalStyle, style } from '@vanilla-extract/css';
export const linksRow = style({
export const scrollableRoot = style({
padding: '0 16px',
display: 'flex',
flexDirection: 'column',
});
export const timeRow = style({
padding: '0 16px',
});
export const linksRow = style({});
export const timeRow = style({});
export const scrollBar = style({
width: 6,
transform: 'translateX(-4px)',
});
export const tableBodyRoot = style({
display: 'flex',
flexDirection: 'column',
position: 'relative',
});
export const addPropertyButton = style({
alignSelf: 'flex-start',
fontSize: cssVar('fontSm'),
color: `${cssVarV2('text/secondary')}`,
padding: '0 4px',
height: 36,
fontWeight: 400,
gap: 6,
'@media': {
print: {
display: 'none',
},
},
selectors: {
[`[data-property-collapsed="true"] &`]: {
display: 'none',
},
},
});
globalStyle(`${addPropertyButton} svg`, {
fontSize: 16,
color: cssVarV2('icon/secondary'),
});
globalStyle(`${addPropertyButton}:hover svg`, {
color: cssVarV2('icon/primary'),
});

View File

@@ -1,25 +1,43 @@
import { Divider, Scrollable } from '@affine/component';
import {
Button,
Divider,
Menu,
PropertyCollapsibleContent,
PropertyCollapsibleSection,
Scrollable,
} from '@affine/component';
import {
type DefaultOpenProperty,
DocPropertiesTable,
DocPropertyRow,
} from '@affine/core/components/doc-properties';
import { CreatePropertyMenuItems } from '@affine/core/components/doc-properties/menu/create-doc-property';
import { LinksRow } from '@affine/core/desktop/dialogs/doc-info/links-row';
import { TimeRow } from '@affine/core/desktop/dialogs/doc-info/time-row';
import { DocDatabaseBacklinkInfo } from '@affine/core/modules/doc-info';
import { DocsSearchService } from '@affine/core/modules/docs-search';
import { useI18n } from '@affine/i18n';
import { LiveData, useLiveData, useService } from '@toeverything/infra';
import { Suspense, useMemo } from 'react';
import { PlusIcon } from '@blocksuite/icons/rc';
import {
type DocCustomPropertyInfo,
DocsService,
LiveData,
useLiveData,
useServices,
} from '@toeverything/infra';
import { Suspense, useCallback, useMemo, useState } from 'react';
import * as styles from './doc-info.css';
export const DocInfoSheet = ({
docId,
defaultOpenProperty,
}: {
docId: string;
defaultOpenProperty?: DefaultOpenProperty;
}) => {
const docsSearchService = useService(DocsSearchService);
const { docsSearchService, docsService } = useServices({
DocsSearchService,
DocsService,
});
const t = useI18n();
const links = useLiveData(
@@ -35,8 +53,16 @@ export const DocInfoSheet = ({
)
);
const [newPropertyId, setNewPropertyId] = useState<string | null>(null);
const onPropertyAdded = useCallback((property: DocCustomPropertyInfo) => {
setNewPropertyId(property.id);
}, []);
const properties = useLiveData(docsService.propertyList.sortedProperties$);
return (
<Scrollable.Root>
<Scrollable.Root className={styles.scrollableRoot}>
<Scrollable.Viewport data-testid="doc-info-menu">
<Suspense>
<TimeRow docId={docId} className={styles.timeRow} />
@@ -61,7 +87,58 @@ export const DocInfoSheet = ({
<Divider size="thinner" />
</>
) : null}
<DocPropertiesTable defaultOpenProperty={defaultOpenProperty} />
<PropertyCollapsibleSection
title={t.t('com.affine.workspace.properties')}
>
<PropertyCollapsibleContent
className={styles.tableBodyRoot}
collapseButtonText={({ hide, isCollapsed }) =>
isCollapsed
? hide === 1
? t['com.affine.page-properties.more-property.one']({
count: hide.toString(),
})
: t['com.affine.page-properties.more-property.more']({
count: hide.toString(),
})
: hide === 1
? t['com.affine.page-properties.hide-property.one']({
count: hide.toString(),
})
: t['com.affine.page-properties.hide-property.more']({
count: hide.toString(),
})
}
>
{properties.map(property => (
<DocPropertyRow
key={property.id}
propertyInfo={property}
defaultOpenEditMenu={newPropertyId === property.id}
/>
))}
<Menu
items={<CreatePropertyMenuItems onCreated={onPropertyAdded} />}
contentOptions={{
onClick(e) {
e.stopPropagation();
},
}}
>
<Button
variant="plain"
prefix={<PlusIcon />}
className={styles.addPropertyButton}
>
{t['com.affine.page-properties.add-property']()}
</Button>
</Menu>
</PropertyCollapsibleContent>
</PropertyCollapsibleSection>
<Divider size="thinner" />
<DocDatabaseBacklinkInfo />
</Suspense>
</Scrollable.Viewport>
<Scrollable.Scrollbar className={styles.scrollBar} />

View File

@@ -1,10 +1,10 @@
import { IconButton, MobileMenu } from '@affine/component';
import { EmptyCollectionDetail } from '@affine/core/components/affine/empty';
import { PageHeader } from '@affine/core/components/mobile';
import { isEmptyCollection } from '@affine/core/desktop/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';

View File

@@ -1,9 +1,9 @@
import { IconButton, MobileMenu } from '@affine/component';
import { PageHeader } from '@affine/core/components/mobile';
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';