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,
|
useDocUtils,
|
||||||
useDuplicateDoc,
|
useDuplicateDoc,
|
||||||
} from '@/docs/doc-management';
|
} from '@/docs/doc-management';
|
||||||
|
import { usePresenterStore } from '@/docs/doc-presenter/stores';
|
||||||
import { useAuth } from '@/features/auth';
|
import { useAuth } from '@/features/auth';
|
||||||
import { useFocusStore, useResponsiveStore } from '@/stores';
|
import { useFocusStore, useResponsiveStore } from '@/stores';
|
||||||
|
|
||||||
@@ -82,14 +83,6 @@ const ModalExport =
|
|||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const PresenterOverlay = dynamic(
|
|
||||||
() =>
|
|
||||||
import('@/docs/doc-presenter').then((mod) => ({
|
|
||||||
default: mod.PresenterOverlay,
|
|
||||||
})),
|
|
||||||
{ ssr: false },
|
|
||||||
);
|
|
||||||
|
|
||||||
interface DocToolBoxProps {
|
interface DocToolBoxProps {
|
||||||
doc: Doc;
|
doc: Doc;
|
||||||
}
|
}
|
||||||
@@ -108,11 +101,13 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
|||||||
const [isModalShareOpen, setIsModalShareOpen] = useState(false);
|
const [isModalShareOpen, setIsModalShareOpen] = useState(false);
|
||||||
const [isModalHistoryOpen, setIsModalHistoryOpen] = useState(false);
|
const [isModalHistoryOpen, setIsModalHistoryOpen] = useState(false);
|
||||||
const [isModalLeaveOpen, setIsModalLeaveOpen] = useState(false);
|
const [isModalLeaveOpen, setIsModalLeaveOpen] = useState(false);
|
||||||
const [isPresenterOpen, setIsPresenterOpen] = useState(false);
|
|
||||||
|
|
||||||
const { restoreFocus, addLastFocus } = useFocusStore();
|
const { restoreFocus, addLastFocus } = useFocusStore();
|
||||||
const { isMobile } = useResponsiveStore();
|
const { isMobile } = useResponsiveStore();
|
||||||
const copyDocLink = useCopyDocLink(doc.id);
|
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({
|
const { mutate: duplicateDoc } = useDuplicateDoc({
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
void router.push(`/docs/${data.id}`);
|
void router.push(`/docs/${data.id}`);
|
||||||
@@ -148,9 +143,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
|||||||
label: t('Present'),
|
label: t('Present'),
|
||||||
icon: <Present width={24} height={24} aria-hidden="true" />,
|
icon: <Present width={24} height={24} aria-hidden="true" />,
|
||||||
callback: () => {
|
callback: () => {
|
||||||
requestAnimationFrame(() => {
|
openPresenter(0);
|
||||||
setIsPresenterOpen(true);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
isHidden: Boolean(doc.deleted_at) || isMobile,
|
isHidden: Boolean(doc.deleted_at) || isMobile,
|
||||||
testId: `docs-actions-present-${doc.id}`,
|
testId: `docs-actions-present-${doc.id}`,
|
||||||
@@ -315,14 +308,6 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
|||||||
doc={doc}
|
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,
|
useTrans,
|
||||||
} from '@/docs/doc-management/';
|
} from '@/docs/doc-management/';
|
||||||
import { KEY_AUTH, setAuthUrl, useAuth } from '@/features/auth';
|
import { KEY_AUTH, setAuthUrl, useAuth } from '@/features/auth';
|
||||||
|
import { PresenterRoot } from '@/features/docs/doc-presenter';
|
||||||
import { getDocChildren, subPageToTree } from '@/features/docs/doc-tree/';
|
import { getDocChildren, subPageToTree } from '@/features/docs/doc-tree/';
|
||||||
import { DocEditorSkeleton, useSkeletonStore } from '@/features/skeletons';
|
import { DocEditorSkeleton, useSkeletonStore } from '@/features/skeletons';
|
||||||
import { MainLayout } from '@/layouts';
|
import { MainLayout } from '@/layouts';
|
||||||
@@ -68,6 +69,7 @@ export function DocLayout() {
|
|||||||
<DocFloatingBar />
|
<DocFloatingBar />
|
||||||
<DocPage id={id} />
|
<DocPage id={id} />
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
|
<PresenterRoot />
|
||||||
</TreeProvider>
|
</TreeProvider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user