@honeydeck/honeydeck 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -241,7 +241,7 @@ Keyboard shortcuts:
241
241
  | `down` / `s` | Next slide |
242
242
  | `up` / `w` | Previous slide |
243
243
  | `o` | Toggle overview mode |
244
- | `p` | Open presenter mode in the current tab |
244
+ | `p` | Open presenter mode in the current tab; on mobile, direct presenter URLs show an unsupported hint |
245
245
  | `f` | Toggle fullscreen |
246
246
  | `Escape` | Exit overview or fullscreen |
247
247
 
@@ -256,9 +256,9 @@ URL state preserves slide and step position:
256
256
  /#/components components tab
257
257
  ```
258
258
 
259
- On touch devices, swipe left/right for next/previous step and swipe up/down for next/previous slide. The navigation bar is always visible on touch devices.
259
+ On touch devices, swipe left/right for next/previous step and swipe up/down for next/previous slide. The navigation bar is always visible on touch devices and hides the presenter-mode button on mobile.
260
260
 
261
- Presenter mode opens with `p` and includes:
261
+ On `md` and wider screens, presenter mode opens with `p` and includes:
262
262
 
263
263
  - current slide and next slide preview
264
264
  - speaker notes from `<Notes>`
package/docs/mobile.md CHANGED
@@ -13,7 +13,7 @@ Honeydeck works on phones and tablets, but mobile presentation mode behaves diff
13
13
  | Text selection | Slide text can be selected normally | Slide text selection is off by default; use the nav bar toggle when you need it |
14
14
  | Zoom | Browser/page zoom or normal browser controls | Honeydeck-controlled slide zoom with pinch, up to `5x` |
15
15
  | Overview | Keyboard selection and mouse clicks | Responsive fixed two-column grid |
16
- | Presenter mode | Current slide, next preview, notes, clock, timer, actions; no navigation buttons | Current slide, notes, navigation buttons; no next preview |
16
+ | Presenter mode | Current slide, next preview, notes, clock, timer, actions | Not supported; direct presenter URLs show a full-page hint with a button back to the slide |
17
17
 
18
18
  ## Tap zones
19
19
 
@@ -93,13 +93,7 @@ Touch scroll belongs to the overview grid and never navigates slides. Tap a slid
93
93
 
94
94
  ## Presenter mode on mobile
95
95
 
96
- Presenter mode keeps the mobile layout compact:
97
-
98
- - Current slide preview
99
- - Speaker notes
100
- - Navigation buttons
101
-
102
- The desktop next-slide preview is hidden on mobile because there is not enough space.
96
+ Presenter mode is not supported below Tailwind's `md` breakpoint. The mobile navigation bar does not offer the presenter-mode button. If someone opens a `/#/presenter/...` URL directly on mobile, Honeydeck shows a full-page hint and a **Go to slide** button that returns to the same slide and step in audience view.
103
97
 
104
98
  ## Tips for mobile-friendly decks
105
99
 
@@ -29,7 +29,7 @@ Boundary behavior:
29
29
  - Hidden by default for clean presentation.
30
30
  - Appears on cursor hover near bottom edge.
31
31
  - Positioned bottom-left.
32
- - Contains: current slide number, navigation arrows, overview button, presenter button, fullscreen button, color mode switch (system / dark / light).
32
+ - Contains: current slide number, navigation arrows, overview button, presenter button on `md` and wider screens, fullscreen button, color mode switch (system / dark / light).
33
33
  - Built-in controls use `lucide-react` icons, imported from the suffixed `...Icon` exports (for example `ChevronLeftIcon`), not inline SVG paths or unsuffixed aliases.
34
34
 
35
35
  When `showSlideNumbers: true`, the current slide number is also shown as a single viewer overlay aligned to the bottom-right corner of the slide canvas. It is not part of individual slide content, so it does not animate during slide transitions. It renders as themed foreground text without a background container.
@@ -60,7 +60,7 @@ Mobile uses touch-specific controls. See the full [Mobile and Touch](mobile.md)
60
60
  - Swipe down → previous slide
61
61
  - Pinch zoom enlarges slide content up to `5x`; dragging pans while zoomed.
62
62
  - Slide text selection is off by default on touch devices; the navigation bar includes a toggle to enable selection when needed.
63
- - Presenter mode on mobile shows only current slide, notes, and navigation buttons (no next slide preview).
63
+ - Presenter mode is not supported on mobile. Direct presenter URLs show a full-page hint with a button back to the same slide and step in audience view.
64
64
 
65
65
  ## Overview Mode
66
66
 
@@ -34,7 +34,7 @@ Includes:
34
34
  - Color mode toggle that also updates audience views and cast receivers
35
35
  - Blank screen toggle (`b`) that makes audience and cast views black while presenter mode shows a `Screen blanked (b)` indicator
36
36
  - Button to cast the audience view to a secondary display when supported; the same control becomes a stop button while casting. Unsupported browsers show a visibly disabled-looking button with hover/accessible text explaining why casting is unavailable.
37
- - Navigation buttons that move through the timeline while staying in presenter mode on mobile. Desktop presenter mode uses keyboard shortcuts instead.
37
+ - Desktop keyboard shortcuts that move through the timeline while staying in presenter mode.
38
38
 
39
39
  ## Speaker Notes
40
40
 
@@ -64,7 +64,7 @@ Content here.
64
64
  ## Opening Presenter Mode
65
65
 
66
66
  - Keyboard shortcut `p` from normal presentation → opens presenter mode in the current tab.
67
- - Navigation controls button in normal presentation.
67
+ - Navigation controls button in normal presentation on `md` and wider screens.
68
68
  - Direct URL: `/#/presenter/1/0`
69
69
 
70
70
  Pressing `p` opens presenter mode in the **current tab**. In presenter mode, press `p` or `Escape` to return to the audience slide view at the same slide and step.
@@ -80,7 +80,7 @@ Presenter mode uses the same timeline keyboard shortcuts as normal slide view:
80
80
  | `↓` / `s` | Next slide |
81
81
  | `↑` / `w` | Previous slide |
82
82
 
83
- Keyboard navigation and presenter navigation buttons keep the URL under `/#/presenter/...`. Presenter controls use `lucide-react` icons imported from suffixed `...Icon` exports.
83
+ Keyboard navigation keeps the URL under `/#/presenter/...`. Presenter controls use `lucide-react` icons imported from suffixed `...Icon` exports.
84
84
 
85
85
  The `Next` preview shows the next timeline state, not just the next slide. Reveals and code step-through highlights therefore preview exactly what the audience will see after the next forward navigation.
86
86
 
@@ -107,9 +107,4 @@ This supports the common laptop/projector setup.
107
107
 
108
108
  ## Mobile Presenter Mode
109
109
 
110
- On mobile, presenter mode shows:
111
- - Current slide
112
- - Notes
113
- - Navigation buttons
114
-
115
- No next slide preview (space constraint).
110
+ Presenter mode is not supported below Tailwind's `md` breakpoint. The navigation bar hides the presenter-mode button on mobile. If someone opens a `/#/presenter/...` URL on mobile, Honeydeck shows a full-page hint and a **Go to slide** button that returns to the same slide and step in audience view.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@honeydeck/honeydeck",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "MDX and React-based presentation framework for AI-friendly slide decks.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -126,7 +126,7 @@ Reference page routes intentionally do not encode slide or step. During one brow
126
126
  | `↓` / `s` | Next slide; in overview, timeline navigation is disabled: arrow keys move overview selection and WASD are no-ops |
127
127
  | `↑` / `w` | Previous slide; in overview, timeline navigation is disabled: arrow keys move overview selection and WASD are no-ops |
128
128
  | `o` | Toggle overview mode |
129
- | `p` | Open presenter mode (same tab) |
129
+ | `p` | Open presenter mode (same tab); presenter mode is unsupported below Tailwind's `md` breakpoint and shows a mobile hint with a button back to the same audience slide/step |
130
130
  | `f` | Toggle fullscreen |
131
131
  | `Escape` | Exit overview; in reference pages, return to slides; browser-native Escape handles fullscreen exit |
132
132
 
@@ -145,7 +145,7 @@ Shown in normal slide view only (not presenter/reference views).
145
145
  - Overview mode button
146
146
  - Layouts reference button (opens `/#/layouts` while preserving the current slide/step as the return target)
147
147
  - Docs website button (opens `https://honeydeck.dev` in a new tab)
148
- - Presenter mode button
148
+ - Presenter mode button on `md` and wider screens
149
149
  - Fullscreen button
150
150
  - Mobile text selection toggle (off by default, enables selecting slide content when needed)
151
151
  - Color mode switch (system → light → dark → system)
@@ -183,12 +183,14 @@ export function NavBar({
183
183
 
184
184
  {/* Presenter mode */}
185
185
  {route.view !== "presenter" && (
186
- <NavBarButton
187
- onClick={() => openPresenter(route)}
188
- label="Presenter mode (p)"
189
- >
190
- <PresentationIcon aria-hidden="true" size={16} />
191
- </NavBarButton>
186
+ <div className="hidden md:block">
187
+ <NavBarButton
188
+ onClick={() => openPresenter(route)}
189
+ label="Presenter mode (p)"
190
+ >
191
+ <PresentationIcon aria-hidden="true" size={16} />
192
+ </NavBarButton>
193
+ </div>
192
194
  )}
193
195
  </div>
194
196
 
@@ -30,10 +30,6 @@
30
30
  */
31
31
 
32
32
  import {
33
- ChevronDownIcon,
34
- ChevronLeftIcon,
35
- ChevronRightIcon,
36
- ChevronUpIcon,
37
33
  ExternalLinkIcon,
38
34
  MonitorOffIcon,
39
35
  PauseIcon,
@@ -57,11 +53,7 @@ import { NotesContext } from "../components/Notes.tsx";
57
53
  import {
58
54
  getSlideRouteFromRoute,
59
55
  navigateTo,
60
- nextSlide,
61
- nextStep,
62
56
  openUrlInNewTab,
63
- previousSlide,
64
- previousStep,
65
57
  } from "../navigation.ts";
66
58
  import {
67
59
  getPresentationAudienceUrl,
@@ -72,7 +64,6 @@ import { SlideCanvas } from "../SlideCanvas.tsx";
72
64
  import { BASE_HEIGHT, BASE_WIDTH, slideData } from "../slideData.ts";
73
65
  import { useSync } from "../sync.ts";
74
66
  import { useKeyboardNav } from "../useKeyboardNav.ts";
75
- import { useSwipeNav } from "../useSwipeNav.ts";
76
67
  import { PresenterCastButton } from "./PresenterCastButton.tsx";
77
68
  import { PresenterNotesPanel } from "./PresenterNotesPanel.tsx";
78
69
  import { getPresenterNextPreview } from "./presenterPreview.ts";
@@ -154,23 +145,6 @@ function SlidePreview({
154
145
  );
155
146
  }
156
147
 
157
- function usePresenterMobile(): boolean {
158
- const [isMobile, setIsMobile] = useState(() => {
159
- if (typeof window === "undefined") return false;
160
- return window.matchMedia("(max-width: 767px)").matches;
161
- });
162
-
163
- useEffect(() => {
164
- const query = window.matchMedia("(max-width: 767px)");
165
- const update = () => setIsMobile(query.matches);
166
- update();
167
- query.addEventListener("change", update);
168
- return () => query.removeEventListener("change", update);
169
- }, []);
170
-
171
- return isMobile;
172
- }
173
-
174
148
  // ---------------------------------------------------------------------------
175
149
  // Component
176
150
  // ---------------------------------------------------------------------------
@@ -197,7 +171,6 @@ export function PresenterView({
197
171
  const [notes, setNotes] = useState<ReactNode>(null);
198
172
  const [isBlankScreen, setIsBlankScreen] = useState(false);
199
173
  const notesContextValue = useMemo(() => ({ setNotes }), []);
200
- const isMobile = usePresenterMobile();
201
174
 
202
175
  const currentIndex = slide - 1; // 0-based
203
176
  const currentStepCount = slideData[currentIndex]?.stepCount ?? 0;
@@ -243,7 +216,6 @@ export function PresenterView({
243
216
  getStepCount,
244
217
  onToggleOverview: () => {},
245
218
  });
246
- useSwipeNav({ enabled: isMobile });
247
219
 
248
220
  // ── Presenter exit + blank screen shortcuts ─────────────────────────────
249
221
  const closePresenter = useCallback(() => {
@@ -305,60 +277,50 @@ export function PresenterView({
305
277
  if (url) openUrlInNewTab(url);
306
278
  }
307
279
 
308
- // ── Navigate (presenter stays on presenter route) ───────────────────────
309
- function goNext() {
310
- nextStep(
311
- { view: "presenter", slide, step },
312
- { slideCount: totalSlides, getStepCount },
313
- );
314
- }
315
-
316
- function goPrev() {
317
- previousStep(
318
- { view: "presenter", slide, step },
319
- { slideCount: totalSlides, getStepCount },
320
- );
321
- }
322
-
323
- function goNextSlide() {
324
- nextSlide({ view: "presenter", slide, step }, { slideCount: totalSlides });
325
- }
326
-
327
- function goPrevSlide() {
328
- previousSlide({ view: "presenter", slide, step });
329
- }
330
-
331
280
  // ---------------------------------------------------------------------------
332
281
  // Render
333
282
  // ---------------------------------------------------------------------------
334
283
 
335
284
  return (
336
- <div className="fixed inset-0 bg-black text-white font-sans grid grid-rows-[1fr_auto_auto] grid-cols-1 overflow-hidden select-none">
337
- {/* ── Blank screen indicator overlay ────────────────────────────── */}
338
- {isBlankScreen && (
339
- <div className="absolute top-4 left-1/2 -translate-x-1/2 z-10 px-4 py-2 rounded-md bg-red-600/90 text-white text-sm font-semibold tracking-wide uppercase">
340
- Screen blanked (b)
285
+ <div className="fixed inset-0 bg-black text-white font-sans overflow-hidden select-none">
286
+ <div className="flex h-full flex-col items-center justify-center gap-5 px-6 text-center md:hidden">
287
+ <div className="max-w-sm space-y-2">
288
+ <h1 className="text-2xl font-semibold">
289
+ Presenter mode is not supported on mobile.
290
+ </h1>
291
+ <p className="text-sm text-white/60">
292
+ Open presenter mode on a larger screen, or return to this slide in
293
+ the audience view.
294
+ </p>
341
295
  </div>
342
- )}
296
+ <button
297
+ type="button"
298
+ onClick={closePresenter}
299
+ className="rounded border border-white/20 bg-white/10 px-4 py-2 text-sm font-[inherit] text-white/85"
300
+ >
301
+ Go to slide
302
+ </button>
303
+ </div>
304
+
305
+ <div className="hidden h-full grid-rows-[1fr_auto] grid-cols-1 md:grid">
306
+ {/* ── Blank screen indicator overlay ────────────────────────────── */}
307
+ {isBlankScreen && (
308
+ <div className="absolute top-4 left-1/2 -translate-x-1/2 z-10 px-4 py-2 rounded-md bg-red-600/90 text-white text-sm font-semibold tracking-wide uppercase">
309
+ Screen blanked (b)
310
+ </div>
311
+ )}
312
+
313
+ {/* ── Top section: current slide plus next/notes column ─────────── */}
314
+ <div className="grid grid-cols-[minmax(0,3fr)_minmax(280px,2fr)] gap-4 px-4 pt-4 pb-2 min-h-0 overflow-hidden">
315
+ {/* Current slide owns NotesContext. Next preview must not overwrite notes. */}
316
+ <NotesContext.Provider value={notesContextValue}>
317
+ <SlidePreview
318
+ slideIndex={currentIndex}
319
+ stepIndex={step}
320
+ label="Current"
321
+ />
322
+ </NotesContext.Provider>
343
323
 
344
- {/* ── Top section: current slide plus desktop next/notes column ─── */}
345
- <div
346
- className={
347
- isMobile
348
- ? "grid grid-cols-1 gap-3 px-3 pt-3 pb-2 min-h-0 overflow-hidden"
349
- : "grid grid-cols-[minmax(0,3fr)_minmax(280px,2fr)] gap-4 px-4 pt-4 pb-2 min-h-0 overflow-hidden"
350
- }
351
- >
352
- {/* Current slide owns NotesContext. Next preview must not overwrite notes. */}
353
- <NotesContext.Provider value={notesContextValue}>
354
- <SlidePreview
355
- slideIndex={currentIndex}
356
- stepIndex={step}
357
- label="Current"
358
- />
359
- </NotesContext.Provider>
360
-
361
- {!isMobile && (
362
324
  <div className="grid grid-rows-[minmax(0,1fr)_minmax(8rem,0.8fr)] gap-4 min-h-0 overflow-hidden">
363
325
  <SlidePreview
364
326
  slideIndex={nextPreview?.slideIndex ?? -1}
@@ -368,177 +330,123 @@ export function PresenterView({
368
330
  />
369
331
  <PresenterNotesPanel notes={notes} />
370
332
  </div>
371
- )}
372
- </div>
373
-
374
- {/* ── Mobile notes live below the current slide ─────────────────── */}
375
- {isMobile && (
376
- <PresenterNotesPanel
377
- notes={notes}
378
- className="mx-3 mb-2 min-h-20 max-h-40"
379
- />
380
- )}
333
+ </div>
381
334
 
382
- {/* ── Bottom bar: counter · mobile nav buttons · timer/actions ──── */}
383
- <div
384
- className={
385
- isMobile
386
- ? "grid grid-cols-[minmax(0,1fr)_auto] items-center px-3 py-2.5 border-t border-white/8 bg-black/30 gap-x-3 gap-y-2"
387
- : "grid grid-cols-[minmax(0,1fr)_auto] items-center px-5 py-2.5 border-t border-white/8 bg-black/30 gap-x-4 gap-y-2"
388
- }
389
- >
390
- {/* Counter + timer */}
391
- <div className="min-w-0 flex flex-wrap items-center gap-3 text-md text-white/60 tabular-nums">
392
- <span className="truncate">
393
- Slide {slide}/{totalSlides}
394
- {currentStepCount > 0 && ` · Step ${step}/${currentStepCount}`}
395
- </span>
396
- {timerState === "idle" && (
397
- <button
398
- type="button"
399
- onClick={startTimer}
400
- title="Start timer"
401
- className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded bg-white/6 text-white/50 hover:text-white/80 text-sm"
402
- >
403
- <PlayIcon aria-hidden="true" size={14} />
404
- Start timer
405
- </button>
406
- )}
407
- {timerState === "running" && (
408
- <>
409
- <button
410
- type="button"
411
- onClick={pauseTimer}
412
- title="Pause timer"
413
- className="text-lg font-semibold text-white tabular-nums bg-white/10 px-2.5 py-0.5 rounded"
414
- >
415
- ⏱ {elapsedTime}
416
- </button>
417
- <button
418
- type="button"
419
- onClick={pauseTimer}
420
- title="Pause timer"
421
- aria-label="Pause timer"
422
- className="text-white/50 hover:text-white/80 inline-flex"
423
- >
424
- <PauseIcon aria-hidden="true" size={16} />
425
- </button>
426
- </>
427
- )}
428
- {timerState === "paused" && (
429
- <>
430
- <span className="text-lg font-semibold text-white/60 tabular-nums bg-white/6 px-2.5 py-0.5 rounded">
431
- ⏱ {elapsedTime}
432
- </span>
433
- <button
434
- type="button"
435
- onClick={continueTimer}
436
- title="Continue timer"
437
- aria-label="Continue timer"
438
- className="text-white/50 hover:text-white/80 inline-flex"
439
- >
440
- <PlayIcon aria-hidden="true" size={16} />
441
- </button>
442
- <button
443
- type="button"
444
- onClick={restartTimer}
445
- title="Restart timer from zero"
446
- aria-label="Restart timer from zero"
447
- className="text-white/30 hover:text-white/60 inline-flex"
448
- >
449
- <RotateCcwIcon aria-hidden="true" size={15} />
450
- </button>
335
+ {/* ── Bottom bar: counter · timer/actions ──────────────────────── */}
336
+ <div className="grid grid-cols-[minmax(0,1fr)_auto] items-center px-5 py-2.5 border-t border-white/8 bg-black/30 gap-x-4 gap-y-2">
337
+ {/* Counter + timer */}
338
+ <div className="min-w-0 flex flex-wrap items-center gap-3 text-md text-white/60 tabular-nums">
339
+ <span className="truncate">
340
+ Slide {slide}/{totalSlides}
341
+ {currentStepCount > 0 && ` · Step ${step}/${currentStepCount}`}
342
+ </span>
343
+ {timerState === "idle" && (
451
344
  <button
452
345
  type="button"
453
- onClick={closeTimer}
454
- title="Close timer"
455
- aria-label="Close timer"
456
- className="text-white/30 hover:text-white/60 inline-flex"
346
+ onClick={startTimer}
347
+ title="Start timer"
348
+ className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded bg-white/6 text-white/50 hover:text-white/80 text-sm"
457
349
  >
458
- <XIcon aria-hidden="true" size={16} />
350
+ <PlayIcon aria-hidden="true" size={14} />
351
+ Start timer
459
352
  </button>
460
- </>
461
- )}
462
- </div>
353
+ )}
354
+ {timerState === "running" && (
355
+ <>
356
+ <button
357
+ type="button"
358
+ onClick={pauseTimer}
359
+ title="Pause timer"
360
+ className="text-lg font-semibold text-white tabular-nums bg-white/10 px-2.5 py-0.5 rounded"
361
+ >
362
+ ⏱ {elapsedTime}
363
+ </button>
364
+ <button
365
+ type="button"
366
+ onClick={pauseTimer}
367
+ title="Pause timer"
368
+ aria-label="Pause timer"
369
+ className="text-white/50 hover:text-white/80 inline-flex"
370
+ >
371
+ <PauseIcon aria-hidden="true" size={16} />
372
+ </button>
373
+ </>
374
+ )}
375
+ {timerState === "paused" && (
376
+ <>
377
+ <span className="text-lg font-semibold text-white/60 tabular-nums bg-white/6 px-2.5 py-0.5 rounded">
378
+ ⏱ {elapsedTime}
379
+ </span>
380
+ <button
381
+ type="button"
382
+ onClick={continueTimer}
383
+ title="Continue timer"
384
+ aria-label="Continue timer"
385
+ className="text-white/50 hover:text-white/80 inline-flex"
386
+ >
387
+ <PlayIcon aria-hidden="true" size={16} />
388
+ </button>
389
+ <button
390
+ type="button"
391
+ onClick={restartTimer}
392
+ title="Restart timer from zero"
393
+ aria-label="Restart timer from zero"
394
+ className="text-white/30 hover:text-white/60 inline-flex"
395
+ >
396
+ <RotateCcwIcon aria-hidden="true" size={15} />
397
+ </button>
398
+ <button
399
+ type="button"
400
+ onClick={closeTimer}
401
+ title="Close timer"
402
+ aria-label="Close timer"
403
+ className="text-white/30 hover:text-white/60 inline-flex"
404
+ >
405
+ <XIcon aria-hidden="true" size={16} />
406
+ </button>
407
+ </>
408
+ )}
409
+ </div>
463
410
 
464
- {/* Nav buttons are mobile-only; desktop uses keyboard shortcuts. */}
465
- {isMobile && (
466
- <div className="order-last col-span-2 flex w-full gap-2 items-center justify-center">
467
- <NavButton
468
- onClick={goPrev}
469
- title="Previous step (←)"
470
- className="h-12 w-16"
411
+ {/* Clock + mode/open/cast buttons */}
412
+ <div className="flex flex-wrap gap-3 items-center justify-self-end">
413
+ <span
414
+ className="text-md tabular-nums text-white/60"
415
+ title="Wall clock"
471
416
  >
472
- <ChevronLeftIcon aria-hidden="true" className="h-6 w-6" />
473
- </NavButton>
474
- <NavButton
475
- onClick={goPrevSlide}
476
- title="Previous slide (↑)"
477
- className="h-12 w-16"
478
- >
479
- <ChevronUpIcon aria-hidden="true" className="h-6 w-6" />
480
- </NavButton>
417
+ {clock}
418
+ </span>
419
+ <ColorModeCycleButton
420
+ colorMode={colorMode}
421
+ onSetColorMode={onSetColorMode}
422
+ iconSize={14}
423
+ className="w-8 h-8 rounded border border-white/20 bg-white/6 text-white/80 inline-flex items-center justify-center"
424
+ />
481
425
  <NavButton
482
- onClick={goNextSlide}
483
- title="Next slide ()"
484
- className="h-12 w-16"
426
+ onClick={toggleBlankScreen}
427
+ title={isBlankScreen ? "Unblank screen (b)" : "Blank screen (b)"}
485
428
  >
486
- <ChevronDownIcon aria-hidden="true" className="h-6 w-6" />
429
+ <MonitorOffIcon
430
+ aria-hidden="true"
431
+ size={16}
432
+ className={isBlankScreen ? "text-red-400" : ""}
433
+ />
487
434
  </NavButton>
488
- <NavButton
489
- onClick={goNext}
490
- title="Next step (→)"
491
- className="h-12 w-16"
435
+ <button
436
+ type="button"
437
+ onClick={openAudienceView}
438
+ className="px-3 py-1 rounded border border-white/20 bg-white/6 text-white/80 text-sm font-[inherit] inline-flex items-center gap-1.5"
492
439
  >
493
- <ChevronRightIcon aria-hidden="true" className="h-6 w-6" />
494
- </NavButton>
495
- </div>
496
- )}
497
-
498
- {/* Clock + mode/open/cast buttons */}
499
- <div
500
- className={
501
- isMobile
502
- ? "col-span-2 flex flex-wrap gap-3 items-center justify-self-start"
503
- : "flex flex-wrap gap-3 items-center justify-self-end"
504
- }
505
- >
506
- <span
507
- className="text-md tabular-nums text-white/60"
508
- title="Wall clock"
509
- >
510
- {clock}
511
- </span>
512
- <ColorModeCycleButton
513
- colorMode={colorMode}
514
- onSetColorMode={onSetColorMode}
515
- iconSize={14}
516
- className="w-8 h-8 rounded border border-white/20 bg-white/6 text-white/80 inline-flex items-center justify-center"
517
- />
518
- <NavButton
519
- onClick={toggleBlankScreen}
520
- title={isBlankScreen ? "Unblank screen (b)" : "Blank screen (b)"}
521
- >
522
- <MonitorOffIcon
523
- aria-hidden="true"
524
- size={16}
525
- className={isBlankScreen ? "text-red-400" : ""}
440
+ Open audience view
441
+ <ExternalLinkIcon aria-hidden="true" size={14} />
442
+ </button>
443
+ <PresenterCastButton
444
+ supported={presentationCast.supported}
445
+ isCasting={presentationCast.isCasting}
446
+ onStartCasting={presentationCast.startCasting}
447
+ onStopCasting={presentationCast.stopCasting}
526
448
  />
527
- </NavButton>
528
- <button
529
- type="button"
530
- onClick={openAudienceView}
531
- className="px-3 py-1 rounded border border-white/20 bg-white/6 text-white/80 text-sm font-[inherit] inline-flex items-center gap-1.5"
532
- >
533
- Open audience view
534
- <ExternalLinkIcon aria-hidden="true" size={14} />
535
- </button>
536
- <PresenterCastButton
537
- supported={presentationCast.supported}
538
- isCasting={presentationCast.isCasting}
539
- onStartCasting={presentationCast.startCasting}
540
- onStopCasting={presentationCast.stopCasting}
541
- />
449
+ </div>
542
450
  </div>
543
451
  </div>
544
452
  </div>
@@ -7,7 +7,7 @@
7
7
  ### Activation
8
8
 
9
9
  - Keyboard shortcut `p` (opens presenter mode in the current tab)
10
- - Navigation controls button
10
+ - Navigation controls button on `md` and wider screens
11
11
  - Direct URL: `/#/presenter/1/0`
12
12
 
13
13
  ### Deactivation
@@ -45,17 +45,15 @@ Includes:
45
45
  - Color mode cycle button (system → light → dark → system). Presenter color mode changes also sync to BroadcastChannel audience views and Presentation API cast receivers.
46
46
  - Blank screen toggle button and `b` keyboard shortcut. While blanked, the presenter sees a `Screen blanked (b)` indicator and audience/cast views see a black screen.
47
47
  - Button to cast the audience view to a secondary display when the Presentation API is supported. Unsupported browsers keep a visibly disabled-looking control in the action row; it does not render extra inline feedback, and its hover title/accessible label explains that Presentation API casting is unavailable. Active casting can be stopped from the same control.
48
- - Presenter navigation buttons provide previous/next timeline-step navigation and previous/next slide navigation on mobile presenter layouts. Desktop presenter layouts do not show navigation buttons. On mobile, the timeline-step buttons sit on the outside edges of the button group (previous step, previous slide, next slide, next step), because step navigation is the primary/default action. Timeline keyboard shortcuts (`→`/`←`/`↓`/`↑`, `d`/`a`/`s`/`w`) update the presenter route and keep the window in presenter mode.
49
- - Presenter navigation uses the shared Honeydeck navigation command abstraction so button, keyboard, and touch inputs share the same semantics as audience view.
48
+ - Timeline keyboard shortcuts (`→`/`←`/`↓`/`↑`, `d`/`a`/`s`/`w`) update the presenter route and keep the window in presenter mode on supported desktop layouts.
50
49
  - Presenter notes are scroll-owned regions: wheel, trackpad, touch scroll, and swipe gestures that start in notes scroll notes and never navigate slides, even at scroll boundaries.
51
- - On mobile presenter layouts, the Current preview may use tap zones and swipe navigation; speaker notes remain scroll-only. Pinch-to-zoom and pinch-to-overview are not required in presenter mode.
52
50
  - Code step-through previews use the same timeline state as audience view, so the Next preview shows the upcoming highlighted code step.
53
51
  - In the Next preview, reveal content from later timeline steps is visible at reduced opacity so the speaker can see what is still coming on that slide. Audience view and the Current preview keep future steps hidden.
54
52
  - When no next timeline state exists (final step of the final slide), the Next preview shows a placeholder (`No next step`) instead of trying to render a missing slide.
55
53
 
56
54
  ### Presenter Responsiveness
57
55
 
58
- Presenter mode uses a two-column preview area (`Current` larger, `Next` smaller), a notes panel, and a bottom status/action bar on desktop. On narrow/mobile screens it switches to a single-column layout and hides the Next preview. The navigation button group is the bottom-most element on mobile and uses larger touch targets than desktop.
56
+ Presenter mode uses a two-column preview area (`Current` larger, `Next` smaller), a notes panel, and a bottom status/action bar on `md` and wider screens. Below Tailwind's `md` breakpoint, presenter mode is not supported: direct presenter URLs show a full-page hint that presenter mode is not supported on mobile and provide a button back to the same slide/step in audience view.
59
57
 
60
58
  ### Presentation Timer
61
59