mirror of
https://github.com/suitenumerique/docs.git
synced 2026-08-17 21:25:43 +02:00
♻️(frontend) drive the presenter from a store and one mount point
Move presenter ownership out of DocToolBox into a top-level PresenterRoot driven by a zustand store, lazy-load the overlay and paint a boot cover while the editor boots. Groundwork for the deep-link and present-from-here features (#2466).
This commit is contained in:
@@ -32,6 +32,7 @@ import {
|
||||
useDocUtils,
|
||||
useDuplicateDoc,
|
||||
} from '@/docs/doc-management';
|
||||
import { usePresenterStore } from '@/docs/doc-presenter/stores';
|
||||
import { useAuth } from '@/features/auth';
|
||||
import { useFocusStore, useResponsiveStore } from '@/stores';
|
||||
|
||||
@@ -82,14 +83,6 @@ const ModalExport =
|
||||
)
|
||||
: null;
|
||||
|
||||
const PresenterOverlay = dynamic(
|
||||
() =>
|
||||
import('@/docs/doc-presenter').then((mod) => ({
|
||||
default: mod.PresenterOverlay,
|
||||
})),
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
interface DocToolBoxProps {
|
||||
doc: Doc;
|
||||
}
|
||||
@@ -108,11 +101,13 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
const [isModalShareOpen, setIsModalShareOpen] = useState(false);
|
||||
const [isModalHistoryOpen, setIsModalHistoryOpen] = useState(false);
|
||||
const [isModalLeaveOpen, setIsModalLeaveOpen] = useState(false);
|
||||
const [isPresenterOpen, setIsPresenterOpen] = useState(false);
|
||||
|
||||
const { restoreFocus, addLastFocus } = useFocusStore();
|
||||
const { isMobile } = useResponsiveStore();
|
||||
const copyDocLink = useCopyDocLink(doc.id);
|
||||
// Deep-link (#2397) and slide/URL sync live in PresenterRoot; here we only
|
||||
// trigger the manual "Present" action.
|
||||
const openPresenter = usePresenterStore((state) => state.open);
|
||||
const { mutate: duplicateDoc } = useDuplicateDoc({
|
||||
onSuccess: (data) => {
|
||||
void router.push(`/docs/${data.id}`);
|
||||
@@ -148,9 +143,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
label: t('Present'),
|
||||
icon: <Present width={24} height={24} aria-hidden="true" />,
|
||||
callback: () => {
|
||||
requestAnimationFrame(() => {
|
||||
setIsPresenterOpen(true);
|
||||
});
|
||||
openPresenter(0);
|
||||
},
|
||||
isHidden: Boolean(doc.deleted_at) || isMobile,
|
||||
testId: `docs-actions-present-${doc.id}`,
|
||||
@@ -315,14 +308,6 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
doc={doc}
|
||||
/>
|
||||
)}
|
||||
{isPresenterOpen && (
|
||||
<PresenterOverlay
|
||||
doc={doc}
|
||||
onClose={() => {
|
||||
setIsPresenterOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Loading } from '@/components';
|
||||
import { useEditorStore } from '@/docs/doc-editor/stores';
|
||||
import { useDocStore } from '@/docs/doc-management';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { usePresenterStore } from '../stores';
|
||||
|
||||
const coverCss = css`
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
`;
|
||||
|
||||
const PresenterOverlay = dynamic(
|
||||
() =>
|
||||
import('./PresenterOverlay').then((mod) => ({
|
||||
default: mod.PresenterOverlay,
|
||||
})),
|
||||
{ ssr: false, loading: () => <Loading $css={coverCss} /> },
|
||||
);
|
||||
|
||||
/**
|
||||
* Single mount point for the presenter, rendered high in the tree (doc page).
|
||||
* Drives the manual "Present" action via `usePresenterStore`, painting a boot
|
||||
* cover until the editor snapshot is available.
|
||||
*/
|
||||
export const PresenterRoot = () => {
|
||||
const { isMobile } = useResponsiveStore();
|
||||
const editor = useEditorStore((state) => state.editor);
|
||||
const { currentDoc } = useDocStore();
|
||||
const { initialSlideIndex, isOpen, close } = usePresenterStore();
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
close();
|
||||
}, [close]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isMobile && isOpen) {
|
||||
close();
|
||||
}
|
||||
}, [isMobile, isOpen, close]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
close();
|
||||
};
|
||||
}, [close]);
|
||||
|
||||
const active = !isMobile && isOpen;
|
||||
const isBooting = active && (!editor || !currentDoc);
|
||||
|
||||
// Let users escape the boot cover if the editor never finishes loading.
|
||||
useEffect(() => {
|
||||
if (!isBooting) {
|
||||
return;
|
||||
}
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
return () => window.removeEventListener('keydown', onKeyDown);
|
||||
}, [isBooting, handleClose]);
|
||||
|
||||
if (!active) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!editor || !currentDoc) {
|
||||
return <Loading $css={coverCss} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<PresenterOverlay
|
||||
doc={currentDoc}
|
||||
initialSlideIndex={initialSlideIndex}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1 +1,2 @@
|
||||
export { PresenterOverlay } from './components/PresenterOverlay';
|
||||
export { PresenterRoot } from './components/PresenterRoot';
|
||||
export * from './stores';
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './usePresenterStore';
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
export interface PresenterState {
|
||||
isOpen: boolean;
|
||||
/** 0-based slide to start the presentation on. */
|
||||
initialSlideIndex: number;
|
||||
open: (index?: number) => void;
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
export const usePresenterStore = create<PresenterState>((set) => ({
|
||||
isOpen: false,
|
||||
initialSlideIndex: 0,
|
||||
open: (index = 0) => {
|
||||
const safeSlideIndex = Number.isFinite(index)
|
||||
? Math.max(0, Math.trunc(index))
|
||||
: 0;
|
||||
set({ isOpen: true, initialSlideIndex: safeSlideIndex });
|
||||
},
|
||||
close: () => {
|
||||
set({ isOpen: false });
|
||||
},
|
||||
}));
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
useTrans,
|
||||
} from '@/docs/doc-management/';
|
||||
import { KEY_AUTH, setAuthUrl, useAuth } from '@/features/auth';
|
||||
import { PresenterRoot } from '@/features/docs/doc-presenter';
|
||||
import { getDocChildren, subPageToTree } from '@/features/docs/doc-tree/';
|
||||
import { DocEditorSkeleton, useSkeletonStore } from '@/features/skeletons';
|
||||
import { MainLayout } from '@/layouts';
|
||||
@@ -68,6 +69,7 @@ export function DocLayout() {
|
||||
<DocFloatingBar />
|
||||
<DocPage id={id} />
|
||||
</MainLayout>
|
||||
<PresenterRoot />
|
||||
</TreeProvider>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user