mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support adding cover to document (#4814)
This commit is contained in:
parent
5daf9d23f5
commit
c0210a5778
@ -2,7 +2,8 @@ import { Op } from 'quill-delta';
|
|||||||
import { HTMLAttributes } from 'react';
|
import { HTMLAttributes } from 'react';
|
||||||
import { Element } from 'slate';
|
import { Element } from 'slate';
|
||||||
import { ViewIconTypePB, ViewLayoutPB } from '@/services/backend';
|
import { ViewIconTypePB, ViewLayoutPB } from '@/services/backend';
|
||||||
import { YXmlText } from 'yjs/dist/src/types/YXmlText';
|
import { PageCover } from '$app_reducers/pages/slice';
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
export interface EditorNode {
|
export interface EditorNode {
|
||||||
id: string;
|
id: string;
|
||||||
@ -162,14 +163,22 @@ export interface MentionPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface EditorProps {
|
export interface EditorProps {
|
||||||
id: string;
|
|
||||||
sharedType?: YXmlText;
|
|
||||||
title?: string;
|
title?: string;
|
||||||
|
cover?: PageCover;
|
||||||
onTitleChange?: (title: string) => void;
|
onTitleChange?: (title: string) => void;
|
||||||
|
onCoverChange?: (cover?: PageCover) => void;
|
||||||
showTitle?: boolean;
|
showTitle?: boolean;
|
||||||
|
id: string;
|
||||||
disableFocus?: boolean;
|
disableFocus?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LocalEditorProps {
|
||||||
|
disableFocus?: boolean;
|
||||||
|
sharedType: Y.XmlText;
|
||||||
|
id: string;
|
||||||
|
caretColor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export enum EditorNodeType {
|
export enum EditorNodeType {
|
||||||
Text = 'text',
|
Text = 'text',
|
||||||
Paragraph = 'paragraph',
|
Paragraph = 'paragraph',
|
||||||
@ -221,7 +230,9 @@ export enum MentionType {
|
|||||||
|
|
||||||
export interface Mention {
|
export interface Mention {
|
||||||
// inline page ref id
|
// inline page ref id
|
||||||
page?: string;
|
page_id?: string;
|
||||||
// reminder date ref id
|
// reminder date ref id
|
||||||
date?: string;
|
date?: string;
|
||||||
|
|
||||||
|
type: MentionType;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.5 1H12.5C13.3284 1 14 1.67157 14 2.5V12.5C14 13.3284 13.3284 14 12.5 14H2.5C1.67157 14 1 13.3284 1 12.5V2.5C1 1.67157 1.67157 1 2.5 1ZM2.5 2C2.22386 2 2 2.22386 2 2.5V8.3636L3.6818 6.6818C3.76809 6.59551 3.88572 6.54797 4.00774 6.55007C4.12975 6.55216 4.24568 6.60372 4.32895 6.69293L7.87355 10.4901L10.6818 7.6818C10.8575 7.50607 11.1425 7.50607 11.3182 7.6818L13 9.3636V2.5C13 2.22386 12.7761 2 12.5 2H2.5ZM2 12.5V9.6364L3.98887 7.64753L7.5311 11.4421L8.94113 13H2.5C2.22386 13 2 12.7761 2 12.5ZM12.5 13H10.155L8.48336 11.153L11 8.6364L13 10.6364V12.5C13 12.7761 12.7761 13 12.5 13ZM6.64922 5.5C6.64922 5.03013 7.03013 4.64922 7.5 4.64922C7.96987 4.64922 8.35078 5.03013 8.35078 5.5C8.35078 5.96987 7.96987 6.35078 7.5 6.35078C7.03013 6.35078 6.64922 5.96987 6.64922 5.5ZM7.5 3.74922C6.53307 3.74922 5.74922 4.53307 5.74922 5.5C5.74922 6.46693 6.53307 7.25078 7.5 7.25078C8.46693 7.25078 9.25078 6.46693 9.25078 5.5C9.25078 4.53307 8.46693 3.74922 7.5 3.74922Z" fill="black"/>
|
<rect x="1.5" y="3" width="13" height="10" rx="1.5" stroke="#333333"/>
|
||||||
|
<circle cx="5.5" cy="6.5" r="1" stroke="#333333"/>
|
||||||
|
<path d="M5 13L10.112 8.45603C10.4211 8.18126 10.8674 8.12513 11.235 8.31482L14.5 10" stroke="#333333"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 342 B |
Binary file not shown.
After Width: | Height: | Size: 275 KiB |
@ -1,7 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
export function Colors() {
|
|
||||||
return <div></div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Colors;
|
|
@ -1,13 +1,22 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { pattern } from '$app/utils/open_url';
|
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
|
|
||||||
export function EmbedLink({ onDone, onEscape }: { onDone?: (value: string) => void; onEscape?: () => void }) {
|
const urlPattern = /^https?:\/\/.+/;
|
||||||
|
|
||||||
|
export function EmbedLink({
|
||||||
|
onDone,
|
||||||
|
onEscape,
|
||||||
|
defaultLink,
|
||||||
|
}: {
|
||||||
|
defaultLink?: string;
|
||||||
|
onDone?: (value: string) => void;
|
||||||
|
onEscape?: () => void;
|
||||||
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState(defaultLink ?? '');
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
@ -15,7 +24,7 @@ export function EmbedLink({ onDone, onEscape }: { onDone?: (value: string) => vo
|
|||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
|
|
||||||
setValue(value);
|
setValue(value);
|
||||||
setError(!pattern.test(value));
|
setError(!urlPattern.test(value));
|
||||||
},
|
},
|
||||||
[setValue, setError]
|
[setValue, setError]
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,123 @@
|
|||||||
|
import React, { SyntheticEvent, useCallback, useState } from 'react';
|
||||||
|
import Popover, { PopoverProps } from '@mui/material/Popover';
|
||||||
|
import { TabPanel, ViewTab, ViewTabs } from '$app/components/database/components/tab_bar/ViewTabs';
|
||||||
|
import SwipeableViews from 'react-swipeable-views';
|
||||||
|
import { PopoverCommonProps } from '$app/components/editor/components/tools/popover';
|
||||||
|
|
||||||
|
export enum TAB_KEY {
|
||||||
|
Colors = 'colors',
|
||||||
|
UPLOAD = 'upload',
|
||||||
|
EMBED_LINK = 'embed_link',
|
||||||
|
UNSPLASH = 'unsplash',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TabOption = {
|
||||||
|
key: TAB_KEY;
|
||||||
|
label: string;
|
||||||
|
Component: React.ComponentType<{
|
||||||
|
onDone?: (value: string) => void;
|
||||||
|
onEscape?: () => void;
|
||||||
|
}>;
|
||||||
|
onDone?: (value: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function UploadTabs({
|
||||||
|
tabOptions,
|
||||||
|
popoverProps,
|
||||||
|
containerStyle,
|
||||||
|
}: {
|
||||||
|
containerStyle?: React.CSSProperties;
|
||||||
|
tabOptions: TabOption[];
|
||||||
|
popoverProps?: PopoverProps;
|
||||||
|
}) {
|
||||||
|
const [tabValue, setTabValue] = useState<TAB_KEY>(() => {
|
||||||
|
return tabOptions[0].key;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleTabChange = useCallback((_: SyntheticEvent, newValue: string) => {
|
||||||
|
setTabValue(newValue as TAB_KEY);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const selectedIndex = tabOptions.findIndex((tab) => tab.key === tabValue);
|
||||||
|
|
||||||
|
const onKeyDown = useCallback(
|
||||||
|
(e: React.KeyboardEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
popoverProps?.onClose?.({}, 'escapeKeyDown');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'Tab') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setTabValue((prev) => {
|
||||||
|
const currentIndex = tabOptions.findIndex((tab) => tab.key === prev);
|
||||||
|
let nextIndex = currentIndex + 1;
|
||||||
|
|
||||||
|
if (e.shiftKey) {
|
||||||
|
nextIndex = currentIndex - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tabOptions[nextIndex % tabOptions.length]?.key ?? tabOptions[0].key;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[popoverProps, tabOptions]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
{...popoverProps}
|
||||||
|
{...PopoverCommonProps}
|
||||||
|
open={popoverProps?.open ?? false}
|
||||||
|
disableAutoFocus={false}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
PaperProps={{
|
||||||
|
style: {
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={containerStyle} className={'flex flex-col gap-4 overflow-hidden'}>
|
||||||
|
<ViewTabs
|
||||||
|
value={tabValue}
|
||||||
|
onChange={handleTabChange}
|
||||||
|
scrollButtons={false}
|
||||||
|
variant='scrollable'
|
||||||
|
allowScrollButtonsMobile
|
||||||
|
className={'min-h-[38px] border-b border-line-divider px-2'}
|
||||||
|
>
|
||||||
|
{tabOptions.map((tab) => {
|
||||||
|
const { key, label } = tab;
|
||||||
|
|
||||||
|
return <ViewTab key={key} iconPosition='start' color='inherit' label={label} value={key} />;
|
||||||
|
})}
|
||||||
|
</ViewTabs>
|
||||||
|
|
||||||
|
<div className={'h-full w-full flex-1 overflow-y-auto overflow-x-hidden'}>
|
||||||
|
<SwipeableViews
|
||||||
|
slideStyle={{
|
||||||
|
overflow: 'hidden',
|
||||||
|
height: '100%',
|
||||||
|
}}
|
||||||
|
axis={'x'}
|
||||||
|
index={selectedIndex}
|
||||||
|
>
|
||||||
|
{tabOptions.map((tab, index) => {
|
||||||
|
const { key, Component, onDone } = tab;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TabPanel className={'flex h-full w-full flex-col'} key={key} index={index} value={selectedIndex}>
|
||||||
|
<Component onDone={onDone} onEscape={() => popoverProps?.onClose?.({}, 'escapeKeyDown')} />
|
||||||
|
</TabPanel>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SwipeableViews>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
export * from './Unsplash';
|
export * from './Unsplash';
|
||||||
export * from './UploadImage';
|
export * from './UploadImage';
|
||||||
export * from './EmbedLink';
|
export * from './EmbedLink';
|
||||||
export * from './Colors';
|
export * from './UploadTabs';
|
||||||
|
@ -1,33 +1,52 @@
|
|||||||
import ViewIconGroup from '$app/components/_shared/view_title/ViewIconGroup';
|
import ViewIconGroup from '$app/components/_shared/view_title/ViewIconGroup';
|
||||||
import { PageIcon } from '$app_reducers/pages/slice';
|
import { PageCover, PageIcon } from '$app_reducers/pages/slice';
|
||||||
import ViewIcon from '$app/components/_shared/view_title/ViewIcon';
|
import ViewIcon from '$app/components/_shared/view_title/ViewIcon';
|
||||||
|
import { ViewCover } from '$app/components/_shared/view_title/cover';
|
||||||
|
|
||||||
function ViewBanner({
|
function ViewBanner({
|
||||||
icon,
|
icon,
|
||||||
hover,
|
hover,
|
||||||
onUpdateIcon,
|
onUpdateIcon,
|
||||||
|
showCover,
|
||||||
|
cover,
|
||||||
|
onUpdateCover,
|
||||||
}: {
|
}: {
|
||||||
icon?: PageIcon;
|
icon?: PageIcon;
|
||||||
hover: boolean;
|
hover: boolean;
|
||||||
onUpdateIcon: (icon: string) => void;
|
onUpdateIcon: (icon: string) => void;
|
||||||
|
showCover: boolean;
|
||||||
|
cover?: PageCover;
|
||||||
|
onUpdateCover?: (cover?: PageCover) => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={'view-banner flex w-full flex-col'}>
|
||||||
<div
|
{showCover && cover && <ViewCover cover={cover} onUpdateCover={onUpdateCover} />}
|
||||||
style={{
|
|
||||||
display: icon ? 'flex' : 'none',
|
<div className={'relative min-h-[65px] px-16 pt-4'}>
|
||||||
}}
|
<div
|
||||||
>
|
style={{
|
||||||
<ViewIcon onUpdateIcon={onUpdateIcon} icon={icon} />
|
display: icon ? 'flex' : 'none',
|
||||||
|
position: cover ? 'absolute' : 'relative',
|
||||||
|
bottom: cover ? '24px' : 'auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ViewIcon onUpdateIcon={onUpdateIcon} icon={icon} />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
opacity: hover ? 1 : 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ViewIconGroup
|
||||||
|
icon={icon}
|
||||||
|
onUpdateIcon={onUpdateIcon}
|
||||||
|
showCover={showCover}
|
||||||
|
cover={cover}
|
||||||
|
onUpdateCover={onUpdateCover}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
</div>
|
||||||
style={{
|
|
||||||
opacity: hover ? 1 : 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ViewIconGroup icon={icon} onUpdateIcon={onUpdateIcon} />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ function ViewIcon({ icon, onUpdateIcon }: { icon?: PageIcon; onUpdateIcon: (icon
|
|||||||
if (!icon) return null;
|
if (!icon) return null;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={`-ml-2 flex rounded p-2 hover:bg-content-blue-50`}>
|
<div className={`view-icon -ml-2 flex rounded p-2`}>
|
||||||
<div onClick={onOpen} className={'h-full w-full cursor-pointer rounded text-6xl'}>
|
<div onClick={onOpen} className={'h-full w-full cursor-pointer rounded text-6xl'}>
|
||||||
{icon.value}
|
{icon.value}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,31 +1,42 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PageIcon } from '$app_reducers/pages/slice';
|
import { CoverType, PageCover, PageIcon } from '$app_reducers/pages/slice';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { randomEmoji } from '$app/utils/emoji';
|
import { randomEmoji } from '$app/utils/emoji';
|
||||||
import { EmojiEmotionsOutlined } from '@mui/icons-material';
|
import { EmojiEmotionsOutlined } from '@mui/icons-material';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
|
import { ReactComponent as ImageIcon } from '$app/assets/image.svg';
|
||||||
|
import { ImageType } from '$app/application/document/document.types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
icon?: PageIcon;
|
icon?: PageIcon;
|
||||||
// onUpdateCover: (coverType: CoverType, cover: string) => void;
|
|
||||||
onUpdateIcon: (icon: string) => void;
|
onUpdateIcon: (icon: string) => void;
|
||||||
|
showCover: boolean;
|
||||||
|
cover?: PageCover;
|
||||||
|
onUpdateCover?: (cover: PageCover) => void;
|
||||||
}
|
}
|
||||||
function ViewIconGroup({ icon, onUpdateIcon }: Props) {
|
|
||||||
|
const defaultCover = {
|
||||||
|
cover_selection_type: CoverType.Asset,
|
||||||
|
cover_selection: 'app_flowy_abstract_cover_2.jpeg',
|
||||||
|
image_type: ImageType.Internal,
|
||||||
|
};
|
||||||
|
|
||||||
|
function ViewIconGroup({ icon, onUpdateIcon, showCover, cover, onUpdateCover }: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const showAddIcon = !icon?.value;
|
const showAddIcon = !icon?.value;
|
||||||
|
|
||||||
|
const showAddCover = !cover && showCover;
|
||||||
|
|
||||||
const onAddIcon = useCallback(() => {
|
const onAddIcon = useCallback(() => {
|
||||||
const emoji = randomEmoji();
|
const emoji = randomEmoji();
|
||||||
|
|
||||||
onUpdateIcon(emoji);
|
onUpdateIcon(emoji);
|
||||||
}, [onUpdateIcon]);
|
}, [onUpdateIcon]);
|
||||||
|
|
||||||
// const onAddCover = useCallback(() => {
|
const onAddCover = useCallback(() => {
|
||||||
// const color = randomColor();
|
onUpdateCover?.(defaultCover);
|
||||||
//
|
}, [onUpdateCover]);
|
||||||
// onUpdateCover(CoverType.Color, color);
|
|
||||||
// }, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex items-center py-2'}>
|
<div className={'flex items-center py-2'}>
|
||||||
@ -34,11 +45,11 @@ function ViewIconGroup({ icon, onUpdateIcon }: Props) {
|
|||||||
{t('document.plugins.cover.addIcon')}
|
{t('document.plugins.cover.addIcon')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{/*{showAddCover && (*/}
|
{showAddCover && (
|
||||||
{/* <Button onClick={onAddCover} color={'inherit'} startIcon={<ImageOutlined />}>*/}
|
<Button onClick={onAddCover} color={'inherit'} startIcon={<ImageIcon />}>
|
||||||
{/* {t('document.plugins.cover.addCover')}*/}
|
{t('document.plugins.cover.addCover')}
|
||||||
{/* </Button>*/}
|
</Button>
|
||||||
{/*)}*/}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import ViewBanner from '$app/components/_shared/view_title/ViewBanner';
|
import ViewBanner from '$app/components/_shared/view_title/ViewBanner';
|
||||||
import { Page, PageIcon } from '$app_reducers/pages/slice';
|
import { Page, PageCover, PageIcon } from '$app_reducers/pages/slice';
|
||||||
import { ViewIconTypePB } from '@/services/backend';
|
import { ViewIconTypePB } from '@/services/backend';
|
||||||
import ViewTitleInput from '$app/components/_shared/view_title/ViewTitleInput';
|
import ViewTitleInput from '$app/components/_shared/view_title/ViewTitleInput';
|
||||||
|
|
||||||
@ -9,9 +9,20 @@ interface Props {
|
|||||||
showTitle?: boolean;
|
showTitle?: boolean;
|
||||||
onTitleChange?: (title: string) => void;
|
onTitleChange?: (title: string) => void;
|
||||||
onUpdateIcon?: (icon: PageIcon) => void;
|
onUpdateIcon?: (icon: PageIcon) => void;
|
||||||
|
forceHover?: boolean;
|
||||||
|
showCover?: boolean;
|
||||||
|
onUpdateCover?: (cover?: PageCover) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ViewTitle({ view, onTitleChange, showTitle = true, onUpdateIcon: onUpdateIconProp }: Props) {
|
function ViewTitle({
|
||||||
|
view,
|
||||||
|
forceHover = false,
|
||||||
|
onTitleChange,
|
||||||
|
showTitle = true,
|
||||||
|
onUpdateIcon: onUpdateIconProp,
|
||||||
|
showCover = false,
|
||||||
|
onUpdateCover,
|
||||||
|
}: Props) {
|
||||||
const [hover, setHover] = useState(false);
|
const [hover, setHover] = useState(false);
|
||||||
const [icon, setIcon] = useState<PageIcon | undefined>(view.icon);
|
const [icon, setIcon] = useState<PageIcon | undefined>(view.icon);
|
||||||
|
|
||||||
@ -38,7 +49,14 @@ function ViewTitle({ view, onTitleChange, showTitle = true, onUpdateIcon: onUpda
|
|||||||
onMouseEnter={() => setHover(true)}
|
onMouseEnter={() => setHover(true)}
|
||||||
onMouseLeave={() => setHover(false)}
|
onMouseLeave={() => setHover(false)}
|
||||||
>
|
>
|
||||||
<ViewBanner icon={icon} hover={hover} onUpdateIcon={onUpdateIcon} />
|
<ViewBanner
|
||||||
|
showCover={showCover}
|
||||||
|
cover={view.cover}
|
||||||
|
icon={icon}
|
||||||
|
hover={hover || forceHover}
|
||||||
|
onUpdateIcon={onUpdateIcon}
|
||||||
|
onUpdateCover={onUpdateCover}
|
||||||
|
/>
|
||||||
{showTitle && (
|
{showTitle && (
|
||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
<ViewTitleInput value={view.name} onChange={onTitleChange} />
|
<ViewTitleInput value={view.name} onChange={onTitleChange} />
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { colorMap } from '$app/utils/color';
|
||||||
|
|
||||||
|
const colors = Object.entries(colorMap);
|
||||||
|
|
||||||
|
function Colors({ onDone }: { onDone?: (value: string) => void }) {
|
||||||
|
return (
|
||||||
|
<div className={'flex flex-wrap justify-center gap-2 p-2 pb-6'}>
|
||||||
|
{colors.map(([name, value]) => (
|
||||||
|
<div
|
||||||
|
key={name}
|
||||||
|
className={'h-9 w-9 cursor-pointer rounded rounded-full'}
|
||||||
|
style={{ backgroundColor: value }}
|
||||||
|
onClick={() => onDone?.(name)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Colors;
|
@ -0,0 +1,91 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { CoverType, PageCover } from '$app_reducers/pages/slice';
|
||||||
|
import { PopoverOrigin } from '@mui/material/Popover';
|
||||||
|
import { EmbedLink, Unsplash, UploadTabs, TabOption, TAB_KEY } from '$app/components/_shared/image_upload';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import Colors from '$app/components/_shared/view_title/cover/Colors';
|
||||||
|
import { ImageType } from '$app/application/document/document.types';
|
||||||
|
|
||||||
|
const initialOrigin: {
|
||||||
|
anchorOrigin: PopoverOrigin;
|
||||||
|
transformOrigin: PopoverOrigin;
|
||||||
|
} = {
|
||||||
|
anchorOrigin: {
|
||||||
|
vertical: 'bottom',
|
||||||
|
horizontal: 'center',
|
||||||
|
},
|
||||||
|
transformOrigin: {
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'center',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function CoverPopover({
|
||||||
|
anchorEl,
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
onUpdateCover,
|
||||||
|
}: {
|
||||||
|
anchorEl: HTMLElement | null;
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onUpdateCover?: (cover?: PageCover) => void;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const tabOptions: TabOption[] = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: t('document.plugins.cover.colors'),
|
||||||
|
key: TAB_KEY.Colors,
|
||||||
|
Component: Colors,
|
||||||
|
onDone: (value: string) => {
|
||||||
|
onUpdateCover?.({
|
||||||
|
cover_selection_type: CoverType.Color,
|
||||||
|
cover_selection: value,
|
||||||
|
image_type: ImageType.Internal,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('document.imageBlock.embedLink.label'),
|
||||||
|
key: TAB_KEY.EMBED_LINK,
|
||||||
|
Component: EmbedLink,
|
||||||
|
onDone: (value: string) => {
|
||||||
|
onUpdateCover?.({
|
||||||
|
cover_selection_type: CoverType.Image,
|
||||||
|
cover_selection: value,
|
||||||
|
image_type: ImageType.External,
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: TAB_KEY.UNSPLASH,
|
||||||
|
label: t('document.imageBlock.unsplash.label'),
|
||||||
|
Component: Unsplash,
|
||||||
|
onDone: (value: string) => {
|
||||||
|
onUpdateCover?.({
|
||||||
|
cover_selection_type: CoverType.Image,
|
||||||
|
cover_selection: value,
|
||||||
|
image_type: ImageType.External,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [onClose, onUpdateCover, t]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UploadTabs
|
||||||
|
popoverProps={{
|
||||||
|
anchorEl,
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
...initialOrigin,
|
||||||
|
}}
|
||||||
|
containerStyle={{ width: 433, maxHeight: 300 }}
|
||||||
|
tabOptions={tabOptions}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CoverPopover;
|
@ -0,0 +1,66 @@
|
|||||||
|
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
|
import { CoverType, PageCover } from '$app_reducers/pages/slice';
|
||||||
|
import { renderColor } from '$app/utils/color';
|
||||||
|
import ViewCoverActions from '$app/components/_shared/view_title/cover/ViewCoverActions';
|
||||||
|
import CoverPopover from '$app/components/_shared/view_title/cover/CoverPopover';
|
||||||
|
import DefaultImage from '$app/assets/images/default_cover.jpg';
|
||||||
|
|
||||||
|
export function ViewCover({ cover, onUpdateCover }: { cover: PageCover; onUpdateCover?: (cover?: PageCover) => void }) {
|
||||||
|
const { cover_selection_type: type, cover_selection: value = '' } = useMemo(() => cover || {}, [cover]);
|
||||||
|
const [showAction, setShowAction] = useState(false);
|
||||||
|
const actionRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [showPopover, setShowPopover] = useState(false);
|
||||||
|
|
||||||
|
const renderCoverColor = useCallback((color: string) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: renderColor(color),
|
||||||
|
}}
|
||||||
|
className={`h-full w-full`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const renderCoverImage = useCallback((url: string) => {
|
||||||
|
return <img draggable={false} src={url} alt={''} className={'h-full w-full object-cover'} />;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleRemoveCover = useCallback(() => {
|
||||||
|
onUpdateCover?.(null);
|
||||||
|
}, [onUpdateCover]);
|
||||||
|
|
||||||
|
const handleClickChange = useCallback(() => {
|
||||||
|
setShowPopover(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onMouseEnter={() => {
|
||||||
|
setShowAction(true);
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
setShowAction(false);
|
||||||
|
}}
|
||||||
|
className={'relative flex h-[255px] w-full'}
|
||||||
|
>
|
||||||
|
{type === CoverType.Asset ? renderCoverImage(DefaultImage) : null}
|
||||||
|
{type === CoverType.Color ? renderCoverColor(value) : null}
|
||||||
|
{type === CoverType.Image ? renderCoverImage(value) : null}
|
||||||
|
<ViewCoverActions
|
||||||
|
show={showAction}
|
||||||
|
ref={actionRef}
|
||||||
|
onRemove={handleRemoveCover}
|
||||||
|
onClickChange={handleClickChange}
|
||||||
|
/>
|
||||||
|
{showPopover && (
|
||||||
|
<CoverPopover
|
||||||
|
open={showPopover}
|
||||||
|
onClose={() => setShowPopover(false)}
|
||||||
|
anchorEl={actionRef.current}
|
||||||
|
onUpdateCover={onUpdateCover}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
import React, { forwardRef } from 'react';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { ReactComponent as DeleteIcon } from '$app/assets/delete.svg';
|
||||||
|
|
||||||
|
function ViewCoverActions(
|
||||||
|
{ show, onRemove, onClickChange }: { show: boolean; onRemove: () => void; onClickChange: () => void },
|
||||||
|
ref: React.ForwardedRef<HTMLDivElement>
|
||||||
|
) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} className={`absolute ${show ? 'flex' : 'opacity-0'} bottom-4 right-10 items-center space-x-2 p-2`}>
|
||||||
|
<div className={'flex items-center space-x-2'}>
|
||||||
|
<Button
|
||||||
|
onClick={onClickChange}
|
||||||
|
className={'min-w-0 p-1.5'}
|
||||||
|
size={'small'}
|
||||||
|
variant={'contained'}
|
||||||
|
color={'inherit'}
|
||||||
|
>
|
||||||
|
{t('document.plugins.cover.changeCover')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={'contained'}
|
||||||
|
size={'small'}
|
||||||
|
className={'min-h-0 min-w-0 p-1.5'}
|
||||||
|
sx={{
|
||||||
|
'.MuiButton-startIcon': {
|
||||||
|
marginRight: '0px',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClick={onRemove}
|
||||||
|
color={'inherit'}
|
||||||
|
startIcon={<DeleteIcon />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default forwardRef(ViewCoverActions);
|
@ -0,0 +1 @@
|
|||||||
|
export * from './ViewCover';
|
@ -1,12 +1,14 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import Editor from '$app/components/editor/Editor';
|
import Editor from '$app/components/editor/Editor';
|
||||||
import { DocumentHeader } from 'src/appflowy_app/components/document/document_header';
|
import { DocumentHeader } from 'src/appflowy_app/components/document/document_header';
|
||||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||||
import { updatePageName } from '$app_reducers/pages/async_actions';
|
import { updatePageName } from '$app_reducers/pages/async_actions';
|
||||||
|
import { PageCover } from '$app_reducers/pages/slice';
|
||||||
|
|
||||||
export function Document({ id }: { id: string }) {
|
export function Document({ id }: { id: string }) {
|
||||||
const page = useAppSelector((state) => state.pages.pageMap[id]);
|
const page = useAppSelector((state) => state.pages.pageMap[id]);
|
||||||
|
|
||||||
|
const [cover, setCover] = useState<PageCover | undefined>(undefined);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const onTitleChange = useCallback(
|
const onTitleChange = useCallback(
|
||||||
@ -21,12 +23,29 @@ export function Document({ id }: { id: string }) {
|
|||||||
[dispatch, id]
|
[dispatch, id]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const view = useMemo(() => {
|
||||||
|
return {
|
||||||
|
...page,
|
||||||
|
cover,
|
||||||
|
};
|
||||||
|
}, [page, cover]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
setCover(undefined);
|
||||||
|
};
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
if (!page) return null;
|
if (!page) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'relative'}>
|
<div className={'relative w-full'}>
|
||||||
<DocumentHeader page={page} />
|
<DocumentHeader onUpdateCover={setCover} page={view} />
|
||||||
<Editor id={id} onTitleChange={onTitleChange} title={page.name} />
|
<div className={'flex w-full justify-center'}>
|
||||||
|
<div className={'max-w-screen w-[964px] min-w-0'}>
|
||||||
|
<Editor id={id} cover={cover} onCoverChange={setCover} onTitleChange={onTitleChange} title={page.name} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
import React, { memo, useCallback } from 'react';
|
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { Page, PageIcon } from '$app_reducers/pages/slice';
|
import { Page, PageCover, PageIcon } from '$app_reducers/pages/slice';
|
||||||
import ViewTitle from '$app/components/_shared/view_title/ViewTitle';
|
import ViewTitle from '$app/components/_shared/view_title/ViewTitle';
|
||||||
import { updatePageIcon } from '$app/application/folder/page.service';
|
import { updatePageIcon } from '$app/application/folder/page.service';
|
||||||
|
|
||||||
interface DocumentHeaderProps {
|
interface DocumentHeaderProps {
|
||||||
page: Page;
|
page: Page;
|
||||||
|
onUpdateCover: (cover?: PageCover) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DocumentHeader({ page }: DocumentHeaderProps) {
|
export function DocumentHeader({ page, onUpdateCover }: DocumentHeaderProps) {
|
||||||
const pageId = page.id;
|
const pageId = page.id;
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [forceHover, setForceHover] = useState(false);
|
||||||
const onUpdateIcon = useCallback(
|
const onUpdateIcon = useCallback(
|
||||||
async (icon: PageIcon) => {
|
async (icon: PageIcon) => {
|
||||||
await updatePageIcon(pageId, icon.value ? icon : undefined);
|
await updatePageIcon(pageId, icon.value ? icon : undefined);
|
||||||
@ -17,10 +20,39 @@ export function DocumentHeader({ page }: DocumentHeaderProps) {
|
|||||||
[pageId]
|
[pageId]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const parent = ref.current?.parentElement;
|
||||||
|
|
||||||
|
if (!parent) return;
|
||||||
|
|
||||||
|
const documentDom = parent.querySelector('.appflowy-editor') as HTMLElement;
|
||||||
|
|
||||||
|
if (!documentDom) return;
|
||||||
|
|
||||||
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
const isMoveInTitle = Boolean(e.target instanceof HTMLElement && e.target.closest('.document-title'));
|
||||||
|
const isMoveInHeader = Boolean(e.target instanceof HTMLElement && e.target.closest('.document-header'));
|
||||||
|
|
||||||
|
setForceHover(isMoveInTitle || isMoveInHeader);
|
||||||
|
};
|
||||||
|
|
||||||
|
documentDom.addEventListener('mousemove', handleMouseMove);
|
||||||
|
return () => {
|
||||||
|
documentDom.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (!page) return null;
|
if (!page) return null;
|
||||||
return (
|
return (
|
||||||
<div className={'document-header select-none px-16 pt-4'}>
|
<div ref={ref} className={'document-header select-none'}>
|
||||||
<ViewTitle showTitle={false} onUpdateIcon={onUpdateIcon} view={page} />
|
<ViewTitle
|
||||||
|
showCover
|
||||||
|
showTitle={false}
|
||||||
|
forceHover={forceHover}
|
||||||
|
onUpdateCover={onUpdateCover}
|
||||||
|
onUpdateIcon={onUpdateIcon}
|
||||||
|
view={page}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -49,6 +49,12 @@ export function wrapFormula(editor: ReactEditor, formula?: string) {
|
|||||||
Transforms.insertNodes(editor, formulaElement, {
|
Transforms.insertNodes(editor, formulaElement, {
|
||||||
select: true,
|
select: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const path = editor.selection?.anchor.path;
|
||||||
|
|
||||||
|
if (path) {
|
||||||
|
editor.select(path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unwrapFormula(editor: ReactEditor) {
|
export function unwrapFormula(editor: ReactEditor) {
|
||||||
|
@ -254,17 +254,23 @@ export const CustomEditor = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
insertMention(editor: ReactEditor, mention: Mention) {
|
insertMention(editor: ReactEditor, mention: Mention) {
|
||||||
const mentionElement = {
|
const mentionElement = [
|
||||||
type: EditorInlineNodeType.Mention,
|
{
|
||||||
children: [{ text: '@' }],
|
type: EditorInlineNodeType.Mention,
|
||||||
data: {
|
children: [{ text: '$' }],
|
||||||
...mention,
|
data: {
|
||||||
|
...mention,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
];
|
||||||
|
|
||||||
Transforms.insertNodes(editor, mentionElement, {
|
Transforms.insertNodes(editor, mentionElement, {
|
||||||
select: true,
|
select: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
editor.collapse({
|
||||||
|
edge: 'end',
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleTodo(editor: ReactEditor, node: TodoListNode) {
|
toggleTodo(editor: ReactEditor, node: TodoListNode) {
|
||||||
|
@ -9,10 +9,8 @@ export const Callout = memo(
|
|||||||
<div contentEditable={false} className={'absolute w-full select-none px-2 pt-[15px]'}>
|
<div contentEditable={false} className={'absolute w-full select-none px-2 pt-[15px]'}>
|
||||||
<CalloutIcon node={node} />
|
<CalloutIcon node={node} />
|
||||||
</div>
|
</div>
|
||||||
<div {...attributes} ref={ref} className={`${attributes.className ?? ''} w-full bg-bg-body py-2`}>
|
<div ref={ref} className={`${attributes.className ?? ''} w-full bg-bg-body py-2`}>
|
||||||
<div
|
<div {...attributes} className={`flex w-full flex-col rounded bg-content-blue-50 py-2 pl-10`}>
|
||||||
className={`flex w-full flex-col rounded border border-solid border-line-divider bg-content-blue-50 py-2 pl-10`}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -24,9 +24,9 @@ export const ImageBlock = memo(
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!selected) onFocusNode();
|
if (!selected) onFocusNode();
|
||||||
}}
|
}}
|
||||||
className={`${className} image-block relative w-full cursor-pointer py-1`}
|
className={`${className} image-block relative w-full cursor-pointer py-1`}
|
||||||
>
|
>
|
||||||
<div ref={ref} className={'absolute left-0 top-0 h-full w-full caret-transparent'}>
|
<div ref={ref} className={'absolute left-0 top-0 h-full w-full select-none caret-transparent'}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
@ -39,7 +39,7 @@ function ImageEmpty({
|
|||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'flex h-[48px] w-full cursor-pointer items-center gap-[10px] bg-content-blue-50 px-4 text-text-caption'
|
'flex h-[48px] w-full cursor-pointer select-none items-center gap-[10px] bg-content-blue-50 px-4 text-text-caption'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ImageIcon />
|
<ImageIcon />
|
||||||
|
@ -1,20 +1,13 @@
|
|||||||
import React, { useCallback, useMemo, SyntheticEvent, useState } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import Popover, { PopoverOrigin } from '@mui/material/Popover/Popover';
|
import { PopoverOrigin } from '@mui/material/Popover/Popover';
|
||||||
import usePopoverAutoPosition from '$app/components/_shared/popover/Popover.hooks';
|
import usePopoverAutoPosition from '$app/components/_shared/popover/Popover.hooks';
|
||||||
import { PopoverCommonProps } from '$app/components/editor/components/tools/popover';
|
|
||||||
import { TabPanel, ViewTab, ViewTabs } from '$app/components/database/components/tab_bar/ViewTabs';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { EmbedLink, Unsplash } from '$app/components/_shared/image_upload';
|
import { EmbedLink, Unsplash, UploadTabs, TabOption, TAB_KEY } from '$app/components/_shared/image_upload';
|
||||||
import SwipeableViews from 'react-swipeable-views';
|
|
||||||
import { CustomEditor } from '$app/components/editor/command';
|
import { CustomEditor } from '$app/components/editor/command';
|
||||||
import { useSlateStatic } from 'slate-react';
|
import { useSlateStatic } from 'slate-react';
|
||||||
import { ImageNode, ImageType } from '$app/application/document/document.types';
|
import { ImageNode, ImageType } from '$app/application/document/document.types';
|
||||||
|
|
||||||
enum TAB_KEY {
|
|
||||||
UPLOAD = 'upload',
|
|
||||||
EMBED_LINK = 'embed_link',
|
|
||||||
UNSPLASH = 'unsplash',
|
|
||||||
}
|
|
||||||
const initialOrigin: {
|
const initialOrigin: {
|
||||||
transformOrigin: PopoverOrigin;
|
transformOrigin: PopoverOrigin;
|
||||||
anchorOrigin: PopoverOrigin;
|
anchorOrigin: PopoverOrigin;
|
||||||
@ -53,7 +46,7 @@ function UploadPopover({
|
|||||||
open,
|
open,
|
||||||
});
|
});
|
||||||
|
|
||||||
const tabOptions = useMemo(() => {
|
const tabOptions: TabOption[] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
// {
|
// {
|
||||||
// label: t('button.upload'),
|
// label: t('button.upload'),
|
||||||
@ -87,102 +80,25 @@ function UploadPopover({
|
|||||||
];
|
];
|
||||||
}, [editor, node, onClose, t]);
|
}, [editor, node, onClose, t]);
|
||||||
|
|
||||||
const [tabValue, setTabValue] = useState<TAB_KEY>(tabOptions[0].key);
|
|
||||||
|
|
||||||
const handleTabChange = useCallback((_: SyntheticEvent, newValue: string) => {
|
|
||||||
setTabValue(newValue as TAB_KEY);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const selectedIndex = tabOptions.findIndex((tab) => tab.key === tabValue);
|
|
||||||
|
|
||||||
const onKeyDown = useCallback(
|
|
||||||
(e: React.KeyboardEvent) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.key === 'Tab') {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setTabValue((prev) => {
|
|
||||||
const currentIndex = tabOptions.findIndex((tab) => tab.key === prev);
|
|
||||||
const nextIndex = (currentIndex + 1) % tabOptions.length;
|
|
||||||
|
|
||||||
return tabOptions[nextIndex]?.key ?? tabOptions[0].key;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[onClose, tabOptions]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<UploadTabs
|
||||||
{...PopoverCommonProps}
|
popoverProps={{
|
||||||
disableAutoFocus={false}
|
anchorEl,
|
||||||
open={open && isEntered}
|
open: open && isEntered,
|
||||||
anchorEl={anchorEl}
|
onClose,
|
||||||
transformOrigin={transformOrigin}
|
transformOrigin,
|
||||||
anchorOrigin={anchorOrigin}
|
anchorOrigin,
|
||||||
onClose={onClose}
|
onMouseDown: (e) => {
|
||||||
onMouseDown={(e) => {
|
e.stopPropagation();
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
onKeyDown={onKeyDown}
|
|
||||||
PaperProps={{
|
|
||||||
style: {
|
|
||||||
padding: 0,
|
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
containerStyle={{
|
||||||
<div
|
maxWidth: paperWidth,
|
||||||
style={{
|
maxHeight: paperHeight,
|
||||||
maxWidth: paperWidth,
|
overflow: 'hidden',
|
||||||
maxHeight: paperHeight,
|
}}
|
||||||
overflow: 'hidden',
|
tabOptions={tabOptions}
|
||||||
}}
|
/>
|
||||||
className={'flex flex-col gap-4'}
|
|
||||||
>
|
|
||||||
<ViewTabs
|
|
||||||
value={tabValue}
|
|
||||||
onChange={handleTabChange}
|
|
||||||
scrollButtons={false}
|
|
||||||
variant='scrollable'
|
|
||||||
allowScrollButtonsMobile
|
|
||||||
className={'min-h-[38px] border-b border-line-divider px-2'}
|
|
||||||
>
|
|
||||||
{tabOptions.map((tab) => {
|
|
||||||
const { key, label } = tab;
|
|
||||||
|
|
||||||
return <ViewTab key={key} iconPosition='start' color='inherit' label={label} value={key} />;
|
|
||||||
})}
|
|
||||||
</ViewTabs>
|
|
||||||
|
|
||||||
<div className={'h-full w-full flex-1 overflow-y-auto overflow-x-hidden'}>
|
|
||||||
<SwipeableViews
|
|
||||||
slideStyle={{
|
|
||||||
overflow: 'hidden',
|
|
||||||
height: '100%',
|
|
||||||
}}
|
|
||||||
axis={'x'}
|
|
||||||
index={selectedIndex}
|
|
||||||
>
|
|
||||||
{tabOptions.map((tab, index) => {
|
|
||||||
const { key, Component, onDone } = tab;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TabPanel className={'flex h-full w-full flex-col'} key={key} index={index} value={selectedIndex}>
|
|
||||||
<Component onDone={onDone} onEscape={onClose} />
|
|
||||||
</TabPanel>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</SwipeableViews>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import { EditorElementProps, PageNode } from '$app/application/document/document
|
|||||||
export const Page = memo(
|
export const Page = memo(
|
||||||
forwardRef<HTMLDivElement, EditorElementProps<PageNode>>(({ node: _, children, ...attributes }, ref) => {
|
forwardRef<HTMLDivElement, EditorElementProps<PageNode>>(({ node: _, children, ...attributes }, ref) => {
|
||||||
const className = useMemo(() => {
|
const className = useMemo(() => {
|
||||||
return `${attributes.className ?? ''} pb-3 text-4xl font-bold`;
|
return `${attributes.className ?? ''} document-title pb-3 text-4xl font-bold`;
|
||||||
}, [attributes.className]);
|
}, [attributes.className]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -5,62 +5,87 @@ import { EditorProps } from '$app/application/document/document.types';
|
|||||||
import { Provider } from '$app/components/editor/provider';
|
import { Provider } from '$app/components/editor/provider';
|
||||||
import { YXmlText } from 'yjs/dist/src/types/YXmlText';
|
import { YXmlText } from 'yjs/dist/src/types/YXmlText';
|
||||||
import { getInsertTarget, getYTarget } from '$app/components/editor/provider/utils/relation';
|
import { getInsertTarget, getYTarget } from '$app/components/editor/provider/utils/relation';
|
||||||
|
import isEqual from 'lodash-es/isEqual';
|
||||||
|
|
||||||
export const CollaborativeEditor = memo(({ id, title, showTitle = true, onTitleChange, disableFocus }: EditorProps) => {
|
export const CollaborativeEditor = memo(
|
||||||
const [sharedType, setSharedType] = useState<YXmlText | null>(null);
|
({ id, title, cover, showTitle = true, onTitleChange, onCoverChange, ...props }: EditorProps) => {
|
||||||
const provider = useMemo(() => {
|
const [sharedType, setSharedType] = useState<YXmlText | null>(null);
|
||||||
setSharedType(null);
|
const provider = useMemo(() => {
|
||||||
return new Provider(id, showTitle);
|
setSharedType(null);
|
||||||
}, [id, showTitle]);
|
return new Provider(id, showTitle);
|
||||||
|
}, [id, showTitle]);
|
||||||
|
|
||||||
const root = useMemo(() => {
|
const root = useMemo(() => {
|
||||||
if (!showTitle || !sharedType || !sharedType.doc) return null;
|
if (!showTitle || !sharedType || !sharedType.doc) return null;
|
||||||
|
|
||||||
return getYTarget(sharedType?.doc, [0]);
|
return getYTarget(sharedType?.doc, [0]);
|
||||||
}, [sharedType, showTitle]);
|
}, [sharedType, showTitle]);
|
||||||
|
|
||||||
const rootText = useMemo(() => {
|
const rootText = useMemo(() => {
|
||||||
if (!root) return null;
|
if (!root) return null;
|
||||||
return getInsertTarget(root, [0]);
|
return getInsertTarget(root, [0]);
|
||||||
}, [root]);
|
}, [root]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!rootText || rootText.toString() === title) return;
|
if (!rootText || rootText.toString() === title) return;
|
||||||
|
|
||||||
if (rootText.length > 0) {
|
if (rootText.length > 0) {
|
||||||
rootText.delete(0, rootText.length);
|
rootText.delete(0, rootText.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
rootText.insert(0, title || '');
|
||||||
|
}, [title, rootText]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!root) return;
|
||||||
|
|
||||||
|
const originalCover = root.getAttribute('data')?.cover;
|
||||||
|
|
||||||
|
if (cover === undefined) return;
|
||||||
|
if (isEqual(originalCover, cover)) return;
|
||||||
|
root.setAttribute('data', { cover: cover ? cover : undefined });
|
||||||
|
}, [cover, root]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!root) return;
|
||||||
|
const rootId = root.getAttribute('blockId');
|
||||||
|
|
||||||
|
if (!rootId) return;
|
||||||
|
|
||||||
|
const getCover = () => {
|
||||||
|
const data = root.getAttribute('data');
|
||||||
|
|
||||||
|
onCoverChange?.(data?.cover);
|
||||||
|
};
|
||||||
|
|
||||||
|
getCover();
|
||||||
|
const onChange = () => {
|
||||||
|
onTitleChange?.(root.toString());
|
||||||
|
getCover();
|
||||||
|
};
|
||||||
|
|
||||||
|
root.observeDeep(onChange);
|
||||||
|
return () => root.unobserveDeep(onChange);
|
||||||
|
}, [onTitleChange, root, onCoverChange]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
provider.connect();
|
||||||
|
const handleConnected = () => {
|
||||||
|
setSharedType(provider.sharedType);
|
||||||
|
};
|
||||||
|
|
||||||
|
provider.on('ready', handleConnected);
|
||||||
|
return () => {
|
||||||
|
setSharedType(null);
|
||||||
|
provider.off('ready', handleConnected);
|
||||||
|
provider.disconnect();
|
||||||
|
};
|
||||||
|
}, [provider]);
|
||||||
|
|
||||||
|
if (!sharedType || id !== provider.id) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
rootText.insert(0, title || '');
|
return <Editor sharedType={sharedType} id={id} {...props} />;
|
||||||
}, [title, rootText]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!root) return;
|
|
||||||
const onChange = () => {
|
|
||||||
onTitleChange?.(root.toString());
|
|
||||||
};
|
|
||||||
|
|
||||||
root.observeDeep(onChange);
|
|
||||||
return () => root.unobserveDeep(onChange);
|
|
||||||
}, [onTitleChange, root]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
provider.connect();
|
|
||||||
const handleConnected = () => {
|
|
||||||
setSharedType(provider.sharedType);
|
|
||||||
};
|
|
||||||
|
|
||||||
provider.on('ready', handleConnected);
|
|
||||||
return () => {
|
|
||||||
setSharedType(null);
|
|
||||||
provider.off('ready', handleConnected);
|
|
||||||
provider.disconnect();
|
|
||||||
};
|
|
||||||
}, [provider]);
|
|
||||||
|
|
||||||
if (!sharedType || id !== provider.id) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
return <Editor sharedType={sharedType} id={id} disableFocus={disableFocus} />;
|
|
||||||
});
|
|
||||||
|
@ -11,7 +11,6 @@ import { useShortcuts } from 'src/appflowy_app/components/editor/plugins/shortcu
|
|||||||
import { BlockActionsToolbar } from '$app/components/editor/components/tools/block_actions';
|
import { BlockActionsToolbar } from '$app/components/editor/components/tools/block_actions';
|
||||||
|
|
||||||
import { CircularProgress } from '@mui/material';
|
import { CircularProgress } from '@mui/material';
|
||||||
import * as Y from 'yjs';
|
|
||||||
import { NodeEntry } from 'slate';
|
import { NodeEntry } from 'slate';
|
||||||
import {
|
import {
|
||||||
DecorateStateProvider,
|
DecorateStateProvider,
|
||||||
@ -22,8 +21,9 @@ import {
|
|||||||
} from '$app/components/editor/stores';
|
} from '$app/components/editor/stores';
|
||||||
import CommandPanel from '../tools/command_panel/CommandPanel';
|
import CommandPanel from '../tools/command_panel/CommandPanel';
|
||||||
import { EditorBlockStateProvider } from '$app/components/editor/stores/block';
|
import { EditorBlockStateProvider } from '$app/components/editor/stores/block';
|
||||||
|
import { LocalEditorProps } from '$app/application/document/document.types';
|
||||||
|
|
||||||
function Editor({ sharedType, disableFocus }: { sharedType: Y.XmlText; id: string; disableFocus?: boolean }) {
|
function Editor({ sharedType, disableFocus, caretColor = 'var(--text-title)' }: LocalEditorProps) {
|
||||||
const { editor, initialValue, handleOnClickEnd, ...props } = useEditor(sharedType);
|
const { editor, initialValue, handleOnClickEnd, ...props } = useEditor(sharedType);
|
||||||
const decorateCodeHighlight = useDecorateCodeHighlight(editor);
|
const decorateCodeHighlight = useDecorateCodeHighlight(editor);
|
||||||
const { onKeyDown: onShortcutsKeyDown } = useShortcuts(editor);
|
const { onKeyDown: onShortcutsKeyDown } = useShortcuts(editor);
|
||||||
@ -74,7 +74,10 @@ function Editor({ sharedType, disableFocus }: { sharedType: Y.XmlText; id: strin
|
|||||||
disableFocus={disableFocus}
|
disableFocus={disableFocus}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
decorate={decorate}
|
decorate={decorate}
|
||||||
className={'px-16 caret-text-title outline-none focus:outline-none'}
|
style={{
|
||||||
|
caretColor,
|
||||||
|
}}
|
||||||
|
className={`px-16 outline-none focus:outline-none`}
|
||||||
/>
|
/>
|
||||||
<CommandPanel />
|
<CommandPanel />
|
||||||
<div onClick={handleOnClickEnd} className={'relative bottom-0 left-0 h-10 w-full cursor-text'} />
|
<div onClick={handleOnClickEnd} className={'relative bottom-0 left-0 h-10 w-full cursor-text'} />
|
||||||
|
@ -27,6 +27,7 @@ import { Text as TextComponent } from '../blocks/text';
|
|||||||
import { Page } from '../blocks/page';
|
import { Page } from '../blocks/page';
|
||||||
import { useElementState } from '$app/components/editor/components/editor/Element.hooks';
|
import { useElementState } from '$app/components/editor/components/editor/Element.hooks';
|
||||||
import UnSupportBlock from '$app/components/editor/components/blocks/_shared/unSupportBlock';
|
import UnSupportBlock from '$app/components/editor/components/blocks/_shared/unSupportBlock';
|
||||||
|
import { renderColor } from '$app/utils/color';
|
||||||
|
|
||||||
function Element({ element, attributes, children }: RenderElementProps) {
|
function Element({ element, attributes, children }: RenderElementProps) {
|
||||||
const node = element;
|
const node = element;
|
||||||
@ -98,8 +99,8 @@ function Element({ element, attributes, children }: RenderElementProps) {
|
|||||||
const data = (node.data as BlockData) || {};
|
const data = (node.data as BlockData) || {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
backgroundColor: data.bg_color,
|
backgroundColor: data.bg_color ? renderColor(data.bg_color) : undefined,
|
||||||
color: data.font_color,
|
color: data.font_color ? renderColor(data.font_color) : undefined,
|
||||||
};
|
};
|
||||||
}, [node.data]);
|
}, [node.data]);
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React, { CSSProperties } from 'react';
|
import React, { CSSProperties } from 'react';
|
||||||
import { RenderLeafProps } from 'slate-react';
|
import { RenderLeafProps } from 'slate-react';
|
||||||
import { Link } from '$app/components/editor/components/inline_nodes/link';
|
import { Link } from '$app/components/editor/components/inline_nodes/link';
|
||||||
|
import { renderColor } from '$app/utils/color';
|
||||||
|
|
||||||
export function Leaf({ attributes, children, leaf }: RenderLeafProps) {
|
export function Leaf({ attributes, children, leaf }: RenderLeafProps) {
|
||||||
let newChildren = children;
|
let newChildren = children;
|
||||||
@ -39,11 +40,11 @@ export function Leaf({ attributes, children, leaf }: RenderLeafProps) {
|
|||||||
const style: CSSProperties = {};
|
const style: CSSProperties = {};
|
||||||
|
|
||||||
if (leaf.font_color) {
|
if (leaf.font_color) {
|
||||||
style['color'] = leaf.font_color.replace('0x', '#');
|
style['color'] = renderColor(leaf.font_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (leaf.bg_color) {
|
if (leaf.bg_color) {
|
||||||
style['backgroundColor'] = leaf.bg_color.replace('0x', '#');
|
style['backgroundColor'] = renderColor(leaf.bg_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (leaf.href) {
|
if (leaf.href) {
|
||||||
|
@ -3,10 +3,10 @@ import React from 'react';
|
|||||||
// Put this at the start and end of an inline component to work around this Chromium bug:
|
// Put this at the start and end of an inline component to work around this Chromium bug:
|
||||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=1249405
|
// https://bugs.chromium.org/p/chromium/issues/detail?id=1249405
|
||||||
|
|
||||||
export const InlineChromiumBugfix = () => (
|
export const InlineChromiumBugfix = ({ className }: { className?: string }) => (
|
||||||
<span
|
<span
|
||||||
contentEditable={false}
|
contentEditable={false}
|
||||||
className={'absolute caret-transparent'}
|
className={`absolute caret-transparent ${className ?? ''}`}
|
||||||
style={{
|
style={{
|
||||||
fontSize: 0,
|
fontSize: 0,
|
||||||
}}
|
}}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { forwardRef, memo, useCallback, MouseEvent, useRef } from 'react';
|
import React, { forwardRef, memo, useCallback, MouseEvent, useRef, useEffect } from 'react';
|
||||||
import { ReactEditor, useSelected, useSlate } from 'slate-react';
|
import { ReactEditor, useSelected, useSlate } from 'slate-react';
|
||||||
import { Transforms } from 'slate';
|
import { Editor, Range, Transforms } from 'slate';
|
||||||
import { EditorElementProps, FormulaNode } from '$app/application/document/document.types';
|
import { EditorElementProps, FormulaNode } from '$app/application/document/document.types';
|
||||||
import FormulaLeaf from '$app/components/editor/components/inline_nodes/inline_formula/FormulaLeaf';
|
import FormulaLeaf from '$app/components/editor/components/inline_nodes/inline_formula/FormulaLeaf';
|
||||||
import FormulaEditPopover from '$app/components/editor/components/inline_nodes/inline_formula/FormulaEditPopover';
|
import FormulaEditPopover from '$app/components/editor/components/inline_nodes/inline_formula/FormulaEditPopover';
|
||||||
@ -18,17 +18,27 @@ export const InlineFormula = memo(
|
|||||||
const selected = useSelected();
|
const selected = useSelected();
|
||||||
const open = Boolean(popoverOpen && selected);
|
const open = Boolean(popoverOpen && selected);
|
||||||
|
|
||||||
|
const isCollapsed = editor.selection && Range.isCollapsed(editor.selection);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selected && isCollapsed && !open) {
|
||||||
|
const afterPoint = editor.selection ? editor.after(editor.selection) : undefined;
|
||||||
|
|
||||||
|
const afterStart = afterPoint ? Editor.start(editor, afterPoint) : undefined;
|
||||||
|
|
||||||
|
if (afterStart) {
|
||||||
|
editor.select(afterStart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [editor, isCollapsed, selected, open]);
|
||||||
|
|
||||||
const handleClick = useCallback(
|
const handleClick = useCallback(
|
||||||
(e: MouseEvent<HTMLSpanElement>) => {
|
(e: MouseEvent<HTMLSpanElement>) => {
|
||||||
const target = e.currentTarget;
|
const target = e.currentTarget;
|
||||||
const path = getNodePath(editor, target);
|
const path = getNodePath(editor, target);
|
||||||
|
|
||||||
ReactEditor.focus(editor);
|
setRange(path);
|
||||||
Transforms.select(editor, path);
|
openPopover();
|
||||||
if (editor.selection) {
|
|
||||||
setRange(editor.selection);
|
|
||||||
openPopover();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[editor, openPopover, setRange]
|
[editor, openPopover, setRange]
|
||||||
);
|
);
|
||||||
@ -103,9 +113,9 @@ export const InlineFormula = memo(
|
|||||||
selected ? 'selected' : ''
|
selected ? 'selected' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<InlineChromiumBugfix />
|
<InlineChromiumBugfix className={'left-0'} />
|
||||||
<FormulaLeaf formula={formula}>{children}</FormulaLeaf>
|
<FormulaLeaf formula={formula}>{children}</FormulaLeaf>
|
||||||
<InlineChromiumBugfix />
|
<InlineChromiumBugfix className={'right-0'} />
|
||||||
</span>
|
</span>
|
||||||
{open && (
|
{open && (
|
||||||
<FormulaEditPopover
|
<FormulaEditPopover
|
||||||
|
@ -7,13 +7,12 @@ import { InlineChromiumBugfix } from '$app/components/editor/components/inline_n
|
|||||||
export const Mention = memo(
|
export const Mention = memo(
|
||||||
forwardRef<HTMLSpanElement, EditorElementProps<MentionNode>>(({ node, children, ...attributes }, ref) => {
|
forwardRef<HTMLSpanElement, EditorElementProps<MentionNode>>(({ node, children, ...attributes }, ref) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<span {...attributes} contentEditable={false} className={`relative cursor-pointer`} ref={ref}>
|
||||||
<span {...attributes} contentEditable={false} ref={ref}>
|
<InlineChromiumBugfix className={'left-0'} />
|
||||||
<InlineChromiumBugfix />
|
<span className={'absolute right-0 top-0 h-full w-0 opacity-0'}>{children}</span>
|
||||||
<MentionLeaf mention={node.data}>{children}</MentionLeaf>
|
<MentionLeaf mention={node.data} />
|
||||||
<InlineChromiumBugfix />
|
<InlineChromiumBugfix className={'right-0'} />
|
||||||
</span>
|
</span>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -5,23 +5,44 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { pageTypeMap } from '$app_reducers/pages/slice';
|
import { pageTypeMap } from '$app_reducers/pages/slice';
|
||||||
import { getPage } from '$app/application/folder/page.service';
|
import { getPage } from '$app/application/folder/page.service';
|
||||||
import { useSelected } from 'slate-react';
|
import { useSelected, useSlate } from 'slate-react';
|
||||||
import { ReactComponent as EyeClose } from '$app/assets/eye_close.svg';
|
import { ReactComponent as EyeClose } from '$app/assets/eye_close.svg';
|
||||||
import { notify } from 'src/appflowy_app/components/_shared/notify';
|
import { notify } from 'src/appflowy_app/components/_shared/notify';
|
||||||
import { subscribeNotifications } from '$app/application/notification';
|
import { subscribeNotifications } from '$app/application/notification';
|
||||||
import { FolderNotification } from '@/services/backend';
|
import { FolderNotification } from '@/services/backend';
|
||||||
|
import { Editor, Range } from 'slate';
|
||||||
|
|
||||||
export function MentionLeaf({ children, mention }: { mention: Mention; children: React.ReactNode }) {
|
export function MentionLeaf({ mention }: { mention: Mention }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [page, setPage] = useState<MentionPage | null>(null);
|
const [page, setPage] = useState<MentionPage | null>(null);
|
||||||
const [error, setError] = useState<boolean>(false);
|
const [error, setError] = useState<boolean>(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const editor = useSlate();
|
||||||
const selected = useSelected();
|
const selected = useSelected();
|
||||||
|
const isCollapsed = editor.selection && Range.isCollapsed(editor.selection);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selected && isCollapsed && page) {
|
||||||
|
const afterPoint = editor.selection ? editor.after(editor.selection) : undefined;
|
||||||
|
|
||||||
|
const afterStart = afterPoint ? Editor.start(editor, afterPoint) : undefined;
|
||||||
|
|
||||||
|
if (afterStart) {
|
||||||
|
editor.select(afterStart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [editor, isCollapsed, selected, page]);
|
||||||
|
|
||||||
const loadPage = useCallback(async () => {
|
const loadPage = useCallback(async () => {
|
||||||
setError(true);
|
setError(true);
|
||||||
if (!mention.page) return;
|
// keep old field for backward compatibility
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-expect-error
|
||||||
|
const pageId = mention.page_id ?? mention.page;
|
||||||
|
|
||||||
|
if (!pageId) return;
|
||||||
try {
|
try {
|
||||||
const page = await getPage(mention.page);
|
const page = await getPage(pageId);
|
||||||
|
|
||||||
setPage(page);
|
setPage(page);
|
||||||
setError(false);
|
setError(false);
|
||||||
@ -29,7 +50,7 @@ export function MentionLeaf({ children, mention }: { mention: Mention; children:
|
|||||||
setPage(null);
|
setPage(null);
|
||||||
setError(true);
|
setError(true);
|
||||||
}
|
}
|
||||||
}, [mention.page]);
|
}, [mention]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void loadPage();
|
void loadPage();
|
||||||
@ -94,31 +115,27 @@ export function MentionLeaf({ children, mention }: { mention: Mention; children:
|
|||||||
}, [page]);
|
}, [page]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={'relative'}>
|
<span
|
||||||
<span
|
className={`mention-inline mx-1 inline-flex select-none items-center gap-1`}
|
||||||
className={'relative mx-1 inline-flex cursor-pointer items-center hover:rounded hover:bg-content-blue-100'}
|
onClick={openPage}
|
||||||
onClick={openPage}
|
contentEditable={false}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: selected ? 'var(--content-blue-100)' : undefined,
|
backgroundColor: selected ? 'var(--content-blue-100)' : undefined,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{page && (
|
{error ? (
|
||||||
|
<>
|
||||||
|
<EyeClose />
|
||||||
|
<span className={'mr-0.5 text-text-caption underline'}>{t('document.mention.deleted')}</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
page && (
|
||||||
<>
|
<>
|
||||||
<span className={'text-sx absolute left-0.5'}>{page.icon?.value || <DocumentSvg />}</span>
|
{page.icon?.value || <DocumentSvg />}
|
||||||
<span className={'ml-6 mr-0.5 underline'}>{page.name || t('document.title.placeholder')}</span>
|
<span className={'mr-1 underline'}>{page.name || t('document.title.placeholder')}</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)
|
||||||
{error && (
|
)}
|
||||||
<>
|
|
||||||
<span className={'text-sx absolute left-0.5'}>
|
|
||||||
<EyeClose />
|
|
||||||
</span>
|
|
||||||
<span className={'ml-6 mr-0.5 text-text-caption underline'}>{t('document.mention.deleted')}</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className={'invisible'}>{children}</span>
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import KeyboardNavigation, {
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { TitleOutlined } from '@mui/icons-material';
|
import { TitleOutlined } from '@mui/icons-material';
|
||||||
import { EditorMarkFormat } from '$app/application/document/document.types';
|
import { EditorMarkFormat } from '$app/application/document/document.types';
|
||||||
|
import { ColorEnum, renderColor } from '$app/utils/color';
|
||||||
|
|
||||||
export interface ColorPickerProps {
|
export interface ColorPickerProps {
|
||||||
onChange?: (format: EditorMarkFormat.FontColor | EditorMarkFormat.BgColor, color: string) => void;
|
onChange?: (format: EditorMarkFormat.FontColor | EditorMarkFormat.BgColor, color: string) => void;
|
||||||
@ -39,8 +40,8 @@ export function ColorPicker({ onEscape, onChange, disableFocus }: ColorPickerPro
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: backgroundColor ?? 'transparent',
|
backgroundColor: backgroundColor ? renderColor(backgroundColor) : 'transparent',
|
||||||
color: color === '' ? 'var(--text-title)' : color,
|
color: color === '' ? 'var(--text-title)' : renderColor(color),
|
||||||
}}
|
}}
|
||||||
className={'flex h-5 w-5 items-center justify-center rounded border border-line-divider'}
|
className={'flex h-5 w-5 items-center justify-center rounded border border-line-divider'}
|
||||||
>
|
>
|
||||||
@ -118,40 +119,40 @@ export function ColorPicker({ onEscape, onChange, disableFocus }: ColorPickerPro
|
|||||||
content: renderColorItem(t('editor.backgroundColorDefault'), '', ''),
|
content: renderColorItem(t('editor.backgroundColorDefault'), '', ''),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: `bg-gray-rgba(161,161,159,0.61)`,
|
key: `bg-lime-${ColorEnum.Lime}`,
|
||||||
content: renderColorItem(t('editor.backgroundColorGray'), '', 'rgba(161,161,159,0.61)'),
|
content: renderColorItem(t('editor.backgroundColorLime'), '', ColorEnum.Lime),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: `bg-brown-rgba(178,93,37,0.65)`,
|
key: `bg-aqua-${ColorEnum.Aqua}`,
|
||||||
content: renderColorItem(t('editor.backgroundColorBrown'), '', 'rgba(178,93,37,0.65)'),
|
content: renderColorItem(t('editor.backgroundColorAqua'), '', ColorEnum.Aqua),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: `bg-orange-rgba(248,156,71,0.65)`,
|
key: `bg-orange-${ColorEnum.Orange}`,
|
||||||
content: renderColorItem(t('editor.backgroundColorOrange'), '', 'rgba(248,156,71,0.65)'),
|
content: renderColorItem(t('editor.backgroundColorOrange'), '', ColorEnum.Orange),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: `bg-yellow-rgba(229,197,137,0.6)`,
|
key: `bg-yellow-${ColorEnum.Yellow}`,
|
||||||
content: renderColorItem(t('editor.backgroundColorYellow'), '', 'rgba(229,197,137,0.6)'),
|
content: renderColorItem(t('editor.backgroundColorYellow'), '', ColorEnum.Yellow),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: `bg-green-rgba(124,189,111,0.65)`,
|
key: `bg-green-${ColorEnum.Green}`,
|
||||||
content: renderColorItem(t('editor.backgroundColorGreen'), '', 'rgba(124,189,111,0.65)'),
|
content: renderColorItem(t('editor.backgroundColorGreen'), '', ColorEnum.Green),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: `bg-blue-rgba(100,174,199,0.71)`,
|
key: `bg-blue-${ColorEnum.Blue}`,
|
||||||
content: renderColorItem(t('editor.backgroundColorBlue'), '', 'rgba(100,174,199,0.71)'),
|
content: renderColorItem(t('editor.backgroundColorBlue'), '', ColorEnum.Blue),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: `bg-purple-rgba(182,114,234,0.63)`,
|
key: `bg-purple-${ColorEnum.Purple}`,
|
||||||
content: renderColorItem(t('editor.backgroundColorPurple'), '', 'rgba(182,114,234,0.63)'),
|
content: renderColorItem(t('editor.backgroundColorPurple'), '', ColorEnum.Purple),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: `bg-pink-rgba(238,142,179,0.6)`,
|
key: `bg-pink-${ColorEnum.Pink}`,
|
||||||
content: renderColorItem(t('editor.backgroundColorPink'), '', 'rgba(238,142,179,0.6)'),
|
content: renderColorItem(t('editor.backgroundColorPink'), '', ColorEnum.Pink),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: `bg-red-rgba(238,88,98,0.64)`,
|
key: `bg-red-${ColorEnum.LightPink}`,
|
||||||
content: renderColorItem(t('editor.backgroundColorRed'), '', 'rgba(238,88,98,0.64)'),
|
content: renderColorItem(t('editor.backgroundColorRed'), '', ColorEnum.LightPink),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -26,6 +26,7 @@ export const canSetColorBlocks: EditorNodeType[] = [
|
|||||||
EditorNodeType.NumberedListBlock,
|
EditorNodeType.NumberedListBlock,
|
||||||
EditorNodeType.ToggleListBlock,
|
EditorNodeType.ToggleListBlock,
|
||||||
EditorNodeType.QuoteBlock,
|
EditorNodeType.QuoteBlock,
|
||||||
|
EditorNodeType.CalloutBlock,
|
||||||
];
|
];
|
||||||
|
|
||||||
export function BlockOperationMenu({
|
export function BlockOperationMenu({
|
||||||
|
@ -52,7 +52,8 @@ export function useMentionPanel({
|
|||||||
|
|
||||||
closePanel(true);
|
closePanel(true);
|
||||||
CustomEditor.insertMention(editor, {
|
CustomEditor.insertMention(editor, {
|
||||||
page: id,
|
page_id: id,
|
||||||
|
type: MentionType.PageRef,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[closePanel, editor]
|
[closePanel, editor]
|
||||||
|
@ -17,7 +17,6 @@ import { ReactComponent as GridIcon } from '$app/assets/grid.svg';
|
|||||||
import { ReactComponent as ImageIcon } from '$app/assets/image.svg';
|
import { ReactComponent as ImageIcon } from '$app/assets/image.svg';
|
||||||
import { DataObjectOutlined, FunctionsOutlined, HorizontalRuleOutlined, MenuBookOutlined } from '@mui/icons-material';
|
import { DataObjectOutlined, FunctionsOutlined, HorizontalRuleOutlined, MenuBookOutlined } from '@mui/icons-material';
|
||||||
import { CustomEditor } from '$app/components/editor/command';
|
import { CustomEditor } from '$app/components/editor/command';
|
||||||
import { randomEmoji } from '$app/utils/emoji';
|
|
||||||
import { KeyboardNavigationOption } from '$app/components/_shared/keyboard_navigation/KeyboardNavigation';
|
import { KeyboardNavigationOption } from '$app/components/_shared/keyboard_navigation/KeyboardNavigation';
|
||||||
import { YjsEditor } from '@slate-yjs/core';
|
import { YjsEditor } from '@slate-yjs/core';
|
||||||
import { useEditorBlockDispatch } from '$app/components/editor/stores/block';
|
import { useEditorBlockDispatch } from '$app/components/editor/stores/block';
|
||||||
@ -133,7 +132,7 @@ export function useSlashCommandPanel({
|
|||||||
|
|
||||||
if (nodeType === EditorNodeType.CalloutBlock) {
|
if (nodeType === EditorNodeType.CalloutBlock) {
|
||||||
Object.assign(data, {
|
Object.assign(data, {
|
||||||
icon: randomEmoji(),
|
icon: '📌',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,6 +27,8 @@ export function Formula() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
|
const selection = editor.selection;
|
||||||
|
|
||||||
if (!selection) return;
|
if (!selection) return;
|
||||||
|
|
||||||
setRange(selection);
|
setRange(selection);
|
||||||
|
@ -83,7 +83,7 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.text-content {
|
.text-content, [data-dark-mode="true"] .text-content {
|
||||||
&.empty-content {
|
&.empty-content {
|
||||||
@apply min-w-[1px];
|
@apply min-w-[1px];
|
||||||
span {
|
span {
|
||||||
@ -94,7 +94,7 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-element:has(.text-placeholder), .divider-node {
|
.text-element:has(.text-placeholder), .divider-node, [data-dark-mode="true"] .text-element:has(.text-placeholder), [data-dark-mode="true"] .divider-node {
|
||||||
::selection {
|
::selection {
|
||||||
@apply bg-transparent;
|
@apply bg-transparent;
|
||||||
}
|
}
|
||||||
@ -151,8 +151,14 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-block, .math-equation-block {
|
.image-block, .math-equation-block, [data-dark-mode="true"] .image-block, [data-dark-mode="true"] .math-equation-block {
|
||||||
::selection {
|
::selection {
|
||||||
@apply bg-transparent;
|
@apply bg-transparent;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-inline {
|
||||||
|
&:hover {
|
||||||
|
@apply bg-fill-list-active rounded;
|
||||||
|
}
|
||||||
}
|
}
|
@ -28,8 +28,11 @@ export class Provider extends EventEmitter {
|
|||||||
sharedType.applyDelta(delta);
|
sharedType.applyDelta(delta);
|
||||||
|
|
||||||
const rootId = this.dataClient.rootId as string;
|
const rootId = this.dataClient.rootId as string;
|
||||||
|
const root = delta[0].insert as Y.XmlText;
|
||||||
|
const data = root.getAttribute('data');
|
||||||
|
|
||||||
sharedType.setAttribute('blockId', rootId);
|
sharedType.setAttribute('blockId', rootId);
|
||||||
|
sharedType.setAttribute('data', data);
|
||||||
|
|
||||||
this.sharedType = sharedType;
|
this.sharedType = sharedType;
|
||||||
this.sharedType?.observeDeep(this.onChange);
|
this.sharedType?.observeDeep(this.onChange);
|
||||||
|
@ -7,76 +7,90 @@ export function generateId() {
|
|||||||
return nanoid(10);
|
return nanoid(10);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function transformToInlineElement(op: Op): Element | null {
|
export function transformToInlineElement(op: Op): Element[] {
|
||||||
const attributes = op.attributes;
|
const attributes = op.attributes;
|
||||||
|
|
||||||
if (!attributes) return null;
|
if (!attributes) return [];
|
||||||
const { formula, mention, ...attrs } = attributes;
|
const { formula, mention, ...attrs } = attributes;
|
||||||
|
|
||||||
if (formula) {
|
if (formula) {
|
||||||
return {
|
const texts = (op.insert as string).split('');
|
||||||
type: EditorInlineNodeType.Formula,
|
|
||||||
data: formula,
|
return texts.map((text) => {
|
||||||
children: [
|
return {
|
||||||
{
|
type: EditorInlineNodeType.Formula,
|
||||||
text: op.insert as string,
|
data: formula,
|
||||||
...attrs,
|
children: [
|
||||||
},
|
{
|
||||||
],
|
text,
|
||||||
};
|
...attrs,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mention) {
|
if (mention) {
|
||||||
return {
|
const texts = (op.insert as string).split('');
|
||||||
type: EditorInlineNodeType.Mention,
|
|
||||||
children: [
|
return texts.map((text) => {
|
||||||
{
|
return {
|
||||||
text: op.insert as string,
|
type: EditorInlineNodeType.Mention,
|
||||||
...attrs,
|
children: [
|
||||||
|
{
|
||||||
|
text,
|
||||||
|
...attrs,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
data: {
|
||||||
|
...(mention as Mention),
|
||||||
},
|
},
|
||||||
],
|
};
|
||||||
data: {
|
});
|
||||||
...(mention as Mention),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getInlinesWithDelta(delta?: Op[]): (Text | Element)[] {
|
export function getInlinesWithDelta(delta?: Op[]): (Text | Element)[] {
|
||||||
return delta && delta.length > 0
|
const newDelta: (Text | Element)[] = [];
|
||||||
? delta.map((op) => {
|
|
||||||
const matchInline = transformToInlineElement(op);
|
|
||||||
|
|
||||||
if (matchInline) {
|
if (!delta || !delta.length)
|
||||||
return matchInline;
|
return [
|
||||||
}
|
{
|
||||||
|
text: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
if (op.attributes) {
|
delta.forEach((op) => {
|
||||||
if ('font_color' in op.attributes && op.attributes['font_color'] === '') {
|
const matchInlines = transformToInlineElement(op);
|
||||||
delete op.attributes['font_color'];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('bg_color' in op.attributes && op.attributes['bg_color'] === '') {
|
if (matchInlines.length > 0) {
|
||||||
delete op.attributes['bg_color'];
|
newDelta.push(...matchInlines);
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if ('code' in op.attributes && !op.attributes['code']) {
|
if (op.attributes) {
|
||||||
delete op.attributes['code'];
|
if ('font_color' in op.attributes && op.attributes['font_color'] === '') {
|
||||||
}
|
delete op.attributes['font_color'];
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
if ('bg_color' in op.attributes && op.attributes['bg_color'] === '') {
|
||||||
text: op.insert as string,
|
delete op.attributes['bg_color'];
|
||||||
...op.attributes,
|
}
|
||||||
};
|
|
||||||
})
|
if ('code' in op.attributes && !op.attributes['code']) {
|
||||||
: [
|
delete op.attributes['code'];
|
||||||
{
|
}
|
||||||
text: '',
|
}
|
||||||
},
|
|
||||||
];
|
newDelta.push({
|
||||||
|
text: op.insert as string,
|
||||||
|
...op.attributes,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return newDelta;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertToSlateValue(data: EditorData, includeRoot: boolean): Element[] {
|
export function convertToSlateValue(data: EditorData, includeRoot: boolean): Element[] {
|
||||||
|
@ -1,12 +1,31 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appflowy-scroll-container {
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.workspaces {
|
.workspaces {
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 0px;
|
width: 0px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.MuiPopover-root, .MuiPaper-root {
|
.MuiPopover-root, .MuiPaper-root {
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-icon {
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(156, 156, 156, 0.20);
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,6 +1,8 @@
|
|||||||
import { ViewIconTypePB, ViewLayoutPB, ViewPB } from '@/services/backend';
|
import { ViewIconTypePB, ViewLayoutPB, ViewPB } from '@/services/backend';
|
||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import isEqual from 'lodash-es/isEqual';
|
import isEqual from 'lodash-es/isEqual';
|
||||||
|
import { ImageType } from '$app/application/document/document.types';
|
||||||
|
import { Nullable } from 'unsplash-js/dist/helpers/typescript';
|
||||||
|
|
||||||
export const pageTypeMap = {
|
export const pageTypeMap = {
|
||||||
[ViewLayoutPB.Document]: 'document',
|
[ViewLayoutPB.Document]: 'document',
|
||||||
@ -14,6 +16,7 @@ export interface Page {
|
|||||||
name: string;
|
name: string;
|
||||||
layout: ViewLayoutPB;
|
layout: ViewLayoutPB;
|
||||||
icon?: PageIcon;
|
icon?: PageIcon;
|
||||||
|
cover?: PageCover;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PageIcon {
|
export interface PageIcon {
|
||||||
@ -21,6 +24,17 @@ export interface PageIcon {
|
|||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum CoverType {
|
||||||
|
Color = 'CoverType.color',
|
||||||
|
Image = 'CoverType.file',
|
||||||
|
Asset = 'CoverType.asset',
|
||||||
|
}
|
||||||
|
export type PageCover = Nullable<{
|
||||||
|
image_type?: ImageType;
|
||||||
|
cover_selection_type?: CoverType;
|
||||||
|
cover_selection?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
export function parserViewPBToPage(view: ViewPB): Page {
|
export function parserViewPBToPage(view: ViewPB): Page {
|
||||||
const icon = view.icon;
|
const icon = view.icon;
|
||||||
|
|
||||||
|
31
frontend/appflowy_tauri/src/appflowy_app/utils/color.ts
Normal file
31
frontend/appflowy_tauri/src/appflowy_app/utils/color.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
export enum ColorEnum {
|
||||||
|
Purple = 'appflowy_them_color_tint1',
|
||||||
|
Pink = 'appflowy_them_color_tint2',
|
||||||
|
LightPink = 'appflowy_them_color_tint3',
|
||||||
|
Orange = 'appflowy_them_color_tint4',
|
||||||
|
Yellow = 'appflowy_them_color_tint5',
|
||||||
|
Lime = 'appflowy_them_color_tint6',
|
||||||
|
Green = 'appflowy_them_color_tint7',
|
||||||
|
Aqua = 'appflowy_them_color_tint8',
|
||||||
|
Blue = 'appflowy_them_color_tint9',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const colorMap = {
|
||||||
|
[ColorEnum.Purple]: 'var(--tint-purple)',
|
||||||
|
[ColorEnum.Pink]: 'var(--tint-pink)',
|
||||||
|
[ColorEnum.LightPink]: 'var(--tint-red)',
|
||||||
|
[ColorEnum.Orange]: 'var(--tint-orange)',
|
||||||
|
[ColorEnum.Yellow]: 'var(--tint-yellow)',
|
||||||
|
[ColorEnum.Lime]: 'var(--tint-lime)',
|
||||||
|
[ColorEnum.Green]: 'var(--tint-green)',
|
||||||
|
[ColorEnum.Aqua]: 'var(--tint-aqua)',
|
||||||
|
[ColorEnum.Blue]: 'var(--tint-blue)',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function renderColor(color: string) {
|
||||||
|
if (colorMap[color as ColorEnum]) {
|
||||||
|
return colorMap[color as ColorEnum];
|
||||||
|
}
|
||||||
|
|
||||||
|
return color.replace('0x', '#');
|
||||||
|
}
|
@ -39,12 +39,21 @@ export const getDesignTokens = (isDark: boolean): ThemeOptions => {
|
|||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
contained: {
|
contained: {
|
||||||
color: 'var(--content-on-fill)',
|
color: 'var(--content-on-fill)',
|
||||||
|
boxShadow: 'var(--shadow)',
|
||||||
},
|
},
|
||||||
containedPrimary: {
|
containedPrimary: {
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: 'var(--fill-default)',
|
backgroundColor: 'var(--fill-default)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
containedInherit: {
|
||||||
|
color: 'var(--text-title)',
|
||||||
|
backgroundColor: isDark ? 'rgba(0, 0, 0, 0.4)' : 'rgba(255, 255, 255, 0.4)',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'var(--bg-body)',
|
||||||
|
boxShadow: 'var(--shadow)',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
MuiButtonBase: {
|
MuiButtonBase: {
|
||||||
|
@ -8,13 +8,7 @@ function DocumentPage() {
|
|||||||
const documentId = params.id;
|
const documentId = params.id;
|
||||||
|
|
||||||
if (!documentId) return null;
|
if (!documentId) return null;
|
||||||
return (
|
return <Document id={documentId} />;
|
||||||
<div className={'flex w-full justify-center'}>
|
|
||||||
<div className={'max-w-screen w-[964px] min-w-0'}>
|
|
||||||
<Document id={documentId} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DocumentPage;
|
export default DocumentPage;
|
||||||
|
@ -92,7 +92,7 @@
|
|||||||
--fill-active: #e0f8ff;
|
--fill-active: #e0f8ff;
|
||||||
--fill-list-hover: #e0f8ff;
|
--fill-list-hover: #e0f8ff;
|
||||||
--fill-list-active: #edeef2;
|
--fill-list-active: #edeef2;
|
||||||
--content-blue-400: #00bcf0;
|
--content-blue-400: rgb(0, 188, 240);
|
||||||
--content-blue-300: #52d1f4;
|
--content-blue-300: #52d1f4;
|
||||||
--content-blue-600: #009fd1;
|
--content-blue-600: #009fd1;
|
||||||
--content-blue-100: #e0f8ff;
|
--content-blue-100: #e0f8ff;
|
||||||
@ -111,7 +111,7 @@
|
|||||||
--function-info: #00bcf0;
|
--function-info: #00bcf0;
|
||||||
--tint-purple: #e8e0ff;
|
--tint-purple: #e8e0ff;
|
||||||
--tint-pink: #ffe7ee;
|
--tint-pink: #ffe7ee;
|
||||||
--tint-red: #ffe7ee;
|
--tint-red: #ffdddd;
|
||||||
--tint-lime: #f5ffdc;
|
--tint-lime: #f5ffdc;
|
||||||
--tint-green: #ddffd6;
|
--tint-green: #ddffd6;
|
||||||
--tint-aqua: #defff1;
|
--tint-aqua: #defff1;
|
||||||
|
@ -134,7 +134,7 @@
|
|||||||
"type": "color"
|
"type": "color"
|
||||||
},
|
},
|
||||||
"red": {
|
"red": {
|
||||||
"value": "#ffe7ee",
|
"value": "#ffdddd",
|
||||||
"type": "color"
|
"type": "color"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1239,6 +1239,8 @@
|
|||||||
"backgroundColorPurple": "Purple background",
|
"backgroundColorPurple": "Purple background",
|
||||||
"backgroundColorPink": "Pink background",
|
"backgroundColorPink": "Pink background",
|
||||||
"backgroundColorRed": "Red background",
|
"backgroundColorRed": "Red background",
|
||||||
|
"backgroundColorLime": "Lime background",
|
||||||
|
"backgroundColorAqua": "Aqua background",
|
||||||
"done": "Done",
|
"done": "Done",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"tint1": "Tint 1",
|
"tint1": "Tint 1",
|
||||||
|
Loading…
Reference in New Issue
Block a user