feat: support custom scrollbar for document (#4936)

feat: support keywords for slash list to search

feat: support right-click to copy,pasted,cut

fix: the hint text should follow the align setting

feat: support get/set latest view

feat: support to show snackbar after delete page

fix: some bugs
This commit is contained in:
Kilu.He 2024-03-21 21:12:37 +08:00 committed by GitHub
parent 40b710d140
commit 370f8a6558
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 769 additions and 224 deletions

View File

@ -52,6 +52,7 @@
"react-beautiful-dnd": "^13.1.1", "react-beautiful-dnd": "^13.1.1",
"react-big-calendar": "^1.8.5", "react-big-calendar": "^1.8.5",
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-custom-scrollbars": "^4.2.1",
"react-datepicker": "^4.23.0", "react-datepicker": "^4.23.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-error-boundary": "^3.1.4", "react-error-boundary": "^3.1.4",
@ -79,8 +80,8 @@
"yjs": "^13.5.51" "yjs": "^13.5.51"
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^1.5.6",
"@svgr/plugin-svgo": "^8.0.1", "@svgr/plugin-svgo": "^8.0.1",
"@tauri-apps/cli": "^1.5.6",
"@types/google-protobuf": "^3.15.12", "@types/google-protobuf": "^3.15.12",
"@types/is-hotkey": "^0.1.7", "@types/is-hotkey": "^0.1.7",
"@types/jest": "^29.5.3", "@types/jest": "^29.5.3",
@ -92,6 +93,7 @@
"@types/react": "^18.0.15", "@types/react": "^18.0.15",
"@types/react-beautiful-dnd": "^13.1.3", "@types/react-beautiful-dnd": "^13.1.3",
"@types/react-color": "^3.0.6", "@types/react-color": "^3.0.6",
"@types/react-custom-scrollbars": "^4.0.13",
"@types/react-datepicker": "^4.19.3", "@types/react-datepicker": "^4.19.3",
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.0.6",
"@types/react-katex": "^3.0.0", "@types/react-katex": "^3.0.0",

View File

@ -103,6 +103,9 @@ dependencies:
react-color: react-color:
specifier: ^2.19.3 specifier: ^2.19.3
version: 2.19.3(react@18.2.0) version: 2.19.3(react@18.2.0)
react-custom-scrollbars:
specifier: ^4.2.1
version: 4.2.1(react-dom@18.2.0)(react@18.2.0)
react-datepicker: react-datepicker:
specifier: ^4.23.0 specifier: ^4.23.0
version: 4.23.0(react-dom@18.2.0)(react@18.2.0) version: 4.23.0(react-dom@18.2.0)(react@18.2.0)
@ -219,6 +222,9 @@ devDependencies:
'@types/react-color': '@types/react-color':
specifier: ^3.0.6 specifier: ^3.0.6
version: 3.0.6 version: 3.0.6
'@types/react-custom-scrollbars':
specifier: ^4.0.13
version: 4.0.13
'@types/react-datepicker': '@types/react-datepicker':
specifier: ^4.19.3 specifier: ^4.19.3
version: 4.19.3(react-dom@18.2.0)(react@18.2.0) version: 4.19.3(react-dom@18.2.0)(react@18.2.0)
@ -2346,6 +2352,12 @@ packages:
'@types/reactcss': 1.2.6 '@types/reactcss': 1.2.6
dev: true dev: true
/@types/react-custom-scrollbars@4.0.13:
resolution: {integrity: sha512-t+15reWgAE1jXlrhaZoxjuH/SQf+EG0rzAzSCzTIkSiP5CDT7KhoExNPwIa6uUxtPkjc3gdW/ry7GetLEwCfGA==}
dependencies:
'@types/react': 18.2.6
dev: true
/@types/react-datepicker@4.19.3(react-dom@18.2.0)(react@18.2.0): /@types/react-datepicker@4.19.3(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-85F9eKWu9fGiD9r4KVVMPYAdkJJswR3Wci9PvqplmB6T+D+VbUqPeKtifg96NZ4nEhufjehW+SX4JLrEWVplWw==} resolution: {integrity: sha512-85F9eKWu9fGiD9r4KVVMPYAdkJJswR3Wci9PvqplmB6T+D+VbUqPeKtifg96NZ4nEhufjehW+SX4JLrEWVplWw==}
dependencies: dependencies:
@ -2655,6 +2667,10 @@ packages:
hasBin: true hasBin: true
dev: true dev: true
/add-px-to-style@1.0.0:
resolution: {integrity: sha512-YMyxSlXpPjD8uWekCQGuN40lV4bnZagUwqa2m/uFv1z/tNImSk9fnXVMUI5qwME/zzI3MMQRvjZ+69zyfSSyew==}
dev: false
/agent-base@6.0.2: /agent-base@6.0.2:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'} engines: {node: '>= 6.0.0'}
@ -3343,6 +3359,14 @@ packages:
esutils: 2.0.3 esutils: 2.0.3
dev: true dev: true
/dom-css@2.1.0:
resolution: {integrity: sha512-w9kU7FAbaSh3QKijL6n59ofAhkkmMJ31GclJIz/vyQdjogfyxcB6Zf8CZyibOERI5o0Hxz30VmJS7+7r5fEj2Q==}
dependencies:
add-px-to-style: 1.0.0
prefix-style: 2.0.1
to-camel-case: 1.0.0
dev: false
/dom-helpers@5.2.1: /dom-helpers@5.2.1:
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
dependencies: dependencies:
@ -5426,6 +5450,10 @@ packages:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'} engines: {node: '>=8'}
/performance-now@2.1.0:
resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
dev: false
/picocolors@1.0.0: /picocolors@1.0.0:
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
@ -5518,6 +5546,10 @@ packages:
source-map-js: 1.0.2 source-map-js: 1.0.2
dev: true dev: true
/prefix-style@2.0.1:
resolution: {integrity: sha512-gdr1MBNVT0drzTq95CbSNdsrBDoHGlb2aDJP/FoY+1e+jSDPOb1Cv554gH2MGiSr2WTcXi/zu+NaFzfcHQkfBQ==}
dev: false
/prelude-ls@1.2.1: /prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@ -5686,6 +5718,12 @@ packages:
resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==} resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==}
dev: false dev: false
/raf@3.4.1:
resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==}
dependencies:
performance-now: 2.1.0
dev: false
/react-beautiful-dnd@13.1.1(react-dom@18.2.0)(react@18.2.0): /react-beautiful-dnd@13.1.1(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==} resolution: {integrity: sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==}
peerDependencies: peerDependencies:
@ -5746,6 +5784,19 @@ packages:
tinycolor2: 1.6.0 tinycolor2: 1.6.0
dev: false dev: false
/react-custom-scrollbars@4.2.1(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-VtJTUvZ7kPh/auZWIbBRceGPkE30XBYe+HktFxuMWBR2eVQQ+Ur6yFJMoaYcNpyGq22uYJ9Wx4UAEcC0K+LNPQ==}
peerDependencies:
react: ^0.14.0 || ^15.0.0 || ^16.0.0
react-dom: ^0.14.0 || ^15.0.0 || ^16.0.0
dependencies:
dom-css: 2.1.0
prop-types: 15.8.1
raf: 3.4.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/react-datepicker@4.23.0(react-dom@18.2.0)(react@18.2.0): /react-datepicker@4.23.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-w+msqlOZ14v6H1UknTKtZw/dw9naFMgAOspf59eY130gWpvy5dvKj/bgsFICDdvxB7PtKWxDcbGlAqCloY1d2A==} resolution: {integrity: sha512-w+msqlOZ14v6H1UknTKtZw/dw9naFMgAOspf59eY130gWpvy5dvKj/bgsFICDdvxB7PtKWxDcbGlAqCloY1d2A==}
peerDependencies: peerDependencies:
@ -6627,16 +6678,32 @@ packages:
/tmpl@1.0.5: /tmpl@1.0.5:
resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==}
/to-camel-case@1.0.0:
resolution: {integrity: sha512-nD8pQi5H34kyu1QDMFjzEIYqk0xa9Alt6ZfrdEMuHCFOfTLhDG5pgTu/aAM9Wt9lXILwlXmWP43b8sav0GNE8Q==}
dependencies:
to-space-case: 1.0.0
dev: false
/to-fast-properties@2.0.0: /to-fast-properties@2.0.0:
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
engines: {node: '>=4'} engines: {node: '>=4'}
/to-no-case@1.0.2:
resolution: {integrity: sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg==}
dev: false
/to-regex-range@5.0.1: /to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'} engines: {node: '>=8.0'}
dependencies: dependencies:
is-number: 7.0.0 is-number: 7.0.0
/to-space-case@1.0.0:
resolution: {integrity: sha512-rLdvwXZ39VOn1IxGL3V6ZstoTbwLRckQmn/U8ZDLuWwIXNpuZDhQ3AiRUlhTbOXFVE9C+dR51wM0CBDhk31VcA==}
dependencies:
to-no-case: 1.0.2
dev: false
/tough-cookie@4.1.3: /tough-cookie@4.1.3:
resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==} resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==}
engines: {node: '>=6'} engines: {node: '>=6'}

View File

@ -19,6 +19,7 @@ import {
FolderEventMoveNestedView, FolderEventMoveNestedView,
FolderEventUpdateView, FolderEventUpdateView,
FolderEventUpdateViewIcon, FolderEventUpdateViewIcon,
FolderEventSetLatestView,
} from '@/services/backend/events/flowy-folder'; } from '@/services/backend/events/flowy-folder';
export async function getPage(id: string) { export async function getPage(id: string) {
@ -149,3 +150,17 @@ export const updatePageIcon = async (viewId: string, icon?: PageIcon) => {
return Promise.reject(result.err); return Promise.reject(result.err);
}; };
export async function setLatestOpenedPage(id: string) {
const payload = new ViewIdPB({
value: id,
});
const res = await FolderEventSetLatestView(payload);
if (res.ok) {
return res.val;
}
return Promise.reject(res.err);
}

View File

@ -0,0 +1,55 @@
import { Scrollbars } from 'react-custom-scrollbars';
import React from 'react';
export interface AFScrollerProps {
children: React.ReactNode;
overflowXHidden?: boolean;
overflowYHidden?: boolean;
className?: string;
style?: React.CSSProperties;
}
export const AFScroller = ({ style, children, overflowXHidden, overflowYHidden, className }: AFScrollerProps) => {
return (
<Scrollbars
autoHide
renderThumbHorizontal={(props) => <div {...props} className='appflowy-scrollbar-thumb-horizontal' />}
renderThumbVertical={(props) => <div {...props} className='appflowy-scrollbar-thumb-vertical' />}
{...(overflowXHidden && {
renderTrackHorizontal: (props) => (
<div
{...props}
style={{
display: 'none',
}}
/>
),
})}
{...(overflowYHidden && {
renderTrackVertical: (props) => (
<div
{...props}
style={{
display: 'none',
}}
/>
),
})}
style={style}
renderView={(props) => (
<div
{...props}
style={{
...props.style,
overflowX: overflowXHidden ? 'hidden' : 'auto',
overflowY: overflowYHidden ? 'hidden' : 'auto',
marginRight: 0,
marginBottom: 0,
}}
className={className}
/>
)}
>
{children}
</Scrollbars>
);
};

View File

@ -0,0 +1 @@
export * from './AFScroller';

View File

@ -2,18 +2,17 @@ import { ReactComponent as AppflowyLogo } from '$app/assets/logo.svg';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { LoginButtonGroup } from '$app/components/auth/LoginButtonGroup'; import { LoginButtonGroup } from '$app/components/auth/LoginButtonGroup';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '$app/components/auth/auth.hooks'; import { useAuth } from '$app/components/auth/auth.hooks';
import { Log } from '$app/utils/log';
export const Welcome = () => { export const Welcome = () => {
const { signInAsAnonymous } = useAuth(); const { signInAsAnonymous } = useAuth();
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate();
return ( return (
<> <>
<form onSubmit={(e) => e.preventDefault()} method='POST'> <form onSubmit={(e) => e.preventDefault()} method='POST'>
<div className='relative flex h-screen w-screen flex-col items-center justify-center gap-12 bg-bg-body text-center text-text-title text-text-title'> <div className='relative flex h-screen w-screen flex-col items-center justify-center gap-12 bg-bg-body text-center text-text-title'>
<div className='flex justify-center' id='appflowy'> <div className='flex justify-center' id='appflowy'>
<AppflowyLogo className={'h-16 w-16'} /> <AppflowyLogo className={'h-16 w-16'} />
</div> </div>
@ -33,9 +32,8 @@ export const Welcome = () => {
onClick={async () => { onClick={async () => {
try { try {
await signInAsAnonymous(); await signInAsAnonymous();
navigate('/');
} catch (e) { } catch (e) {
console.error(e); Log.error(e);
} }
}} }}
> >

View File

@ -1,4 +1,4 @@
import { currentUserActions, LoginState } from '$app_reducers/current-user/slice'; import { currentUserActions, LoginState, parseWorkspaceSettingPBToSetting } from '$app_reducers/current-user/slice';
import { AuthenticatorPB, ProviderTypePB, UserNotification, UserProfilePB } from '@/services/backend/events/flowy-user'; import { AuthenticatorPB, ProviderTypePB, UserNotification, UserProfilePB } from '@/services/backend/events/flowy-user';
import { UserService } from '$app/application/user/user.service'; import { UserService } from '$app/application/user/user.service';
import { AuthService } from '$app/application/user/auth.service'; import { AuthService } from '$app/application/user/auth.service';
@ -48,7 +48,7 @@ export const useAuth = () => {
displayName: userProfile.name, displayName: userProfile.name,
iconUrl: userProfile.icon_url, iconUrl: userProfile.icon_url,
isAuthenticated: true, isAuthenticated: true,
workspaceSetting: workspaceSetting, workspaceSetting: workspaceSetting ? parseWorkspaceSettingPBToSetting(workspaceSetting) : undefined,
isLocal, isLocal,
}) })
); );

View File

@ -8,7 +8,6 @@ function FilterSettings({ onToggleCollection }: { onToggleCollection: (forceOpen
const { t } = useTranslation(); const { t } = useTranslation();
const filtersCount = useFiltersCount(); const filtersCount = useFiltersCount();
const highlight = filtersCount > 0; const highlight = filtersCount > 0;
const [filterAnchorEl, setFilterAnchorEl] = useState<null | HTMLElement>(null); const [filterAnchorEl, setFilterAnchorEl] = useState<null | HTMLElement>(null);
const open = Boolean(filterAnchorEl); const open = Boolean(filterAnchorEl);

View File

@ -24,7 +24,7 @@ function PlaceholderContent({ node, ...attributes }: { node: Element; className?
}, [editor, node]); }, [editor, node]);
const className = useMemo(() => { const className = useMemo(() => {
return `text-placeholder ${attributes.className ?? ''}`; return `text-placeholder select-none ${attributes.className ?? ''}`;
}, [attributes.className]); }, [attributes.className]);
const unSelectedPlaceholder = useMemo(() => { const unSelectedPlaceholder = useMemo(() => {

View File

@ -20,7 +20,6 @@ export const Text = memo(
> >
{renderIcon()} {renderIcon()}
<Placeholder isEmpty={isEmpty} node={node} /> <Placeholder isEmpty={isEmpty} node={node} />
<span className={`text-content ${isEmpty ? 'empty-text' : ''}`}>{children}</span> <span className={`text-content ${isEmpty ? 'empty-text' : ''}`}>{children}</span>
</span> </span>
); );

View File

@ -12,8 +12,9 @@ export const CollaborativeEditor = memo(
const [sharedType, setSharedType] = useState<YXmlText | null>(null); const [sharedType, setSharedType] = useState<YXmlText | null>(null);
const provider = useMemo(() => { const provider = useMemo(() => {
setSharedType(null); setSharedType(null);
return new Provider(id, showTitle);
}, [id, showTitle]); return new Provider(id);
}, [id]);
const root = useMemo(() => { const root = useMemo(() => {
if (!showTitle || !sharedType || !sharedType.doc) return null; if (!showTitle || !sharedType || !sharedType.doc) return null;
@ -70,17 +71,18 @@ export const CollaborativeEditor = memo(
useEffect(() => { useEffect(() => {
provider.connect(); provider.connect();
const handleConnected = () => { const handleConnected = () => {
setSharedType(provider.sharedType); setSharedType(provider.sharedType);
}; };
provider.on('ready', handleConnected); provider.on('ready', handleConnected);
void provider.initialDocument(showTitle);
return () => { return () => {
setSharedType(null);
provider.off('ready', handleConnected); provider.off('ready', handleConnected);
provider.disconnect(); provider.disconnect();
}; };
}, [provider]); }, [provider, showTitle]);
if (!sharedType || id !== provider.id) { if (!sharedType || id !== provider.id) {
return null; return null;

View File

@ -47,6 +47,20 @@ export function useEditor(sharedType: Y.XmlText) {
}, [editor]); }, [editor]);
const handleOnClickEnd = useCallback(() => { const handleOnClickEnd = useCallback(() => {
const path = [editor.children.length - 1];
const node = Editor.node(editor, path) as NodeEntry<Element>;
const latestNodeIsEmpty = CustomEditor.isEmptyText(editor, node[0]);
if (latestNodeIsEmpty) {
ReactEditor.focus(editor);
editor.select(path);
editor.collapse({
edge: 'end',
});
return;
}
CustomEditor.insertEmptyLineAtEnd(editor); CustomEditor.insertEmptyLineAtEnd(editor);
}, [editor]); }, [editor]);

View File

@ -1,4 +1,4 @@
import React, { memo, useCallback } from 'react'; import React, { useCallback } from 'react';
import { import {
useDecorateCodeHighlight, useDecorateCodeHighlight,
useEditor, useEditor,
@ -90,4 +90,4 @@ function Editor({ sharedType, disableFocus, caretColor = 'var(--text-title)' }:
); );
} }
export default memo(Editor); export default Editor;

View File

@ -2,8 +2,6 @@ import React, { useCallback, useEffect, useState } from 'react';
import { Mention, MentionPage } from '$app/application/document/document.types'; import { Mention, MentionPage } from '$app/application/document/document.types';
import { ReactComponent as DocumentSvg } from '$app/assets/document.svg'; import { ReactComponent as DocumentSvg } from '$app/assets/document.svg';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
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, useSlate } 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';
@ -11,15 +9,17 @@ 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'; import { Editor, Range } from 'slate';
import { useAppDispatch } from '$app/stores/store';
import { openPage } from '$app_reducers/pages/async_actions';
export function MentionLeaf({ mention }: { mention: Mention }) { 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 editor = useSlate(); const editor = useSlate();
const selected = useSelected(); const selected = useSelected();
const isCollapsed = editor.selection && Range.isCollapsed(editor.selection); const isCollapsed = editor.selection && Range.isCollapsed(editor.selection);
const dispatch = useAppDispatch();
useEffect(() => { useEffect(() => {
if (selected && isCollapsed && page) { if (selected && isCollapsed && page) {
@ -56,16 +56,14 @@ export function MentionLeaf({ mention }: { mention: Mention }) {
void loadPage(); void loadPage();
}, [loadPage]); }, [loadPage]);
const openPage = useCallback(() => { const handleOpenPage = useCallback(() => {
if (!page) { if (!page) {
notify.error(t('document.mention.deletedContent')); notify.error(t('document.mention.deletedContent'));
return; return;
} }
const pageType = pageTypeMap[page.layout]; void dispatch(openPage(page.id));
}, [page, dispatch, t]);
navigate(`/page/${pageType}/${page.id}`);
}, [navigate, page, t]);
useEffect(() => { useEffect(() => {
if (!page) return; if (!page) return;
@ -117,7 +115,7 @@ export function MentionLeaf({ mention }: { mention: Mention }) {
return ( return (
<span <span
className={`mention-inline mx-1 inline-flex select-none items-center gap-1`} className={`mention-inline mx-1 inline-flex select-none items-center gap-1`}
onClick={openPage} onClick={handleOpenPage}
contentEditable={false} contentEditable={false}
style={{ style={{
backgroundColor: selected ? 'var(--content-blue-100)' : undefined, backgroundColor: selected ? 'var(--content-blue-100)' : undefined,

View File

@ -1,8 +1,9 @@
import { RefObject, useCallback, useEffect, useState } from 'react'; import { RefObject, useCallback, useEffect, useState } from 'react';
import { ReactEditor, useSlate } from 'slate-react'; import { ReactEditor, useSlate } from 'slate-react';
import { findEventRange, getBlockActionsPosition } from '$app/components/editor/components/tools/block_actions/utils'; import { findEventNode, getBlockActionsPosition } from '$app/components/editor/components/tools/block_actions/utils';
import { Element, Editor, Range } from 'slate'; import { Element, Editor, Range } from 'slate';
import { EditorNodeType } from '$app/application/document/document.types'; import { EditorNodeType } from '$app/application/document/document.types';
import { Log } from '$app/utils/log';
export function useBlockActionsToolbar(ref: RefObject<HTMLDivElement>, contextMenuVisible: boolean) { export function useBlockActionsToolbar(ref: RefObject<HTMLDivElement>, contextMenuVisible: boolean) {
const editor = useSlate(); const editor = useSlate();
@ -45,37 +46,55 @@ export function useBlockActionsToolbar(ref: RefObject<HTMLDivElement>, contextMe
} }
let range: Range | null = null; let range: Range | null = null;
let node;
try { try {
range = ReactEditor.findEventRange(editor, e); range = ReactEditor.findEventRange(editor, e);
} catch { } catch {
const editorDom = ReactEditor.toDOMNode(editor, editor); const editorDom = ReactEditor.toDOMNode(editor, editor);
const rect = editorDom.getBoundingClientRect();
const isOverLeftBoundary = e.clientX < rect.left + 64;
const isOverRightBoundary = e.clientX > rect.right - 64;
let newX = e.clientX;
range = findEventRange(editor, { if (isOverLeftBoundary) {
...e, newX = rect.left + 64;
clientX: e.clientX + editorDom.offsetWidth / 2, }
clientY: e.clientY,
if (isOverRightBoundary) {
newX = rect.right - 64;
}
node = findEventNode(editor, {
x: newX,
y: e.clientY,
}); });
} }
if (!range) { if (!range && !node) {
Log.warn('No range and node found');
return; return;
} else if (range) {
const match = editor.above({
match: (n) => {
return !Editor.isEditor(n) && Element.isElement(n) && n.blockId !== undefined;
},
at: range,
});
if (!match) {
close();
return;
}
node = match[0] as Element;
} }
const match = editor.above({ if (!node) {
match: (n) => {
return !Editor.isEditor(n) && Element.isElement(n) && n.blockId !== undefined;
},
at: range,
});
if (!match) {
close(); close();
return; return;
} }
const node = match[0] as Element;
if (node.type === EditorNodeType.Page) return; if (node.type === EditorNodeType.Page) return;
const blockElement = ReactEditor.toDOMNode(editor, node); const blockElement = ReactEditor.toDOMNode(editor, node);

View File

@ -9,6 +9,9 @@ import { PopoverProps } from '@mui/material/Popover';
import { EditorSelectedBlockContext } from '$app/components/editor/stores/selected'; import { EditorSelectedBlockContext } from '$app/components/editor/stores/selected';
import withErrorBoundary from '$app/components/_shared/error_boundary/withError'; import withErrorBoundary from '$app/components/_shared/error_boundary/withError';
import { CustomEditor } from '$app/components/editor/command';
import isEqual from 'lodash-es/isEqual';
import { Range } from 'slate';
const Toolbar = () => { const Toolbar = () => {
const ref = useRef<HTMLDivElement | null>(null); const ref = useRef<HTMLDivElement | null>(null);
@ -38,10 +41,42 @@ const Toolbar = () => {
if (!node) return; if (!node) return;
const nodeDom = ReactEditor.toDOMNode(editor, node); const nodeDom = ReactEditor.toDOMNode(editor, node);
const onContextMenu = (e: MouseEvent) => { const onContextMenu = (e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
const { clientX, clientY } = e; const { clientX, clientY } = e;
e.stopPropagation();
const { selection } = editor;
const editorRange = ReactEditor.findEventRange(editor, e);
if (!editorRange || !selection) return;
const rangeBlock = CustomEditor.getBlock(editor, editorRange);
const selectedBlock = CustomEditor.getBlock(editor, selection);
if (
Range.intersection(selection, editorRange) ||
(rangeBlock && selectedBlock && isEqual(rangeBlock[1], selectedBlock[1]))
) {
const windowSelection = window.getSelection();
const range = windowSelection?.rangeCount ? windowSelection?.getRangeAt(0) : null;
const isCollapsed = windowSelection?.isCollapsed;
if (windowSelection && !isCollapsed) {
if (range && range.endOffset === 0 && range.startContainer !== range.endContainer) {
const newRange = range.cloneRange();
newRange.setEnd(range.startContainer, range.startOffset);
windowSelection.removeAllRanges();
windowSelection.addRange(newRange);
}
}
return;
}
e.preventDefault();
popoverPropsRef.current = { popoverPropsRef.current = {
transformOrigin: { transformOrigin: {
vertical: 'top', vertical: 'top',

View File

@ -2,7 +2,6 @@ import { ReactEditor } from 'slate-react';
import { getEditorDomNode, getHeadingCssProperty } from '$app/components/editor/plugins/utils'; import { getEditorDomNode, getHeadingCssProperty } from '$app/components/editor/plugins/utils';
import { Element } from 'slate'; import { Element } from 'slate';
import { EditorNodeType, HeadingNode } from '$app/application/document/document.types'; import { EditorNodeType, HeadingNode } from '$app/application/document/document.types';
import { Log } from '$app/utils/log';
export function getBlockActionsPosition(editor: ReactEditor, blockElement: HTMLElement) { export function getBlockActionsPosition(editor: ReactEditor, blockElement: HTMLElement) {
const editorDom = getEditorDomNode(editor); const editorDom = getEditorDomNode(editor);
@ -35,41 +34,25 @@ export function getBlockCssProperty(node: Element) {
} }
/** /**
* Resolve can not find the range when the drop occurs on the icon.
* @param editor * @param editor
* @param e * @param e
*/ */
export function findEventRange(editor: ReactEditor, e: MouseEvent) { export function findEventNode(
const { clientX: x, clientY: y } = e; editor: ReactEditor,
{
x,
y,
}: {
x: number;
y: number;
}
) {
const element = document.elementFromPoint(x, y);
const nodeDom = element?.closest('[data-block-type]');
// Else resolve a range from the caret position where the drop occured. if (nodeDom) {
let domRange; return ReactEditor.toSlateNode(editor, nodeDom) as Element;
const { document } = ReactEditor.getWindow(editor);
// COMPAT: In Firefox, `caretRangeFromPoint` doesn't exist. (2016/07/25)
if (document.caretRangeFromPoint) {
domRange = document.caretRangeFromPoint(x, y);
} else if ('caretPositionFromPoint' in document && typeof document.caretPositionFromPoint === 'function') {
const position = document.caretPositionFromPoint(x, y);
if (position) {
domRange = document.createRange();
domRange.setStart(position.offsetNode, position.offset);
domRange.setEnd(position.offsetNode, position.offset);
}
} }
if (!domRange) { return null;
Log.warn('Could not find a range from the caret position.');
return null;
}
try {
return ReactEditor.toSlateRange(editor, domRange, {
exactMatch: false,
suppressThrow: false,
});
} catch {
return null;
}
} }

View File

@ -20,87 +20,16 @@ import { CustomEditor } from '$app/components/editor/command';
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';
import {
enum SlashCommandPanelTab { headingTypes,
BASIC = 'basic', headingTypeToLevelMap,
MEDIA = 'media', reorderSlashOptions,
DATABASE = 'database', SlashAliases,
ADVANCED = 'advanced', SlashCommandPanelTab,
} slashOptionGroup,
slashOptionMapToEditorNodeType,
export enum SlashOptionType { SlashOptionType,
Paragraph, } from '$app/components/editor/components/tools/command_panel/slash_command_panel/const';
TodoList,
Heading1,
Heading2,
Heading3,
BulletedList,
NumberedList,
Quote,
ToggleList,
Divider,
Callout,
Code,
Grid,
MathEquation,
Image,
}
const slashOptionGroup = [
{
key: SlashCommandPanelTab.BASIC,
options: [
SlashOptionType.Paragraph,
SlashOptionType.TodoList,
SlashOptionType.Heading1,
SlashOptionType.Heading2,
SlashOptionType.Heading3,
SlashOptionType.BulletedList,
SlashOptionType.NumberedList,
SlashOptionType.Quote,
SlashOptionType.ToggleList,
SlashOptionType.Divider,
SlashOptionType.Callout,
],
},
{
key: SlashCommandPanelTab.MEDIA,
options: [SlashOptionType.Code, SlashOptionType.Image],
},
{
key: SlashCommandPanelTab.DATABASE,
options: [SlashOptionType.Grid],
},
{
key: SlashCommandPanelTab.ADVANCED,
options: [SlashOptionType.MathEquation],
},
];
const slashOptionMapToEditorNodeType = {
[SlashOptionType.Paragraph]: EditorNodeType.Paragraph,
[SlashOptionType.TodoList]: EditorNodeType.TodoListBlock,
[SlashOptionType.Heading1]: EditorNodeType.HeadingBlock,
[SlashOptionType.Heading2]: EditorNodeType.HeadingBlock,
[SlashOptionType.Heading3]: EditorNodeType.HeadingBlock,
[SlashOptionType.BulletedList]: EditorNodeType.BulletedListBlock,
[SlashOptionType.NumberedList]: EditorNodeType.NumberedListBlock,
[SlashOptionType.Quote]: EditorNodeType.QuoteBlock,
[SlashOptionType.ToggleList]: EditorNodeType.ToggleListBlock,
[SlashOptionType.Divider]: EditorNodeType.DividerBlock,
[SlashOptionType.Callout]: EditorNodeType.CalloutBlock,
[SlashOptionType.Code]: EditorNodeType.CodeBlock,
[SlashOptionType.Grid]: EditorNodeType.GridBlock,
[SlashOptionType.MathEquation]: EditorNodeType.EquationBlock,
[SlashOptionType.Image]: EditorNodeType.ImageBlock,
};
const headingTypeToLevelMap: Record<string, number> = {
[SlashOptionType.Heading1]: 1,
[SlashOptionType.Heading2]: 2,
[SlashOptionType.Heading3]: 3,
};
const headingTypes = [SlashOptionType.Heading1, SlashOptionType.Heading2, SlashOptionType.Heading3];
export function useSlashCommandPanel({ export function useSlashCommandPanel({
searchText, searchText,
@ -281,6 +210,7 @@ export function useSlashCommandPanel({
key: group.key, key: group.key,
content: <div className={'px-3 pb-1 pt-2 text-sm'}>{groupTypeToLabelMap[group.key]}</div>, content: <div className={'px-3 pb-1 pt-2 text-sm'}>{groupTypeToLabelMap[group.key]}</div>,
children: group.options children: group.options
.map((type) => { .map((type) => {
return { return {
key: type, key: type,
@ -297,8 +227,12 @@ export function useSlashCommandPanel({
newSearchText = searchText.slice(1); newSearchText = searchText.slice(1);
} }
return label.toLowerCase().includes(newSearchText.toLowerCase()); return (
}), label.toLowerCase().includes(newSearchText.toLowerCase()) ||
SlashAliases[option.key].some((alias) => alias.startsWith(newSearchText.toLowerCase()))
);
})
.sort(reorderSlashOptions(searchText)),
}; };
}) })
.filter((group) => group.children.length > 0); .filter((group) => group.children.length > 0);

View File

@ -1,10 +1,8 @@
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import KeyboardNavigation from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; import KeyboardNavigation from '$app/components/_shared/keyboard_navigation/KeyboardNavigation';
import { import { useSlashCommandPanel } from '$app/components/editor/components/tools/command_panel/slash_command_panel/SlashCommandPanel.hooks';
SlashOptionType,
useSlashCommandPanel,
} from '$app/components/editor/components/tools/command_panel/slash_command_panel/SlashCommandPanel.hooks';
import { useSlateStatic } from 'slate-react'; import { useSlateStatic } from 'slate-react';
import { SlashOptionType } from '$app/components/editor/components/tools/command_panel/slash_command_panel/const';
const noResultBuffer = 2; const noResultBuffer = 2;

View File

@ -0,0 +1,174 @@
import { EditorNodeType } from '$app/application/document/document.types';
export enum SlashCommandPanelTab {
BASIC = 'basic',
MEDIA = 'media',
DATABASE = 'database',
ADVANCED = 'advanced',
}
export enum SlashOptionType {
Paragraph,
TodoList,
Heading1,
Heading2,
Heading3,
BulletedList,
NumberedList,
Quote,
ToggleList,
Divider,
Callout,
Code,
Grid,
MathEquation,
Image,
}
export const slashOptionGroup = [
{
key: SlashCommandPanelTab.BASIC,
options: [
SlashOptionType.Paragraph,
SlashOptionType.TodoList,
SlashOptionType.Heading1,
SlashOptionType.Heading2,
SlashOptionType.Heading3,
SlashOptionType.BulletedList,
SlashOptionType.NumberedList,
SlashOptionType.Quote,
SlashOptionType.ToggleList,
SlashOptionType.Divider,
SlashOptionType.Callout,
],
},
{
key: SlashCommandPanelTab.MEDIA,
options: [SlashOptionType.Code, SlashOptionType.Image],
},
{
key: SlashCommandPanelTab.DATABASE,
options: [SlashOptionType.Grid],
},
{
key: SlashCommandPanelTab.ADVANCED,
options: [SlashOptionType.MathEquation],
},
];
export const slashOptionMapToEditorNodeType = {
[SlashOptionType.Paragraph]: EditorNodeType.Paragraph,
[SlashOptionType.TodoList]: EditorNodeType.TodoListBlock,
[SlashOptionType.Heading1]: EditorNodeType.HeadingBlock,
[SlashOptionType.Heading2]: EditorNodeType.HeadingBlock,
[SlashOptionType.Heading3]: EditorNodeType.HeadingBlock,
[SlashOptionType.BulletedList]: EditorNodeType.BulletedListBlock,
[SlashOptionType.NumberedList]: EditorNodeType.NumberedListBlock,
[SlashOptionType.Quote]: EditorNodeType.QuoteBlock,
[SlashOptionType.ToggleList]: EditorNodeType.ToggleListBlock,
[SlashOptionType.Divider]: EditorNodeType.DividerBlock,
[SlashOptionType.Callout]: EditorNodeType.CalloutBlock,
[SlashOptionType.Code]: EditorNodeType.CodeBlock,
[SlashOptionType.Grid]: EditorNodeType.GridBlock,
[SlashOptionType.MathEquation]: EditorNodeType.EquationBlock,
[SlashOptionType.Image]: EditorNodeType.ImageBlock,
};
export const headingTypeToLevelMap: Record<string, number> = {
[SlashOptionType.Heading1]: 1,
[SlashOptionType.Heading2]: 2,
[SlashOptionType.Heading3]: 3,
};
export const headingTypes = [SlashOptionType.Heading1, SlashOptionType.Heading2, SlashOptionType.Heading3];
export const SlashAliases = {
[SlashOptionType.Paragraph]: ['paragraph', 'text', 'block', 'textblock'],
[SlashOptionType.TodoList]: [
'list',
'todo',
'todolist',
'checkbox',
'block',
'todoblock',
'checkboxblock',
'todolistblock',
],
[SlashOptionType.Heading1]: ['h1', 'heading1', 'block', 'headingblock', 'h1block'],
[SlashOptionType.Heading2]: ['h2', 'heading2', 'block', 'headingblock', 'h2block'],
[SlashOptionType.Heading3]: ['h3', 'heading3', 'block', 'headingblock', 'h3block'],
[SlashOptionType.BulletedList]: [
'list',
'bulleted',
'block',
'bulletedlist',
'bulletedblock',
'listblock',
'bulletedlistblock',
'bulletelist',
],
[SlashOptionType.NumberedList]: [
'list',
'numbered',
'block',
'numberedlist',
'numberedblock',
'listblock',
'numberedlistblock',
'numberlist',
],
[SlashOptionType.Quote]: ['quote', 'block', 'quoteblock'],
[SlashOptionType.ToggleList]: ['list', 'toggle', 'block', 'togglelist', 'toggleblock', 'listblock', 'togglelistblock'],
[SlashOptionType.Divider]: ['divider', 'hr', 'block', 'dividerblock', 'line', 'lineblock'],
[SlashOptionType.Callout]: ['callout', 'info', 'block', 'calloutblock'],
[SlashOptionType.Code]: ['code', 'code', 'block', 'codeblock', 'media'],
[SlashOptionType.Grid]: ['grid', 'table', 'block', 'gridblock', 'database'],
[SlashOptionType.MathEquation]: [
'math',
'equation',
'block',
'mathblock',
'mathequation',
'mathequationblock',
'advanced',
],
[SlashOptionType.Image]: ['img', 'image', 'block', 'imageblock', 'media'],
};
export const reorderSlashOptions = (searchText: string) => {
return (
a: {
key: SlashOptionType;
},
b: {
key: SlashOptionType;
}
) => {
const compareIndex = (option: SlashOptionType) => {
const aliases = SlashAliases[option];
if (aliases) {
for (const alias of aliases) {
if (alias.startsWith(searchText)) {
return -1;
}
}
}
return 0;
};
const compareLength = (option: SlashOptionType) => {
const aliases = SlashAliases[option];
if (aliases) {
for (const alias of aliases) {
if (alias.length < searchText.length) {
return -1;
}
}
}
return 0;
};
return compareIndex(a.key) - compareIndex(b.key) || compareLength(a.key) - compareLength(b.key);
};
};

View File

@ -108,19 +108,61 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) {
} }
.text-placeholder { .text-placeholder {
@apply absolute left-[5px] w-full transform -translate-y-1/2 pointer-events-none select-none whitespace-nowrap;
&:after { &:after {
@apply text-text-placeholder absolute left-[5px] top-1/2 transform -translate-y-1/2 pointer-events-none select-none whitespace-nowrap; @apply text-text-placeholder absolute top-0;
content: (attr(placeholder)); content: (attr(placeholder));
} }
} }
.has-start-icon > .text-placeholder { .block-align-center {
&:after { .text-placeholder {
@apply left-[29px]; &:after {
@apply left-[calc(50%-5px)]
}
}
.has-start-icon .text-placeholder {
&:after {
@apply left-[calc(50%+7px)];
}
}
}
.block-align-left {
.text-placeholder {
&:after {
@apply left-0;
}
}
.has-start-icon .text-placeholder {
&:after {
@apply left-[24px];
}
} }
} }
.block-align-right {
.text-placeholder {
@apply relative w-fit order-2;
&:after {
@apply relative top-1/2 left-[-6px];
}
}
.text-content {
@apply order-1;
}
.has-start-icon .text-placeholder {
&:after {
@apply left-[-6px];
}
}
}
.formula-inline { .formula-inline {
&.selected { &.selected {
@apply rounded bg-content-blue-100; @apply rounded bg-content-blue-100;

View File

@ -71,8 +71,13 @@ export function withBlockDelete(editor: ReactEditor) {
}); });
} }
// if the current node is not a paragraph, convert it to a paragraph // if the current node is not a paragraph, convert it to a paragraph(except code block and callout block)
if (node.type !== EditorNodeType.Paragraph && node.type !== EditorNodeType.Page) { if (
![EditorNodeType.Paragraph, EditorNodeType.CalloutBlock, EditorNodeType.CodeBlock].includes(
node.type as EditorNodeType
) &&
node.type !== EditorNodeType.Page
) {
CustomEditor.turnToBlock(editor, { type: EditorNodeType.Paragraph }); CustomEditor.turnToBlock(editor, { type: EditorNodeType.Paragraph });
return; return;
} }

View File

@ -13,6 +13,7 @@ describe('Transform events to actions', () => {
let provider: Provider; let provider: Provider;
beforeEach(() => { beforeEach(() => {
provider = new Provider(generateId()); provider = new Provider(generateId());
provider.initialDocument(true);
provider.connect(); provider.connect();
applyActions.mockClear(); applyActions.mockClear();
}); });

View File

@ -11,6 +11,7 @@ describe('Provider connected', () => {
beforeEach(() => { beforeEach(() => {
provider = new Provider(generateId()); provider = new Provider(generateId());
provider.initialDocument(true);
provider.connect(); provider.connect();
applyActions.mockClear(); applyActions.mockClear();
}); });

View File

@ -13,10 +13,9 @@ export class Provider extends EventEmitter {
dataClient: DataClient; dataClient: DataClient;
// get origin data after document updated // get origin data after document updated
backupDoc: Y.Doc = new Y.Doc(); backupDoc: Y.Doc = new Y.Doc();
constructor(public id: string, includeRoot?: boolean) { constructor(public id: string) {
super(); super();
this.dataClient = new DataClient(id); this.dataClient = new DataClient(id);
void this.initialDocument(includeRoot);
this.document.on('update', this.documentUpdate); this.document.on('update', this.documentUpdate);
} }

View File

@ -1,11 +1,24 @@
import React, { ReactNode, useEffect } from 'react'; import React, { ReactNode, useEffect, useMemo } from 'react';
import SideBar from '$app/components/layout/side_bar/SideBar'; import SideBar from '$app/components/layout/side_bar/SideBar';
import TopBar from '$app/components/layout/top_bar/TopBar'; import TopBar from '$app/components/layout/top_bar/TopBar';
import { useAppSelector } from '$app/stores/store'; import { useAppSelector } from '$app/stores/store';
import './layout.scss'; import './layout.scss';
import { AFScroller } from '../_shared/scroller';
import { useNavigate } from 'react-router-dom';
import { pageTypeMap } from '$app_reducers/pages/slice';
function Layout({ children }: { children: ReactNode }) { function Layout({ children }: { children: ReactNode }) {
const { isCollapsed, width } = useAppSelector((state) => state.sidebar); const { isCollapsed, width } = useAppSelector((state) => state.sidebar);
const currentUser = useAppSelector((state) => state.currentUser);
const navigate = useNavigate();
const { id: latestOpenViewId, layout } = useMemo(
() =>
currentUser?.workspaceSetting?.latestView || {
id: undefined,
layout: undefined,
},
[currentUser?.workspaceSetting?.latestView]
);
useEffect(() => { useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => { const onKeyDown = (e: KeyboardEvent) => {
@ -19,6 +32,14 @@ function Layout({ children }: { children: ReactNode }) {
window.removeEventListener('keydown', onKeyDown); window.removeEventListener('keydown', onKeyDown);
}; };
}, []); }, []);
useEffect(() => {
if (latestOpenViewId) {
const pageType = pageTypeMap[layout];
navigate(`/page/${pageType}/${latestOpenViewId}`);
}
}, [latestOpenViewId, navigate, layout]);
return ( return (
<> <>
<div className='flex h-screen w-[100%] select-none text-sm text-text-title'> <div className='flex h-screen w-[100%] select-none text-sm text-text-title'>
@ -30,14 +51,15 @@ function Layout({ children }: { children: ReactNode }) {
}} }}
> >
<TopBar /> <TopBar />
<div <AFScroller
overflowXHidden
style={{ style={{
height: 'calc(100vh - 64px)', height: 'calc(100vh - 64px)',
}} }}
className={'appflowy-layout appflowy-scroll-container select-none overflow-y-auto overflow-x-hidden'} className={'appflowy-layout appflowy-scroll-container select-none'}
> >
{children} {children}
</div> </AFScroller>
</div> </div>
</div> </div>
</> </>

View File

@ -3,23 +3,22 @@ import { useLoadExpandedPages } from '$app/components/layout/bread_crumb/Breadcr
import Breadcrumbs from '@mui/material/Breadcrumbs'; import Breadcrumbs from '@mui/material/Breadcrumbs';
import Link from '@mui/material/Link'; import Link from '@mui/material/Link';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import { Page, pageTypeMap } from '$app_reducers/pages/slice'; import { Page } from '$app_reducers/pages/slice';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { getPageIcon } from '$app/hooks/page.hooks'; import { getPageIcon } from '$app/hooks/page.hooks';
import { useAppDispatch } from '$app/stores/store';
import { openPage } from '$app_reducers/pages/async_actions';
function Breadcrumb() { function Breadcrumb() {
const { t } = useTranslation(); const { t } = useTranslation();
const { isTrash, pagePath, currentPage } = useLoadExpandedPages(); const { isTrash, pagePath, currentPage } = useLoadExpandedPages();
const navigate = useNavigate(); const dispatch = useAppDispatch();
const navigateToPage = useCallback( const navigateToPage = useCallback(
(page: Page) => { (page: Page) => {
const pageType = pageTypeMap[page.layout]; void dispatch(openPage(page.id));
navigate(`/page/${pageType}/${page.id}`);
}, },
[navigate] [dispatch]
); );
if (!currentPage) { if (!currentPage) {

View File

@ -32,10 +32,16 @@
.appflowy-scroll-container { .appflowy-scroll-container {
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 0px; width: 0;
} }
} }
.appflowy-scrollbar-thumb-horizontal, .appflowy-scrollbar-thumb-vertical {
background-color: var(--scrollbar-thumb);
border-radius: 4px;
opacity: 60%;
}
.workspaces { .workspaces {
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 0px; width: 0px;

View File

@ -1,9 +1,9 @@
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect } from 'react';
import { pagesActions, pageTypeMap, parserViewPBToPage } from '$app_reducers/pages/slice'; import { pagesActions, parserViewPBToPage } from '$app_reducers/pages/slice';
import { useAppDispatch, useAppSelector } from '$app/stores/store'; import { useAppDispatch, useAppSelector } from '$app/stores/store';
import { FolderNotification, ViewLayoutPB } from '@/services/backend'; import { FolderNotification, ViewLayoutPB } from '@/services/backend';
import { useNavigate, useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { updatePageName } from '$app_reducers/pages/async_actions'; import { openPage, updatePageName } from '$app_reducers/pages/async_actions';
import { createPage, deletePage, duplicatePage, getChildPages } from '$app/application/folder/page.service'; import { createPage, deletePage, duplicatePage, getChildPages } from '$app/application/folder/page.service';
import { subscribeNotifications } from '$app/application/notification'; import { subscribeNotifications } from '$app/application/notification';
@ -82,14 +82,10 @@ export function usePageActions(pageId: string) {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const params = useParams(); const params = useParams();
const currentPageId = params.id; const currentPageId = params.id;
const navigate = useNavigate();
const onPageClick = useCallback(() => { const onPageClick = useCallback(() => {
if (!page) return; void dispatch(openPage(pageId));
const pageType = pageTypeMap[page.layout]; }, [dispatch, pageId]);
navigate(`/page/${pageType}/${pageId}`);
}, [navigate, page, pageId]);
const onAddPage = useCallback( const onAddPage = useCallback(
async (layout: ViewLayoutPB) => { async (layout: ViewLayoutPB) => {
@ -112,21 +108,19 @@ export function usePageActions(pageId: string) {
); );
dispatch(pagesActions.expandPage(pageId)); dispatch(pagesActions.expandPage(pageId));
const pageType = pageTypeMap[layout]; await dispatch(openPage(newViewId));
navigate(`/page/${pageType}/${newViewId}`);
}, },
[dispatch, navigate, pageId] [dispatch, pageId]
); );
const onDeletePage = useCallback(async () => { const onDeletePage = useCallback(async () => {
if (currentPageId === pageId) { if (currentPageId === pageId) {
navigate(`/`); dispatch(pagesActions.setTrashSnackbar(true));
} }
await deletePage(pageId); await deletePage(pageId);
dispatch(pagesActions.deletePages([pageId])); dispatch(pagesActions.deletePages([pageId]));
}, [dispatch, currentPageId, navigate, pageId]); }, [dispatch, pageId, currentPageId]);
const onDuplicatePage = useCallback(async () => { const onDuplicatePage = useCallback(async () => {
await duplicatePage(page); await duplicatePage(page);

View File

@ -43,7 +43,7 @@ function Resizer() {
<div <div
onMouseDown={onResizeStart} onMouseDown={onResizeStart}
style={{ style={{
left: `${width - 8}px`, left: `${width - 4}px`,
}} }}
className={'fixed top-0 z-10 h-screen cursor-col-resize'} className={'fixed top-0 z-10 h-screen cursor-col-resize'}
> >

View File

@ -0,0 +1,105 @@
import React, { useEffect } from 'react';
import { Alert, Snackbar } from '@mui/material';
import { useAppDispatch, useAppSelector } from '$app/stores/store';
import { useParams } from 'react-router-dom';
import { pagesActions } from '$app_reducers/pages/slice';
import Slide, { SlideProps } from '@mui/material/Slide';
import { useTranslation } from 'react-i18next';
import Button from '@mui/material/Button';
import { useTrashActions } from '$app/components/trash/Trash.hooks';
import { openPage } from '$app_reducers/pages/async_actions';
function SlideTransition(props: SlideProps) {
return <Slide {...props} direction='down' />;
}
function DeletePageSnackbar() {
const firstViewId = useAppSelector((state) => {
const workspaceId = state.workspace.currentWorkspaceId;
const children = workspaceId ? state.pages.relationMap[workspaceId] : undefined;
if (!children) return null;
return children[0];
});
const showTrashSnackbar = useAppSelector((state) => state.pages.showTrashSnackbar);
const dispatch = useAppDispatch();
const { onPutback, onDelete } = useTrashActions();
const { id } = useParams();
const { t } = useTranslation();
useEffect(() => {
dispatch(pagesActions.setTrashSnackbar(false));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);
const handleBack = () => {
if (firstViewId) {
void dispatch(openPage(firstViewId));
}
};
const handleClose = (toBack = true) => {
dispatch(pagesActions.setTrashSnackbar(false));
if (toBack) {
handleBack();
}
};
const handleRestore = () => {
if (!id) return;
void onPutback(id);
handleClose(false);
};
const handleDelete = () => {
if (!id) return;
void onDelete([id]);
if (!firstViewId) {
handleClose(false);
return;
}
handleBack();
};
return (
<Snackbar
anchorOrigin={{
vertical: 'top',
horizontal: 'center',
}}
open={showTrashSnackbar}
onClose={() => handleClose()}
TransitionComponent={SlideTransition}
>
<Alert
className={'flex items-center'}
onClose={() => handleClose()}
severity='info'
variant='standard'
sx={{
width: '100%',
'.MuiAlert-action': {
padding: 0,
},
}}
>
<div className={'flex h-full w-full items-center justify-center gap-3'}>
<span>{t('deletePagePrompt.text')}</span>
<Button onClick={handleRestore} size={'small'} color={'primary'} variant={'text'}>
{t('deletePagePrompt.restore')}
</Button>
<Button onClick={handleDelete} size={'small'} color={'error'} variant={'text'}>
{t('deletePagePrompt.deletePermanent')}
</Button>
</div>
</Alert>
</Snackbar>
);
}
export default DeletePageSnackbar;

View File

@ -2,12 +2,13 @@ import React from 'react';
import CollapseMenuButton from '$app/components/layout/collapse_menu_button/CollapseMenuButton'; import CollapseMenuButton from '$app/components/layout/collapse_menu_button/CollapseMenuButton';
import { useAppSelector } from '$app/stores/store'; import { useAppSelector } from '$app/stores/store';
import Breadcrumb from '$app/components/layout/bread_crumb/BreadCrumb'; import Breadcrumb from '$app/components/layout/bread_crumb/BreadCrumb';
import DeletePageSnackbar from '$app/components/layout/top_bar/DeletePageSnackbar';
function TopBar() { function TopBar() {
const sidebarIsCollapsed = useAppSelector((state) => state.sidebar.isCollapsed); const sidebarIsCollapsed = useAppSelector((state) => state.sidebar.isCollapsed);
return ( return (
<div className={'flex h-[64px] select-none border-b border-line-divider p-4'}> <div className={'appflowy-top-bar flex h-[64px] select-none border-b border-line-divider p-4'}>
{sidebarIsCollapsed && ( {sidebarIsCollapsed && (
<div className={'mr-2 pt-[3px]'}> <div className={'mr-2 pt-[3px]'}>
<CollapseMenuButton /> <CollapseMenuButton />
@ -18,6 +19,7 @@ function TopBar() {
<Breadcrumb /> <Breadcrumb />
</div> </div>
</div> </div>
<DeletePageSnackbar />
</div> </div>
); );
} }

View File

@ -6,7 +6,7 @@ import { subscribeNotifications } from '$app/application/notification';
import { FolderNotification, ViewLayoutPB } from '@/services/backend'; import { FolderNotification, ViewLayoutPB } from '@/services/backend';
import * as workspaceService from '$app/application/folder/workspace.service'; import * as workspaceService from '$app/application/folder/workspace.service';
import { createCurrentWorkspaceChildView } from '$app/application/folder/workspace.service'; import { createCurrentWorkspaceChildView } from '$app/application/folder/workspace.service';
import { useNavigate } from 'react-router-dom'; import { openPage } from '$app_reducers/pages/async_actions';
export function useLoadWorkspaces() { export function useLoadWorkspaces() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -108,8 +108,7 @@ export function useLoadWorkspace(workspace: WorkspaceItem) {
} }
export function useWorkspaceActions(workspaceId: string) { export function useWorkspaceActions(workspaceId: string) {
const navigate = useNavigate(); const dispatch = useAppDispatch();
const newPage = useCallback(async () => { const newPage = useCallback(async () => {
const { id } = await createCurrentWorkspaceChildView({ const { id } = await createCurrentWorkspaceChildView({
name: '', name: '',
@ -117,8 +116,19 @@ export function useWorkspaceActions(workspaceId: string) {
parent_view_id: workspaceId, parent_view_id: workspaceId,
}); });
navigate(`/page/document/${id}`); dispatch(
}, [navigate, workspaceId]); pagesActions.addPage({
page: {
id: id,
parentId: workspaceId,
layout: ViewLayoutPB.Document,
name: '',
},
isLast: true,
})
);
void dispatch(openPage(id));
}, [dispatch, workspaceId]);
return { return {
newPage, newPage,

View File

@ -5,6 +5,7 @@ import Workspace from './Workspace';
import TrashButton from '$app/components/layout/workspace_manager/TrashButton'; import TrashButton from '$app/components/layout/workspace_manager/TrashButton';
import { useAppSelector } from '@/appflowy_app/stores/store'; import { useAppSelector } from '@/appflowy_app/stores/store';
import { LoginState } from '$app_reducers/current-user/slice'; import { LoginState } from '$app_reducers/current-user/slice';
import { AFScroller } from '$app/components/_shared/scroller';
function WorkspaceManager() { function WorkspaceManager() {
const { workspaces, currentWorkspace, initializeWorkspaces } = useLoadWorkspaces(); const { workspaces, currentWorkspace, initializeWorkspaces } = useLoadWorkspaces();
@ -19,13 +20,13 @@ function WorkspaceManager() {
return ( return (
<div className={'workspaces flex h-full select-none flex-col justify-between'}> <div className={'workspaces flex h-full select-none flex-col justify-between'}>
<div className={'mt-4 flex w-full flex-1 select-none flex-col overflow-y-auto overflow-x-hidden'}> <AFScroller overflowXHidden className={'mt-4 flex w-full flex-1 select-none flex-col'}>
<div className={'flex-1'}> <div className={'flex-1'}>
{workspaces.map((workspace) => ( {workspaces.map((workspace) => (
<Workspace opened={currentWorkspace?.id === workspace.id} key={workspace.id} workspace={workspace} /> <Workspace opened={currentWorkspace?.id === workspace.id} key={workspace.id} workspace={workspace} />
))} ))}
</div> </div>
</div> </AFScroller>
<div className={'flex w-[100%] items-center px-2'}> <div className={'flex w-[100%] items-center px-2'}>
<TrashButton /> <TrashButton />
</div> </div>

View File

@ -16,7 +16,6 @@ import { Login } from '$app/components/settings/Login';
import SwipeableViews from 'react-swipeable-views'; import SwipeableViews from 'react-swipeable-views';
import { useAppDispatch, useAppSelector } from '$app/stores/store'; import { useAppDispatch, useAppSelector } from '$app/stores/store';
import { currentUserActions, LoginState } from '$app_reducers/current-user/slice'; import { currentUserActions, LoginState } from '$app_reducers/current-user/slice';
import { useNavigate } from 'react-router-dom';
export const SettingsDialog = (props: DialogProps) => { export const SettingsDialog = (props: DialogProps) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -44,17 +43,14 @@ export const SettingsDialog = (props: DialogProps) => {
const currentRoute = routes[routes.length - 1]; const currentRoute = routes[routes.length - 1];
const navigate = useNavigate();
useEffect(() => { useEffect(() => {
if (lastLoginStateRef.current === LoginState.Loading && loginState === LoginState.Success) { if (lastLoginStateRef.current === LoginState.Loading && loginState === LoginState.Success) {
navigate('/');
handleClose(); handleClose();
return; return;
} }
lastLoginStateRef.current = loginState; lastLoginStateRef.current = loginState;
}, [loginState, handleClose, navigate]); }, [loginState, handleClose]);
return ( return (
<Dialog <Dialog

View File

@ -1,6 +1,7 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { WorkspaceSettingPB } from '@/services/backend/models/flowy-folder/workspace'; import { WorkspaceSettingPB } from '@/services/backend/models/flowy-folder/workspace';
import { ThemeModePB as ThemeMode } from '@/services/backend'; import { ThemeModePB as ThemeMode } from '@/services/backend';
import { Page, parserViewPBToPage } from '$app_reducers/pages/slice';
export { ThemeMode }; export { ThemeMode };
@ -23,6 +24,20 @@ export enum LoginState {
Error = 'error', Error = 'error',
} }
export interface UserWorkspaceSetting {
workspaceId: string;
latestView?: Page;
hasLatestView: boolean;
}
export function parseWorkspaceSettingPBToSetting(workspaceSetting: WorkspaceSettingPB): UserWorkspaceSetting {
return {
workspaceId: workspaceSetting.workspace_id,
latestView: workspaceSetting.latest_view ? parserViewPBToPage(workspaceSetting.latest_view) : undefined,
hasLatestView: !!workspaceSetting.latest_view,
};
}
export interface ICurrentUser { export interface ICurrentUser {
id?: number; id?: number;
deviceId?: string; deviceId?: string;
@ -31,7 +46,7 @@ export interface ICurrentUser {
token?: string; token?: string;
iconUrl?: string; iconUrl?: string;
isAuthenticated: boolean; isAuthenticated: boolean;
workspaceSetting?: WorkspaceSettingPB; workspaceSetting?: UserWorkspaceSetting;
userSetting: UserSetting; userSetting: UserSetting;
isLocal: boolean; isLocal: boolean;
loginState?: LoginState; loginState?: LoginState;
@ -71,6 +86,13 @@ export const currentUserSlice = createSlice({
resetLoginState: (state) => { resetLoginState: (state) => {
state.loginState = undefined; state.loginState = undefined;
}, },
setLatestView: (state, action: PayloadAction<Page>) => {
if (state.workspaceSetting) {
state.workspaceSetting.latestView = action.payload;
state.workspaceSetting.hasLatestView = true;
}
},
}, },
}); });

View File

@ -1,8 +1,9 @@
import { createAsyncThunk } from '@reduxjs/toolkit'; import { createAsyncThunk } from '@reduxjs/toolkit';
import { RootState } from '$app/stores/store'; import { RootState } from '$app/stores/store';
import { pagesActions } from '$app_reducers/pages/slice'; import { pagesActions } from '$app_reducers/pages/slice';
import { movePage, updatePage } from '$app/application/folder/page.service'; import { movePage, setLatestOpenedPage, updatePage } from '$app/application/folder/page.service';
import debounce from 'lodash-es/debounce'; import debounce from 'lodash-es/debounce';
import { currentUserActions } from '$app_reducers/current-user/slice';
export const movePageThunk = createAsyncThunk( export const movePageThunk = createAsyncThunk(
'pages/movePage', 'pages/movePage',
@ -91,3 +92,15 @@ export const updatePageName = createAsyncThunk(
} }
} }
); );
export const openPage = createAsyncThunk('pages/openPage', async (id: string, thunkAPI) => {
const { dispatch, getState } = thunkAPI;
const { pageMap } = (getState() as RootState).pages;
const page = pageMap[id];
if (!page) return;
dispatch(currentUserActions.setLatestView(page));
await setLatestOpenedPage(id);
});

View File

@ -56,6 +56,7 @@ export interface PageState {
pageMap: Record<string, Page>; pageMap: Record<string, Page>;
relationMap: Record<string, string[] | undefined>; relationMap: Record<string, string[] | undefined>;
expandedIdMap: Record<string, boolean>; expandedIdMap: Record<string, boolean>;
showTrashSnackbar: boolean;
} }
export const initialState: PageState = { export const initialState: PageState = {
@ -65,6 +66,7 @@ export const initialState: PageState = {
acc[id] = true; acc[id] = true;
return acc; return acc;
}, {} as Record<string, boolean>), }, {} as Record<string, boolean>),
showTrashSnackbar: false,
}; };
export const pagesSlice = createSlice({ export const pagesSlice = createSlice({
@ -201,6 +203,10 @@ export const pagesSlice = createSlice({
storeExpandedPageIds(ids); storeExpandedPageIds(ids);
}, },
setTrashSnackbar(state, action: PayloadAction<boolean>) {
state.showTrashSnackbar = action.payload;
},
}, },
}); });

View File

@ -1,6 +1,6 @@
/** /**
* Do not edit directly * Do not edit directly
* Generated on Mon, 29 Jan 2024 03:52:24 GMT * Generated on Tue, 19 Mar 2024 03:48:58 GMT
* Generated from $pnpm css:variables * Generated from $pnpm css:variables
*/ */
@ -36,7 +36,7 @@
--base-light-color-light-green: #ddffd6; --base-light-color-light-green: #ddffd6;
--base-light-color-light-aqua: #defff1; --base-light-color-light-aqua: #defff1;
--base-light-color-light-blue: #e1fbff; --base-light-color-light-blue: #e1fbff;
--base-light-color-light-red: #ffe7ee; --base-light-color-light-red: #ffdddd;
--base-black-neutral-100: #252F41; --base-black-neutral-100: #252F41;
--base-black-neutral-200: #313c51; --base-black-neutral-200: #313c51;
--base-black-neutral-300: #3c4557; --base-black-neutral-300: #3c4557;
@ -88,7 +88,7 @@
--fill-hover: #005174; --fill-hover: #005174;
--fill-toolbar: #0F111C; --fill-toolbar: #0F111C;
--fill-selector: #232b38; --fill-selector: #232b38;
--fill-list-active: #252F41; --fill-list-active: #3c4557;
--fill-list-hover: #005174; --fill-list-hover: #005174;
--content-blue-400: #00bcf0; --content-blue-400: #00bcf0;
--content-blue-300: #52d1f4; --content-blue-300: #52d1f4;
@ -116,4 +116,6 @@
--tint-aqua: #1B3849; --tint-aqua: #1B3849;
--tint-orange: #5E3C3C; --tint-orange: #5E3C3C;
--shadow: 0px 0px 25px 0px rgba(0,0,0,0.3); --shadow: 0px 0px 25px 0px rgba(0,0,0,0.3);
--scrollbar-track: #252F41;
--scrollbar-thumb: #3c4557;
} }

View File

@ -1,6 +1,6 @@
/** /**
* Do not edit directly * Do not edit directly
* Generated on Mon, 29 Jan 2024 03:52:24 GMT * Generated on Tue, 19 Mar 2024 03:48:58 GMT
* Generated from $pnpm css:variables * Generated from $pnpm css:variables
*/ */
@ -36,7 +36,7 @@
--base-light-color-light-green: #ddffd6; --base-light-color-light-green: #ddffd6;
--base-light-color-light-aqua: #defff1; --base-light-color-light-aqua: #defff1;
--base-light-color-light-blue: #e1fbff; --base-light-color-light-blue: #e1fbff;
--base-light-color-light-red: #ffe7ee; --base-light-color-light-red: #ffdddd;
--base-black-neutral-100: #252F41; --base-black-neutral-100: #252F41;
--base-black-neutral-200: #313c51; --base-black-neutral-200: #313c51;
--base-black-neutral-300: #3c4557; --base-black-neutral-300: #3c4557;
@ -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: rgb(0, 188, 240); --content-blue-400: #00bcf0;
--content-blue-300: #52d1f4; --content-blue-300: #52d1f4;
--content-blue-600: #009fd1; --content-blue-600: #009fd1;
--content-blue-100: #e0f8ff; --content-blue-100: #e0f8ff;
@ -119,4 +119,6 @@
--tint-orange: #ffefe3; --tint-orange: #ffefe3;
--tint-yellow: #fff2cd; --tint-yellow: #fff2cd;
--shadow: 0px 0px 10px 0px rgba(0,0,0,0.1); --shadow: 0px 0px 10px 0px rgba(0,0,0,0.1);
--scrollbar-thumb: #bdbdbd;
--scrollbar-track: #edeef2;
} }

View File

@ -1,6 +1,6 @@
/** /**
* Do not edit directly * Do not edit directly
* Generated on Mon, 29 Jan 2024 03:52:24 GMT * Generated on Tue, 19 Mar 2024 03:48:58 GMT
* Generated from $pnpm css:variables * Generated from $pnpm css:variables
*/ */

View File

@ -1,6 +1,6 @@
/** /**
* Do not edit directly * Do not edit directly
* Generated on Mon, 29 Jan 2024 03:52:24 GMT * Generated on Tue, 19 Mar 2024 03:48:58 GMT
* Generated from $pnpm css:variables * Generated from $pnpm css:variables
*/ */
@ -67,5 +67,9 @@ module.exports = {
"lime": "var(--tint-lime)", "lime": "var(--tint-lime)",
"aqua": "var(--tint-aqua)", "aqua": "var(--tint-aqua)",
"orange": "var(--tint-orange)" "orange": "var(--tint-orange)"
},
"scrollbar": {
"track": "var(--scrollbar-track)",
"thumb": "var(--scrollbar-thumb)"
} }
} }

View File

@ -80,7 +80,7 @@
}, },
"list": { "list": {
"active": { "active": {
"value": "{Base.black.neutral.100}", "value": "{Base.black.neutral.300}",
"type": "color" "type": "color"
}, },
"hover": { "hover": {
@ -207,5 +207,15 @@
"type": "innerShadow" "type": "innerShadow"
}, },
"type": "boxShadow" "type": "boxShadow"
},
"scrollbar": {
"track": {
"value": "{Base.black.neutral.100}",
"type": "color"
},
"thumb": {
"value": "{Base.black.neutral.300}",
"type": "color"
}
} }
} }

View File

@ -219,5 +219,15 @@
"type": "dropShadow" "type": "dropShadow"
}, },
"type": "boxShadow" "type": "boxShadow"
},
"scrollbar": {
"thumb": {
"value": "{Base.Light.neutral.500}",
"type": "color"
},
"track": {
"value": "{Base.Light.neutral.100}",
"type": "color"
}
} }
} }