init: the first public commit for AFFiNE

This commit is contained in:
DarkSky
2022-07-22 15:49:21 +08:00
commit e3e3741393
1451 changed files with 108124 additions and 0 deletions
@@ -0,0 +1,3 @@
/* eslint-disable no-restricted-imports */
export { autocompleteClasses } from '@mui/material/Autocomplete';
export { useAutocomplete } from '@mui/material';
@@ -0,0 +1,98 @@
import type {
FC,
MouseEventHandler,
CSSProperties,
PropsWithChildren,
} from 'react';
import { styled } from '../styled';
import { buttonStatus } from './constants';
import { cx } from '../clsx';
/* Temporary solution, needs to be adjusted */
const SIZE_SMALL = 'small' as const;
const SIZE_MIDDLE = 'middle' as const;
const SIZE_LARGE = 'large' as const;
const SIZE_CONFIG = {
[SIZE_SMALL]: {
iconSize: '14px',
areaSize: '20px',
},
[SIZE_MIDDLE]: {
iconSize: '20px',
areaSize: '32px',
},
[SIZE_LARGE]: {
iconSize: '24px',
areaSize: '36px',
},
} as const;
type SizeType = keyof typeof SIZE_CONFIG;
interface IconButtonProps {
onClick?: MouseEventHandler;
disabled?: boolean;
style?: CSSProperties;
className?: string;
}
export const IconButton: FC<PropsWithChildren<IconButtonProps>> = ({
children,
disabled,
onClick,
className,
...props
}) => {
return (
<Container
{...props}
onClick={disabled ? undefined : onClick}
disabled={disabled}
className={cx({ [buttonStatus.disabled]: disabled }, className)}
>
{children}
</Container>
);
};
const Container = styled('button')(({ theme }) => {
return {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
width: '32px',
height: '32px',
backgroundColor: theme.affine.palette.white,
color: theme.affine.palette.icons,
padding: theme.affine.spacing.iconPadding,
borderRadius: '5px',
'& svg': {
width: '20px',
height: '20px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
},
'&:hover': {
backgroundColor: theme.affine.palette.hover,
},
[`&${buttonStatus.hover}`]: {
backgroundColor: theme.affine.palette.hover,
},
'&:focus': {
color: theme.affine.palette.primary,
},
[`&.${buttonStatus.focus}`]: {
color: theme.affine.palette.primary,
},
[`&${buttonStatus.disabled}`]: {
cursor: 'not-allowed',
},
};
});
@@ -0,0 +1,31 @@
import ButtonUnstyled, {
buttonUnstyledClasses,
} from '@mui/base/ButtonUnstyled';
import { styled } from '../styled';
export const BaseButton = styled(ButtonUnstyled)`
fontFamily: 'Helvetica,Arial,"Microsoft Yahei",SimHei,sans-serif',
font-size: 0.875rem;
border-radius: 8px;
padding: 4px 8px;
transition: all 150ms ease;
cursor: pointer;
border: none;
&:hover {
}
&.${buttonUnstyledClasses.active} {
}
&.${buttonUnstyledClasses.focusVisible} {
box-shadow: 0 4px 20px 0 rgba(61, 71, 82, 0.1),
0 0 0 5px rgba(0, 127, 255, 0.5);
outline: none;
}
&.${buttonUnstyledClasses.disabled} {
opacity: 0.5;
cursor: not-allowed;
}
`;
@@ -0,0 +1,6 @@
export const buttonStatus = {
hover: '.hover',
focus: '.focus',
active: '.focus',
disabled: '.disabled',
};
+3
View File
@@ -0,0 +1,3 @@
export { BaseButton } from './base-button';
export { ListButton } from './list-button';
export { IconButton } from './IconButton';
@@ -0,0 +1,60 @@
import React from 'react';
import clsx from 'clsx';
import style9 from 'style9';
import { BaseButton } from './base-button';
import { SvgIconProps } from '../svg-icon';
const styles = style9.create({
item: {
display: 'flex',
alignItems: 'center',
width: '220px',
paddingLeft: '22px',
paddingTop: '4px',
paddingBottom: '4px',
marginTop: '6px',
marginBottom: '6px',
borderRadius: '5px',
color: '#98ACBD',
},
item_hover: {
backgroundColor: 'rgba(152, 172, 189, 0.1)',
},
item_text: {
fontSize: '15px',
lineHeight: '17px',
textAlign: 'justify',
letterSpacing: '1.5px',
marginLeft: '21px',
color: '#4C6275',
},
});
type ListButtonProps = {
className?: string;
onClick: () => void;
onMouseOver?: () => void;
content?: string;
children?: () => JSX.Element;
hover?: boolean;
icon?: React.FC<SvgIconProps>;
};
export const ListButton = (props: ListButtonProps) => {
const MenuIcon = props.icon;
return (
<BaseButton
onClick={props.onClick}
onMouseOver={props.onMouseOver}
className={clsx(
styles('item', { item_hover: props.hover }),
props.className
)}
>
{MenuIcon && <MenuIcon sx={{ width: 20, height: 20 }} />}
<span className={styles('item_text')}>{props.content}</span>
{props.children}
</BaseButton>
);
};
@@ -0,0 +1,187 @@
import { useRef, useState, ReactElement } from 'react';
import {
MuiGrow as Grow,
MuiPopper as Popper,
MuiPopperPlacementType as PopperPlacementType,
} from '../mui';
import { styled } from '../styled';
import { ArrowRightIcon } from '@toeverything/components/icons';
import { Divider } from '../divider';
export interface CascaderItemProps {
title: string;
shortcut?: string;
callback?: () => void;
subItems?: CascaderItemProps[];
children?: ReactElement | Array<never>;
icon?: ReactElement;
isDivide?: boolean;
}
interface ItemProps extends CascaderItemProps {
onClose?: () => void;
}
interface CascaderProps {
items: ItemProps[];
anchorEl: Element;
placement: PopperPlacementType;
open: boolean;
onClose: () => void;
children?: ReactElement | never[];
icon?: ReactElement;
}
function CascaderItem(props: ItemProps) {
const { title, subItems, callback, onClose, children, icon, isDivide } =
props;
const item_ref = useRef(null);
const [open, setOpen] = useState(false);
if (isDivide) {
return <Divider></Divider>;
}
const on_click_item = () => {
if ((subItems && subItems.length > 0) || children) {
return;
}
callback && callback();
onClose();
};
return (
<CascaderMenuItem
ref={item_ref}
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
onClick={() => on_click_item()}
>
<IconContainer>{icon}</IconContainer>
<div style={{ display: 'flex', flexGrow: 1 }}>
<div
style={{
flexGrow: 1,
}}
>
{title}
</div>
{children || (subItems && subItems.length > 0) ? (
<Triangle>
<ArrowRightIcon />
</Triangle>
) : null}
</div>
{((subItems && subItems.length > 0) || children) && (
<Cascader
items={subItems}
anchorEl={item_ref.current}
placement="right-start"
open={open}
onClose={onClose}
children={children}
/>
)}
</CascaderMenuItem>
);
}
export function Cascader(props: CascaderProps) {
const { items, anchorEl, placement, open, onClose, children } = props;
return (
<Popper
open={open}
anchorEl={anchorEl}
role={undefined}
placement={placement}
transition
style={{ zIndex: 1000 }}
onMouseLeave={onClose}
>
{({ TransitionProps }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin: 'left bottom',
}}
>
<MenuPaper>
{children ? children : null}
<ul>
{items.map(item => {
return (
<CascaderItem
key={item.title}
title={item.title}
shortcut={item.shortcut}
callback={item.callback}
subItems={item.subItems || []}
onClose={onClose}
children={item.children}
icon={item.icon}
isDivide={item.isDivide}
/>
);
})}
</ul>
</MenuPaper>
</Grow>
)}
</Popper>
);
}
const MenuPaper = styled('div')(({ theme }) => ({
fontFamily: 'PingFang SC',
background: '#FFF',
boxShadow: '0px 1px 10px rgba(152, 172, 189, 0.6)',
borderRadius: '10px 0px 10px 10px',
color: '#4C6275',
fontWeight: '400',
padding: '4px 8px',
}));
const CascaderMenuItem = styled('li')(({ theme }) => ({
fontWeight: '400 !important',
width: '268px',
height: '32px',
lineHeight: '32px',
display: 'flex',
fontSize: '14px',
borderRadius: '5px',
cursor: 'pointer',
'&:hover': {
backgroundColor: '#F5F7F8',
},
}));
const IconContainer = styled('div')(({ theme }) => ({
display: 'flex',
height: '32px',
margin: '0 8px',
flexDirection: 'column',
justifyContent: 'center',
lineHeight: '32px',
fontSize: '20px',
'&, & > svg': {
width: '20px',
},
'& > svg': {
height: '20px',
},
}));
const Triangle = styled('div')(({ theme }) => ({
display: 'flex',
height: '32px',
marginRight: '8px',
flexDirection: 'column',
justifyContent: 'center',
lineHeight: '32px',
fontSize: '20px',
'&, & > svg': {
width: '20px',
},
'& > svg': {
height: '20px',
},
}));
+1
View File
@@ -0,0 +1 @@
export * from './Cascader';
@@ -0,0 +1,28 @@
import * as React from 'react';
/* eslint-disable no-restricted-imports */
import MuiCheckbox, { type CheckboxProps } from '@mui/material/Checkbox';
import {
CheckBoxUncheckIcon,
CheckBoxCheckIcon,
} from '@toeverything/components/icons';
import { styled } from '../styled';
export { CheckboxProps };
export const Checkbox = (props: CheckboxProps) => {
return (
<StyledCheckbox
icon={<CheckBoxUncheckIcon />}
checkedIcon={<CheckBoxCheckIcon />}
{...props}
/>
);
};
const StyledCheckbox = styled(MuiCheckbox)`
padding: 0;
color: #b9cad5;
&.Mui-checked {
color: #b9cad5;
}
`;
@@ -0,0 +1,2 @@
export { Checkbox } from './Checkbox';
export type { CheckboxProps } from './Checkbox';
+10
View File
@@ -0,0 +1,10 @@
/**
* Caution! this module must not include unstyled components import from `@mui/base`, otherwise, it will break the ClassNameGenerator.
* ❌ import { ... } from '@mui/base';
* ✅ import { ... } from '@mui/base/utils'; // must be specific base module
*
* Issue: https://github.com/mui/material-ui/issues/30011#issuecomment-1024993401
*/
// eslint-disable-next-line import/prefer-default-export
export { unstable_ClassNameGenerator } from '@mui/base/className';
@@ -0,0 +1,21 @@
import { styled } from '../styled';
interface ClickableProps {
active?: boolean;
}
export const Clickable = styled('div')<ClickableProps>(({ theme, active }) => {
return {
...theme.affine.typography.sm,
backgroundColor: active
? theme.affine.palette.hover
: theme.affine.palette.white,
cursor: 'pointer',
color: theme.affine.palette.menu,
borderRadius: '5px',
'&:hover': {
backgroundColor: theme.affine.palette.hover,
},
};
});
@@ -0,0 +1 @@
export { Clickable } from './Clickable';
+2
View File
@@ -0,0 +1,2 @@
import clsx from 'clsx';
export const cx = clsx;
+14
View File
@@ -0,0 +1,14 @@
import React from 'react';
import {
Calendar as ReactCalendar,
type CalendarProps,
} from 'react-date-range';
import { useTheme } from '../theme';
export { CalendarProps };
export const Calendar = (props: CalendarProps) => {
const theme = useTheme();
return <ReactCalendar color={theme.affine.palette.primary} {...props} />;
};
+20
View File
@@ -0,0 +1,20 @@
import {
DateRange as ReactDateRange,
type DateRangeProps,
type Range,
} from 'react-date-range';
import { useTheme } from '../theme';
export { DateRangeProps, Range };
export const DateRange = (props: DateRangeProps) => {
const theme = useTheme();
return (
<ReactDateRange
color={theme.affine.palette.primary}
rangeColors={[theme.affine.palette.primary]}
{...props}
/>
);
};
+11
View File
@@ -0,0 +1,11 @@
import 'react-date-range/dist/styles.css'; // main css file
import 'react-date-range/dist/theme/default.css'; // theme css file
// export { DateRange } from 'react-date-range';
export { DateRange } from './DateRange';
export { Calendar } from './Calendar';
export type { DateRangeProps, Range } from './DateRange';
export type { CalendarProps } from './Calendar';
// export const NewDateRange = DateRange;
@@ -0,0 +1,52 @@
import type { FC, ReactNode, CSSProperties } from 'react';
import { styled } from '../styled';
import { MuiDivider } from '../mui';
import type { MuiDividerProps } from '../mui';
interface DividerProps {
orientation?: 'horizontal' | 'vertical';
textAlign?: 'center' | 'start' | 'end';
children?: ReactNode;
className?: string;
style?: CSSProperties;
}
const _textAlignMap: Record<
DividerProps['textAlign'],
MuiDividerProps['textAlign']
> = {
center: 'center',
start: 'left',
end: 'right',
};
export const Divider: FC<DividerProps> = ({
orientation = 'horizontal',
textAlign = 'center',
children,
}) => {
return (
<StyledMuiDivider
orientation={orientation}
textAlign={_textAlignMap[textAlign]}
variant="fullWidth"
>
{children}
</StyledMuiDivider>
);
};
const StyledMuiDivider = styled(MuiDivider)<Pick<DividerProps, 'orientation'>>(
({ orientation }) => {
if (orientation === 'horizontal') {
return {
marginTop: '6px',
marginBottom: '6px',
};
}
return {
marginLeft: '6px',
marginRight: '6px',
};
}
);
+1
View File
@@ -0,0 +1 @@
export * from './Divider';
@@ -0,0 +1,62 @@
import { useState, useEffect, useRef } from 'react';
type Equal<X, Y, TrueValue, FalseValue> =
// prettier-ignore
(<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2)
? TrueValue
: FalseValue;
type ExtractEvent<K extends keyof DocumentEventMap, E> = Equal<
DocumentEventMap[K],
E,
K,
never
>;
type MouseEventName = {
[K in keyof DocumentEventMap]: ExtractEvent<K, MouseEvent>;
}[keyof DocumentEventMap];
type TouchEventName = {
[K in keyof DocumentEventMap]: ExtractEvent<K, TouchEvent>;
}[keyof DocumentEventMap];
type PointerEventName = {
[K in keyof DocumentEventMap]: ExtractEvent<K, PointerEvent>;
}[keyof DocumentEventMap];
interface EventAwayProps {
eventName: MouseEventName | TouchEventName | PointerEventName;
anchor: HTMLElement | HTMLElement[];
}
export const useEventAway = ({ eventName, anchor }: EventAwayProps) => {
const [away, set_away] = useState(true);
const anchors = useRef<HTMLElement[]>([]);
anchors.current = Array.isArray(anchor) ? anchor : [anchor];
useEffect(() => {
const listener = (event: MouseEvent | TouchEvent | PointerEvent) => {
if (event.composedPath) {
const composed_path = event.composedPath();
const found = composed_path.find(node =>
anchors.current.includes(node as HTMLElement)
);
set_away(!found);
} else {
const found = anchors.current.find(anchor =>
anchor.contains(event.target as HTMLElement)
);
set_away(!found);
}
};
document.addEventListener(eventName, listener);
return () => {
document.removeEventListener(eventName, listener);
};
}, [eventName]);
return { away };
};
+52
View File
@@ -0,0 +1,52 @@
// Base abstract feature for all UI components
export { Theme, useTheme, withTheme, ThemeProvider } from './theme';
export { styled } from './styled';
export type { SxProps } from './styled';
export * from './mui';
export * from './svg-icon';
export { StaticDatePicker } from '@mui/x-date-pickers/StaticDatePicker';
export { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
export { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
export {
PickersDay,
type PickersDayProps,
} from '@mui/x-date-pickers/PickersDay';
// Components
export { BaseButton, ListButton, IconButton } from './button';
export { TransitionsModal } from './model';
export { Popover, PopoverContainer } from './popover';
export type { PopoverProps } from './popover';
export { Popper } from './popper';
export type { PopperProps, PopperHandler } from './popper';
export { OldSelect, Select, Option } from './select';
export { message } from './message';
export { Input } from './input';
export type { InputProps } from './input';
export { Tooltip } from './tooltip';
export { usePatchNodes } from './patch-elements';
export type { PatchNode, UnPatchNode } from './patch-elements';
export { Tag } from './tag';
export type { TagProps } from './tag';
export { Divider } from './divider';
export { autocompleteClasses, useAutocomplete } from './autocomplete';
export { Slider } from './slider';
export { Typography } from './typography';
export { ListItem, ListIcon } from './list';
export { Clickable } from './clickable';
export { DateRange, Calendar } from './date';
export type { DateRangeProps, Range, CalendarProps } from './date';
export { Radio } from './radio';
export type { RadioProps } from './radio';
export { Checkbox } from './checkbox';
export type { CheckboxProps } from './checkbox';
export * from './cascader';
export { Switch } from './switch';
export type { SwitchProps } from './switch';
/* types */
export type { SvgIconProps } from './svg-icon';
export * from './clsx';
+64
View File
@@ -0,0 +1,64 @@
import { forwardRef, type ForwardedRef } from 'react';
/* eslint-disable no-restricted-imports */
import InputUnstyled, {
inputUnstyledClasses,
type InputUnstyledProps,
} from '@mui/base/InputUnstyled';
import { styled } from '../styled';
/**
* Input is extend by mui InputUnstyled
*
* InputUnstyled Demos:
*
* - [Input](https://mui.com/base/react-input/)
*
* InputUnstyled API:
*
* - [InputUnstyled API](https://mui.com/base/api/input-unstyled/)
*
* **/
export type InputProps = InputUnstyledProps;
export const Input = forwardRef(function CustomInput(
props: InputUnstyledProps,
ref: ForwardedRef<HTMLDivElement>
) {
const { components, ...other } = props;
return (
<InputUnstyled
components={{
Root: StyledInputRoot,
Input: StyledInputElement,
...components,
}}
{...other}
ref={ref}
/>
);
});
const StyledInputRoot = styled('div')(({ theme }) => ({
height: '32px',
display: 'flex',
border: `1px solid ${theme.affine.palette.borderColor}`,
borderRadius: '10px',
color: `${theme.affine.palette.secondaryText}`,
padding: '0 12px',
fontSize: '14px',
lineHeight: '1.5',
transition: 'border .1s',
[`&.${inputUnstyledClasses.focused}`]: {
borderColor: `${theme.affine.palette.primary}`,
},
}));
const StyledInputElement = styled('input')(({ theme }) => ({
fontSize: '14px',
lineHeight: '1.5',
color: `${theme.affine.palette.secondaryText}`,
flexGrow: 1,
'&::placeholder': {
color: `${theme.affine.palette.borderColor}`,
},
}));
+2
View File
@@ -0,0 +1,2 @@
export { Input } from './Input';
export type { InputProps } from './Input';
+12
View File
@@ -0,0 +1,12 @@
import { styled } from '../styled';
export const ListIcon = styled('div')(({ theme }) => {
return {
color: theme.affine.palette.icons,
height: '20px',
'& svg': {
fontSize: '20px',
},
};
});
+35
View File
@@ -0,0 +1,35 @@
import type { FC, PropsWithChildren, CSSProperties } from 'react';
import { Clickable } from '../clickable';
import { styled } from '../styled';
interface ListItemProps {
active?: boolean;
onClick?: () => void;
className?: string;
style?: CSSProperties;
}
export const ListItem: FC<PropsWithChildren<ListItemProps>> = ({
active,
children,
onClick,
className,
style,
}) => {
return (
<Container
active={active}
onClick={onClick}
className={className}
style={style}
>
{children}
</Container>
);
};
const Container = styled(Clickable)({
display: 'flex',
alignItems: 'center',
padding: '6px 12px',
});
+2
View File
@@ -0,0 +1,2 @@
export { ListItem } from './ListItem';
export { ListIcon } from './ListIcon';
+48
View File
@@ -0,0 +1,48 @@
import type { FC, ReactNode } from 'react';
import { createRoot } from 'react-dom/client';
import { styled } from '../styled';
import type { ContainerProps } from './types';
interface ShowProps {
Container: FC<ContainerProps>;
/**
* 自动关闭延时,单位毫秒。设为 0 时,不自动关闭。默认 2000
*/
duration?: number;
content: ReactNode;
}
export const show = ({ Container, duration = 2000, content }: ShowProps) => {
const root_element = document.createElement('div');
document.body.appendChild(root_element);
function close() {
document.body.removeChild(root_element);
}
const react_root = createRoot(root_element);
react_root.render(
<PortalContainer>
<Container content={content} duration={duration} close={close} />
</PortalContainer>
);
if (duration > 0) {
setTimeout(() => {
close();
}, duration);
}
return close;
};
const PortalContainer = styled('div')({
position: 'fixed',
top: '100px',
left: '50%',
transform: 'translateX(-50%)',
backgroundColor: '#fff',
zIndex: 100,
});
+18
View File
@@ -0,0 +1,18 @@
import type { ReactNode } from 'react';
import { show } from './base';
import { Success } from './success';
interface SuccessProps {
/**
* 自动关闭延时,单位毫秒。默认2000
*/
duration?: number;
content: ReactNode;
}
export const message = {
success({ duration, content }: SuccessProps) {
return show({ Container: Success, duration, content });
},
};
@@ -0,0 +1,14 @@
import type { FC } from 'react';
import { styled } from '../styled';
import type { ContainerProps } from './types';
export const Success: FC<ContainerProps> = ({ content }) => {
return <Container>{content}</Container>;
};
const Container = styled('div')({
maxWidth: '200px',
backgroundColor: 'rgba(64, 223, 155)',
borderRadius: '4px',
padding: '8px',
});
+7
View File
@@ -0,0 +1,7 @@
import type { ReactNode } from 'react';
export interface ContainerProps {
content: ReactNode;
duration: number;
close: () => void;
}
+44
View File
@@ -0,0 +1,44 @@
import { useState } from 'react';
import { styled } from '@mui/system';
import ModalUnstyled from '@mui/base/ModalUnstyled';
// eslint-disable-next-line no-restricted-imports
import Fade from '@mui/material/Fade';
import { MuiClickAwayListener } from '../mui';
const Modal = styled(ModalUnstyled)`
position: fixed;
z-index: 1300;
right: 0;
bottom: 0;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
`;
type TransitionsModalProps = {
open: boolean;
onClose: () => void;
children: JSX.Element;
};
export const TransitionsModal = (props: TransitionsModalProps) => {
return (
<Modal
aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description"
open={props.open}
onClose={props.onClose}
closeAfterTransition
>
<MuiClickAwayListener onClickAway={props.onClose}>
<Fade in={props.open} timeout={300}>
{props.children}
</Fade>
</MuiClickAwayListener>
</Modal>
);
};
+240
View File
@@ -0,0 +1,240 @@
/**
* There are many places that rely on Mui in the current project, which are gathered in this file
*/
/* eslint-disable no-restricted-imports */
import {
Box,
Collapse,
IconButton,
CircularProgress,
Divider,
Typography,
Menu,
MenuItem,
Avatar,
Popover,
TextField,
Modal,
Button,
List,
ListItem,
ListItemText,
Tooltip,
tooltipClasses,
Tabs,
Tab,
OutlinedInput,
InputAdornment,
Grow,
Paper,
MenuList,
ClickAwayListener,
Popper,
Select,
Switch,
Grid,
Container,
Snackbar,
InputBase,
FormControlLabel,
Checkbox,
Input,
Radio,
Zoom,
} from '@mui/material';
import type {
PopoverProps,
PopperPlacementType,
SelectChangeEvent,
TooltipProps,
DividerProps,
} from '@mui/material';
import { ListItemButton } from '@mui/material';
export { alpha } from '@mui/system';
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export type MuiPopoverProps = PopoverProps;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export type MuiPopperPlacementType = PopperPlacementType;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export type MuiSelectChangeEvent<T = string> = SelectChangeEvent<T>;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export type MuiTooltipProps = TooltipProps;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export type MuiDividerProps = DividerProps;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiBox = Box;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiCollapse = Collapse;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiIconButton = IconButton;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiCircularProgress = CircularProgress;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiDivider = Divider;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiTypography = Typography;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiMenu = Menu;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiMenuItem = MenuItem;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiAvatar = Avatar;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiPopover = Popover;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiTextField = TextField;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiModal = Modal;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiButton = Button;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiList = List;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiListItem = ListItem;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiListItemText = ListItemText;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiListItemButton = ListItemButton;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiTooltip = Tooltip;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiTabs = Tabs;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiTab = Tab;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiOutlinedInput = OutlinedInput;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiInputAdornment = InputAdornment;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiGrow = Grow;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiPaper = Paper;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiMenuList = MenuList;
export const MuiClickAwayListener = ClickAwayListener;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiPopper = Popper;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiSelect = Select;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiSwitch = Switch;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiGrid = Grid;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiContainer = Container;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const muiTooltipClasses = tooltipClasses;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiSnackbar = Snackbar;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiInputBase = InputBase;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiFormControlLabel = FormControlLabel;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiCheckbox = Checkbox;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiInput = Input;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiZoom = Zoom;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiRadio = Radio;
@@ -0,0 +1,39 @@
import type { ReactNode } from 'react';
import { useState, Fragment } from 'react';
import { has } from '@toeverything/utils';
export type UnPatchNode = () => void;
export type PatchNode = (key: string, node: ReactNode) => UnPatchNode;
export const usePatchNodes = () => {
const [nodes, set_nodes] = useState<Record<string, ReactNode>>({});
const patch_node: PatchNode = (key: string, node: ReactNode) => {
set_nodes(oldNodes => ({ ...oldNodes, [key]: node }));
return () => {
set_nodes(oldNodes => {
const nodes = { ...oldNodes };
delete nodes[key];
return nodes;
});
};
};
const has_node = (key: string) => {
return has(nodes, key);
};
const patched_nodes = (
<>
{Object.entries(nodes).map(([key, node]) => {
return <Fragment key={key}>{node}</Fragment>;
})}
</>
);
return {
patch: patch_node,
has: has_node,
patchedNodes: patched_nodes,
};
};
@@ -0,0 +1,52 @@
import type { MuiPopperPlacementType as PopperPlacementType } from '../mui';
import React, { forwardRef, type PropsWithChildren } from 'react';
import { type PopperHandler, Popper } from '../popper';
import { PopoverContainer } from './container';
import type { PopoverProps, PopoverDirection } from './interface';
const placementToContainerDirection: Record<
PopperPlacementType,
PopoverDirection
> = {
top: 'none',
'top-start': 'left-bottom',
'top-end': 'right-bottom',
right: 'none',
'right-start': 'left-top',
'right-end': 'left-bottom',
bottom: 'none',
'bottom-start': 'left-top',
'bottom-end': 'right-top',
left: 'none',
'left-start': 'right-top',
'left-end': 'right-bottom',
auto: 'none',
'auto-start': 'none',
'auto-end': 'none',
};
export const Popover = forwardRef<
PopperHandler,
PropsWithChildren<PopoverProps>
>((props, ref) => {
const { popoverDirection, placement, content, children, style } = props;
return (
<Popper
{...props}
ref={ref}
content={
<PopoverContainer
style={style}
direction={
popoverDirection ||
placementToContainerDirection[placement]
}
>
{content}
</PopoverContainer>
}
>
{children}
</Popper>
);
});
@@ -0,0 +1,27 @@
import { styled } from '../styled';
import { PopoverContainerProps } from './interface';
const border_radius_map: Record<PopoverContainerProps['direction'], string> = {
none: '10px',
'left-top': '0 10px 10px 10px',
'left-bottom': '10px 10px 10px 0',
'right-top': '10px 0 10px 10px',
'right-bottom': '10px 10px 0 10px',
};
export const PopoverContainer = styled('div')<
Pick<PopoverContainerProps, 'direction'>
>(({ theme, direction, style }) => {
const shadow = theme.affine.shadows.shadowSxDownLg;
const white = theme.affine.palette.white;
const border_radius =
border_radius_map[direction] || border_radius_map['left-top'];
return {
boxShadow: shadow,
borderRadius: border_radius,
padding: '8px 4px',
backgroundColor: white,
...style,
};
});
+3
View File
@@ -0,0 +1,3 @@
export * from './Popover';
export * from './interface';
export { PopoverContainer } from './container';
@@ -0,0 +1,23 @@
import type { ReactNode, CSSProperties } from 'react';
import { PopperProps } from '../popper';
export type PopoverDirection =
| 'none'
| 'left-top'
| 'left-bottom'
| 'right-top'
| 'right-bottom';
export interface PopoverContainerProps {
children?: ReactNode;
/**
* The pop-up window points to. The pop-up window has three rounded corners, one is a right angle, and the right angle is the direction of the pop-up window.
*/
direction?: PopoverDirection;
style?: CSSProperties;
}
export type PopoverProps = {
// Popover border-radius style will auto set by placement, popoverDirection can custom set it
popoverDirection?: PopoverDirection;
style?: CSSProperties;
} & PopperProps;
@@ -0,0 +1,92 @@
import React, { forwardRef } from 'react';
import { PopperArrowProps } from './interface';
import { styled } from '../styled';
export const PopperArrow = forwardRef<HTMLElement, PopperArrowProps>(
({ placement }, ref) => {
return <StyledArrow placement={placement} ref={ref} />;
}
);
const getArrowStyle = (placement: PopperArrowProps['placement']) => {
if (placement.indexOf('bottom') === 0) {
return {
top: 0,
left: 0,
marginTop: '-0.9em',
width: '3em',
height: '1em',
'&::before': {
borderWidth: '0 1em 1em 1em',
borderColor: `transparent transparent #98ACBD transparent`,
},
};
}
if (placement.indexOf('top') === 0) {
return {
bottom: 0,
left: 0,
marginBottom: '-0.9em',
width: '3em',
height: '1em',
'&::before': {
borderWidth: '1em 1em 0 1em',
borderColor: `#98ACBD transparent transparent transparent`,
},
};
}
if (placement.indexOf('left') === 0) {
return {
right: 0,
marginRight: '-0.9em',
height: '3em',
width: '1em',
'&::before': {
borderWidth: '1em 0 1em 1em',
borderColor: `transparent transparent transparent #98ACBD`,
},
};
}
if (placement.indexOf('right') === 0) {
return {
left: 0,
marginLeft: '-0.9em',
height: '3em',
width: '1em',
'&::before': {
borderWidth: '1em 1em 1em 0',
borderColor: `transparent #98ACBD transparent transparent`,
},
};
}
return {
display: 'none',
};
};
const StyledArrow = styled('span')<{
placement: PopperArrowProps['placement'];
}>(({ placement }) => {
return {
position: 'absolute',
fontSize: '7px',
width: '3em',
'::before': {
content: '""',
margin: 'auto',
display: 'block',
width: 0,
height: 0,
borderStyle: 'solid',
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
},
...getArrowStyle(placement),
};
});
+186
View File
@@ -0,0 +1,186 @@
import React, {
forwardRef,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react';
import type { PropsWithChildren } from 'react';
import {
MuiPopper,
MuiClickAwayListener as ClickAwayListener,
MuiGrow as Grow,
} from '../mui';
import { styled } from '../styled';
import { PopperProps, PopperHandler, VirtualElement } from './interface';
import { useTheme } from '../theme';
import { PopperArrow } from './PopoverArrow';
export const Popper = forwardRef<PopperHandler, PropsWithChildren<PopperProps>>(
(
{
children,
content,
anchor: propsAnchor,
placement = 'top-start',
defaultVisible = false,
container,
keepMounted = false,
visible: propsVisible,
trigger = 'hover',
pointerEnterDelay = 100,
pointerLeaveDelay = 100,
onVisibleChange,
popoverStyle,
popoverClassName,
anchorStyle,
anchorClassName,
zIndex,
offset = [0, 5],
showArrow = false,
},
ref
) => {
const [anchorEl, setAnchorEl] = useState<VirtualElement>(null);
const [visible, setVisible] = useState(defaultVisible);
const [arrowRef, setArrowRef] = useState<HTMLElement>(null);
const pointerLeaveTimer = useRef<number>();
const pointerEnterTimer = useRef<number>();
const visibleControlledByParent = typeof propsVisible !== 'undefined';
const isAnchorCustom = typeof propsAnchor !== 'undefined';
const hasHoverTrigger = useMemo(() => {
return (
trigger === 'hover' ||
(Array.isArray(trigger) && trigger.includes('hover'))
);
}, [trigger]);
const hasClickTrigger = useMemo(() => {
return (
trigger === 'click' ||
(Array.isArray(trigger) && trigger.includes('click'))
);
}, [trigger]);
const theme = useTheme();
const onPointerEnterHandler = () => {
if (!hasHoverTrigger || visibleControlledByParent) {
return;
}
window.clearTimeout(pointerLeaveTimer.current);
pointerEnterTimer.current = window.setTimeout(() => {
setVisible(true);
}, pointerEnterDelay);
};
const onPointerLeaveHandler = () => {
if (!hasHoverTrigger || visibleControlledByParent) {
return;
}
window.clearTimeout(pointerEnterTimer.current);
pointerLeaveTimer.current = window.setTimeout(() => {
setVisible(false);
}, pointerLeaveDelay);
};
useEffect(() => {
onVisibleChange?.(visible);
}, [visible, onVisibleChange]);
useImperativeHandle(ref, () => {
return {
setVisible: (visible: boolean) => {
!visibleControlledByParent && setVisible(visible);
},
};
});
return (
<ClickAwayListener
onClickAway={() => {
setVisible(false);
}}
>
<Container>
{isAnchorCustom ? null : (
<div
ref={(dom: HTMLDivElement) => setAnchorEl(dom)}
onClick={() => {
if (
!hasClickTrigger ||
visibleControlledByParent
) {
return;
}
setVisible(!visible);
}}
onPointerEnter={onPointerEnterHandler}
onPointerLeave={onPointerLeaveHandler}
style={anchorStyle}
className={anchorClassName}
>
{children}
</div>
)}
<MuiPopper
open={
visibleControlledByParent ? propsVisible : visible
}
sx={{ zIndex: zIndex || theme.affine.zIndex.popover }}
anchorEl={isAnchorCustom ? propsAnchor : anchorEl}
placement={placement}
container={container}
keepMounted={keepMounted}
transition
modifiers={[
{
name: 'offset',
options: {
offset,
},
},
{
name: 'arrow',
enabled: showArrow,
options: {
element: arrowRef,
},
},
]}
>
{({ TransitionProps }) => (
<Grow {...TransitionProps}>
<div
onPointerEnter={onPointerEnterHandler}
onPointerLeave={onPointerLeaveHandler}
style={popoverStyle}
className={popoverClassName}
>
{showArrow && (
<PopperArrow
placement={placement}
ref={setArrowRef}
/>
)}
{content}
</div>
</Grow>
)}
</MuiPopper>
</Container>
</ClickAwayListener>
);
}
);
// The children of ClickAwayListener must be a DOM Node to judge whether the click is outside, use node.contains
const Container = styled('div')({
display: 'contents',
});
+2
View File
@@ -0,0 +1,2 @@
export * from './Popper';
export * from './interface';
@@ -0,0 +1,70 @@
import type { CSSProperties, ReactNode } from 'react';
import type { MuiPopperPlacementType as PopperPlacementType } from '../mui';
export type VirtualElement = {
getBoundingClientRect: () => ClientRect | DOMRect;
contextElement?: Element;
};
export type PopperHandler = {
setVisible: (visible: boolean) => void;
};
export type PopperArrowProps = {
placement: PopperPlacementType;
};
export type PopperProps = {
// Popover content
content: ReactNode;
// Position of Popover
placement?: PopperPlacementType;
// The popover will pop up based on the anchor position
// And if this parameter is passed, children will not be rendered
anchor?: VirtualElement | (() => VirtualElement);
// Whether the default is implicit
defaultVisible?: boolean;
// Used to manually control the visibility of the Popover
visible?: boolean;
// A HTML element or function that returns one. The container will have the portal children appended to it.
// By default, it uses the body of the top-level document object, so it's simply document.body most of the time.
container?: HTMLElement;
// Always keep the children in the DOM. This prop can be useful in SEO situation or when you want to maximize the responsiveness of the Popper
keepMounted?: boolean;
// TODO: support focus
trigger?: 'hover' | 'click' | 'focus' | ('click' | 'hover' | 'focus')[];
// How long does it take for the mouse to display the Popover, in milliseconds
pointerEnterDelay?: number;
// How long does it take to hide the Popover after the mouse moves out, in milliseconds
pointerLeaveDelay?: number;
// Callback fired when the component closed or open
onVisibleChange?: (visible: boolean) => void;
// Popover container style
popoverStyle?: CSSProperties;
// Popover container class name
popoverClassName?: string;
// Anchor style
anchorStyle?: CSSProperties;
// Anchor class name
anchorClassName?: string;
// Popover z-index
zIndex?: number;
offset?: [number, number];
showArrow?: boolean;
};
+30
View File
@@ -0,0 +1,30 @@
import * as React from 'react';
/* eslint-disable no-restricted-imports */
import MuiRadio, { type RadioProps } from '@mui/material/Radio';
import { styled } from '../styled';
// TODO: sync online icon
// import {
// SingleSelectBoxUncheckIcon,
// SingleSelectBoxCheckIcon,
// } from '@toeverything/components/icons';
export { RadioProps };
export const Radio = (props: RadioProps) => {
return (
<StyledRadio
// icon={<SingleSelectBoxUncheckIcon />}
// checkedIcon={<SingleSelectBoxCheckIcon />}
{...props}
/>
);
};
const StyledRadio = styled(MuiRadio)`
padding: 0;
color: #b9cad5;
&.Mui-checked {
color: #b9cad5;
}
`;
+2
View File
@@ -0,0 +1,2 @@
export { Radio } from './Radio';
export type { RadioProps } from './Radio';
@@ -0,0 +1,48 @@
import { useCallback } from 'react';
import { styled } from '../styled';
import type { FC, CSSProperties, ChangeEvent } from 'react';
/**
* WARNING: This component is about to be deprecated, use Select replace
*
* **/
const StyledSelect = styled('select')({
border: '1px solid #aaa',
});
interface Props {
value: string;
options?: Array<{ label?: string; value?: string; key?: string }>;
onChange: (value: string) => void;
extraStyle?: CSSProperties;
}
export const OldSelect: FC<Props> = ({
value,
options,
onChange,
extraStyle,
}: Props) => {
const onSelectChange = useCallback(
(e: ChangeEvent<HTMLSelectElement>) => {
onChange(e.target.value);
},
[onChange]
);
return (
<StyledSelect
style={extraStyle}
value={value}
onChange={onSelectChange}
>
{options?.map(option => {
return (
<option key={option.value} value={option.value}>
{option.label}
</option>
);
})}
</StyledSelect>
);
};
+45
View File
@@ -0,0 +1,45 @@
/* eslint-disable no-restricted-imports */
import OptionUnstyled, {
optionUnstyledClasses,
} from '@mui/base/OptionUnstyled';
import { styled } from '../styled';
/**
* Option is extend by mui OptionUnstyled
*
* OptionUnstyled API:
*
* - [SelectUnstyled API](https://mui.com/zh/base/api/option-unstyled/)
*
* **/
export const Option = styled(OptionUnstyled)(({ theme }) => ({
height: '32px',
listStyle: 'none',
padding: '0 12px',
borderRadius: '5px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
'&:last-of-type': {
borderBottom: 'none',
},
[`&.${optionUnstyledClasses.selected}`]: {
backgroundColor: '#F5F7F8',
},
[`&.${optionUnstyledClasses.highlighted}`]: {
backgroundColor: '#F5F7F8',
},
[`&.${optionUnstyledClasses.highlighted}.${optionUnstyledClasses.selected}`]:
{},
[`&.${optionUnstyledClasses.disabled}`]: {},
[`&:hover:not(.${optionUnstyledClasses.disabled})`]: {
backgroundColor: '#F5F7F8',
},
}));
@@ -0,0 +1,47 @@
import { forwardRef, type ForwardedRef } from 'react';
/* eslint-disable no-restricted-imports */
import OptionGroupUnstyled, {
OptionGroupUnstyledProps,
} from '@mui/base/OptionGroupUnstyled';
import { styled } from '../styled';
const StyledGroupRoot = styled('li')`
list-style: none;
`;
const StyledGroupHeader = styled('span')`
display: block;
padding: 15px 0 5px 10px;
font-size: 0.75em;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05rem;
color: #6f7e8c;
`;
const StyledGroupOptions = styled('ul')`
list-style: none;
margin-left: 0;
padding: 0;
> li {
padding-left: 20px;
}
`;
/**
* TODO: The designer has not yet given a style for OptionGroup, the sample style is used here
* **/
export const OptionGroup = forwardRef(function CustomOptionGroup(
props: OptionGroupUnstyledProps,
ref: ForwardedRef<HTMLLIElement>
) {
const components: OptionGroupUnstyledProps['components'] = {
Root: StyledGroupRoot,
Label: StyledGroupHeader,
List: StyledGroupOptions,
...props.components,
};
return <OptionGroupUnstyled {...props} ref={ref} components={components} />;
});
+129
View File
@@ -0,0 +1,129 @@
import {
forwardRef,
type CSSProperties,
type ForwardedRef,
type RefAttributes,
type ReactNode,
} from 'react';
/* eslint-disable no-restricted-imports */
import SelectUnstyled, {
type SelectUnstyledProps,
selectUnstyledClasses,
} from '@mui/base/SelectUnstyled';
/* eslint-disable no-restricted-imports */
import PopperUnstyled from '@mui/base/PopperUnstyled';
import { styled } from '../styled';
type ExtendSelectProps = {
// Width is always used custom, it will be set to root button and popover
width?: number | string;
style?: CSSProperties;
listboxStyle?: CSSProperties;
placeholder?: ReactNode;
};
/**
* Select is extend by mui SelectUnstyled
*
* SelectUnstyled Demos:
*
* - [SelectUnstyled](https://mui.com/zh/base/react-select/)
*
* SelectUnstyled API:
*
* - [SelectUnstyled API](https://mui.com/zh/base/api/select-unstyled/)
*
* **/
export const Select = forwardRef(function CustomSelect<TValue>(
props: ExtendSelectProps & SelectUnstyledProps<TValue>,
ref: ForwardedRef<HTMLUListElement>
) {
const { width = '100%', style, listboxStyle, placeholder } = props;
const components: SelectUnstyledProps<TValue>['components'] = {
// Root: generateStyledRoot({ width, ...style }),
Root: forwardRef((rootProps, rootRef) => (
<StyledRoot
ref={rootRef}
{...rootProps}
style={{
width,
...style,
}}
>
{rootProps.children || (
<StyledPlaceholder>{placeholder}</StyledPlaceholder>
)}
</StyledRoot>
)),
Listbox: forwardRef((listboxProps, listboxRef) => (
<StyledListbox
ref={listboxRef}
{...listboxProps}
style={{ width, ...listboxStyle }}
/>
)),
Popper: StyledPopper,
...props.components,
};
return <SelectUnstyled {...props} ref={ref} components={components} />;
}) as <TValue>(
props: ExtendSelectProps &
SelectUnstyledProps<TValue> &
RefAttributes<HTMLUListElement>
) => JSX.Element;
const StyledRoot = styled('div')(({ theme }) => ({
height: '32px',
border: `1px solid ${theme.affine.palette.borderColor}`,
borderRadius: '10px',
color: theme.affine.palette.secondaryText,
padding: '0 12px',
fontSize: '14px',
transition: 'border .1s',
textAlign: 'left',
paddingLeft: '12px',
paddingRight: ' 30px',
background: '#fff',
display: 'flex',
alignItems: 'center',
position: 'relative',
'&:hover': {},
[`&.${selectUnstyledClasses.focusVisible}`]: {},
[`&.${selectUnstyledClasses.expanded}`]: {
borderColor: `${theme.affine.palette.primary}`,
'&::after': {
content: '"▴"',
},
},
'&::after': {
content: '"▾"',
position: ' absolute',
top: '0',
bottom: '0',
right: '12px',
margin: 'auto',
lineHeight: '32px',
},
}));
const StyledListbox = styled('ul')(({ theme }) => ({
fontSize: '14px',
padding: '8px 4px',
color: theme.affine.palette.secondaryText,
background: '#fff',
borderRadius: '10px',
overflow: 'auto',
boxShadow: theme.affine.shadows.shadowSxDownLg,
}));
const StyledPopper = styled(PopperUnstyled)`
z-index: 1;
`;
const StyledPlaceholder = styled('div')(({ theme }) => ({
color: `${theme.affine.palette.borderColor}`,
}));
+4
View File
@@ -0,0 +1,4 @@
export { Select } from './Select';
export { Option } from './Option';
export { OptionGroup } from './OptionGroup';
export { OldSelect } from './OldSelect';
+115
View File
@@ -0,0 +1,115 @@
import type { FC } from 'react';
import { SliderUnstyled, sliderUnstyledClasses } from '@mui/base';
import type { SliderUnstyledProps } from '@mui/base';
import { alpha } from '@mui/system';
import { styled } from '../styled';
interface SliderProps {
defaultValue?: SliderUnstyledProps['defaultValue'];
step?: number;
min?: number;
max?: number;
marks?: SliderUnstyledProps['marks'];
value?: SliderUnstyledProps['value'];
onChange?: SliderUnstyledProps['onChange'];
}
export const Slider: FC<SliderProps> = props => {
return <StyledSlider {...props} />;
};
const StyledSlider = styled(SliderUnstyled)(
({ theme }) => `
color: ${theme.affine.palette.primaryText};
height: 2px;
width: 100%;
padding: 13px 0;
display: inline-block;
position: relative;
cursor: pointer;
touch-action: none;
-webkit-tap-highlight-color: transparent;
opacity: 0.75;
&:hover {
opacity: 1;
}
&.${sliderUnstyledClasses.disabled} {
pointer-events: none;
cursor: default;
color: #bdbdbd;
}
& .${sliderUnstyledClasses.rail} {
display: block;
position: absolute;
width: 100%;
height: 1px;
border-radius: 2px;
background-color: ${theme.affine.palette.tagHover};
opacity: 0.38;
}
& .${sliderUnstyledClasses.track} {
display: block;
position: absolute;
height: 1px;
border-radius: 2px;
background-color: currentColor;
}
& .${sliderUnstyledClasses.thumb} {
position: absolute;
width: 8px;
height: 8px;
margin-left: -6px;
margin-top: -5px;
box-sizing: border-box;
border-radius: 50%;
outline: 0;
background-color: currentColor;
top: 50%;
transform: translateY(10%);
:hover,
&.${sliderUnstyledClasses.focusVisible} {
box-shadow: 0 0 0 0.25rem ${alpha(
theme.palette.mode === 'light' ? '#1976d2' : '#90caf9',
0.15
)};
}
&.${sliderUnstyledClasses.active} {
box-shadow: 0 0 0 0.25rem ${alpha(
theme.palette.mode === 'light' ? '#1976d2' : '#90caf9',
0.3
)};
}
}
& .${sliderUnstyledClasses.mark} {
position: absolute;
width: 4px;
height: 4px;
border-radius: 50%;
background-color: ${theme.affine.palette.tagHover};
top: 50%;
transform: translate(-50%, -40%);
}
& .${sliderUnstyledClasses.markActive} {
background-color: ${theme.affine.palette.primaryText};
}
& .${sliderUnstyledClasses.valueLabel} {
font-family: IBM Plex Sans;
font-size: 14px;
display: block;
position: relative;
top: -1.6em;
text-align: center;
transform: translateX(-50%);
}
`
);
+1
View File
@@ -0,0 +1 @@
export { Slider } from './Slider';
+73
View File
@@ -0,0 +1,73 @@
// eslint-disable-next-line no-restricted-imports
import { styled as muiStyled } from '@mui/material/styles';
import { ReactHTML, ReactSVG } from 'react';
import isPropValid from '@emotion/is-prop-valid';
export type { SxProps } from '@mui/system';
// Props that will be passed to DOM
const ALLOW_LIST_PROPS: string[] = [];
const isSimpleElement = (
component: unknown
): component is keyof ReactHTML | keyof ReactSVG => {
// TODO: improve this
return typeof component === 'string';
};
// See https://emotion.sh/docs/typescript#forwarding-props
const isValidProp = (prop: string): boolean =>
isPropValid(prop) || ALLOW_LIST_PROPS.includes(prop);
/**
* Forward prop except transient prop
*
* Support [Transient props](https://styled-components.com/docs/api#transient-props)
* See https://github.com/styled-components/styled-components/pull/3052
*/
const isNotTransientProp = (prop: string): boolean => !prop.startsWith('$'); // || prop !== 'as';
/**
* Workaround for the [Unknown Prop Warning](https://reactjs.org/warnings/unknown-prop.html) when passing custom props to styled components
*
* > React does not recognize the `xxxxxx` prop on a DOM element.
* If you intentionally want it to appear in the DOM as a custom attribute, spell it as lowercase `xxxxx` instead.
* If you accidentally passed it from a parent component,
* remove it from the DOM element.
*
* For a detailed discussion, see https://github.com/emotion-js/emotion/issues/655
*
* We simulate the behavior of the [styled-components](https://github.com/styled-components).
*
* > If the styled target is a simple element (e.g. styled.div),
* styled-components passes through any known HTML attribute to the DOM.
* If it is a custom React component (e.g. styled(MyComponent)),
* styled-components passes through all props.
*
* See https://styled-components.com/docs/basics#passed-props
*/
export const styled: typeof muiStyled = (
component: Parameters<typeof muiStyled>[0],
options?: Parameters<typeof muiStyled>[1]
): ReturnType<typeof muiStyled> => {
// @ts-ignore Fix https://github.com/nrwl/nx/issues/4508
if (options?.shouldForwardProp) {
// Explicitly declared, no need handle
return muiStyled(component, options);
}
if (!options) {
options = {};
}
if (!isSimpleElement(component)) {
// Is custom React component, pass props except transient props through
// @ts-ignore Fix https://github.com/nrwl/nx/issues/4508
options.shouldForwardProp = isNotTransientProp;
return muiStyled(component, options);
}
// Is simple element, pass only valid props
// @ts-ignore Fix https://github.com/nrwl/nx/issues/4508
options.shouldForwardProp = isValidProp;
return muiStyled(component, options);
};
+8
View File
@@ -0,0 +1,8 @@
/* eslint-disable no-restricted-imports */
import type { SvgIconProps as MuiSvgIconProps } from '@mui/material';
export { SvgIcon } from '@mui/material';
export interface SvgIconProps extends Omit<MuiSvgIconProps, 'color'> {
color?: string;
}
+119
View File
@@ -0,0 +1,119 @@
import type { CSSProperties } from 'react';
import { styled } from '../styled';
import { useSwitch, UseSwitchParameters } from '@mui/base/SwitchUnstyled';
import { ReactNode } from 'react';
/**
* Switch is extend by mui SwitchUnstyled
*
* SwitchUnstyled Demos:
* - [SwitchUnstyled](https://mui.com/zh/base/react-switch/)
*
* SwitchUnstyled API:
* - [SwitchUnstyled API](https://mui.com/zh/base/api/switch-unstyled/)
*
* Add some props to use in business:
* - `checkedLabel` and `uncheckedLabel` is used to show switch label, and you can write label custom by props `label`
* - `width` is used to limit the wrapper, it should always set if `checkedLabel` and `uncheckedLabel` is differentotherwise wrapper will shake when label change
*
* Easy Demo:
* ```jsx
* <Switch checkedLabel="ON" uncheckedLabel="OFF" width={50} />
* ```
*
* **/
export type SwitchProps = {
label?: (params: { checked: boolean; disabled: boolean }) => ReactNode;
checkedLabel?: string;
uncheckedLabel?: string;
style?: CSSProperties;
width?: number | string;
} & UseSwitchParameters;
export const Switch = (props: SwitchProps) => {
const { label, checkedLabel, uncheckedLabel, style, width } = props;
const { getInputProps, checked, disabled } = useSwitch(props);
return (
<StyledWrapper style={{ ...style, width }}>
<StyledLabel checked={checked} disable={disabled}>
{checked ? checkedLabel : uncheckedLabel}
{label?.({ checked, disabled })}
</StyledLabel>
<StyledRoot>
<StyleTrack checked={checked}>
<StyledThumb checked={checked} disable={disabled} />
</StyleTrack>
<StyledSwitchInput {...getInputProps()} />
</StyledRoot>
</StyledWrapper>
);
};
const StyledRoot = styled('span')`
display: inline-block;
position: relative;
width: 22px;
height: 12px;
`;
const StyledSwitchInput = styled('input')`
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
opacity: 0;
z-index: 1;
margin: 0;
cursor: pointer;
`;
const StyledThumb = styled('span')<{
disable: boolean;
checked: boolean;
}>(({ theme, checked, disable }) => {
return {
position: 'absolute',
display: 'block',
backgroundColor: checked ? '#fff' : theme.affine.palette.primary,
width: '8px',
height: '8px',
borderRadius: '2px',
top: '2px',
left: '2px',
transform: checked ? 'translateX(10px)' : '',
transition:
'transform 0.15s cubic-bezier(0.4, 0, 0.2, 1), background-color .15s',
};
});
const StyleTrack = styled('span')<{ checked: boolean }>(
({ theme, checked }) => ({
backgroundColor: checked ? theme.affine.palette.primary : '#fff',
border: `1px solid ${theme.affine.palette.primary}`,
borderRadius: '3px',
width: '100%',
height: '100%',
display: 'block',
transition: 'background-color .15s, border-color .15s',
})
);
const StyledWrapper = styled('label')`
display: inline-flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
`;
const StyledLabel = styled('div')<{ disable: boolean; checked: boolean }>(
({ theme, disable, checked }) => {
return {
display: 'inline-flex',
color: theme.affine.palette.primary,
fontSize: '12px',
marginRight: '5px',
};
}
);
+2
View File
@@ -0,0 +1,2 @@
export { Switch } from './Switch';
export type { SwitchProps } from './Switch';
+57
View File
@@ -0,0 +1,57 @@
import type {
FC,
ChangeEventHandler,
PropsWithChildren,
CSSProperties,
ReactNode,
} from 'react';
import { MouseEventHandler } from 'react';
import CloseIcon from '@mui/icons-material/Close';
import { StyledTag } from './style';
export interface TagProps {
style?: CSSProperties;
onClick?: MouseEventHandler;
closeable?: boolean;
onClose?: () => void;
startElement?: ReactNode;
endElement?: ReactNode;
}
export const Tag: FC<PropsWithChildren<TagProps>> = ({
onClick,
style,
children,
closeable,
onClose,
startElement,
endElement,
}) => {
return (
<StyledTag
className={`affine-tag ${closeable ? 'affine-tag--closeable' : ''}`}
style={{
...style,
}}
onClick={onClick}
>
<div className="affine-tag__wrapper">
{startElement}
<div className="affine-tag__content">{children}</div>
{endElement}
{closeable && (
<div
className="affine-tag__closeWrapper"
onClick={e => {
e.stopPropagation();
onClose?.();
}}
>
<CloseIcon style={{ fontSize: 12 }} />
</div>
)}
</div>
</StyledTag>
);
};
+2
View File
@@ -0,0 +1,2 @@
export { Tag } from './Tag';
export type { TagProps } from './Tag';
+53
View File
@@ -0,0 +1,53 @@
import { styled } from '../styled';
export const StyledTag = styled('div')`
height: 20px;
display: inline-flex;
align-items: center;
border-radius: 11px;
padding: 0 8px;
font-size: 12px;
cursor: pointer;
position: relative;
.affine-tag__wrapper {
display: flex;
align-items: center;
align-content: center;
}
.affine-tag__content {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 20px;
flex-grow: 1;
}
.affine-tag__closeWrapper {
height: 100%;
position: absolute;
right: 0;
top: 0;
bottom: 0;
margin: auto;
display: flex;
align-items: center;
padding: 0 5px;
opacity: 0;
transition: opacity 0.15s;
pointer-events: none;
}
&.affine-tag--closeable {
&:hover {
.affine-tag__wrapper {
width: calc(100% - 8px);
}
.affine-tag__closeWrapper {
opacity: 1;
pointer-events: auto;
}
}
}
`;
+5
View File
@@ -0,0 +1,5 @@
# Theme
## Size
xs(extra-small) < sm(small) < md(middle) < lg(large) < xl(extra-large)
+231
View File
@@ -0,0 +1,231 @@
export default {
inherit: 'inherit',
transparent: 'transparent',
black: '#000',
white: '#fff',
primary: '#3E6FDB',
danger: '#f87171',
success: '#4ade80',
warning: '#facc15',
info: '#7dd3fc',
'slate-50': '#f8fafc',
'slate-100': '#f1f5f9',
'slate-200': '#e2e8f0',
'slate-300': '#cbd5e1',
'slate-400': '#94a3b8',
'slate-500': '#64748b',
'slate-600': '#475569',
'slate-700': '#334155',
'slate-800': '#1e293b',
'slate-900': '#0f172a',
'gray-50': '#f9fafb',
'gray-100': '#f3f4f6',
'gray-200': '#e5e7eb',
'gray-300': '#d1d5db',
'gray-400': '#9ca3af',
'gray-500': '#6b7280',
'gray-600': '#4b5563',
'gray-700': '#374151',
'gray-800': '#1f2937',
'gray-900': '#111827',
'zinc-50': '#fafafa',
'zinc-100': '#f4f4f5',
'zinc-200': '#e4e4e7',
'zinc-300': '#d4d4d8',
'zinc-400': '#a1a1aa',
'zinc-500': '#71717a',
'zinc-600': '#52525b',
'zinc-700': '#3f3f46',
'zinc-800': '#27272a',
'zinc-900': '#18181b',
'neutral-50': '#fafafa',
'neutral-100': '#f5f5f5',
'neutral-200': '#e5e5e5',
'neutral-300': '#d4d4d4',
'neutral-400': '#a3a3a3',
'neutral-500': '#737373',
'neutral-600': '#525252',
'neutral-700': '#404040',
'neutral-800': '#262626',
'neutral-900': '#171717',
'stone-50': '#fafaf9',
'stone-100': '#f5f5f4',
'stone-200': '#e7e5e4',
'stone-300': '#d6d3d1',
'stone-400': '#a8a29e',
'stone-500': '#78716c',
'stone-600': '#57534e',
'stone-700': '#44403c',
'stone-800': '#292524',
'stone-900': '#1c1917',
'red-50': '#fef2f2',
'red-100': '#fee2e2',
'red-200': '#fecaca',
'red-300': '#fca5a5',
'red-400': '#f87171',
'red-500': '#ef4444',
'red-600': '#dc2626',
'red-700': '#b91c1c',
'red-800': '#991b1b',
'red-900': '#7f1d1d',
'orange-50': '#fff7ed',
'orange-100': '#ffedd5',
'orange-200': '#fed7aa',
'orange-300': '#fdba74',
'orange-400': '#fb923c',
'orange-500': '#f97316',
'orange-600': '#ea580c',
'orange-700': '#c2410c',
'orange-800': '#9a3412',
'orange-900': '#7c2d12',
'amber-50': '#fffbeb',
'amber-100': '#fef3c7',
'amber-200': '#fde68a',
'amber-300': '#fcd34d',
'amber-400': '#fbbf24',
'amber-500': '#f59e0b',
'amber-600': '#d97706',
'amber-700': '#b45309',
'amber-800': '#92400e',
'amber-900': '#78350f',
'yellow-50': '#fefce8',
'yellow-100': '#fef9c3',
'yellow-200': '#fef08a',
'yellow-300': '#fde047',
'yellow-400': '#facc15',
'yellow-500': '#eab308',
'yellow-600': '#ca8a04',
'yellow-700': '#a16207',
'yellow-800': '#854d0e',
'yellow-900': '#713f12',
'lime-50': '#f7fee7',
'lime-100': '#ecfccb',
'lime-200': '#d9f99d',
'lime-300': '#bef264',
'lime-400': '#a3e635',
'lime-500': '#84cc16',
'lime-600': '#65a30d',
'lime-700': '#4d7c0f',
'lime-800': '#3f6212',
'lime-900': '#365314',
'green-50': '#f0fdf4',
'green-100': '#dcfce7',
'green-200': '#bbf7d0',
'green-300': '#86efac',
'green-400': '#4ade80',
'green-500': '#22c55e',
'green-600': '#16a34a',
'green-700': '#15803d',
'green-800': '#166534',
'green-900': '#14532d',
'emerald-50': '#ecfdf5',
'emerald-100': '#d1fae5',
'emerald-200': '#a7f3d0',
'emerald-300': '#6ee7b7',
'emerald-400': '#34d399',
'emerald-500': '#10b981',
'emerald-600': '#059669',
'emerald-700': '#047857',
'emerald-800': '#065f46',
'emerald-900': '#064e3b',
'teal-50': '#f0fdfa',
'teal-100': '#ccfbf1',
'teal-200': '#99f6e4',
'teal-300': '#5eead4',
'teal-400': '#2dd4bf',
'teal-500': '#14b8a6',
'teal-600': '#0d9488',
'teal-700': '#0f766e',
'teal-800': '#115e59',
'teal-900': '#134e4a',
'cyan-50': '#ecfeff',
'cyan-100': '#cffafe',
'cyan-200': '#a5f3fc',
'cyan-300': '#67e8f9',
'cyan-400': '#22d3ee',
'cyan-500': '#06b6d4',
'cyan-600': '#0891b2',
'cyan-700': '#0e7490',
'cyan-800': '#155e75',
'cyan-900': '#164e63',
'sky-50': '#f0f9ff',
'sky-100': '#e0f2fe',
'sky-200': '#bae6fd',
'sky-300': '#7dd3fc',
'sky-400': '#38bdf8',
'sky-500': '#0ea5e9',
'sky-600': '#0284c7',
'sky-700': '#0369a1',
'sky-800': '#075985',
'sky-900': '#0c4a6e',
'blue-50': '#eff6ff',
'blue-100': '#dbeafe',
'blue-200': '#bfdbfe',
'blue-300': '#93c5fd',
'blue-400': '#60a5fa',
'blue-500': '#3b82f6',
'blue-600': '#2563eb',
'blue-700': '#1d4ed8',
'blue-800': '#1e40af',
'blue-900': '#1e3a8a',
'indigo-50': '#eef2ff',
'indigo-100': '#e0e7ff',
'indigo-200': '#c7d2fe',
'indigo-300': '#a5b4fc',
'indigo-400': '#818cf8',
'indigo-500': '#6366f1',
'indigo-600': '#4f46e5',
'indigo-700': '#4338ca',
'indigo-800': '#3730a3',
'indigo-900': '#312e81',
'violet-50': '#f5f3ff',
'violet-100': '#ede9fe',
'violet-200': '#ddd6fe',
'violet-300': '#c4b5fd',
'violet-400': '#a78bfa',
'violet-500': '#8b5cf6',
'violet-600': '#7c3aed',
'violet-700': '#6d28d9',
'violet-800': '#5b21b6',
'violet-900': '#4c1d95',
'purple-50': '#faf5ff',
'purple-100': '#f3e8ff',
'purple-200': '#e9d5ff',
'purple-300': '#d8b4fe',
'purple-400': '#c084fc',
'purple-500': '#a855f7',
'purple-600': '#9333ea',
'purple-700': '#7e22ce',
'purple-800': '#6b21a8',
'purple-900': '#581c87',
'fuchsia-50': '#fdf4ff',
'fuchsia-100': '#fae8ff',
'fuchsia-200': '#f5d0fe',
'fuchsia-300': '#f0abfc',
'fuchsia-400': '#e879f9',
'fuchsia-500': '#d946ef',
'fuchsia-600': '#c026d3',
'fuchsia-700': '#a21caf',
'fuchsia-800': '#86198f',
'fuchsia-900': '#701a75',
'pink-50': '#fdf2f8',
'pink-100': '#fce7f3',
'pink-200': '#fbcfe8',
'pink-300': '#f9a8d4',
'pink-400': '#f472b6',
'pink-500': '#ec4899',
'pink-600': '#db2777',
'pink-700': '#be185d',
'pink-800': '#9d174d',
'pink-900': '#831843',
'rose-50': '#fff1f2',
'rose-100': '#ffe4e6',
'rose-200': '#fecdd3',
'rose-300': '#fda4af',
'rose-400': '#fb7185',
'rose-500': '#f43f5e',
'rose-600': '#e11d48',
'rose-700': '#be123c',
'rose-800': '#9f1239',
'rose-900': '#881337',
};
+7
View File
@@ -0,0 +1,7 @@
import { Theme as theme } from './theme';
export { ThemeProvider, useTheme, withTheme } from './utils';
/**
* @deprecated use useTheme / withTheme instead
*/
export const Theme = theme;
+253
View File
@@ -0,0 +1,253 @@
import ColorObject from './color';
// import { ThemeOptions } from '@mui/material/styles';
/**
* @deprecated Please use the new {@link ThemeOptions} type.
*/
interface ThemeOptionsLegacy {
palette: Palette;
typography: Typography;
shadows?: Shadows;
border?: StringWithNone;
spacing?: Spacing;
shape?: Shape;
}
interface Shape {
xsBorderRadius?: string;
borderRadius?: string;
smBorderRadius?: string;
lgBorderRadius?: string;
}
interface Spacing {
xsSpacing: string;
smSpacing?: string;
main?: string;
lgSpacing?: string;
}
interface Palette {
primary?: Action;
success?: Action;
info?: Action;
error?: Action;
warning?: Action;
text?: Action;
}
interface Action {
main: string;
active?: string;
hover?: string;
hoverOpacity?: number;
selected?: string;
selectedOpacity?: number;
disabled?: string;
disabledOpacity?: number;
disabledBackground?: string;
focus?: string;
focusOpacity?: number;
activatedOpacity?: number;
}
interface Typography {
fontSize?: string;
fontFamily?: string;
xsFontSize?: string;
lgFontSize?: string;
fontWeight?: number;
xsFontWeight?: number;
lineHeight?: string;
lgFontWeight?: number;
button?: Font;
body1?: Font;
body2?: Font;
h1?: Font;
h2?: Font;
h3?: Font;
h4?: Font;
h5?: Font;
page?: Font;
quote?: Font;
callout?: Font;
}
interface Shadows {
none: 'none';
shadowSxDownLg: string;
}
type StringWithNone = [
'none',
string?,
string?,
string?,
string?,
string?,
string?,
string?,
string?,
string?,
string?,
string?,
string?,
string?,
string?,
string?,
string?,
string?,
string?,
string?,
string?,
string?,
string?,
string?,
string?
];
interface Font {
fontSize?: string;
lineHeight?: string;
fontFamily?: string;
fontWeight?: number;
}
export const Theme = {
palette: {
/**
* figma: white
*/
white: '#ffffff',
/**
* figma: icons
*/
icons: '#98ACBD',
/**
* figma: T-menu
*/
menu: '#4C6275',
/** separator line color for menus and icons */
menuSeparator: '#E0E6EB',
/**
* figma: hover
*/
hover: '#F5F7F8',
/**
* figma: tag hover
*/
tagHover: '#E0E6EB',
borderColor: '#E0E6EB',
placeholderColor: '#E0E6EB',
/**
* figma: brand color
*/
primary: '#3E6FDB',
/**
* figma: operation failed
*/
error: '#F1675E',
/**
* figma: warning color
*/
warning: '#F1BE5E',
/**
* figma: operation succeeded
*/
success: '#40DF9B',
/**
* figma: text
*/
primaryText: '#3A4C5C',
/**
* figma: subtext color
*/
secondaryText: '#4C6275',
/**
* selected
*/
textSelected: 'rgba(152, 172, 189, 0.1)',
/**
* text hover
*/
textHover: '#ECF1FB',
},
typography: {
button: {
fontSize: '16px',
},
body1: {
fontSize: '16px',
lineHeight: '22px',
},
h1: {
fontSize: '28px',
lineHeight: '40px',
},
h2: {
fontSize: '24px',
lineHeight: '34px',
},
h3: {
fontSize: '20px',
lineHeight: '28px',
},
h4: {
fontSize: '16px',
lineHeight: '22px',
},
page: {
fontSize: '36px',
lineHeight: '44px',
},
callout: {
fontSize: '36px',
lineHeight: '54px',
},
quote: {
fontSize: '36px',
lineHeight: '54px',
},
tooltip: {
fontSize: '12px',
lineHeight: '18px',
},
sm: {
fontSize: '14px',
lineHeight: '20px',
},
xs: {
fontSize: '12px',
lineHeight: '18px',
},
base: {
fontSize: '16px',
lineHeight: '22px',
},
articleTitle: {
fontSize: '36px',
lineHeight: '54px',
},
},
shadows: {
none: 'none',
shadowSxDownLg: '0px 1px 10px rgba(152, 172, 189, 0.6)',
},
border: ['none'],
spacing: {
xsSpacing: '4px',
smSpacing: '12px',
main: '16px',
lgSpacing: '20px',
iconPadding: '6px',
},
shape: {
borderRadius: '10px',
xsBorderRadius: '4px',
smBorderRadius: '8px',
lgBorderRadius: '20px',
},
zIndex: {
header: 100,
popover: 200,
tooltip: 300,
modal: 400,
message: 500,
},
} as const;
export type ThemeOptions = typeof Theme;
+39
View File
@@ -0,0 +1,39 @@
import type { FC, PropsWithChildren, ReactNode } from 'react';
// eslint-disable-next-line no-restricted-imports
import {
createTheme,
ThemeProvider as MuiThemeProvider,
useTheme as muiUseTheme,
Theme as MuiTheme,
} from '@mui/material/styles';
import type { ThemeOptions as AffineThemeOptions } from './theme';
import { Theme } from './theme';
declare module '@mui/material/styles' {
interface Theme {
affine: AffineThemeOptions;
}
// allow configuration using `createTheme`
interface ThemeOptions {
affine: AffineThemeOptions;
}
}
const theme = createTheme({
affine: Theme,
});
export const ThemeProvider: FC<{ children?: ReactNode }> = ({ children }) => {
return <MuiThemeProvider theme={theme}>{children}</MuiThemeProvider>;
};
export const useTheme = () => muiUseTheme();
export const withTheme = <T,>(
Component: FC<T & { theme: MuiTheme }>
): FC<T> => {
return props => {
const theme = useTheme();
return <Component {...props} theme={theme} />;
};
};
@@ -0,0 +1,36 @@
import React, {
forwardRef,
type PropsWithChildren,
type CSSProperties,
useState,
} from 'react';
import { type PopperHandler, type PopperProps, Popper } from '../popper';
import type { TooltipProps } from './interface';
import { useTheme } from '../theme';
const useTooltipStyle = (): CSSProperties => {
const theme = useTheme();
return {
backgroundColor: theme.affine.palette.icons,
color: theme.affine.palette.white,
...theme.affine.typography.tooltip,
padding: '4px 8px',
borderRadius: theme.shape.borderRadius,
};
};
export const Tooltip = forwardRef<
PopperHandler,
PropsWithChildren<PopperProps & TooltipProps>
>((props, ref) => {
const style = useTooltipStyle();
return (
<Popper
ref={ref}
popoverStyle={style}
placement="top"
showArrow
{...props}
/>
);
});
+1
View File
@@ -0,0 +1 @@
export * from './Tooltip';
@@ -0,0 +1,3 @@
export type TooltipProps = {
showArrow?: boolean;
};
@@ -0,0 +1,17 @@
import { styled } from '../styled';
import type { ThemeOptions } from '../theme/theme';
type TypographyType = keyof ThemeOptions['typography'];
interface TypographyProps {
/**
* Binding with typography in theme.
* Default: sm
*/
type?: TypographyType;
}
export const Typography = styled('span')<TypographyProps>(({ theme, type }) => {
const config = theme.affine.typography[type] || theme.affine.typography.sm;
return config;
});
@@ -0,0 +1 @@
export { Typography } from './Typo';