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

@@ -0,0 +1,139 @@
import { Button, Modal } from '@affine/component';
import { useI18n } from '@affine/i18n';
import clsx from 'clsx';
import {
type CSSProperties,
forwardRef,
type HTMLProps,
type ReactNode,
} from 'react';
import { PageHeader } from '../page-header';
import * as styles from './styles.css';
interface ConfigModalProps {
onBack?: () => void;
onDone?: () => void;
open: boolean;
onOpenChange: (open: boolean) => void;
title?: ReactNode;
children: ReactNode;
variant?: 'popup' | 'page';
}
/**
* A modal with a page header for configuring something on mobile (preferable to be fullscreen)
*/
export const ConfigModal = ({
onBack,
onDone,
open,
onOpenChange,
title,
children,
variant = 'page',
}: ConfigModalProps) => {
const t = useI18n();
return (
<Modal
onOpenChange={onOpenChange}
open={open}
fullScreen={variant === 'page'}
width="100%"
minHeight={0}
animation="slideBottom"
withoutCloseButton
contentOptions={{
onClick: e => {
e.stopPropagation();
},
className:
variant === 'page'
? styles.pageModalContent
: styles.popupModalContent,
}}
>
{variant === 'page' ? (
<PageHeader
back={!!onBack}
backAction={onBack}
suffix={
onDone ? (
<Button
style={{
fontSize: 17,
fontWeight: 600,
}}
className={styles.doneButton}
variant="plain"
onClick={onDone}
>
{t['Done']()}
</Button>
) : undefined
}
>
<div className={styles.pageTitle}>{title}</div>
</PageHeader>
) : null}
<div
className={
variant === 'page' ? styles.pageContent : styles.popupContent
}
>
{variant === 'page' ? null : (
<div className={styles.popupTitle}>{title}</div>
)}
{children}
{variant === 'popup' && onDone ? (
<Button
variant="primary"
className={styles.bottomDoneButton}
onClick={onDone}
>
{t['Done']()}
</Button>
) : null}
</div>
</Modal>
);
};
export const ConfigRow = forwardRef<HTMLDivElement, HTMLProps<HTMLDivElement>>(
function ConfigRow({ children, className, ...attrs }, ref) {
return (
<div className={clsx(styles.rowItem, className)} ref={ref} {...attrs}>
{children}
</div>
);
}
);
export interface SettingGroupProps
extends Omit<HTMLProps<HTMLDivElement>, 'title'> {
title?: ReactNode;
contentClassName?: string;
contentStyle?: CSSProperties;
}
export const ConfigRowGroup = forwardRef<HTMLDivElement, SettingGroupProps>(
function ConfigRowGroup(
{ children, title, className, contentClassName, contentStyle, ...attrs },
ref
) {
return (
<div className={clsx(styles.group, className)} ref={ref} {...attrs}>
{title ? <div className={styles.groupTitle}>{title}</div> : null}
<div
className={clsx(styles.groupContent, contentClassName)}
style={contentStyle}
>
{children}
</div>
</div>
);
}
);
ConfigModal.RowGroup = ConfigRowGroup;
ConfigModal.Row = ConfigRow;

View File

@@ -0,0 +1,89 @@
import {
bodyEmphasized,
footnoteRegular,
} from '@toeverything/theme/typography';
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const pageModalContent = style({
padding: 0,
overflowY: 'auto',
backgroundColor: cssVarV2('layer/background/secondary'),
});
export const popupModalContent = style({});
export const pageTitle = style([
bodyEmphasized,
{
color: cssVarV2('text/primary'),
display: 'inline-flex',
alignItems: 'center',
gap: 8,
},
]);
export const popupTitle = style([
bodyEmphasized,
{
display: 'flex',
alignItems: 'center',
gap: 8,
color: cssVarV2('text/primary'),
},
]);
export const pageContent = style({
padding: '24px 16px',
display: 'flex',
flexDirection: 'column',
gap: 16,
});
export const popupContent = style({
display: 'flex',
flexDirection: 'column',
gap: 16,
});
export const doneButton = style([
bodyEmphasized,
{
color: cssVarV2('button/primary'),
},
]);
export const bottomDoneButton = style({
width: '100%',
});
export const group = style({
display: 'flex',
flexDirection: 'column',
gap: 4,
width: '100%',
});
export const groupTitle = style([
footnoteRegular,
{
color: cssVarV2('text/tertiary'),
padding: 4,
},
]);
export const groupContent = style({
background: cssVarV2('layer/background/primary'),
borderRadius: 12,
padding: 4,
display: 'flex',
flexDirection: 'column',
gap: 8,
});
export const rowItem = style({
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '8px 4px',
});

View File

@@ -0,0 +1,2 @@
export * from './config-modal';
export * from './page-header';

View File

@@ -0,0 +1,115 @@
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

@@ -0,0 +1,52 @@
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,
});