diff --git a/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterDividerHint.tsx b/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterDividerHint.tsx
new file mode 100644
index 000000000..352f9730e
--- /dev/null
+++ b/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterDividerHint.tsx
@@ -0,0 +1,271 @@
+import { Button } from '@gouvfr-lasuite/cunningham-react';
+import {
+ Divider as DividerIcon,
+ Info,
+ XMark,
+} from '@gouvfr-lasuite/ui-kit/icons';
+import { useTranslation } from 'react-i18next';
+import { css } from 'styled-components';
+
+import { Box, Text } from '@/components';
+
+interface PresenterDividerHintProps {
+ onDismiss: () => void;
+}
+
+// Shared by every text node in the hint (the line-height/flex/etc. differ and
+// stay per-element).
+const marianneFontCss = css`
+ font-family: Marianne, var(--c--globals--font--families--base);
+`;
+
+const hintCss = css`
+ position: relative;
+ flex-direction: column !important;
+ align-items: flex-start;
+ width: 309px;
+ height: 196px;
+ box-sizing: border-box;
+ overflow: hidden;
+ border: 1px solid var(--c--contextuals--border--surface--primary, #dfe2ea);
+ border-radius: 8px;
+ background: var(--c--contextuals--background--surface--primary, #fff);
+ box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.05);
+`;
+
+const hintHeaderCss = css`
+ position: relative;
+ flex-direction: row !important;
+ align-items: center;
+ gap: 12px;
+ width: 100%;
+ height: 60px;
+ box-sizing: border-box;
+ flex-shrink: 0;
+ overflow: hidden;
+ padding: 12px 32px 12px 12px;
+ border-bottom: 1px solid
+ var(--c--contextuals--border--surface--primary, #dfe2ea);
+ background: var(--c--contextuals--background--surface--tertiary, #f6f8f9);
+ color: var(--c--contextuals--content--semantic--neutral--tertiary, #626a80);
+
+ & > svg {
+ width: 24px;
+ height: 24px;
+ flex-shrink: 0;
+ }
+`;
+
+const closeButtonCss = css`
+ position: absolute;
+ top: 4px;
+ right: 4px;
+
+ .c__button {
+ width: 24px;
+ min-width: 24px;
+ height: 24px;
+ min-height: 24px;
+ padding: 4px;
+ border-radius: 4px;
+ }
+
+ .c__button svg {
+ width: 16px;
+ height: 16px;
+ }
+`;
+
+const hintBodyCss = css`
+ position: relative;
+ width: 100%;
+ height: 136px;
+ flex-shrink: 0;
+ overflow: hidden;
+`;
+
+const commandCss = css`
+ position: absolute;
+ top: 24px;
+ left: 60px;
+ flex-direction: row !important;
+ align-items: center;
+ justify-content: center;
+ gap: 2px;
+ height: 24px;
+ padding-right: 2px;
+ border-radius: 2px;
+ color: var(--c--contextuals--content--semantic--neutral--primary, #222631);
+ font-family: Inter, var(--c--globals--font--families--base);
+
+ &::before {
+ position: absolute;
+ inset: -2px -2px -2px -3px;
+ border-radius: 4px;
+ background: var(
+ --c--contextuals--background--semantic--overlay--primary,
+ rgba(24, 27, 36, 0.05)
+ );
+ content: '';
+ }
+`;
+
+const filteredMenuCss = css`
+ position: absolute;
+ top: 53px;
+ left: 55px;
+ width: 200px;
+ overflow: hidden;
+ border: 1px solid var(--c--contextuals--border--surface--primary, #dfe2ea);
+ border-radius: 8px;
+ background: var(--c--contextuals--background--surface--primary, #fff);
+ box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.05);
+`;
+
+const filteredTitleSectionCss = css`
+ flex-direction: row !important;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ box-sizing: border-box;
+ padding: 8px 8px 2px;
+`;
+
+const filteredItemCss = css`
+ flex-direction: row !important;
+ align-items: center;
+ gap: 6px;
+ width: 100%;
+ height: 32px;
+ box-sizing: border-box;
+ padding: 8px;
+ background: var(
+ --c--contextuals--background--semantic--overlay--primary,
+ rgba(24, 27, 36, 0.05)
+ );
+
+ svg {
+ width: 16px;
+ height: 16px;
+ flex-shrink: 0;
+ }
+`;
+
+export const PresenterDividerHint = ({
+ onDismiss,
+}: PresenterDividerHintProps) => {
+ const { t } = useTranslation();
+ return (
+
+
+
+
+ {t('You can use the divider to tell Docs where to split your slides')}
+
+
+ }
+ onClick={onDismiss}
+ />
+
+
+
+
+
+ /
+
+
+ Divider|
+
+
+
+
+
+ {t('Filtered results')}
+
+
+
+
+
+ {t('Divider')}
+
+
+ ---
+
+
+
+
+
+ );
+};
diff --git a/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx b/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx
index 5f64d135b..02691546e 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx
@@ -13,10 +13,11 @@ import { PRESENTER_WINDOW_RADIUS } from '../constants';
import { useBrowserFullscreen } from '../hooks/useBrowserFullscreen';
import { useCopyPresenterLink } from '../hooks/useCopyPresenterLink';
import { usePresenterShortcuts } from '../hooks/usePresenterShortcuts';
-import { getSlideTitle, useSlides } from '../hooks/useSlides';
+import { getSlideTitle, hasDividerBlock, useSlides } from '../hooks/useSlides';
import { PresenterBlock, PresenterSlideData } from '../types';
import { printPresenterSlides } from '../utils_print';
+import { PresenterDividerHint } from './PresenterDividerHint';
import { PresenterDocsLogo } from './PresenterDocsLogo';
import { PresenterFloatingBar } from './PresenterFloatingBar';
import { PresenterSlide } from './PresenterSlide';
@@ -53,6 +54,13 @@ const docsLogoCss = css`
z-index: 1;
`;
+const dividerHintCss = css`
+ position: fixed;
+ top: 2rem;
+ left: 2rem;
+ z-index: 1;
+`;
+
export const PresenterOverlay = ({
doc,
onClose,
@@ -72,16 +80,20 @@ export const PresenterOverlay = ({
const snapshotBlocks = snapshotRef.current;
const contentSlides = useSlides(snapshotBlocks as PresenterBlock[]);
+ const hasDividers = useMemo(
+ () => hasDividerBlock(snapshotBlocks as PresenterBlock[]),
+ [snapshotBlocks],
+ );
const title = useMemo(() => {
const { titleWithoutEmoji } = getEmojiAndTitle(doc.title ?? '');
return titleWithoutEmoji.trim() || t('Untitled document');
}, [doc.title, t]);
const slides = useMemo(
() => [
- { kind: 'title', title, showDividerHint: false },
+ { kind: 'title', title, showDividerHint: !hasDividers },
...contentSlides.map((blocks) => ({ kind: 'content' as const, blocks })),
],
- [contentSlides, title],
+ [contentSlides, hasDividers, title],
);
// Clamp to a valid slide already at init (not only via the effect below), so
// an out-of-range deep-link (e.g. `slide=99`) never emits a transient
@@ -90,6 +102,7 @@ export const PresenterOverlay = ({
Math.min(Math.max(0, initialIndex), slides.length - 1),
);
const [isExportingPdf, setIsExportingPdf] = useState(false);
+ const [isDividerHintDismissed, setIsDividerHintDismissed] = useState(false);
const total = slides.length;
const clamp = useCallback(
@@ -168,6 +181,11 @@ export const PresenterOverlay = ({
}, [currentIndex, total]);
const frameRef = useRef(null);
+ const shouldShowDividerHint =
+ currentIndex === 0 &&
+ !isDividerHintDismissed &&
+ slides[0]?.kind === 'title' &&
+ slides[0].showDividerHint;
useEffect(() => {
const currentSlide = slides[currentIndex];
@@ -219,6 +237,14 @@ export const PresenterOverlay = ({
))}
+ {shouldShowDividerHint && (
+
+ setIsDividerHintDismissed(true)}
+ />
+
+ )}
+
diff --git a/src/frontend/apps/impress/src/features/docs/doc-presenter/hooks/useSlides.ts b/src/frontend/apps/impress/src/features/docs/doc-presenter/hooks/useSlides.ts
index a62711beb..84e2d8671 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-presenter/hooks/useSlides.ts
+++ b/src/frontend/apps/impress/src/features/docs/doc-presenter/hooks/useSlides.ts
@@ -262,6 +262,15 @@ export const useSlides = (blocks: T[]): T[][] => {
return useMemo(() => splitBlocksIntoSlides(blocks), [blocks]);
};
+/** Whether the block tree contains at least one divider (at any depth). */
+export const hasDividerBlock = (
+ blocks: T[],
+): boolean =>
+ blocks.some(
+ (block) =>
+ block.type === 'divider' || hasDividerBlock(block.children ?? []),
+ );
+
const getBlocksInReadingOrder = (
blocks: T[],
): PresenterBlock[] => {