mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-11-22 18:42:48 +03:00
feat: center peek view (#7073)
<div class='graphite__hidden'> <div>🎥 Video uploaded on Graphite:</div> <a href="https://app.graphite.dev/media/video/T2klNLEk0wxLh4NRDzhk/7380c06e-880e-424a-9204-8cfb5e924978.mp4"> <img src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/T2klNLEk0wxLh4NRDzhk/7380c06e-880e-424a-9204-8cfb5e924978.mp4"> </a> </div> <video src="https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/T2klNLEk0wxLh4NRDzhk/7380c06e-880e-424a-9204-8cfb5e924978.mp4">Kapture 2024-05-30 at 19.42.46.mp4</video>
This commit is contained in:
parent
b65c01c5e1
commit
ea0059fa1b
@ -337,7 +337,7 @@ export class LiveData<T = unknown>
|
|||||||
distinctUntilChanged(comparator?: (previous: T, current: T) => boolean) {
|
distinctUntilChanged(comparator?: (previous: T, current: T) => boolean) {
|
||||||
return LiveData.from(
|
return LiveData.from(
|
||||||
this.pipe(distinctUntilChanged(comparator)),
|
this.pipe(distinctUntilChanged(comparator)),
|
||||||
null as any
|
null as T
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
|
import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||||
import { useJournalHelper } from '@affine/core/hooks/use-journal';
|
import { useJournalHelper } from '@affine/core/hooks/use-journal';
|
||||||
|
import { PeekViewService } from '@affine/core/modules/peek-view';
|
||||||
import { WorkbenchLink } from '@affine/core/modules/workbench';
|
import { WorkbenchLink } from '@affine/core/modules/workbench';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { LinkedPageIcon, TodayIcon } from '@blocksuite/icons';
|
import { LinkedPageIcon, TodayIcon } from '@blocksuite/icons';
|
||||||
import type { DocCollection } from '@blocksuite/store';
|
import type { DocCollection } from '@blocksuite/store';
|
||||||
import type { PropsWithChildren } from 'react';
|
import { useService } from '@toeverything/infra';
|
||||||
|
import { type PropsWithChildren, useCallback, useRef } from 'react';
|
||||||
|
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
@ -64,8 +66,30 @@ export function AffinePageReference({
|
|||||||
t,
|
t,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ref = useRef<HTMLAnchorElement>(null);
|
||||||
|
|
||||||
|
const peekView = useService(PeekViewService).peekView;
|
||||||
|
|
||||||
|
const onClick = useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
if (e.shiftKey && ref.current) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
peekView.open(ref.current);
|
||||||
|
return true; // means this click is handled
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
[peekView]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkbenchLink to={`/${pageId}`} className={styles.pageReferenceLink}>
|
<WorkbenchLink
|
||||||
|
ref={ref}
|
||||||
|
to={`/${pageId}`}
|
||||||
|
onClick={onClick}
|
||||||
|
className={styles.pageReferenceLink}
|
||||||
|
>
|
||||||
{Wrapper ? <Wrapper>{el}</Wrapper> : el}
|
{Wrapper ? <Wrapper>{el}</Wrapper> : el}
|
||||||
</WorkbenchLink>
|
</WorkbenchLink>
|
||||||
);
|
);
|
||||||
|
@ -2,7 +2,6 @@ import { cssVar } from '@toeverything/theme';
|
|||||||
import { style } from '@vanilla-extract/css';
|
import { style } from '@vanilla-extract/css';
|
||||||
export const linkItemRoot = style({
|
export const linkItemRoot = style({
|
||||||
color: 'inherit',
|
color: 'inherit',
|
||||||
display: 'contents',
|
|
||||||
});
|
});
|
||||||
export const root = style({
|
export const root = style({
|
||||||
display: 'inline-flex',
|
display: 'inline-flex',
|
||||||
|
@ -19,7 +19,7 @@ export interface MenuItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
|||||||
|
|
||||||
export interface MenuLinkItemProps extends MenuItemProps {
|
export interface MenuLinkItemProps extends MenuItemProps {
|
||||||
to: To;
|
to: To;
|
||||||
linkComponent?: React.ComponentType<{ to: To; className: string }>;
|
linkComponent?: React.ComponentType<{ to: To; className?: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const stopPropagation: React.MouseEventHandler = e => {
|
const stopPropagation: React.MouseEventHandler = e => {
|
||||||
|
@ -4,6 +4,7 @@ import {
|
|||||||
useLitPortalFactory,
|
useLitPortalFactory,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
import { useJournalInfoHelper } from '@affine/core/hooks/use-journal';
|
import { useJournalInfoHelper } from '@affine/core/hooks/use-journal';
|
||||||
|
import { PeekViewService } from '@affine/core/modules/peek-view';
|
||||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
import type { BlockSpec } from '@blocksuite/block-std';
|
import type { BlockSpec } from '@blocksuite/block-std';
|
||||||
import {
|
import {
|
||||||
@ -30,6 +31,7 @@ import { AffinePageReference } from '../../affine/reference-link';
|
|||||||
import { BlocksuiteEditorJournalDocTitle } from './journal-doc-title';
|
import { BlocksuiteEditorJournalDocTitle } from './journal-doc-title';
|
||||||
import {
|
import {
|
||||||
patchNotificationService,
|
patchNotificationService,
|
||||||
|
patchPeekViewService,
|
||||||
patchReferenceRenderer,
|
patchReferenceRenderer,
|
||||||
type ReferenceReactRenderer,
|
type ReferenceReactRenderer,
|
||||||
} from './specs/custom/spec-patchers';
|
} from './specs/custom/spec-patchers';
|
||||||
@ -66,6 +68,7 @@ interface BlocksuiteEditorProps {
|
|||||||
|
|
||||||
const usePatchSpecs = (page: Doc, specs: BlockSpec[]) => {
|
const usePatchSpecs = (page: Doc, specs: BlockSpec[]) => {
|
||||||
const [reactToLit, portals] = useLitPortalFactory();
|
const [reactToLit, portals] = useLitPortalFactory();
|
||||||
|
const peekViewService = useService(PeekViewService);
|
||||||
const referenceRenderer: ReferenceReactRenderer = useMemo(() => {
|
const referenceRenderer: ReferenceReactRenderer = useMemo(() => {
|
||||||
return function customReference(reference) {
|
return function customReference(reference) {
|
||||||
const pageId = reference.delta.attributes?.reference?.pageId;
|
const pageId = reference.delta.attributes?.reference?.pageId;
|
||||||
@ -83,8 +86,9 @@ const usePatchSpecs = (page: Doc, specs: BlockSpec[]) => {
|
|||||||
patchReferenceRenderer(patched, reactToLit, referenceRenderer),
|
patchReferenceRenderer(patched, reactToLit, referenceRenderer),
|
||||||
confirmModal
|
confirmModal
|
||||||
);
|
);
|
||||||
|
patched = patchPeekViewService(patched, peekViewService);
|
||||||
return patched;
|
return patched;
|
||||||
}, [confirmModal, reactToLit, referenceRenderer, specs]);
|
}, [confirmModal, peekViewService, reactToLit, referenceRenderer, specs]);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
patchedSpecs,
|
patchedSpecs,
|
||||||
|
@ -6,6 +6,9 @@ import {
|
|||||||
type ToastOptions,
|
type ToastOptions,
|
||||||
type useConfirmModal,
|
type useConfirmModal,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
|
import type { PeekViewService } from '@affine/core/modules/peek-view';
|
||||||
|
import type { ActivePeekView } from '@affine/core/modules/peek-view/entities/peek-view';
|
||||||
|
import { DebugLogger } from '@affine/debug';
|
||||||
import type { BlockSpec } from '@blocksuite/block-std';
|
import type { BlockSpec } from '@blocksuite/block-std';
|
||||||
import type {
|
import type {
|
||||||
AffineReference,
|
AffineReference,
|
||||||
@ -15,6 +18,8 @@ import type {
|
|||||||
import { LitElement, type TemplateResult } from 'lit';
|
import { LitElement, type TemplateResult } from 'lit';
|
||||||
import React, { createElement, type ReactNode } from 'react';
|
import React, { createElement, type ReactNode } from 'react';
|
||||||
|
|
||||||
|
const logger = new DebugLogger('affine::spec-patchers');
|
||||||
|
|
||||||
export type ReferenceReactRenderer = (
|
export type ReferenceReactRenderer = (
|
||||||
reference: AffineReference
|
reference: AffineReference
|
||||||
) => React.ReactElement;
|
) => React.ReactElement;
|
||||||
@ -190,3 +195,27 @@ export function patchNotificationService(
|
|||||||
});
|
});
|
||||||
return specs;
|
return specs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function patchPeekViewService(
|
||||||
|
specs: BlockSpec[],
|
||||||
|
service: PeekViewService
|
||||||
|
) {
|
||||||
|
const rootSpec = specs.find(
|
||||||
|
spec => spec.schema.model.flavour === 'affine:page'
|
||||||
|
) as BlockSpec<string, RootService>;
|
||||||
|
|
||||||
|
if (!rootSpec) {
|
||||||
|
return specs;
|
||||||
|
}
|
||||||
|
|
||||||
|
patchSpecService(rootSpec, pageService => {
|
||||||
|
pageService.peekViewService = {
|
||||||
|
peek: (target: ActivePeekView['target']) => {
|
||||||
|
logger.debug('center peek', target);
|
||||||
|
service.peekView.open(target);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return specs;
|
||||||
|
}
|
||||||
|
@ -18,6 +18,7 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
|||||||
import {
|
import {
|
||||||
DeleteIcon,
|
DeleteIcon,
|
||||||
DeletePermanentlyIcon,
|
DeletePermanentlyIcon,
|
||||||
|
DualLinkIcon,
|
||||||
DuplicateIcon,
|
DuplicateIcon,
|
||||||
EditIcon,
|
EditIcon,
|
||||||
FavoritedIcon,
|
FavoritedIcon,
|
||||||
@ -25,7 +26,6 @@ import {
|
|||||||
FilterIcon,
|
FilterIcon,
|
||||||
FilterMinusIcon,
|
FilterMinusIcon,
|
||||||
MoreVerticalIcon,
|
MoreVerticalIcon,
|
||||||
OpenInNewIcon,
|
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
ResetIcon,
|
ResetIcon,
|
||||||
SplitViewIcon,
|
SplitViewIcon,
|
||||||
@ -170,7 +170,7 @@ export const PageOperationCell = ({
|
|||||||
style={{ marginBottom: 4 }}
|
style={{ marginBottom: 4 }}
|
||||||
preFix={
|
preFix={
|
||||||
<MenuIcon>
|
<MenuIcon>
|
||||||
<OpenInNewIcon />
|
<DualLinkIcon />
|
||||||
</MenuIcon>
|
</MenuIcon>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -231,7 +231,7 @@ export const CollectionSidebarNavItem = ({
|
|||||||
<span>{collection.name}</span>
|
<span>{collection.name}</span>
|
||||||
</SidebarMenuLinkItem>
|
</SidebarMenuLinkItem>
|
||||||
<Collapsible.Content className={styles.collapsibleContent}>
|
<Collapsible.Content className={styles.collapsibleContent}>
|
||||||
<div style={{ marginLeft: 20, marginTop: -4 }}>
|
<div className={styles.docsListContainer}>
|
||||||
{pagesToRender.map(page => {
|
{pagesToRender.map(page => {
|
||||||
return (
|
return (
|
||||||
<Doc
|
<Doc
|
||||||
|
@ -1,19 +1,21 @@
|
|||||||
import { useBlockSuitePageReferences } from '@affine/core/hooks/use-block-suite-page-references';
|
import { useBlockSuitePageReferences } from '@affine/core/hooks/use-block-suite-page-references';
|
||||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
import {
|
||||||
|
WorkbenchLink,
|
||||||
|
WorkbenchService,
|
||||||
|
} from '@affine/core/modules/workbench';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
||||||
import type { DocCollection, DocMeta } from '@blocksuite/store';
|
import type { DocCollection, DocMeta } from '@blocksuite/store';
|
||||||
import { useDraggable } from '@dnd-kit/core';
|
import { useDraggable } from '@dnd-kit/core';
|
||||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||||
import { DocsService, useLiveData, useService } from '@toeverything/infra';
|
import { DocsService, useLiveData, useService } from '@toeverything/infra';
|
||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type DNDIdentifier,
|
type DNDIdentifier,
|
||||||
getDNDId,
|
getDNDId,
|
||||||
} from '../../../../hooks/affine/use-global-dnd-helper';
|
} from '../../../../hooks/affine/use-global-dnd-helper';
|
||||||
import { useNavigateHelper } from '../../../../hooks/use-navigate-helper';
|
import { MenuLinkItem } from '../../../app-sidebar';
|
||||||
import { MenuItem as CollectionItem } from '../../../app-sidebar';
|
|
||||||
import { DragMenuItemOverlay } from '../components/drag-menu-item-overlay';
|
import { DragMenuItemOverlay } from '../components/drag-menu-item-overlay';
|
||||||
import { PostfixItem } from '../components/postfix-item';
|
import { PostfixItem } from '../components/postfix-item';
|
||||||
import { ReferencePage } from '../components/reference-page';
|
import { ReferencePage } from '../components/reference-page';
|
||||||
@ -50,11 +52,6 @@ export const Doc = ({
|
|||||||
return docMode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />;
|
return docMode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />;
|
||||||
}, [docMode]);
|
}, [docMode]);
|
||||||
|
|
||||||
const { jumpToPage } = useNavigateHelper();
|
|
||||||
const clickDoc = useCallback(() => {
|
|
||||||
jumpToPage(docCollection.id, doc.id);
|
|
||||||
}, [jumpToPage, doc.id, docCollection.id]);
|
|
||||||
|
|
||||||
const references = useBlockSuitePageReferences(docCollection, docId);
|
const references = useBlockSuitePageReferences(docCollection, docId);
|
||||||
const referencesToRender = references.filter(
|
const referencesToRender = references.filter(
|
||||||
id => allPageMeta[id] && !allPageMeta[id]?.trash
|
id => allPageMeta[id] && !allPageMeta[id]?.trash
|
||||||
@ -78,11 +75,12 @@ export const Doc = ({
|
|||||||
data-draggable={true}
|
data-draggable={true}
|
||||||
data-dragging={isDragging}
|
data-dragging={isDragging}
|
||||||
>
|
>
|
||||||
<CollectionItem
|
<MenuLinkItem
|
||||||
data-testid="collection-page"
|
data-testid="collection-page"
|
||||||
data-type="collection-list-item"
|
data-type="collection-list-item"
|
||||||
icon={icon}
|
icon={icon}
|
||||||
onClick={clickDoc}
|
to={`/${docId}`}
|
||||||
|
linkComponent={WorkbenchLink}
|
||||||
className={styles.title}
|
className={styles.title}
|
||||||
active={active}
|
active={active}
|
||||||
collapsed={referencesToRender.length > 0 ? collapsed : undefined}
|
collapsed={referencesToRender.length > 0 ? collapsed : undefined}
|
||||||
@ -101,7 +99,7 @@ export const Doc = ({
|
|||||||
{...listeners}
|
{...listeners}
|
||||||
>
|
>
|
||||||
{doc.title || t['Untitled']()}
|
{doc.title || t['Untitled']()}
|
||||||
</CollectionItem>
|
</MenuLinkItem>
|
||||||
<Collapsible.Content className={styles.collapsibleContent}>
|
<Collapsible.Content className={styles.collapsibleContent}>
|
||||||
{referencesToRender.map(id => {
|
{referencesToRender.map(id => {
|
||||||
return (
|
return (
|
||||||
|
@ -150,3 +150,9 @@ export const emptyCollectionNewButton = style({
|
|||||||
height: '28px',
|
height: '28px',
|
||||||
fontSize: cssVar('fontXs'),
|
fontSize: cssVar('fontXs'),
|
||||||
});
|
});
|
||||||
|
export const docsListContainer = style({
|
||||||
|
marginLeft: 20,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 4,
|
||||||
|
});
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import { useBlockSuitePageReferences } from '@affine/core/hooks/use-block-suite-page-references';
|
import { useBlockSuitePageReferences } from '@affine/core/hooks/use-block-suite-page-references';
|
||||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
import {
|
||||||
|
WorkbenchLink,
|
||||||
|
WorkbenchService,
|
||||||
|
} from '@affine/core/modules/workbench';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
||||||
import type { DocCollection, DocMeta } from '@blocksuite/store';
|
import type { DocCollection, DocMeta } from '@blocksuite/store';
|
||||||
@ -60,10 +63,11 @@ export const ReferencePage = ({
|
|||||||
data-type="reference-page"
|
data-type="reference-page"
|
||||||
data-testid={`reference-page-${pageId}`}
|
data-testid={`reference-page-${pageId}`}
|
||||||
active={active}
|
active={active}
|
||||||
to={`/workspace/${docCollection.id}/${pageId}`}
|
to={`/${pageId}`}
|
||||||
icon={icon}
|
icon={icon}
|
||||||
collapsed={collapsible ? collapsed : undefined}
|
collapsed={collapsible ? collapsed : undefined}
|
||||||
onCollapsedChange={setCollapsed}
|
onCollapsedChange={setCollapsed}
|
||||||
|
linkComponent={WorkbenchLink}
|
||||||
postfix={
|
postfix={
|
||||||
<PostfixItem
|
<PostfixItem
|
||||||
docCollection={docCollection}
|
docCollection={docCollection}
|
||||||
|
@ -3,7 +3,10 @@ import {
|
|||||||
parseDNDId,
|
parseDNDId,
|
||||||
} from '@affine/core/hooks/affine/use-global-dnd-helper';
|
} from '@affine/core/hooks/affine/use-global-dnd-helper';
|
||||||
import { useBlockSuitePageReferences } from '@affine/core/hooks/use-block-suite-page-references';
|
import { useBlockSuitePageReferences } from '@affine/core/hooks/use-block-suite-page-references';
|
||||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
import {
|
||||||
|
WorkbenchLink,
|
||||||
|
WorkbenchService,
|
||||||
|
} from '@affine/core/modules/workbench';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
||||||
import { type AnimateLayoutChanges, useSortable } from '@dnd-kit/sortable';
|
import { type AnimateLayoutChanges, useSortable } from '@dnd-kit/sortable';
|
||||||
@ -102,7 +105,8 @@ export const FavouriteDocSidebarNavItem = ({
|
|||||||
data-dragging={isDragging}
|
data-dragging={isDragging}
|
||||||
className={draggableMenuItemStyles.draggableMenuItem}
|
className={draggableMenuItemStyles.draggableMenuItem}
|
||||||
active={linkActive}
|
active={linkActive}
|
||||||
to={`/workspace/${workspace.id}/${pageId}`}
|
to={`/${pageId}`}
|
||||||
|
linkComponent={WorkbenchLink}
|
||||||
collapsed={collapsible ? collapsed : undefined}
|
collapsed={collapsible ? collapsed : undefined}
|
||||||
onCollapsedChange={setCollapsed}
|
onCollapsedChange={setCollapsed}
|
||||||
postfix={
|
postfix={
|
||||||
|
@ -5,6 +5,7 @@ import { configureCloudModule } from './cloud';
|
|||||||
import { configureCollectionModule } from './collection';
|
import { configureCollectionModule } from './collection';
|
||||||
import { configureFindInPageModule } from './find-in-page';
|
import { configureFindInPageModule } from './find-in-page';
|
||||||
import { configureNavigationModule } from './navigation';
|
import { configureNavigationModule } from './navigation';
|
||||||
|
import { configurePeekViewModule } from './peek-view';
|
||||||
import { configurePermissionsModule } from './permissions';
|
import { configurePermissionsModule } from './permissions';
|
||||||
import { configureWorkspacePropertiesModule } from './properties';
|
import { configureWorkspacePropertiesModule } from './properties';
|
||||||
import { configureRightSidebarModule } from './right-sidebar';
|
import { configureRightSidebarModule } from './right-sidebar';
|
||||||
@ -28,6 +29,7 @@ export function configureCommonModules(framework: Framework) {
|
|||||||
configureShareDocsModule(framework);
|
configureShareDocsModule(framework);
|
||||||
configureTelemetryModule(framework);
|
configureTelemetryModule(framework);
|
||||||
configureFindInPageModule(framework);
|
configureFindInPageModule(framework);
|
||||||
|
configurePeekViewModule(framework);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function configureImpls(framework: Framework) {
|
export function configureImpls(framework: Framework) {
|
||||||
|
@ -0,0 +1,139 @@
|
|||||||
|
import type { BlockElement } from '@blocksuite/block-std';
|
||||||
|
import {
|
||||||
|
AffineReference,
|
||||||
|
type EmbedLinkedDocModel,
|
||||||
|
type EmbedSyncedDocModel,
|
||||||
|
type SurfaceRefBlockComponent,
|
||||||
|
type SurfaceRefBlockModel,
|
||||||
|
} from '@blocksuite/blocks';
|
||||||
|
import { type DocMode, Entity, LiveData } from '@toeverything/infra';
|
||||||
|
|
||||||
|
export type PeekViewTarget =
|
||||||
|
| HTMLElement
|
||||||
|
| BlockElement
|
||||||
|
| AffineReference
|
||||||
|
| HTMLAnchorElement
|
||||||
|
| { docId: string; blockId?: string };
|
||||||
|
|
||||||
|
export type DocPeekViewInfo = {
|
||||||
|
docId: string;
|
||||||
|
blockId?: string;
|
||||||
|
mode?: DocMode;
|
||||||
|
xywh?: `[${number},${number},${number},${number}]`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ActivePeekView = {
|
||||||
|
target: PeekViewTarget;
|
||||||
|
info: DocPeekViewInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
import type { BlockModel } from '@blocksuite/store';
|
||||||
|
|
||||||
|
const EMBED_DOC_FLAVOURS = [
|
||||||
|
'affine:embed-linked-doc',
|
||||||
|
'affine:embed-synced-doc',
|
||||||
|
];
|
||||||
|
|
||||||
|
const isEmbedDocModel = (
|
||||||
|
blockModel: BlockModel
|
||||||
|
): blockModel is EmbedSyncedDocModel | EmbedLinkedDocModel => {
|
||||||
|
return EMBED_DOC_FLAVOURS.includes(blockModel.flavour);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSurfaceRefModel = (
|
||||||
|
blockModel: BlockModel
|
||||||
|
): blockModel is SurfaceRefBlockModel => {
|
||||||
|
return blockModel.flavour === 'affine:surface-ref';
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveLinkToDoc = (href: string) => {
|
||||||
|
// http://xxx/workspace/48__RTCSwASvWZxyAk3Jw/-Uge-K6SYcAbcNYfQ5U-j#xxxx
|
||||||
|
// to { workspaceId: '48__RTCSwASvWZxyAk3Jw', docId: '-Uge-K6SYcAbcNYfQ5U-j', blockId: 'xxxx' }
|
||||||
|
|
||||||
|
const [_, workspaceId, docId, blockId] =
|
||||||
|
href.match(/\/workspace\/([^/]+)\/([^#]+)(?:#(.+))?/) || [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see /packages/frontend/core/src/router.tsx
|
||||||
|
*/
|
||||||
|
const excludedPaths = ['all', 'collection', 'tag', 'trash'];
|
||||||
|
|
||||||
|
if (!docId || excludedPaths.includes(docId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { workspaceId, docId, blockId };
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolvePeekInfoFromPeekTarget(
|
||||||
|
peekTarget?: PeekViewTarget
|
||||||
|
): DocPeekViewInfo | null {
|
||||||
|
if (!peekTarget) return null;
|
||||||
|
if (peekTarget instanceof AffineReference) {
|
||||||
|
if (peekTarget.refMeta) {
|
||||||
|
return {
|
||||||
|
docId: peekTarget.refMeta.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if ('model' in peekTarget) {
|
||||||
|
const blockModel = peekTarget.model;
|
||||||
|
if (isEmbedDocModel(blockModel)) {
|
||||||
|
return {
|
||||||
|
docId: blockModel.pageId,
|
||||||
|
};
|
||||||
|
} else if (isSurfaceRefModel(blockModel)) {
|
||||||
|
const refModel = (peekTarget as SurfaceRefBlockComponent).referenceModel;
|
||||||
|
// refModel can be null if the reference is invalid
|
||||||
|
if (refModel) {
|
||||||
|
const docId =
|
||||||
|
'doc' in refModel ? refModel.doc.id : refModel.surface.doc.id;
|
||||||
|
return {
|
||||||
|
docId,
|
||||||
|
mode: 'edgeless',
|
||||||
|
xywh: refModel.xywh,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (peekTarget instanceof HTMLAnchorElement) {
|
||||||
|
const maybeDoc = resolveLinkToDoc(peekTarget.href);
|
||||||
|
if (maybeDoc) {
|
||||||
|
return {
|
||||||
|
docId: maybeDoc.docId,
|
||||||
|
blockId: maybeDoc.blockId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if ('docId' in peekTarget) {
|
||||||
|
return {
|
||||||
|
docId: peekTarget.docId,
|
||||||
|
blockId: peekTarget.blockId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PeekViewEntity extends Entity {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly _active$ = new LiveData<ActivePeekView | null>(null);
|
||||||
|
private readonly _show$ = new LiveData<boolean>(false);
|
||||||
|
|
||||||
|
active$ = this._active$.distinctUntilChanged();
|
||||||
|
show$ = this._show$
|
||||||
|
.map(show => show && this._active$.value !== null)
|
||||||
|
.distinctUntilChanged();
|
||||||
|
|
||||||
|
open = (target: ActivePeekView['target']) => {
|
||||||
|
const resolvedInfo = resolvePeekInfoFromPeekTarget(target);
|
||||||
|
if (!resolvedInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._active$.next({ target, info: resolvedInfo });
|
||||||
|
this._show$.next(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
close = () => {
|
||||||
|
this._show$.next(false);
|
||||||
|
};
|
||||||
|
}
|
11
packages/frontend/core/src/modules/peek-view/index.ts
Normal file
11
packages/frontend/core/src/modules/peek-view/index.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import type { Framework } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import { PeekViewEntity } from './entities/peek-view';
|
||||||
|
import { PeekViewService } from './services/peek-view';
|
||||||
|
|
||||||
|
export function configurePeekViewModule(framework: Framework) {
|
||||||
|
framework.service(PeekViewService).entity(PeekViewEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { PeekViewEntity, PeekViewService };
|
||||||
|
export { PeekViewManagerModal, useInsidePeekView } from './view';
|
@ -0,0 +1,9 @@
|
|||||||
|
import { OnEvent, Service } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import { WorkbenchLocationChanged } from '../../workbench/services/workbench';
|
||||||
|
import { PeekViewEntity } from '../entities/peek-view';
|
||||||
|
|
||||||
|
@OnEvent(WorkbenchLocationChanged, e => e.peekView.close)
|
||||||
|
export class PeekViewService extends Service {
|
||||||
|
public readonly peekView = this.framework.createEntity(PeekViewEntity);
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const root = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
height: '100%',
|
||||||
|
gap: 8,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const button = style({
|
||||||
|
color: cssVar('iconColor'),
|
||||||
|
boxShadow: cssVar('shadow2'),
|
||||||
|
borderRadius: 8,
|
||||||
|
fontSize: '20px !important',
|
||||||
|
':hover': {
|
||||||
|
background: cssVar('hoverColorFilled'),
|
||||||
|
},
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
});
|
@ -0,0 +1,144 @@
|
|||||||
|
import { IconButton, Tooltip } from '@affine/component';
|
||||||
|
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
||||||
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
|
import {
|
||||||
|
CloseIcon,
|
||||||
|
DualLinkIcon,
|
||||||
|
ExpandFullIcon,
|
||||||
|
SplitViewIcon,
|
||||||
|
} from '@blocksuite/icons';
|
||||||
|
import { type DocMode, useService } from '@toeverything/infra';
|
||||||
|
import { clsx } from 'clsx';
|
||||||
|
import {
|
||||||
|
type HTMLAttributes,
|
||||||
|
type MouseEventHandler,
|
||||||
|
type ReactElement,
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import { WorkbenchService } from '../../workbench';
|
||||||
|
import { PeekViewService } from '../services/peek-view';
|
||||||
|
import * as styles from './doc-peek-controls.css';
|
||||||
|
import { useDoc } from './utils';
|
||||||
|
|
||||||
|
type ControlButtonProps = {
|
||||||
|
nameKey: string;
|
||||||
|
icon: ReactElement;
|
||||||
|
name: string;
|
||||||
|
onClick: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ControlButton = ({
|
||||||
|
icon,
|
||||||
|
nameKey,
|
||||||
|
name,
|
||||||
|
onClick,
|
||||||
|
}: ControlButtonProps) => {
|
||||||
|
const handleClick: MouseEventHandler = useCallback(
|
||||||
|
e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
onClick();
|
||||||
|
},
|
||||||
|
[onClick]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip content={name}>
|
||||||
|
<IconButton
|
||||||
|
data-testid="peek-view-control"
|
||||||
|
data-action-name={nameKey}
|
||||||
|
size="large"
|
||||||
|
type="default"
|
||||||
|
onClick={handleClick}
|
||||||
|
icon={icon}
|
||||||
|
className={styles.button}
|
||||||
|
withoutHoverStyle
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type DocPeekViewControlsProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
docId: string;
|
||||||
|
blockId?: string;
|
||||||
|
mode?: DocMode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DocPeekViewControls = ({
|
||||||
|
docId,
|
||||||
|
blockId,
|
||||||
|
mode,
|
||||||
|
className,
|
||||||
|
...rest
|
||||||
|
}: DocPeekViewControlsProps) => {
|
||||||
|
const peekView = useService(PeekViewService).peekView;
|
||||||
|
const workbench = useService(WorkbenchService).workbench;
|
||||||
|
const { jumpToPageBlock } = useNavigateHelper();
|
||||||
|
const t = useAFFiNEI18N();
|
||||||
|
const { doc, workspace } = useDoc(docId);
|
||||||
|
const controls = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
icon: <CloseIcon />,
|
||||||
|
nameKey: 'close',
|
||||||
|
name: t['com.affine.peek-view-controls.close'](),
|
||||||
|
onClick: peekView.close,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <ExpandFullIcon />,
|
||||||
|
name: t['com.affine.peek-view-controls.open-doc'](),
|
||||||
|
nameKey: 'open',
|
||||||
|
onClick: () => {
|
||||||
|
// todo: for frame blocks, we should mimic "view in edgeless" button behavior
|
||||||
|
blockId
|
||||||
|
? jumpToPageBlock(workspace.id, docId, blockId)
|
||||||
|
: workbench.openPage(docId);
|
||||||
|
if (mode) {
|
||||||
|
doc?.setMode(mode);
|
||||||
|
}
|
||||||
|
peekView.close();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
environment.isDesktop && {
|
||||||
|
icon: <SplitViewIcon />,
|
||||||
|
nameKey: 'split-view',
|
||||||
|
name: t['com.affine.peek-view-controls.open-doc-in-split-view'](),
|
||||||
|
onClick: () => {
|
||||||
|
workbench.openPage(docId, { at: 'beside' });
|
||||||
|
peekView.close();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
!environment.isDesktop && {
|
||||||
|
icon: <DualLinkIcon />,
|
||||||
|
nameKey: 'new-tab',
|
||||||
|
name: t['com.affine.peek-view-controls.open-doc-in-new-tab'](),
|
||||||
|
onClick: () => {
|
||||||
|
window.open(
|
||||||
|
`/workspace/${workspace.id}/${docId}#${blockId ?? ''}`,
|
||||||
|
'_blank'
|
||||||
|
);
|
||||||
|
peekView.close();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
].filter((opt): opt is ControlButtonProps => Boolean(opt));
|
||||||
|
}, [
|
||||||
|
blockId,
|
||||||
|
doc,
|
||||||
|
docId,
|
||||||
|
jumpToPageBlock,
|
||||||
|
mode,
|
||||||
|
peekView,
|
||||||
|
t,
|
||||||
|
workbench,
|
||||||
|
workspace.id,
|
||||||
|
]);
|
||||||
|
return (
|
||||||
|
<div {...rest} className={clsx(styles.root, className)}>
|
||||||
|
{controls.map(option => (
|
||||||
|
<ControlButton key={option.nameKey} {...option} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,9 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const editor = style({
|
||||||
|
vars: {
|
||||||
|
'--affine-editor-width': '100%',
|
||||||
|
'--affine-editor-side-padding': '160px',
|
||||||
|
},
|
||||||
|
minHeight: '100%',
|
||||||
|
});
|
@ -0,0 +1,170 @@
|
|||||||
|
import { Scrollable } from '@affine/component';
|
||||||
|
import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton';
|
||||||
|
import { AffineErrorBoundary } from '@affine/core/components/affine/affine-error-boundary';
|
||||||
|
import { BlockSuiteEditor } from '@affine/core/components/blocksuite/block-suite-editor';
|
||||||
|
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
||||||
|
import { PageNotFound } from '@affine/core/pages/404';
|
||||||
|
import { Bound, type EdgelessRootService } from '@blocksuite/blocks';
|
||||||
|
import { DisposableGroup } from '@blocksuite/global/utils';
|
||||||
|
import { type AffineEditorContainer, AIProvider } from '@blocksuite/presets';
|
||||||
|
import type { DocMode } from '@toeverything/infra';
|
||||||
|
import { DocsService, FrameworkScope, useService } from '@toeverything/infra';
|
||||||
|
import { forwardRef, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { WorkbenchService } from '../../workbench';
|
||||||
|
import { PeekViewService } from '../services/peek-view';
|
||||||
|
import * as styles from './doc-peek-view.css';
|
||||||
|
import { useDoc } from './utils';
|
||||||
|
|
||||||
|
const DocPreview = forwardRef<
|
||||||
|
AffineEditorContainer,
|
||||||
|
{ docId: string; blockId?: string; mode?: DocMode }
|
||||||
|
>(function DocPreview({ docId, blockId, mode }, ref) {
|
||||||
|
const { doc, workspace, loading } = useDoc(docId);
|
||||||
|
const { jumpToTag } = useNavigateHelper();
|
||||||
|
const workbench = useService(WorkbenchService).workbench;
|
||||||
|
const peekView = useService(PeekViewService).peekView;
|
||||||
|
const [editor, setEditor] = useState<AffineEditorContainer | null>(null);
|
||||||
|
|
||||||
|
const onRef = (editor: AffineEditorContainer) => {
|
||||||
|
if (typeof ref === 'function') {
|
||||||
|
ref(editor);
|
||||||
|
} else if (ref) {
|
||||||
|
ref.current = editor;
|
||||||
|
}
|
||||||
|
setEditor(editor);
|
||||||
|
};
|
||||||
|
|
||||||
|
const docs = useService(DocsService);
|
||||||
|
const [resolvedMode, setResolvedMode] = useState<DocMode | undefined>(mode);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mode || !resolvedMode) {
|
||||||
|
setResolvedMode(docs.list.doc$(docId).value?.mode$.value || 'page');
|
||||||
|
}
|
||||||
|
}, [docId, docs.list, resolvedMode, mode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const disposable = AIProvider.slots.requestContinueInChat.on(() => {
|
||||||
|
if (doc) {
|
||||||
|
workbench.openPage(doc.id);
|
||||||
|
peekView.close();
|
||||||
|
// chat panel open is already handled in <DetailPageImpl />
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
disposable.dispose();
|
||||||
|
};
|
||||||
|
}, [doc, peekView, workbench, workspace.id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const disposableGroup = new DisposableGroup();
|
||||||
|
if (editor) {
|
||||||
|
editor.updateComplete
|
||||||
|
.then(() => {
|
||||||
|
const rootService = editor.host.std.spec.getService('affine:page');
|
||||||
|
// doc change event inside peek view should be handled by peek view
|
||||||
|
disposableGroup.add(
|
||||||
|
rootService.slots.docLinkClicked.on(({ docId, blockId }) => {
|
||||||
|
peekView.open({ docId, blockId });
|
||||||
|
})
|
||||||
|
);
|
||||||
|
// todo: no tag peek view yet
|
||||||
|
disposableGroup.add(
|
||||||
|
rootService.slots.tagClicked.on(({ tagId }) => {
|
||||||
|
jumpToTag(workspace.id, tagId);
|
||||||
|
peekView.close();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
disposableGroup.dispose();
|
||||||
|
};
|
||||||
|
}, [editor, jumpToTag, peekView, workspace.id]);
|
||||||
|
|
||||||
|
// if sync engine has been synced and the page is null, show 404 page.
|
||||||
|
if (!doc || !resolvedMode) {
|
||||||
|
return loading || !resolvedMode ? (
|
||||||
|
<PageDetailSkeleton key="current-page-is-null" />
|
||||||
|
) : (
|
||||||
|
<PageNotFound noPermission />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AffineErrorBoundary>
|
||||||
|
<Scrollable.Root>
|
||||||
|
<Scrollable.Viewport className="affine-page-viewport">
|
||||||
|
<FrameworkScope scope={doc.scope}>
|
||||||
|
<BlockSuiteEditor
|
||||||
|
ref={onRef}
|
||||||
|
className={styles.editor}
|
||||||
|
mode={resolvedMode}
|
||||||
|
defaultSelectedBlockId={blockId}
|
||||||
|
page={doc.blockSuiteDoc}
|
||||||
|
/>
|
||||||
|
</FrameworkScope>
|
||||||
|
</Scrollable.Viewport>
|
||||||
|
<Scrollable.Scrollbar />
|
||||||
|
</Scrollable.Root>
|
||||||
|
</AffineErrorBoundary>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const DocPeekView = ({
|
||||||
|
docId,
|
||||||
|
blockId,
|
||||||
|
mode,
|
||||||
|
}: {
|
||||||
|
docId: string;
|
||||||
|
blockId?: string;
|
||||||
|
mode?: DocMode;
|
||||||
|
}) => {
|
||||||
|
return <DocPreview mode={mode} docId={docId} blockId={blockId} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SurfaceRefPeekView = ({
|
||||||
|
docId,
|
||||||
|
xywh,
|
||||||
|
}: {
|
||||||
|
docId: string;
|
||||||
|
xywh: `[${number},${number},${number},${number}]`;
|
||||||
|
}) => {
|
||||||
|
const [editorRef, setEditorRef] = useState<AffineEditorContainer | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let mounted = true;
|
||||||
|
if (editorRef) {
|
||||||
|
editorRef.host.updateComplete
|
||||||
|
.then(() => {
|
||||||
|
if (mounted) {
|
||||||
|
const viewport = {
|
||||||
|
xywh: xywh,
|
||||||
|
padding: [60, 20, 20, 20] as [number, number, number, number],
|
||||||
|
};
|
||||||
|
const rootService =
|
||||||
|
editorRef.host.std.spec.getService<EdgelessRootService>(
|
||||||
|
'affine:page'
|
||||||
|
);
|
||||||
|
rootService.viewport.setViewportByBound(
|
||||||
|
Bound.deserialize(viewport.xywh),
|
||||||
|
viewport.padding
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
console.error(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
mounted = false;
|
||||||
|
};
|
||||||
|
}, [editorRef, xywh]);
|
||||||
|
|
||||||
|
return <DocPreview ref={setEditorRef} docId={docId} mode={'edgeless'} />;
|
||||||
|
};
|
@ -0,0 +1,2 @@
|
|||||||
|
export { useInsidePeekView } from './modal-container';
|
||||||
|
export { PeekViewManagerModal } from './peek-view-manager';
|
@ -0,0 +1,154 @@
|
|||||||
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import { createVar, keyframes, style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const animationTimeout = createVar();
|
||||||
|
export const transformOrigin = createVar();
|
||||||
|
|
||||||
|
const contentShow = keyframes({
|
||||||
|
from: {
|
||||||
|
opacity: 0,
|
||||||
|
transform: 'scale(0.10)',
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
opacity: 1,
|
||||||
|
transform: 'scale(1)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const contentHide = keyframes({
|
||||||
|
to: {
|
||||||
|
opacity: 0,
|
||||||
|
transform: 'scale(0.10)',
|
||||||
|
},
|
||||||
|
from: {
|
||||||
|
opacity: 1,
|
||||||
|
transform: 'scale(1)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const fadeIn = keyframes({
|
||||||
|
from: {
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const fadeOut = keyframes({
|
||||||
|
from: {
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const slideRight = keyframes({
|
||||||
|
from: {
|
||||||
|
transform: 'translateX(-200%)',
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
transform: 'translateX(0)',
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const slideLeft = keyframes({
|
||||||
|
from: {
|
||||||
|
transform: 'translateX(0)',
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
transform: 'translateX(-200%)',
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const modalOverlay = style({
|
||||||
|
position: 'fixed',
|
||||||
|
inset: 0,
|
||||||
|
zIndex: cssVar('zIndexModal'),
|
||||||
|
backgroundColor: cssVar('black30'),
|
||||||
|
willChange: 'opacity',
|
||||||
|
selectors: {
|
||||||
|
'&[data-state=entered], &[data-state=entering]': {
|
||||||
|
animation: `${fadeIn} ${animationTimeout} ease-in-out`,
|
||||||
|
animationFillMode: 'forwards',
|
||||||
|
},
|
||||||
|
'&[data-state=exited], &[data-state=exiting]': {
|
||||||
|
animation: `${fadeOut} ${animationTimeout} ${animationTimeout} ease-in-out`,
|
||||||
|
animationFillMode: 'backwards',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const modalContentWrapper = style({
|
||||||
|
position: 'fixed',
|
||||||
|
inset: 0,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: cssVar('zIndexModal'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const modalContentContainer = style({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
width: '90%',
|
||||||
|
height: '90%',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const modalContent = style({
|
||||||
|
flex: 1,
|
||||||
|
height: '100%',
|
||||||
|
backgroundColor: cssVar('backgroundOverlayPanelColor'),
|
||||||
|
boxShadow: '0px 0px 0px 2.23px rgba(0, 0, 0, 0.08)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
minHeight: 300,
|
||||||
|
// :focus-visible will set outline
|
||||||
|
outline: 'none',
|
||||||
|
position: 'relative',
|
||||||
|
willChange: 'transform, opacity',
|
||||||
|
transformOrigin: transformOrigin,
|
||||||
|
selectors: {
|
||||||
|
'&[data-state=entered], &[data-state=entering]': {
|
||||||
|
animationFillMode: 'forwards',
|
||||||
|
animationName: contentShow,
|
||||||
|
animationDuration: animationTimeout,
|
||||||
|
animationTimingFunction: 'cubic-bezier(0.42, 0, 0.58, 1)',
|
||||||
|
},
|
||||||
|
'&[data-state=exited], &[data-state=exiting]': {
|
||||||
|
animationFillMode: 'forwards',
|
||||||
|
animationName: contentHide,
|
||||||
|
animationDuration: animationTimeout,
|
||||||
|
animationTimingFunction: 'cubic-bezier(0.42, 0, 0.58, 1)',
|
||||||
|
animationDelay: animationTimeout,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const modalControls = style({
|
||||||
|
flexShrink: 0,
|
||||||
|
zIndex: -1,
|
||||||
|
minWidth: '48px',
|
||||||
|
padding: '8px 0 0 16px',
|
||||||
|
opacity: 0,
|
||||||
|
transformOrigin: transformOrigin,
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
selectors: {
|
||||||
|
'&[data-state=entered], &[data-state=entering]': {
|
||||||
|
animationName: slideRight,
|
||||||
|
animationDuration: animationTimeout,
|
||||||
|
animationFillMode: 'forwards',
|
||||||
|
animationTimingFunction: 'ease-in-out',
|
||||||
|
animationDelay: `calc(${animationTimeout} / 2)`,
|
||||||
|
},
|
||||||
|
'&[data-state=exited], &[data-state=exiting]': {
|
||||||
|
animationName: slideLeft,
|
||||||
|
animationDuration: animationTimeout,
|
||||||
|
animationFillMode: 'forwards',
|
||||||
|
animationTimingFunction: 'ease-in-out',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
@ -0,0 +1,127 @@
|
|||||||
|
import * as Dialog from '@radix-ui/react-dialog';
|
||||||
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
type PropsWithChildren,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import useTransition from 'react-transition-state';
|
||||||
|
|
||||||
|
import * as styles from './modal-container.css';
|
||||||
|
|
||||||
|
const animationTimeout = 200;
|
||||||
|
|
||||||
|
const contentOptions: Dialog.DialogContentProps = {
|
||||||
|
['data-testid' as string]: 'peek-view-modal',
|
||||||
|
onPointerDownOutside: e => {
|
||||||
|
const el = e.target as HTMLElement;
|
||||||
|
if (el.closest('[data-peek-view-wrapper]')) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
padding: 0,
|
||||||
|
backgroundColor: cssVar('backgroundPrimaryColor'),
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// a dummy context to let elements know they are inside a peek view
|
||||||
|
export const PeekViewContext = createContext<Record<string, never> | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
const emptyContext = {};
|
||||||
|
|
||||||
|
export const useInsidePeekView = () => {
|
||||||
|
const context = useContext(PeekViewContext);
|
||||||
|
return !!context;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getElementScreenPositionCenter(target: HTMLElement) {
|
||||||
|
const rect = target.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (rect.top === 0 || rect.left === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollTop = window.scrollY || document.documentElement.scrollTop;
|
||||||
|
const scrollLeft = window.scrollX || document.documentElement.scrollLeft;
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: rect.x + scrollLeft + rect.width / 2,
|
||||||
|
y: rect.y + scrollTop + rect.height / 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PeekViewModalContainer = ({
|
||||||
|
onOpenChange,
|
||||||
|
open,
|
||||||
|
target,
|
||||||
|
controls,
|
||||||
|
children,
|
||||||
|
onAnimateEnd,
|
||||||
|
}: PropsWithChildren<{
|
||||||
|
open: boolean;
|
||||||
|
target?: HTMLElement;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
controls: React.ReactNode;
|
||||||
|
onAnimateEnd?: () => void;
|
||||||
|
}>) => {
|
||||||
|
const [{ status }, toggle] = useTransition({
|
||||||
|
timeout: animationTimeout * 2,
|
||||||
|
onStateChange(event) {
|
||||||
|
if (event.current.status === 'exited' && onAnimateEnd) {
|
||||||
|
onAnimateEnd();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const [transformOrigin, setTransformOrigin] = useState<string | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
toggle(open);
|
||||||
|
const bondingBox = target ? getElementScreenPositionCenter(target) : null;
|
||||||
|
setTransformOrigin(
|
||||||
|
bondingBox ? `${bondingBox.x}px ${bondingBox.y}px` : null
|
||||||
|
);
|
||||||
|
}, [open, target]);
|
||||||
|
return (
|
||||||
|
<PeekViewContext.Provider value={emptyContext}>
|
||||||
|
<Dialog.Root modal open={status !== 'exited'} onOpenChange={onOpenChange}>
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Overlay
|
||||||
|
className={styles.modalOverlay}
|
||||||
|
data-state={status}
|
||||||
|
style={assignInlineVars({
|
||||||
|
[styles.transformOrigin]: transformOrigin,
|
||||||
|
[styles.animationTimeout]: `${animationTimeout}ms`,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
data-peek-view-wrapper
|
||||||
|
className={styles.modalContentWrapper}
|
||||||
|
style={assignInlineVars({
|
||||||
|
[styles.transformOrigin]: transformOrigin,
|
||||||
|
[styles.animationTimeout]: `${animationTimeout}ms`,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className={styles.modalContentContainer}>
|
||||||
|
<Dialog.Content
|
||||||
|
{...contentOptions}
|
||||||
|
className={styles.modalContent}
|
||||||
|
data-state={status}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Dialog.Content>
|
||||||
|
<div data-state={status} className={styles.modalControls}>
|
||||||
|
{controls}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
|
</PeekViewContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,61 @@
|
|||||||
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import type { ActivePeekView } from '../entities/peek-view';
|
||||||
|
import { PeekViewService } from '../services/peek-view';
|
||||||
|
import { DocPeekViewControls } from './doc-peek-controls';
|
||||||
|
import { DocPeekView, SurfaceRefPeekView } from './doc-peek-view';
|
||||||
|
import { PeekViewModalContainer } from './modal-container';
|
||||||
|
|
||||||
|
function renderPeekView({ info }: ActivePeekView) {
|
||||||
|
if (info.mode === 'edgeless' && info.xywh) {
|
||||||
|
return <SurfaceRefPeekView docId={info.docId} xywh={info.xywh} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DocPeekView mode={info.mode} docId={info.docId} blockId={info.blockId} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderControls = ({ info }: ActivePeekView) => {
|
||||||
|
return (
|
||||||
|
<DocPeekViewControls
|
||||||
|
mode={info.mode}
|
||||||
|
docId={info.docId}
|
||||||
|
blockId={info.docId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PeekViewManagerModal = () => {
|
||||||
|
const peekViewEntity = useService(PeekViewService).peekView;
|
||||||
|
const activePeekView = useLiveData(peekViewEntity.active$);
|
||||||
|
const show = useLiveData(peekViewEntity.show$);
|
||||||
|
|
||||||
|
const preview = useMemo(() => {
|
||||||
|
return activePeekView ? renderPeekView(activePeekView) : null;
|
||||||
|
}, [activePeekView]);
|
||||||
|
|
||||||
|
const controls = useMemo(() => {
|
||||||
|
return activePeekView ? renderControls(activePeekView) : null;
|
||||||
|
}, [activePeekView]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PeekViewModalContainer
|
||||||
|
open={show}
|
||||||
|
target={
|
||||||
|
activePeekView?.target instanceof HTMLElement
|
||||||
|
? activePeekView.target
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
controls={controls}
|
||||||
|
onOpenChange={open => {
|
||||||
|
if (!open) {
|
||||||
|
peekViewEntity.close();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{preview}
|
||||||
|
</PeekViewModalContainer>
|
||||||
|
);
|
||||||
|
};
|
39
packages/frontend/core/src/modules/peek-view/view/utils.ts
Normal file
39
packages/frontend/core/src/modules/peek-view/view/utils.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import type { Doc } from '@toeverything/infra';
|
||||||
|
import {
|
||||||
|
DocsService,
|
||||||
|
useLiveData,
|
||||||
|
useService,
|
||||||
|
WorkspaceService,
|
||||||
|
} from '@toeverything/infra';
|
||||||
|
import { useEffect, useLayoutEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export const useDoc = (pageId: string) => {
|
||||||
|
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||||
|
const docsService = useService(DocsService);
|
||||||
|
const docRecordList = docsService.list;
|
||||||
|
const docListReady = useLiveData(docRecordList.isReady$);
|
||||||
|
const docRecord = docRecordList.doc$(pageId).value;
|
||||||
|
|
||||||
|
const [doc, setDoc] = useState<Doc | null>(null);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!docRecord) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { doc: opened, release } = docsService.open(pageId);
|
||||||
|
setDoc(opened);
|
||||||
|
return () => {
|
||||||
|
release();
|
||||||
|
};
|
||||||
|
}, [docRecord, docsService, pageId]);
|
||||||
|
|
||||||
|
// set sync engine priority target
|
||||||
|
useEffect(() => {
|
||||||
|
currentWorkspace.engine.doc.setPriority(pageId, 10);
|
||||||
|
return () => {
|
||||||
|
currentWorkspace.engine.doc.setPriority(pageId, 5);
|
||||||
|
};
|
||||||
|
}, [currentWorkspace, pageId]);
|
||||||
|
|
||||||
|
return { doc, workspace: currentWorkspace, loading: !docListReady };
|
||||||
|
};
|
@ -2,17 +2,14 @@ import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-h
|
|||||||
import { popupWindow } from '@affine/core/utils';
|
import { popupWindow } from '@affine/core/utils';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import type { To } from 'history';
|
import type { To } from 'history';
|
||||||
import { useCallback } from 'react';
|
import { forwardRef, useCallback } from 'react';
|
||||||
|
|
||||||
import { WorkbenchService } from '../services/workbench';
|
import { WorkbenchService } from '../services/workbench';
|
||||||
|
|
||||||
export const WorkbenchLink = ({
|
export const WorkbenchLink = forwardRef<
|
||||||
to,
|
HTMLAnchorElement,
|
||||||
onClick,
|
React.PropsWithChildren<{ to: To } & React.HTMLProps<HTMLAnchorElement>>
|
||||||
...other
|
>(function WorkbenchLink({ to, onClick, ...other }, ref) {
|
||||||
}: React.PropsWithChildren<
|
|
||||||
{ to: To } & React.HTMLProps<HTMLAnchorElement>
|
|
||||||
>) => {
|
|
||||||
const workbench = useService(WorkbenchService).workbench;
|
const workbench = useService(WorkbenchService).workbench;
|
||||||
const { appSettings } = useAppSettingHelper();
|
const { appSettings } = useAppSettingHelper();
|
||||||
const basename = useLiveData(workbench.basename$);
|
const basename = useLiveData(workbench.basename$);
|
||||||
@ -21,11 +18,11 @@ export const WorkbenchLink = ({
|
|||||||
(typeof to === 'string' ? to : `${to.pathname}${to.search}${to.hash}`);
|
(typeof to === 'string' ? to : `${to.pathname}${to.search}${to.hash}`);
|
||||||
const handleClick = useCallback(
|
const handleClick = useCallback(
|
||||||
(event: React.MouseEvent<HTMLAnchorElement>) => {
|
(event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
if (onClick?.(event)) {
|
if (onClick?.(event)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
if (event.ctrlKey || event.metaKey) {
|
if (event.ctrlKey || event.metaKey) {
|
||||||
if (appSettings.enableMultiView && environment.isDesktop) {
|
if (appSettings.enableMultiView && environment.isDesktop) {
|
||||||
@ -42,5 +39,5 @@ export const WorkbenchLink = ({
|
|||||||
|
|
||||||
// eslint suspicious runtime error
|
// eslint suspicious runtime error
|
||||||
// eslint-disable-next-line react/no-danger-with-children
|
// eslint-disable-next-line react/no-danger-with-children
|
||||||
return <a {...other} href={link} onClick={handleClick} />;
|
return <a {...other} ref={ref} href={link} onClick={handleClick} />;
|
||||||
};
|
});
|
||||||
|
@ -84,7 +84,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
|||||||
|
|
||||||
const doc = useService(DocService).doc;
|
const doc = useService(DocService).doc;
|
||||||
const docRecordList = useService(DocsService).list;
|
const docRecordList = useService(DocsService).list;
|
||||||
const { openPage, jumpToTag } = useNavigateHelper();
|
const { openPage, jumpToPageBlock, jumpToTag } = useNavigateHelper();
|
||||||
const [editor, setEditor] = useState<AffineEditorContainer | null>(null);
|
const [editor, setEditor] = useState<AffineEditorContainer | null>(null);
|
||||||
const workspace = useService(WorkspaceService).workspace;
|
const workspace = useService(WorkspaceService).workspace;
|
||||||
const globalContext = useService(GlobalContextService).globalContext;
|
const globalContext = useService(GlobalContextService).globalContext;
|
||||||
@ -199,10 +199,11 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
doc.setMode(mode);
|
doc.setMode(mode);
|
||||||
// fixme: it seems pageLinkClicked is not triggered sometimes?
|
|
||||||
disposable.add(
|
disposable.add(
|
||||||
pageService.slots.docLinkClicked.on(({ docId }) => {
|
pageService.slots.docLinkClicked.on(({ docId, blockId }) => {
|
||||||
return openPage(docCollection.id, docId);
|
return blockId
|
||||||
|
? jumpToPageBlock(docCollection.id, docId, blockId)
|
||||||
|
: openPage(docCollection.id, docId);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
disposable.add(
|
disposable.add(
|
||||||
@ -226,8 +227,9 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
|||||||
doc,
|
doc,
|
||||||
mode,
|
mode,
|
||||||
docRecordList,
|
docRecordList,
|
||||||
openPage,
|
jumpToPageBlock,
|
||||||
docCollection.id,
|
docCollection.id,
|
||||||
|
openPage,
|
||||||
jumpToTag,
|
jumpToTag,
|
||||||
workspace.id,
|
workspace.id,
|
||||||
]
|
]
|
||||||
|
@ -24,6 +24,7 @@ import { PaymentDisableModal } from '../components/affine/payment-disable';
|
|||||||
import { useAsyncCallback } from '../hooks/affine-async-hooks';
|
import { useAsyncCallback } from '../hooks/affine-async-hooks';
|
||||||
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||||
import { AuthService } from '../modules/cloud/services/auth';
|
import { AuthService } from '../modules/cloud/services/auth';
|
||||||
|
import { PeekViewManagerModal } from '../modules/peek-view';
|
||||||
import { WorkspaceSubPath } from '../shared';
|
import { WorkspaceSubPath } from '../shared';
|
||||||
|
|
||||||
const SettingModal = lazy(() =>
|
const SettingModal = lazy(() =>
|
||||||
@ -218,6 +219,7 @@ export function CurrentWorkspaceModals() {
|
|||||||
<CloudQuotaModal />
|
<CloudQuotaModal />
|
||||||
)}
|
)}
|
||||||
<AiLoginRequiredModal />
|
<AiLoginRequiredModal />
|
||||||
|
<PeekViewManagerModal />
|
||||||
{environment.isDesktop && <FindInPageModal />}
|
{environment.isDesktop && <FindInPageModal />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1323,5 +1323,9 @@
|
|||||||
"com.affine.ai-onboarding.edgeless.get-started": "Get Started",
|
"com.affine.ai-onboarding.edgeless.get-started": "Get Started",
|
||||||
"com.affine.ai-onboarding.edgeless.purchase": "Upgrade to Unlimited Usage",
|
"com.affine.ai-onboarding.edgeless.purchase": "Upgrade to Unlimited Usage",
|
||||||
"will delete member": "will delete member",
|
"will delete member": "will delete member",
|
||||||
"com.affine.keyboardShortcuts.copy-private-link": "Copy Private Link"
|
"com.affine.keyboardShortcuts.copy-private-link": "Copy Private Link",
|
||||||
|
"com.affine.peek-view-controls.close": "Close",
|
||||||
|
"com.affine.peek-view-controls.open-doc": "Open this doc",
|
||||||
|
"com.affine.peek-view-controls.open-doc-in-new-tab": "Open in new tab",
|
||||||
|
"com.affine.peek-view-controls.open-doc-in-split-view": "Open in split view"
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,20 @@ vi.mock('@blocksuite/presets', () => ({
|
|||||||
DocTitle: vi.fn(),
|
DocTitle: vi.fn(),
|
||||||
EdgelessEditor: vi.fn(),
|
EdgelessEditor: vi.fn(),
|
||||||
PageEditor: vi.fn(),
|
PageEditor: vi.fn(),
|
||||||
|
AIProvider: {
|
||||||
|
slots: new Proxy(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
get: () => ({
|
||||||
|
on: vi.fn(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
provide: vi.fn(),
|
||||||
|
},
|
||||||
|
AIEdgelessRootBlockSpec: {},
|
||||||
|
AIParagraphBlockSpec: {},
|
||||||
|
AIPageRootBlockSpec: {},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (typeof window !== 'undefined' && HTMLCanvasElement) {
|
if (typeof window !== 'undefined' && HTMLCanvasElement) {
|
@ -28,7 +28,7 @@ export default defineConfig({
|
|||||||
test: {
|
test: {
|
||||||
setupFiles: [
|
setupFiles: [
|
||||||
resolve(rootDir, './scripts/setup/lit.ts'),
|
resolve(rootDir, './scripts/setup/lit.ts'),
|
||||||
resolve(rootDir, './scripts/setup/lottie-web.ts'),
|
resolve(rootDir, './scripts/setup/vi-mock.ts'),
|
||||||
resolve(rootDir, './scripts/setup/global.ts'),
|
resolve(rootDir, './scripts/setup/global.ts'),
|
||||||
],
|
],
|
||||||
include: [
|
include: [
|
||||||
|
Loading…
Reference in New Issue
Block a user