Skip to content

✨(frontend) add the presenter mode feature#2321

Open
PanchoutNathan wants to merge 5 commits into
mainfrom
feature/presenter-mode
Open

✨(frontend) add the presenter mode feature#2321
PanchoutNathan wants to merge 5 commits into
mainfrom
feature/presenter-mode

Conversation

@PanchoutNathan
Copy link
Copy Markdown
Contributor

@PanchoutNathan PanchoutNathan commented May 25, 2026

Purpose

Adds a built-in presenter mode to any document. Press Present in the
doc actions menu and the document becomes a slide deck — splits on each
divider block, navigates with ←/→ Space Home/End, toggles browser
fullscreen with F, closes with Esc. Snapshot-based: live edits do not
affect the running presentation.

Each slide is dynamically scaled to fit the viewport (Notion-style
pattern: outer wrapper + transform: scale on an 800px inner). Scale is
clamped to [0.7, 1.5]; vertical scroll only kicks in when the content
can't fit even at min scale, and the slide top is always reachable.
Short slides centre vertically, long slides scroll past a small top
padding to reach the viewport edge. Smooth cross-fade between slides.

To avoid the reviewer having to create a set of slides, here is a markdown file to copy and paste into a docs file to test the mode:
test-slides.md

Demo

Enregistrement.de.l.ecran.2026-05-25.a.17.33.55.mp4

@PanchoutNathan PanchoutNathan self-assigned this May 25, 2026
@PanchoutNathan PanchoutNathan changed the title Feature/presenter mode ✨(frontend) add the presenter mode feature May 25, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 25, 2026

Size Change: +5.62 kB (+0.13%)

Total Size: 4.32 MB

📦 View Changed
Filename Size Change
apps/impress/out/_next/static/36d37ab5/_buildManifest.js 674 B +674 B (new file) 🆕
apps/impress/out/_next/static/75211ebf/_buildManifest.js 0 B -656 B (removed) 🏆
apps/impress/out/_next/static/chunks/377.js 23.3 kB +23.3 kB (new file) 🆕
apps/impress/out/_next/static/chunks/1546.js 1.7 kB +1.7 kB (new file) 🆕
apps/impress/out/_next/static/chunks/1859.js 3.31 kB +3.31 kB (new file) 🆕
apps/impress/out/_next/static/chunks/5815.js 3.95 kB +1.3 kB (+49.21%) 🚨
apps/impress/out/_next/static/chunks/7559.js 0 B -24.6 kB (removed) 🏆
apps/impress/out/_next/static/chunks/a52a7088.js 578 B +578 B (new file) 🆕

compressed-size-action

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 25, 2026

Review Change Stack

Walkthrough

This PR introduces a complete presenter mode feature for document presentations. The implementation includes four foundational hooks (fullscreen control, slide scaling, keyboard shortcuts, slide partitioning), three UI components (PresenterFloatingBar, PresenterSlide, PresenterOverlay), integration into DocToolBox with a "Present" dropdown option, presentation tuning constants, unit and E2E tests, and a SideModal TypeScript type refinement. CHANGELOG updated.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • AntoLC
  • Ovgodd
  • jmaupetit
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main addition: presenter mode feature for the frontend application.
Description check ✅ Passed The description thoroughly explains the presenter mode feature, its activation method, keyboard shortcuts, scaling behavior, and provides testing resources.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/presenter-mode

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Around line 19-21: There are duplicate "### Added" headings; move the entry
"✨(frontend) add the presenter mode" into the existing "### Added" section (the
first "### Added" under the [Unreleased] header) instead of creating a second
"### Added" block, ensuring only one "### Added" subsection exists and that the
moved bullet becomes part of that list.

In `@src/frontend/apps/e2e/__tests__/app-impress/presenter-mode.spec.ts`:
- Around line 58-64: The test currently uses a fixed sleep
(page.waitForTimeout(200)) after calling document.exitFullscreen(), which is
flaky; replace that sleep with a deterministic wait that observes fullscreen
state instead. After the page.evaluate block that checks
document.fullscreenElement and calls document.exitFullscreen(), remove
page.waitForTimeout and add a wait using Playwright's waiting primitive (e.g.,
page.waitForFunction or equivalent) to wait until document.fullscreenElement is
null (or until the app-specific presenter/fullscreen DOM state updates), then
proceed to page.keyboard.press('Escape').

In
`@src/frontend/apps/impress/src/features/docs/doc-presenter/__tests__/useSlides.spec.ts`:
- Around line 5-15: The test helper factories para, heading, divider, and image
currently rely on inferred return types; add explicit return type annotations
(e.g., para(): Block, heading(): Block, divider(): Block, image(): Block) and
import the Block type used by the app's document model so each helper is
declared to return Block to ensure the mock objects conform to the expected
interface.

In
`@src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx`:
- Around line 67-75: When useSlides(snapshotBlocks) can return an empty array,
normalize slides to ensure at least one fallback empty slide before deriving
total/currentIndex/clamp to avoid invalid "1 / 0" presenter state; replace usage
of slides with a normalizedSlides (or reassign slides) that is slides.length ?
slides : [emptySlidePlaceholder], then compute total = normalizedSlides.length
and use normalizedSlides wherever slides was used (affecting useSlides result
handling, currentIndex state logic, clamp callback, and any rendering that reads
slides).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 429763d2-80bd-4dd4-94f1-9ff2d2da0476

📥 Commits

Reviewing files that changed from the base of the PR and between 24d58a1 and a5d2591.

⛔ Files ignored due to path filters (1)
  • src/frontend/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (18)
  • .gitignore
  • CHANGELOG.md
  • src/frontend/apps/e2e/__tests__/app-impress/presenter-mode.spec.ts
  • src/frontend/apps/impress/src/components/modal/SideModal.tsx
  • src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx
  • src/frontend/apps/impress/src/features/docs/doc-presenter/__tests__/useBrowserFullscreen.spec.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/__tests__/useFitScale.spec.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/__tests__/usePresenterShortcuts.spec.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/__tests__/useSlides.spec.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterFloatingBar.tsx
  • src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx
  • src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterSlide.tsx
  • src/frontend/apps/impress/src/features/docs/doc-presenter/constants.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/hooks/useBrowserFullscreen.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/hooks/useFitScale.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/hooks/usePresenterShortcuts.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/hooks/useSlides.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/index.ts

Comment thread CHANGELOG.md Outdated
Comment on lines +58 to +64
await page.evaluate(async () => {
if (document.fullscreenElement) {
await document.exitFullscreen();
}
});
await page.waitForTimeout(200);
await page.keyboard.press('Escape');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace fixed 200ms sleep with a deterministic fullscreen/state wait.

waitForTimeout(200) is flaky in CI and can race with React state updates. Wait for fullscreen exit/state observable condition instead.

💡 Suggested fix
     await page.evaluate(async () => {
       if (document.fullscreenElement) {
         await document.exitFullscreen();
       }
     });
-    await page.waitForTimeout(200);
+    await page.waitForFunction(() => document.fullscreenElement === null);
+    await expect(
+      overlay.getByRole('button', { name: 'Enter fullscreen' }),
+    ).toBeVisible();
     await page.keyboard.press('Escape');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await page.evaluate(async () => {
if (document.fullscreenElement) {
await document.exitFullscreen();
}
});
await page.waitForTimeout(200);
await page.keyboard.press('Escape');
await page.evaluate(async () => {
if (document.fullscreenElement) {
await document.exitFullscreen();
}
});
await page.waitForFunction(() => document.fullscreenElement === null);
await expect(
overlay.getByRole('button', { name: 'Enter fullscreen' }),
).toBeVisible();
await page.keyboard.press('Escape');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/apps/e2e/__tests__/app-impress/presenter-mode.spec.ts` around
lines 58 - 64, The test currently uses a fixed sleep (page.waitForTimeout(200))
after calling document.exitFullscreen(), which is flaky; replace that sleep with
a deterministic wait that observes fullscreen state instead. After the
page.evaluate block that checks document.fullscreenElement and calls
document.exitFullscreen(), remove page.waitForTimeout and add a wait using
Playwright's waiting primitive (e.g., page.waitForFunction or equivalent) to
wait until document.fullscreenElement is null (or until the app-specific
presenter/fullscreen DOM state updates), then proceed to
page.keyboard.press('Escape').

Comment on lines +5 to +15
const para = (text = 'hello') => ({
type: 'paragraph',
content: text === '' ? [] : [{ type: 'text', text }],
});
const heading = (text = 'Title', level = 1) => ({
type: 'heading',
content: [{ type: 'text', text }],
props: { level },
});
const divider = () => ({ type: 'divider' });
const image = () => ({ type: 'image', props: { url: 'x' } });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Consider adding explicit return type annotations to test helpers.

While TypeScript can infer the return types, adding explicit type annotations (e.g., para(): Block, heading(): Block) improves maintainability and ensures the mock objects conform to the expected Block interface.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/apps/impress/src/features/docs/doc-presenter/__tests__/useSlides.spec.ts`
around lines 5 - 15, The test helper factories para, heading, divider, and image
currently rely on inferred return types; add explicit return type annotations
(e.g., para(): Block, heading(): Block, divider(): Block, image(): Block) and
import the Block type used by the app's document model so each helper is
declared to return Block to ensure the mock objects conform to the expected
interface.

Comment on lines +67 to +75
const slides = useSlides(snapshotBlocks as { type: string }[]);
const [currentIndex, setCurrentIndex] = useState(0);

const total = slides.length;
const clamp = useCallback(
(i: number) => Math.max(0, Math.min(i, total - 1)),
[total],
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize empty slide sets to avoid invalid 1 / 0 presenter state.

When useSlides(...) returns [], the overlay shows no slide while the toolbar still computes from index=0 and total=0 (1 / 0). Add a fallback single empty slide before deriving total and rendering.

💡 Suggested fix
-  const slides = useSlides(snapshotBlocks as { type: string }[]);
+  const slides = useSlides(snapshotBlocks as { type: string }[]);
+  const normalizedSlides =
+    slides.length > 0 ? slides : ([[]] as { type: string }[][]);
   const [currentIndex, setCurrentIndex] = useState(0);

-  const total = slides.length;
+  const total = normalizedSlides.length;
@@
-        {mountedIndices.map((i) => (
+        {mountedIndices.map((i) => (
           <PresenterSlide
             key={i}
-            blocks={slides[i] as unknown[]}
+            blocks={normalizedSlides[i] as unknown[]}
             frameRef={frameRef}
             isCurrent={i === currentIndex}

Also applies to: 109-117, 133-150

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx`
around lines 67 - 75, When useSlides(snapshotBlocks) can return an empty array,
normalize slides to ensure at least one fallback empty slide before deriving
total/currentIndex/clamp to avoid invalid "1 / 0" presenter state; replace usage
of slides with a normalizedSlides (or reassign slides) that is slides.length ?
slides : [emptySlidePlaceholder], then compute total = normalizedSlides.length
and use normalizedSlides wherever slides was used (affecting useSlides result
handling, currentIndex state logic, clamp callback, and any rendering that reads
slides).

@PanchoutNathan PanchoutNathan force-pushed the feature/presenter-mode branch from a5d2591 to 8757609 Compare May 25, 2026 15:31
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx (1)

67-71: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize empty slide sets before computing presenter state.

At Line 70, total can be 0 when useSlides(...) returns no slides, which leads to an invalid 1 / 0 counter and no mounted slide. Normalize once and use the normalized array for total and rendering.

💡 Suggested fix
-  const slides = useSlides(snapshotBlocks as { type: string }[]);
+  const slides = useSlides(snapshotBlocks as { type: string }[]);
+  const normalizedSlides =
+    slides.length > 0 ? slides : ([[]] as { type: string }[][]);
   const [currentIndex, setCurrentIndex] = useState(0);

-  const total = slides.length;
+  const total = normalizedSlides.length;
@@
-            blocks={slides[i] as unknown[]}
+            blocks={normalizedSlides[i] as unknown[]}

Also applies to: 133-137, 147-149

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx`
around lines 67 - 71, The code assumes useSlides(snapshotBlocks) returns a
non-empty array; normalize slides by assigning a safeSlides variable (e.g.,
const safeSlides = slides.length ? slides : [{ type: 'empty' }]) and use
safeSlides everywhere instead of slides when computing total, rendering, and in
functions like clamp; ensure total is derived from safeSlides.length and that
currentIndex is clamped into [0, total-1] via
setCurrentIndex(Math.min(Math.max(currentIndex, 0), total - 1)) or in the clamp
callback, and update usages in useSlides, total, clamp,
currentIndex/setCurrentIndex and any rendering logic (also apply the same
normalization to the other presenter state sections noted) so you never divide
by zero or mount with an invalid index.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In
`@src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx`:
- Around line 67-71: The code assumes useSlides(snapshotBlocks) returns a
non-empty array; normalize slides by assigning a safeSlides variable (e.g.,
const safeSlides = slides.length ? slides : [{ type: 'empty' }]) and use
safeSlides everywhere instead of slides when computing total, rendering, and in
functions like clamp; ensure total is derived from safeSlides.length and that
currentIndex is clamped into [0, total-1] via
setCurrentIndex(Math.min(Math.max(currentIndex, 0), total - 1)) or in the clamp
callback, and update usages in useSlides, total, clamp,
currentIndex/setCurrentIndex and any rendering logic (also apply the same
normalization to the other presenter state sections noted) so you never divide
by zero or mount with an invalid index.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: bc034e76-1fc6-4e9d-a20b-0ded7d3aa653

📥 Commits

Reviewing files that changed from the base of the PR and between a5d2591 and 8757609.

⛔ Files ignored due to path filters (1)
  • src/frontend/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (17)
  • CHANGELOG.md
  • src/frontend/apps/e2e/__tests__/app-impress/presenter-mode.spec.ts
  • src/frontend/apps/impress/src/components/modal/SideModal.tsx
  • src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx
  • src/frontend/apps/impress/src/features/docs/doc-presenter/__tests__/useBrowserFullscreen.spec.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/__tests__/useFitScale.spec.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/__tests__/usePresenterShortcuts.spec.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/__tests__/useSlides.spec.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterFloatingBar.tsx
  • src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx
  • src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterSlide.tsx
  • src/frontend/apps/impress/src/features/docs/doc-presenter/constants.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/hooks/useBrowserFullscreen.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/hooks/useFitScale.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/hooks/usePresenterShortcuts.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/hooks/useSlides.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/index.ts

@PanchoutNathan PanchoutNathan force-pushed the feature/presenter-mode branch from 8757609 to 5698fb1 Compare May 29, 2026 06:41
Update @gouvfr-lasuite/cunningham-react from 4.2.0 to 4.3.0 and
@gouvfr-lasuite/ui-kit from 0.19.10 to 0.20.1, and regenerate the
cunningham design tokens accordingly. The new ui-kit drops the
"black" font weight and ships rounded form border-radius defaults.
cunningham-react 4.3.0 splits Modal props per variant. Switch
AlertModal and SideModal from the now-removed ModalProps alias to
the more precise ModalDefaultVariantProps type so the modal
wrappers keep type-checking against the new API.
Add a presenter overlay that turns the current document into a
slide deck. The editor's blocks are snapshot at open time and
split into slides on each divider; navigation is driven by
keyboard shortcuts and a floating bar with browser fullscreen
support. The overlay is wired to the doc header toolbox via a
new "Present" entry, lazy-loaded to keep the editor bundle lean.
Cover the three hooks that drive the presenter overlay:
useSlides for divider-based block segmentation,
useBrowserFullscreen for the Fullscreen API wrapper,
and usePresenterShortcuts for the keyboard navigation bindings.
Add a Playwright spec that opens the presenter overlay from the
doc options menu, walks through a multi-slide document built with
dividers, and verifies that Escape closes the overlay.
@PanchoutNathan PanchoutNathan force-pushed the feature/presenter-mode branch from 5698fb1 to 38d10bd Compare May 29, 2026 06:53
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx`:
- Around line 92-97: PresenterOverlay currently calls enter() inside useEffect
after the dynamic chunk loads (via next/dynamic ssr:false) which can miss the
browser's transient user activation window and silently swallow errors; update
the logic so the fullscreen request is attempted from the original user click
(before crossing the dynamic-import boundary) or at minimum check
navigator.userActivation.isActive and handle enter()'s Promise rejection in
PresenterOverlay (and log/propagate the error) and implement a short
retry/fallback path if activation has expired; reference the enter() and
exitIfOwned() functions in PresenterOverlay and ensure any swallowed promises
are awaited/handled instead of using void.

In `@src/frontend/apps/impress/src/features/docs/doc-presenter/constants.ts`:
- Line 14: The constant PRESENTER_SLIDE_DESIGN_WIDTH is currently 900 but must
match the presenter contract of 800px; update PRESENTER_SLIDE_DESIGN_WIDTH to
800 and verify any scaling/viewport-fit calculations that reference
PRESENTER_SLIDE_DESIGN_WIDTH (e.g., slide scaling logic or CSS-in-JS that
consumes this constant) remain correct after changing the value so the computed
scale and viewport behavior align with the 800px inner container.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2e51ab05-8e28-4c5f-b99c-f465e0234dd2

📥 Commits

Reviewing files that changed from the base of the PR and between 8757609 and 5698fb1.

⛔ Files ignored due to path filters (1)
  • src/frontend/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (17)
  • CHANGELOG.md
  • src/frontend/apps/e2e/__tests__/app-impress/presenter-mode.spec.ts
  • src/frontend/apps/impress/src/components/modal/SideModal.tsx
  • src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx
  • src/frontend/apps/impress/src/features/docs/doc-presenter/__tests__/useBrowserFullscreen.spec.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/__tests__/useFitScale.spec.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/__tests__/usePresenterShortcuts.spec.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/__tests__/useSlides.spec.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterFloatingBar.tsx
  • src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx
  • src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterSlide.tsx
  • src/frontend/apps/impress/src/features/docs/doc-presenter/constants.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/hooks/useBrowserFullscreen.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/hooks/useFitScale.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/hooks/usePresenterShortcuts.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/hooks/useSlides.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/index.ts

Comment on lines +92 to +97
useEffect(() => {
void enter();
return () => {
void exitIfOwned();
};
}, [enter, exitIfOwned]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does Element.requestFullscreen require transient user activation, and is that activation preserved across an async dynamic import that resolves before the call?

💡 Result:

Yes, Element.requestFullscreen requires transient user activation [1][2]. Transient user activation is a temporary state granted by the browser when a user performs a direct, meaningful interaction (such as a pointerdown, pointerup, or keydown event) [2][3]. Regarding your question about asynchronous operations: yes, transient user activation is designed to be preserved across asynchronous boundaries, such as an await on a dynamic import, provided the operation happens within the lifetime of the activation [4]. When a user interaction occurs, the browser grants the transient activation state to the window [2][4]. This state persists for a short duration (typically around one second) [4] and is not immediately lost when JavaScript execution yields to the event loop via an await keyword [4]. As long as your call to element.requestFullscreen occurs after the async import resolves—but before the activation timer expires—the browser will consider the request to have been initiated by the user [4]. If the code execution takes too long and the activation window expires, the call will fail [2][4]. You can programmatically check if the activation is still active using navigator.userActivation.isActive [2][3].

Citations:


Fullscreen activation likely survives the async dynamic import, but first-open latency can still break it
Browsers grant transient user activation for fullscreen and preserve it across async boundaries (e.g., await/dynamic import) as long as requestFullscreen runs before the activation window expires. Since PresenterOverlay calls enter() in useEffect after the next/dynamic (ssr:false) chunk loads (src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx, lines 92-97), first open can still fail in real conditions when the chunk load exceeds that short activation window—and void enter() would swallow any rejection. Check navigator.userActivation.isActive (and/or handle enter() rejections) and consider triggering fullscreen directly from the original click handler before crossing the dynamic-import boundary (or adding a retry/fallback).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx`
around lines 92 - 97, PresenterOverlay currently calls enter() inside useEffect
after the dynamic chunk loads (via next/dynamic ssr:false) which can miss the
browser's transient user activation window and silently swallow errors; update
the logic so the fullscreen request is attempted from the original user click
(before crossing the dynamic-import boundary) or at minimum check
navigator.userActivation.isActive and handle enter()'s Promise rejection in
PresenterOverlay (and log/propagate the error) and implement a short
retry/fallback path if activation has expired; reference the enter() and
exitIfOwned() functions in PresenterOverlay and ensure any swallowed promises
are awaited/handled instead of using void.

* inner wrapper renders at this exact width then `transform: scale(...)`
* fits it into the available viewport.
*/
export const PRESENTER_SLIDE_DESIGN_WIDTH = 900;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Align slide design width with the presenter contract (800px).

PRESENTER_SLIDE_DESIGN_WIDTH is set to 900, but the PR objective states scaling is based on an 800px inner container. This mismatch will skew scaling and viewport-fit behavior.

Suggested fix
-export const PRESENTER_SLIDE_DESIGN_WIDTH = 900;
+export const PRESENTER_SLIDE_DESIGN_WIDTH = 800;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const PRESENTER_SLIDE_DESIGN_WIDTH = 900;
export const PRESENTER_SLIDE_DESIGN_WIDTH = 800;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/apps/impress/src/features/docs/doc-presenter/constants.ts` at
line 14, The constant PRESENTER_SLIDE_DESIGN_WIDTH is currently 900 but must
match the presenter contract of 800px; update PRESENTER_SLIDE_DESIGN_WIDTH to
800 and verify any scaling/viewport-fit calculations that reference
PRESENTER_SLIDE_DESIGN_WIDTH (e.g., slide scaling logic or CSS-in-JS that
consumes this constant) remain correct after changing the value so the computed
scale and viewport behavior align with the 800px inner container.

@PanchoutNathan PanchoutNathan requested a review from AntoLC May 29, 2026 06:59
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
src/frontend/apps/impress/src/features/docs/doc-presenter/constants.ts (1)

14-14: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Slide design width still mismatches the 800px presenter contract.

PRESENTER_SLIDE_DESIGN_WIDTH remains 900, while the PR objective specifies an 800px inner container. This skews the width-based scale (scaleW = availW / designWidth) and viewport-fit behavior. Confirm whether the contract is 800 or 900 and align the constant (and the pinned OPTS.designWidth in useFitScale.spec.ts) accordingly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/apps/impress/src/features/docs/doc-presenter/constants.ts` at
line 14, PRESENTER_SLIDE_DESIGN_WIDTH is set to 900 but the presenter contract
expects 800; update PRESENTER_SLIDE_DESIGN_WIDTH to 800 and make the
corresponding change to the pinned OPTS.designWidth in useFitScale.spec.ts so
both the runtime constant and the test fixture use the same 800px design width;
locate and update the constant export PRESENTER_SLIDE_DESIGN_WIDTH and the
OPTS.designWidth reference in useFitScale.spec.ts to keep scaleW = availW /
designWidth consistent with the 800px contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/frontend/apps/e2e/__tests__/app-impress/presenter-mode.spec.ts`:
- Around line 186-188: The test hard-codes 900 for the slide design width;
import and use the source-of-truth constant PRESENTER_SLIDE_DESIGN_WIDTH instead
of the literal so the assertion uses PRESENTER_SLIDE_DESIGN_WIDTH * scale;
update the assertion in presenter-mode.spec.ts (where dims.outerWidth is
compared) to reference PRESENTER_SLIDE_DESIGN_WIDTH and ensure the import is
added at the top of the test file.

In
`@src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx`:
- Around line 189-197: The Present icon in DocToolBox.tsx is inconsistent with
other dropdown icons: replace the raw <Present /> used in the menu item (the
object whose callback calls setIsPresenterOpen) with the ui-kit-styled version
used elsewhere — e.g. render Present with width={24} height={24} and
aria-hidden="true" (or use the ui-kit Icon wrapper API if available) so the
dropdown entry matches other icons and is marked decorative for assistive tech;
keep the rest of the menu item (show, testId, callback) unchanged.

In
`@src/frontend/apps/impress/src/features/docs/doc-presenter/__tests__/useSlides.spec.ts`:
- Line 35: The tests use `as any` on mock helpers (e.g., image(), para(),
heading(), divider()) when calling `isEmptyBlock`; remove these casts by
declaring a shared `Block` type that covers all variants used by `isEmptyBlock`
and annotate each helper's return type to `Block` (or the appropriate union
member) so the mocks are type-compatible; then delete the `as any` casts from
the test expectations (references: isEmptyBlock, image, para, heading, divider).

In
`@src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx`:
- Around line 60-65: PresenterOverlay currently only records the prior focus via
addLastFocus in its useEffect but never moves focus into the dialog (it sets
role="dialog" and aria-modal="true" but no focus management), so update
PresenterOverlay to, on mount/open, call addLastFocus(document.activeElement)
then move focus into the dialog container (e.g., set a ref on the dialog
element, ensure it has tabIndex={-1}, and call ref.current.focus()) and ensure
focus is restored on unmount via useFocusStore; alternatively, if you prefer a
trap, integrate a focus-trap (or use an existing useFocusTrap hook) around the
dialog to contain keyboard focus while open—refer to the PresenterOverlay
component, its useEffect, addLastFocus, and useFocusStore when implementing the
change.

---

Duplicate comments:
In `@src/frontend/apps/impress/src/features/docs/doc-presenter/constants.ts`:
- Line 14: PRESENTER_SLIDE_DESIGN_WIDTH is set to 900 but the presenter contract
expects 800; update PRESENTER_SLIDE_DESIGN_WIDTH to 800 and make the
corresponding change to the pinned OPTS.designWidth in useFitScale.spec.ts so
both the runtime constant and the test fixture use the same 800px design width;
locate and update the constant export PRESENTER_SLIDE_DESIGN_WIDTH and the
OPTS.designWidth reference in useFitScale.spec.ts to keep scaleW = availW /
designWidth consistent with the 800px contract.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 24b27351-2ad1-4726-9f46-c447c4b59b28

📥 Commits

Reviewing files that changed from the base of the PR and between 5698fb1 and 38d10bd.

⛔ Files ignored due to path filters (1)
  • src/frontend/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (17)
  • CHANGELOG.md
  • src/frontend/apps/e2e/__tests__/app-impress/presenter-mode.spec.ts
  • src/frontend/apps/impress/src/components/modal/SideModal.tsx
  • src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx
  • src/frontend/apps/impress/src/features/docs/doc-presenter/__tests__/useBrowserFullscreen.spec.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/__tests__/useFitScale.spec.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/__tests__/usePresenterShortcuts.spec.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/__tests__/useSlides.spec.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterFloatingBar.tsx
  • src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx
  • src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterSlide.tsx
  • src/frontend/apps/impress/src/features/docs/doc-presenter/constants.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/hooks/useBrowserFullscreen.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/hooks/useFitScale.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/hooks/usePresenterShortcuts.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/hooks/useSlides.ts
  • src/frontend/apps/impress/src/features/docs/doc-presenter/index.ts

Comment on lines +186 to +188
// The core invariant: outer width = DESIGN_WIDTH (900) × scale,
// within a 5px tolerance for sub-pixel rounding.
expect(Math.abs(dims.outerWidth - 900 * scale)).toBeLessThan(5);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the design-width constant value used by the scaler.
fd -t f 'constants.ts' --full-path doc-presenter --exec rg -nP 'DESIGN_WIDTH|WINDOW_RADIUS|MIN_SCALE|MAX_SCALE|PADDING' {}
rg -nP 'DESIGN_WIDTH' src/frontend/apps/impress/src/features/docs/doc-presenter

Repository: suitenumerique/docs

Length of output: 834


🏁 Script executed:

#!/bin/bash
# Inspect the assertion in the spec and look for 800/900 and constant usage.
sed -n '160,210p' src/frontend/apps/e2e/__tests__/app-impress/presenter-mode.spec.ts

rg -n "800|900|PRESENTER_SLIDE_DESIGN_WIDTH|DESIGN_WIDTH" src/frontend/apps/e2e/__tests__/app-impress/presenter-mode.spec.ts

# Show the constant definition to confirm the source of truth.
sed -n '1,80p' src/frontend/apps/impress/src/features/docs/doc-presenter/constants.ts

Repository: suitenumerique/docs

Length of output: 3931


Hard-coded 900 matches the source design width, but avoid duplicating the literal. The spec’s invariant uses 900 * scale, and the source-of-truth constant PRESENTER_SLIDE_DESIGN_WIDTH is set to 900, so the assertion isn’t inconsistent with the implementation. Prefer importing PRESENTER_SLIDE_DESIGN_WIDTH in the e2e test instead of duplicating 900 for future-proofing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/apps/e2e/__tests__/app-impress/presenter-mode.spec.ts` around
lines 186 - 188, The test hard-codes 900 for the slide design width; import and
use the source-of-truth constant PRESENTER_SLIDE_DESIGN_WIDTH instead of the
literal so the assertion uses PRESENTER_SLIDE_DESIGN_WIDTH * scale; update the
assertion in presenter-mode.spec.ts (where dims.outerWidth is compared) to
reference PRESENTER_SLIDE_DESIGN_WIDTH and ensure the import is added at the top
of the test file.

Comment on lines +189 to +197
{
label: t('Present'),
icon: <Present />,
callback: () => {
setIsPresenterOpen(true);
},
show: !doc.deleted_at && !isSmallMobile,
testId: `docs-actions-present-${doc.id}`,
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

@gouvfr-lasuite/ui-kit icons component props width height aria-hidden

💡 Result:

I couldn’t find any authoritative, component-level documentation for a dedicated “Icon” component inside @gouvfr-lasuite/ui-kit that explicitly lists props named width, height, or aria-hidden. What I can support from sources: 1) @gouvfr-lasuite/ui-kit does include an “Icon” component (described as “Material icons”), but the sources retrieved here don’t show its prop types or a public API table for width/height/aria-hidden. [1][2] 2) For accessibility, a common and standards-backed rule is that decorative icons should be hidden from the accessibility tree using aria-hidden="true". This behavior is described by MDN: aria-hidden="true" removes the element (and its children) from the accessibility tree. [3] Practical answer (what you should do, given the missing API props evidence): - Use aria-hidden="true" on purely decorative icons (e.g., SVG/Material icon spans used only for decoration), per MDN guidance on aria-hidden. [3] - For width/height: since no @gouvfr-lasuite/ui-kit prop evidence was found, size the icon via CSS (e.g., setting font-size for Material Icons or applying width/height to the underlying SVG/element) or via whatever size prop is actually implemented by the library (not found in the retrieved sources). If you paste the actual TS/React code snippet you’re using with the Icon component (or the Icon component import path, e.g. where Icon is exported from in @gouvfr-lasuite/ui-kit), I can run a more targeted lookup for the exact prop names and confirm whether width/height are supported as component props or only via styling.

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect the exact surrounding code in DocToolBox.tsx
FILE="src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx"
if [ -f "$FILE" ]; then
  echo "=== $FILE (lines around 175-215) ==="
  sed -n '165,215p' "$FILE" | nl -ba | sed 's/^/ /'
else
  echo "Missing file: $FILE"
fi

# 2) Find the import source for Present and other icons in this file
echo "=== Imports in $FILE (top part) ==="
sed -n '1,120p' "$FILE" | nl -ba | sed 's/^/ /'

# 3) Locate where Present is used/imported elsewhere in the repo
echo "=== Repo-wide search for Present icon usage ==="
rg -n "Present\s*[>,)]|<Present\b|from ['\"]`@gouvfr-lasuite/ui-kit/icons`['\"][^;]*|`@gouvfr-lasuite/ui-kit/icons`" -S .

# 4) If node_modules is present in the sandbox, inspect ui-kit types/implementation for Present
if [ -d "node_modules/@gouvfr-lasuite/ui-kit" ]; then
  echo "=== node_modules/@gouvfr-lasuite/ui-kit present types/implementation ==="
  # Try to find Present export
  rg -n "export (const|function|class) Present|Present\s*:" node_modules/@gouvfr-lasuite/ui-kit -S || true
  # Try to find icon component prop types for width/height/aria-hidden
  rg -n "aria-hidden|width\s*\?|height\s*\?" node_modules/@gouvfr-lasuite/ui-kit -S || true
else
  echo "node_modules/@gouvfr-lasuite/ui-kit not present in this sandbox."
fi

Repository: suitenumerique/docs

Length of output: 236


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx"

echo "=== Verify file exists ==="
ls -la "$FILE" 2>/dev/null || { echo "Missing file: $FILE"; exit 1; }

echo "=== $FILE (lines 175-215) ==="
sed -n '175,215p' "$FILE" | cat -n

echo "=== $FILE imports (lines 1-120) ==="
sed -n '1,120p' "$FILE" | cat -n

echo "=== Repo-wide search: where Present is imported/used ==="
rg -n --hidden --glob '!.git/*' --glob '!**/dist/**' --glob '!**/build/**' \
  "from ['\"]`@gouvfr-lasuite/ui-kit/icons`['\"][^;]*Present|<Present\b|Present\s*\(" .

echo "=== Repo-wide search: icon size/accessibility patterns used elsewhere ==="
rg -n --hidden --glob '!.git/*' --glob '!**/dist/**' --glob '!**/build/**' \
  "width=\{24\}\s*height=\{24\}|aria-hidden=\{?\"true\"\\}?" \
  src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx

# Inspect node_modules types/impl if available (best-effort)
if [ -d "node_modules/@gouvfr-lasuite/ui-kit" ]; then
  echo "=== node_modules/@gouvfr-lasuite/ui-kit exists; searching for Present ==="
  rg -n "Present" node_modules/@gouvfr-lasuite/ui-kit -S | head -n 50 || true
  rg -n "export .*Present|Present\s*:" node_modules/@gouvfr-lasuite/ui-kit -S | head -n 50 || true
else
  echo "=== node_modules/@gouvfr-lasuite/ui-kit not present ==="
fi

Repository: suitenumerique/docs

Length of output: 7833


🌐 Web query:

@gouvfr-lasuite/ui-kit/icons Present width height aria-hidden

💡 Result:

I couldn’t find any authoritative, source-backed documentation stating specific default width/height values for the icons provided by @gouvfr-lasuite/ui-kit/icons. What I can confirm from the official accessibility guidance that relates to the “aria-hidden” part: - For decorative icons, using aria-hidden="true" is recommended; aria-hidden="true" removes the element (and its children) from the accessibility tree [1]. - If an icon conveys meaning or duplicates/overlaps accessible text, aria-hidden should be overridden (or not used) so assistive tech users aren’t misled [1]. So, for the query “Present width height aria-hidden”: - aria-hidden: set aria-hidden="true" when the icon is purely decorative; otherwise provide an accessible name/label and don’t hide it [1]. - width/height: the sources found do not specify a fixed width/height for @gouvfr-lasuite/ui-kit/icons; you likely need to refer to the actual Icon component/props (or the underlying SVG/icon size utilities) in that repository, which weren’t surfaced by the searches I ran. If you share the exact component name or the code snippet you’re using (e.g., the React <Icon...> usage or the SVG markup), I can pinpoint the exact default width/height behavior and whether aria-hidden is set by default in that implementation.

Citations:


🌐 Web query:

site:npmjs.com @gouvfr-lasuite/ui-kit/icons Present

💡 Result:

There is no public npm package named @gouvfr-lasuite/ui-kit/icons. The French State Design System (DSFR - Système de Design de l'État) provides its official icons and pictograms through the primary package @gouvfr/dsfr [1]. In this package, icons are located within the dist/icons directory of the @gouvfr/dsfr distribution [1][2]. To use them, developers typically: 1. Include the necessary CSS files from dist/utility/icons/ (e.g., icons-system.min.css) [1][2]. 2. Reference the icons using standard CSS classes as described in the official DSFR documentation [1][2]. If you are working with specific framework integrations, you may be using community-maintained wrappers such as @codegouvfr/react-dsfr [3] or @betagouv/ngx-dsfr [2], which provide their own methods for handling DSFR assets and components. Please verify your dependency name or consult the official Systeme de Design documentation for the most accurate implementation details.

Citations:


🌐 Web query:

site:github.com @gouvfr-lasuite/ui-kit Present icon component props width height aria-hidden

💡 Result:

I couldn’t find any public GitHub repository/package named @gouvfr-lasuite/ui-kit, nor any “Present” icon React component exposing width, height, or aria-hidden props.

What is public under the La Suite numérique GitHub org is suitenumerique/cunningham (their design system). Its React package is @gouvfr-lasuite/cunningham-react, and icons are imported as CSS “Material Icons” (not as per-icon React components), e.g. via:

  • @import "@gouvfr-lasuite/cunningham-react/icons"; (github.com)

So, in the public codebase I can access, there isn’t a “Present” icon component with width/height/aria-hidden props to document.

If you share the exact GitHub repo URL (or the file path for the icon component), I can extract the precise props from the source.

Citations:


🌐 Web query:

"export" "Present" "@gouvfr-lasuite/ui-kit" "icons"

💡 Result:

I can’t find (in the sources I retrieved) any documentation or code that matches the exact query phrase “export Present @gouvfr-lasuite/ui-kit icons”. What I can confirm is how icons are provided/used by the La Suite UI Kit package. 1) Icons in @gouvfr-lasuite/ui-kit - The npm package @gouvfr-lasuite/ui-kit includes an Icon component described as “Material icons” [1]. - The examples show importing @gouvfr-lasuite/ui-kit “style” separately, e.g. import "@gouvfr-lasuite/ui-kit/style" [1][2]. 2) Where to look in the upstream repo - The ui-kit GitHub repository has an icon/ folder, which is likely where the icon assets/components live [2]. 3) About “export” and “Present” in your query - In the sources retrieved, “export” appears only in the generic sense of importing modules or exporting dist assets for the separate @gouvfr-lasuite/integration package, not for an “export Present … icons” pattern tied to @gouvfr-lasuite/ui-kit [3][4]. - “Present” is not found as part of an @gouvfr-lasuite/ui-kit icon export/usage specification in the retrieved results. If you paste the exact file/snippet you’re trying to interpret (or the error message you’re seeing), I can do a targeted search for the specific “Present”/“export” usage within the ui-kit codebase (e.g., whether it’s a React component, an icon name, a barrel export in an index.ts, etc.).

Citations:


Make the Present dropdown icon consistent (24×24 + decorative a11y)

In DocToolBox.tsx, every other dropdown option icon is rendered with width={24} height={24} aria-hidden="true", but Present is currently icon: <Present />. Update Present so it’s sized to 24×24 and hidden from assistive tech as a decorative icon (using the ui-kit’s supported API or by applying equivalent attributes to the rendered element).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx`
around lines 189 - 197, The Present icon in DocToolBox.tsx is inconsistent with
other dropdown icons: replace the raw <Present /> used in the menu item (the
object whose callback calls setIsPresenterOpen) with the ui-kit-styled version
used elsewhere — e.g. render Present with width={24} height={24} and
aria-hidden="true" (or use the ui-kit Icon wrapper API if available) so the
dropdown entry matches other icons and is marked decorative for assistive tech;
keep the rest of the menu item (show, testId, callback) unchanged.

});

test('image is never empty', () => {
expect(isEmptyBlock(image() as any)).toBe(false);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Remove as any type assertions for better type safety.

The as any casts bypass TypeScript's type checking. This suggests the mock helpers don't return types compatible with isEmptyBlock's parameter type. The proper fix is to define or import a Block type (or union of block variants), ensure all helpers return that type, and remove these assertions.

🔧 Suggested approach
  1. Import or define a Block type that covers all variants (paragraph, heading, divider, image):
type Block = 
  | { type: 'paragraph'; content: Array<{type: 'text'; text: string}> }
  | { type: 'heading'; content: Array<{type: 'text'; text: string}>; props: {level: number} }
  | { type: 'divider' }
  | { type: 'image'; props: {url: string} };
  1. Add return type annotations to helpers (addressing the past review comment):
const para = (text = 'hello'): Block => ({ /* ... */ });
const heading = (text = 'Title', level = 1): Block => ({ /* ... */ });
const divider = (): Block => ({ type: 'divider' });
const image = (): Block => ({ type: 'image', props: { url: 'x' } });
  1. Remove the as any casts from lines 35, 39, and 44.

Also applies to: 39-39, 44-44

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/apps/impress/src/features/docs/doc-presenter/__tests__/useSlides.spec.ts`
at line 35, The tests use `as any` on mock helpers (e.g., image(), para(),
heading(), divider()) when calling `isEmptyBlock`; remove these casts by
declaring a shared `Block` type that covers all variants used by `isEmptyBlock`
and annotate each helper's return type to `Block` (or the appropriate union
member) so the mocks are type-compatible; then delete the `as any` casts from
the test expectations (references: isEmptyBlock, image, para, heading, divider).

Comment on lines +60 to +65
useEffect(() => {
if (typeof document === 'undefined') {
return;
}
addLastFocus(document.activeElement as HTMLElement | null);
}, [addLastFocus]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP --type=ts --type=tsx -C3 '\b(focus-?trap|FocusTrap|trapFocus|useFocusTrap)\b'
rg -nP -C3 'aria-modal' --glob '*.tsx'

Repository: suitenumerique/docs

Length of output: 1837


🏁 Script executed:

#!/bin/bash
# Search for existing focus-trap/focus-management utilities (case-insensitive)
rg -n -i --glob '*.{ts,tsx,js,jsx}' '\b(focus-?trap|usefocustrap|trapFocus|FocusTrap|useFocusTrap)\b'

# Search for common focus-trap related libraries/usages (case-insensitive)
rg -n -i --glob '*.{ts,tsx,js,jsx}' '\b(focus-trap|focus trap|tabindex="-1"|restore focus|focusin|lastfocus)\b'

# Find the dialog container in PresenterOverlay to see how focus is managed there
rg -n --glob '*.tsx' 'PresenterOverlay\.tsx|Presenter mode|addLastFocus|role="dialog"|aria-modal="true"' src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx

Repository: suitenumerique/docs

Length of output: 297


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx"

echo "---- PresenterOverlay.tsx (relevant sections) ----"
nl -ba "$FILE" | sed -n '1,120p'
nl -ba "$FILE" | sed -n '120,220p'

echo "---- Find useFocusStore definition ----"
rg -n --glob '*.{ts,tsx}' '\buseFocusStore\b' src/frontend/apps/impress | head -n 50

rg -n --glob '*.{ts,tsx}' '\buseFocusStore\b' src/frontend | head -n 50

echo "---- Locate definition file(s) ----"
defs=$(rg -n --glob '*.{ts,tsx}' 'function useFocusStore|const useFocusStore|export function useFocusStore|export const useFocusStore' src/frontend -S || true)
echo "$defs" | head -n 50

# If we found any definition lines, print surrounding code.
echo "---- Print surrounding code for top definition hits ----"
echo "$defs" | head -n 10 | while read -r line; do
  path=$(echo "$line" | cut -d: -f1)
  lineno=$(echo "$line" | cut -d: -f2)
  if [ -f "$path" ]; then
    echo "### $path:$lineno"
    nl -ba "$path" | sed -n "$((lineno-30)),$((lineno+80))p" | head -n 200
  fi
done

echo "---- Search for focus-trap/focus management in useFocusStore related files ----"
# Search within the directory tree where definition seems to be.
# (We’ll just search for "trap" and "focus" keywords globally for now to catch utilities.)
rg -n --glob '*.{ts,tsx}' '\b(trapFocus|focus-?trap|useFocusTrap|FocusTrap)\b' src/frontend --hidden || true
rg -n --glob '*.{ts,tsx}' '\b(tabindex\s*=\s*"-1"|focus\s*\(|focusin|restore focus|lastFocus|addLastFocus)\b' src/frontend --hidden || true

Repository: suitenumerique/docs

Length of output: 175


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx"

echo "---- PresenterOverlay.tsx (top: import/props + around useEffect) ----"
sed -n '1,120p' "$FILE" | awk '{printf "%4d:%s\n", NR, $0}'

echo "---- PresenterOverlay.tsx (around dialog markup) ----"
sed -n '110,200p' "$FILE" | awk '{printf "%4d:%s\n", NR+110, $0}'

echo "---- Find useFocusStore definition/import in this file ----"
rg -n --glob '*.{ts,tsx}' '\buseFocusStore\b' "$FILE"

echo "---- Find useFocusStore definition(s) in src/frontend ----"
rg -n --glob '*.{ts,tsx}' '\buseFocusStore\b' src/frontend | head -n 50

echo "---- Locate likely definition file for useFocusStore ----"
defs=$(rg -n --glob '*.{ts,tsx}' -S '(export\s+(const|function)\s+useFocusStore\b|function\s+useFocusStore\b|const\s+useFocusStore\b|useFocusStore\s*=\s*\()' src/frontend || true)
echo "$defs" | head -n 50

first_def=$(echo "$defs" | head -n 1 | awk -F: '{print $1}')
first_def_line=$(echo "$defs" | head -n 1 | awk -F: '{print $2}')
if [ -n "${first_def:-}" ]; then
  echo "---- Printing around first useFocusStore definition: $first_def:$first_def_line ----"
  start=$((first_def_line-40))
  end=$((first_def_line+120))
  sed -n "${start},${end}p" "$first_def" | awk -v s="$start" '{printf "%4d:%s\n", NR+s, $0}'
else
  echo "No useFocusStore definition matched the patterns; continuing with a keyword search for addLastFocus/add/remove focus."
fi

echo "---- Search addLastFocus implementation/usages ----"
rg -n --glob '*.{ts,tsx}' '\baddLastFocus\b' src/frontend | head -n 50

# If we find addLastFocus implementation, print around it.
add_impl=$(rg -n --glob '*.{ts,tsx}' -S '\b(addLastFocus)\b' src/frontend | head -n 1 || true)
add_path=$(echo "$add_impl" | awk -F: '{print $1}')
add_line=$(echo "$add_impl" | awk -F: '{print $2}')
if [ -n "${add_path:-}" ] && [ -n "${add_line:-}" ]; then
  echo "---- Printing around first addLastFocus hit: $add_path:$add_line ----"
  start=$((add_line-40))
  end=$((add_line+120))
  sed -n "${start},${end}p" "$add_path" | awk -v s="$start" '{printf "%4d:%s\n", NR+s, $0}'
fi

echo "---- Search for any focus trap utilities/usages again (broader) ----"
rg -n --glob '*.{ts,tsx}' -i '\b(focus\s*-?\s*trap|usefocustrap|focustrap|tabindex\s*=\s*"-1"|onfocusin|focusin|keydown.*Tab|keydown.*Tab|lastFocus|restore\s*focus|trap\s*focus)\b' src/frontend --hidden || true

Repository: suitenumerique/docs

Length of output: 11970


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Basic file sizes
wc -l src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx
wc -l src/frontend/apps/impress/src/stores/useFocusStore.tsx

# 2) PresenterOverlay.tsx focus-related markup/props
echo "---- PresenterOverlay.tsx focus-related lines (search for tabIndex/autoFocus/ref focus/keydown Tab) ----"
rg -n --glob '*.tsx' -i 'tabindex|autofocus|tab\s*trap|trap|restoreFocus|addLastFocus|focus\s*\(|focusin|keydown.*Tab|aria-modal' \
  src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx

echo "---- PresenterOverlay.tsx around dialog container (120-200) ----"
sed -n '110,200p' src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx | awk '{printf "%4d:%s\n", NR+109, $0}'

echo "---- PresenterOverlay.tsx useEffect/addLastFocus (45-80) ----"
sed -n '40,90p' src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx | awk '{printf "%4d:%s\n", NR+39, $0}'

# 3) Full useFocusStore implementation (small file expected)
echo "---- useFocusStore.tsx ----"
sed -n '1,220p' src/frontend/apps/impress/src/stores/useFocusStore.tsx | awk '{printf "%4d:%s\n", NR, $0}'

# 4) Search for any focus-trap utilities elsewhere
echo "---- Search for focus-trap utilities by name ----"
rg -n -i --glob '*.{ts,tsx,js,jsx}' '\b(focus-?trap|usefocustrap|trapFocus|FocusTrap)\b' src/frontend/apps/impress/src || true

echo "---- Search for focus-trap patterns (broader) ----"
rg -n -i --glob '*.{ts,tsx,js,jsx}' '(tabindex\s*=\s*"-1"|onKeyDown.*Tab|keydown.*Tab|focus\s*trap|restoreFocus\()' src/frontend/apps/impress/src || true

# 5) Ensure parent/close path calls restoreFocus
echo "---- Where restoreFocus is called (global search in impress app) ----"
rg -n --glob '*.{ts,tsx}' '\brestoreFocus\b' src/frontend/apps/impress/src || true

Repository: suitenumerique/docs

Length of output: 9567


Move initial focus into the PresenterOverlay dialog on open.

PresenterOverlay.tsx sets role="dialog" / aria-modal="true", but the mount effect only records the previously focused element (addLastFocus(document.activeElement)); useFocusStore just stores lastFocusedElement and restores it on close—no focus is moved into the dialog, and no focus-trap is used in this component.

useEffect(() => {
  if (typeof document === 'undefined') {
    return;
  }
  addLastFocus(document.activeElement as HTMLElement | null);
}, [addLastFocus]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx`
around lines 60 - 65, PresenterOverlay currently only records the prior focus
via addLastFocus in its useEffect but never moves focus into the dialog (it sets
role="dialog" and aria-modal="true" but no focus management), so update
PresenterOverlay to, on mount/open, call addLastFocus(document.activeElement)
then move focus into the dialog container (e.g., set a ref on the dialog
element, ensure it has tabIndex={-1}, and call ref.current.focus()) and ensure
focus is restored on unmount via useFocusStore; alternatively, if you prefer a
trap, integrate a focus-trap (or use an existing useFocusTrap hook) around the
dialog to contain keyboard focus while open—refer to the PresenterOverlay
component, its useEffect, addLastFocus, and useFocusStore when implementing the
change.

@AntoLC AntoLC linked an issue May 29, 2026 that may be closed by this pull request
@AntoLC AntoLC added frontend feature add a new feature preview labels May 29, 2026
@github-actions
Copy link
Copy Markdown
Contributor

🚀 Preview will be available at https://2321-docs.ppr-docs.beta.numerique.gouv.fr/

You can use the existing account with these credentials:

  • username: docs
  • password: docs

You can also create a new account if you want to.

Once this Pull Request is merged, the preview will be destroyed.

Copy link
Copy Markdown
Collaborator

@AntoLC AntoLC left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good feature 🎉

Comment on lines +60 to +65
useEffect(() => {
if (typeof document === 'undefined') {
return;
}
addLastFocus(document.activeElement as HTMLElement | null);
}, [addLastFocus]);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't have to add the last focus element here, you can just remove this part I think, it is better to add it onClick on the element that was the trigger, but I think it is done already. The target does not seems correct as well.

Comment thread src/frontend/yarn.lock
"@babel/helper-plugin-utils" "^7.27.1"

"@babel/plugin-transform-modules-systemjs@7.29.4", "@babel/plugin-transform-modules-systemjs@^7.27.1":
"@babel/plugin-transform-modules-systemjs@^7.27.1":
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should remove this commit (⬆️(frontend) bump cunningham-react and ui-kit), seems like it has been updated in another PR.

Comment on lines +16 to +18
if (typeof document === 'undefined') {
return;
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think all this gard are necessary, useEffect is trigger after render, and all the useCallback are trigger thanks to a user interaction, so document must be defined.

Comment on lines +71 to +77

const cleaned = groups
.map((group) => group.filter((b) => !isEmptyBlock(b)))
.filter((group) => group.length > 0);

return cleaned.length > 0 ? cleaned : [[]];
};
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you do a cleaning ?
By doing so you potentially loose data.

I can see the interlinks by example are not displayed, if we add new custom block, they will potentially not be displayed because of the cleaning I think.

image image

Same problem with certain break lines, some area are removed:
image
image

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems quite overengineered.

Comment on lines +63 to +68
<Box
$direction="row"
$align="center"
$css={barCss}
role="toolbar"
aria-label={t('Presenter controls')}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be nice to get the focus here when opening the presenter mode, to have the tab directly focusing the action buttons.
Not sure if it is the best way to do so, maybe you have a opinion @Ovgodd ?

@virgile-dev
Copy link
Copy Markdown
Collaborator

@PanchoutNathan @AntoLC do you also have too big request error when trying to access ?
https://2321-docs.ppr-docs.beta.numerique.gouv.fr/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Presentation mode (docs to slides)

3 participants