Presentation: fit frames edge-to-edge and add a Miro-style control bar (prev / N of M / next / exit) #93

Open
opened 2026-04-28 13:01:04 +00:00 by AhmedHanafy725 · 3 comments
Member

Problems

Followup to issue #87. Two gaps in the current presentation experience:

  1. Frames don't fill the viewport. frames.js::focusFrame adds a hardcoded padding = 40 on every side before computing the fit scale. The result is visible gutters around every frame, even when the user expects the frame to fill the screen edge-to-edge like a slide.
  2. No on-screen controls. Once presentation mode is active the only way to navigate is keyboard arrows / Esc. There's no slide counter, no visible prev / next affordance, and no Exit button. Users who came in via the Present button toolbar have no obvious way to advance or leave.

Miro / FigJam / Figma all solve this with a small floating control bar at the bottom-center of the viewport with: previous arrow, X of N slide counter, next arrow, and an exit button.

Reproduction

  1. Place 3 frames on a board.
  2. Click Present.
  3. Observe: each frame appears with visible gutters; there's no on-screen indicator of 1 / 3 and no buttons to advance.

Affected files

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/frames.js -- drop the 40px padding (or expose it as a near-zero constant); add a small notifier callback so the host UI can refresh the slide counter; add getCurrentIndex() / getTotal() accessors.
  • crates/hero_whiteboard_ui/templates/web/board.html -- add the floating control bar + the slide counter element; toggle them via body.wb-presenting; wire buttons to WhiteboardFrames.prevFrame() / nextFrame() / stopPresentation(); subscribe to the notifier so the counter updates when the user advances.

No server / SDK / openrpc / DB changes.

Expected behavior

  • A frame fills the viewport with no padding (or at most 1-2 px so its dashed border isn't sliced off). The frame's aspect ratio is preserved -- if it's narrower than the viewport, the leftover area on the sides is just the canvas background; if it's shorter than the viewport, the leftover area is on the top/bottom. The point is no fixed gutter is forced.
  • A control bar appears at the bottom-center of the viewport while presentation mode is active, with:
    • A previous arrow button (left chevron). Disabled / dimmed at the first frame.
    • A slide counter X / N (e.g. 2 / 5).
    • A next arrow button (right chevron). Disabled / dimmed at the last frame.
    • An exit button (close X) at the right edge of the bar.
  • All buttons have hover + focus styling that matches the rest of the app (var(--wb-...)).
  • Clicking prev / next behaves the same as WhiteboardFrames.prevFrame() / nextFrame() already does (zoom-to-fit the new frame); clicking exit calls stopPresentation().
  • Keyboard navigation continues to work alongside the buttons.
  • The control bar is invisible when not in presentation mode.
  • Layout works in light + dark themes via var(--wb-...).

Acceptance criteria

  • Each frame fills the viewport with no fixed padding; aspect-ratio fit is preserved.
  • A floating control bar appears at the bottom-center of the viewport in presentation mode.
  • The counter reads X / N and updates when navigating.
  • Prev arrow is disabled on the first frame; next arrow is disabled on the last frame.
  • Exit button calls stopPresentation().
  • Keyboard nav (Arrow keys / Space / PageDown / PageUp / Esc) still works.
  • Control bar is hidden when not in presentation mode.
  • Layout uses var(--wb-...) tokens; works in light + dark.
  • No regression in the no-frames toast or the chrome-hide CSS introduced by issue #87.
  • Diff is contained to frames.js and board.html.
  • cargo check / clippy / fmt --check / test clean.

Notes

  • The cleanest way to push counter updates is a callback registered by the host (WhiteboardFrames.onChange(fn)), which WhiteboardFrames calls inside startPresentation, nextFrame, prevFrame, and stopPresentation. The host's listener updates the counter element and toggles the prev/next disabled state.
  • The existing WhiteboardFrames.isPresentationMode() is enough to gate the control bar visibility through CSS (body.wb-presenting).
  • Don't introduce overlay markup that intercepts canvas events outside the bar -- use pointer-events: auto on the bar and pointer-events: none on its wrapping container if a wrapper is needed, so the canvas stays interactive (e.g. for two-finger zoom on touchpads).
  • The bar should not capture keyboard focus when buttons are clicked; <button type="button"> is fine.
## Problems Followup to issue #87. Two gaps in the current presentation experience: 1. **Frames don't fill the viewport.** `frames.js::focusFrame` adds a hardcoded `padding = 40` on every side before computing the fit scale. The result is visible gutters around every frame, even when the user expects the frame to fill the screen edge-to-edge like a slide. 2. **No on-screen controls.** Once presentation mode is active the only way to navigate is keyboard arrows / Esc. There's no slide counter, no visible prev / next affordance, and no Exit button. Users who came in via the Present button toolbar have no obvious way to advance or leave. Miro / FigJam / Figma all solve this with a small floating control bar at the bottom-center of the viewport with: previous arrow, `X of N` slide counter, next arrow, and an exit button. ## Reproduction 1. Place 3 frames on a board. 2. Click Present. 3. Observe: each frame appears with visible gutters; there's no on-screen indicator of `1 / 3` and no buttons to advance. ## Affected files - `crates/hero_whiteboard_ui/static/web/js/whiteboard/frames.js` -- drop the 40px padding (or expose it as a near-zero constant); add a small notifier callback so the host UI can refresh the slide counter; add `getCurrentIndex()` / `getTotal()` accessors. - `crates/hero_whiteboard_ui/templates/web/board.html` -- add the floating control bar + the slide counter element; toggle them via `body.wb-presenting`; wire buttons to `WhiteboardFrames.prevFrame()` / `nextFrame()` / `stopPresentation()`; subscribe to the notifier so the counter updates when the user advances. No server / SDK / openrpc / DB changes. ## Expected behavior - A frame fills the viewport with **no** padding (or at most 1-2 px so its dashed border isn't sliced off). The frame's aspect ratio is preserved -- if it's narrower than the viewport, the leftover area on the sides is just the canvas background; if it's shorter than the viewport, the leftover area is on the top/bottom. The point is no fixed gutter is forced. - A control bar appears at the bottom-center of the viewport while presentation mode is active, with: - A previous arrow button (left chevron). Disabled / dimmed at the first frame. - A slide counter `X / N` (e.g. `2 / 5`). - A next arrow button (right chevron). Disabled / dimmed at the last frame. - An exit button (close `X`) at the right edge of the bar. - All buttons have hover + focus styling that matches the rest of the app (`var(--wb-...)`). - Clicking prev / next behaves the same as `WhiteboardFrames.prevFrame() / nextFrame()` already does (zoom-to-fit the new frame); clicking exit calls `stopPresentation()`. - Keyboard navigation continues to work alongside the buttons. - The control bar is invisible when not in presentation mode. - Layout works in light + dark themes via `var(--wb-...)`. ## Acceptance criteria - [ ] Each frame fills the viewport with no fixed padding; aspect-ratio fit is preserved. - [ ] A floating control bar appears at the bottom-center of the viewport in presentation mode. - [ ] The counter reads `X / N` and updates when navigating. - [ ] Prev arrow is disabled on the first frame; next arrow is disabled on the last frame. - [ ] Exit button calls `stopPresentation()`. - [ ] Keyboard nav (Arrow keys / Space / PageDown / PageUp / Esc) still works. - [ ] Control bar is hidden when not in presentation mode. - [ ] Layout uses `var(--wb-...)` tokens; works in light + dark. - [ ] No regression in the no-frames toast or the chrome-hide CSS introduced by issue #87. - [ ] Diff is contained to `frames.js` and `board.html`. - [ ] `cargo check / clippy / fmt --check / test` clean. ## Notes - The cleanest way to push counter updates is a callback registered by the host (`WhiteboardFrames.onChange(fn)`), which `WhiteboardFrames` calls inside `startPresentation`, `nextFrame`, `prevFrame`, and `stopPresentation`. The host's listener updates the counter element and toggles the prev/next disabled state. - The existing `WhiteboardFrames.isPresentationMode()` is enough to gate the control bar visibility through CSS (`body.wb-presenting`). - Don't introduce overlay markup that intercepts canvas events outside the bar -- use `pointer-events: auto` on the bar and `pointer-events: none` on its wrapping container if a wrapper is needed, so the canvas stays interactive (e.g. for two-finger zoom on touchpads). - The bar should not capture keyboard focus when buttons are clicked; `<button type="button">` is fine.
Author
Member

Implementation Spec for Issue #93

Objective

Polish the existing presentation mode (issue #87): drop the 40 px gutter so the focused frame fills the viewport edge-to-edge, and add a Miro-style floating control bar (< · X / N · > ✕) at the bottom-center. Aspect-ratio fit is preserved; only the forced padding is removed.

Requirements

  • focusFrame no longer adds 40 px on each side. The frame fills the viewport up to its longer dimension; the leftover area on the perpendicular axis is just the canvas background (not a fixed gutter).
  • A floating control bar shows in presentation mode with: previous arrow, X / N counter, next arrow, exit (close ) button. Bottom-center, ~16 px from the bottom edge.
  • Prev arrow is disabled / dimmed at the first frame; next at the last.
  • Counter updates whenever start / next / prev / stop runs.
  • Exit button calls WhiteboardFrames.stopPresentation().
  • Keyboard nav (Esc / arrows / space / page-keys, wired in #87) keeps working alongside the buttons.
  • Bar is hidden when not in presentation mode (CSS via body.wb-presenting).
  • Themed via var(--wb-...) CSS variables for light + dark.
  • No regression in the no-frames toast or chrome-hide CSS from #87.
  • Diff stays in frames.js and board.html.
  • cargo check / clippy / fmt --check / test clean.

Files to Modify

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/frames.js — drop the 40 px padding in focusFrame; add a setOnChange(fn) API + invoke the registered callback inside startPresentation, nextFrame, prevFrame, and stopPresentation; expose getCurrentIndex() and getTotal() accessors.
  • crates/hero_whiteboard_ui/templates/web/board.html — add the floating control bar markup; CSS in {% block head %}; register the change callback at startup so the counter + button states stay in sync.

No server / SDK / openrpc / DB changes.

Implementation Plan

Step 1: frames.js — edge-to-edge fit + change notifier + accessors

Files: crates/hero_whiteboard_ui/static/web/js/whiteboard/frames.js

  1. In focusFrame, drop the gutter:
    var fw = bg.width();
    var fh = bg.height();
    
    Remove the padding = 40 constant and the + padding * 2 math.
  2. Add module-scope _onChange = null; and a setOnChange(fn) setter on the public API. Define a small _emit() helper that calls _onChange({ index, total, mode }) if set, where mode is 'present' while presentation is active and 'idle' after stopPresentation.
  3. Call _emit() at the end of startPresentation (after focusFrame(frames[0])), at the end of nextFrame and prevFrame (after focusFrame(...)), and at the end of stopPresentation (so the host can hide the bar / reset the counter).
  4. Add getCurrentIndex() and getTotal() to the public API.
  5. Update the public-API return object to include setOnChange, getCurrentIndex, getTotal.

Dependencies: none.

Step 2: board.html — floating control bar + counter wiring

Files: crates/hero_whiteboard_ui/templates/web/board.html

  1. CSS in the existing {% block head %} <style> block:
    #presentation-controls {
        display: none;
        position: fixed;
        bottom: 16px;
        left: 50%;
        transform: translateX(-50%);
        z-index: 10001;
        background: var(--wb-surface);
        border: 1px solid var(--wb-border);
        border-radius: 24px;
        box-shadow: 0 4px 16px rgba(0,0,0,0.20);
        padding: 4px 8px;
        align-items: center;
        gap: 4px;
        pointer-events: auto;
    }
    body.wb-presenting #presentation-controls { display: flex; }
    #presentation-controls .pres-btn {
        background: transparent;
        border: none;
        color: var(--wb-text);
        font-size: 14px;
        padding: 6px 10px;
        border-radius: 6px;
        cursor: pointer;
    }
    #presentation-controls .pres-btn:hover:not(:disabled) { background: var(--wb-bg); }
    #presentation-controls .pres-btn:disabled { opacity: 0.4; cursor: default; }
    #presentation-controls .pres-counter {
        font-size: 12px;
        color: var(--wb-text-muted);
        min-width: 56px;
        text-align: center;
        font-variant-numeric: tabular-nums;
    }
    #presentation-controls .pres-divider {
        width: 1px;
        align-self: stretch;
        background: var(--wb-border);
        margin: 4px 4px;
    }
    
    min-width on the counter avoids the bar resizing as the index ticks 1 / 9 → 10 / 9.
  2. Markup in {% block content %} (next to the existing frames-empty-toast):
    <div id="presentation-controls" role="group" aria-label="Presentation controls">
        <button type="button" class="pres-btn" id="pres-prev" title="Previous (Left arrow)" aria-label="Previous slide"><i class="bi bi-chevron-left"></i></button>
        <span class="pres-counter" id="pres-counter">1 / 1</span>
        <button type="button" class="pres-btn" id="pres-next" title="Next (Right arrow / Space)" aria-label="Next slide"><i class="bi bi-chevron-right"></i></button>
        <span class="pres-divider"></span>
        <button type="button" class="pres-btn" id="pres-exit" title="Exit (Esc)" aria-label="Exit presentation"><i class="bi bi-x-lg"></i></button>
    </div>
    
  3. JS in {% block scripts %} (inside the same <script> block that already has the helpers):
    (function () {
        function bindPresentation() {
            if (typeof WhiteboardFrames === 'undefined' || !WhiteboardFrames.setOnChange) return;
            var prevBtn = document.getElementById('pres-prev');
            var nextBtn = document.getElementById('pres-next');
            var exitBtn = document.getElementById('pres-exit');
            var counter = document.getElementById('pres-counter');
            if (!prevBtn || !nextBtn || !exitBtn || !counter) return;
            prevBtn.addEventListener('click', function () { WhiteboardFrames.prevFrame(); });
            nextBtn.addEventListener('click', function () { WhiteboardFrames.nextFrame(); });
            exitBtn.addEventListener('click', function () { WhiteboardFrames.stopPresentation(); });
            WhiteboardFrames.setOnChange(function (state) {
                if (!state || state.mode !== 'present') return;
                counter.textContent = (state.index + 1) + ' / ' + state.total;
                prevBtn.disabled = state.index <= 0;
                nextBtn.disabled = state.index >= state.total - 1;
            });
        }
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', bindPresentation);
        } else {
            bindPresentation();
        }
    })();
    

Dependencies: Step 1 (depends on setOnChange / getCurrentIndex / getTotal).

Acceptance Criteria

  • In presentation mode, each frame fills the viewport with no fixed padding (aspect-ratio fit preserved).
  • A floating control bar at the bottom-center shows prev / X / N counter / next / exit.
  • Counter updates when navigating.
  • Prev disabled on first frame; next disabled on last.
  • Exit button stops presentation.
  • Keyboard nav still works alongside the buttons.
  • Bar is hidden when not in presentation mode.
  • Light + dark themes both look correct.
  • No regression in no-frames toast or chrome-hide CSS.
  • cargo check / clippy / fmt --check / test clean.

Notes

  • The bar uses pointer-events: auto and lives at z-index: 10001 so it sits above the canvas and the chrome-hide CSS doesn't need extra rules. The chrome-hide rules in #87 only target .wb-navbar / .wb-toolbar / .wb-minimap / .wb-zoom / #properties-panel#presentation-controls is unaffected.
  • The single setOnChange slot is sufficient (only one host listener at a time). Extending to multiple subscribers is unnecessary overhead.
  • The bar is hidden via display: none when not presenting, then body.wb-presenting #presentation-controls { display: flex; } flips it on; the CSS already adds the host's body class via wb-presenting.
  • Don't add a slide-thumbnails sidebar in this iteration; the issue's focus is the prev/counter/next/exit affordance + edge-to-edge fit.
## Implementation Spec for Issue #93 ### Objective Polish the existing presentation mode (issue #87): drop the 40 px gutter so the focused frame fills the viewport edge-to-edge, and add a Miro-style floating control bar (`< · X / N · > ✕`) at the bottom-center. Aspect-ratio fit is preserved; only the forced padding is removed. ### Requirements - `focusFrame` no longer adds 40 px on each side. The frame fills the viewport up to its longer dimension; the leftover area on the perpendicular axis is just the canvas background (not a fixed gutter). - A floating control bar shows in presentation mode with: previous arrow, `X / N` counter, next arrow, exit (close `✕`) button. Bottom-center, ~16 px from the bottom edge. - Prev arrow is disabled / dimmed at the first frame; next at the last. - Counter updates whenever `start / next / prev / stop` runs. - Exit button calls `WhiteboardFrames.stopPresentation()`. - Keyboard nav (Esc / arrows / space / page-keys, wired in #87) keeps working alongside the buttons. - Bar is hidden when not in presentation mode (CSS via `body.wb-presenting`). - Themed via `var(--wb-...)` CSS variables for light + dark. - No regression in the no-frames toast or chrome-hide CSS from #87. - Diff stays in `frames.js` and `board.html`. - `cargo check / clippy / fmt --check / test` clean. ### Files to Modify - `crates/hero_whiteboard_ui/static/web/js/whiteboard/frames.js` — drop the 40 px padding in `focusFrame`; add a `setOnChange(fn)` API + invoke the registered callback inside `startPresentation`, `nextFrame`, `prevFrame`, and `stopPresentation`; expose `getCurrentIndex()` and `getTotal()` accessors. - `crates/hero_whiteboard_ui/templates/web/board.html` — add the floating control bar markup; CSS in `{% block head %}`; register the change callback at startup so the counter + button states stay in sync. No server / SDK / openrpc / DB changes. ### Implementation Plan #### Step 1: `frames.js` — edge-to-edge fit + change notifier + accessors Files: `crates/hero_whiteboard_ui/static/web/js/whiteboard/frames.js` 1. In `focusFrame`, drop the gutter: ```js var fw = bg.width(); var fh = bg.height(); ``` Remove the `padding = 40` constant and the `+ padding * 2` math. 2. Add module-scope `_onChange = null;` and a `setOnChange(fn)` setter on the public API. Define a small `_emit()` helper that calls `_onChange({ index, total, mode })` if set, where `mode` is `'present'` while presentation is active and `'idle'` after `stopPresentation`. 3. Call `_emit()` at the end of `startPresentation` (after `focusFrame(frames[0])`), at the end of `nextFrame` and `prevFrame` (after `focusFrame(...)`), and at the end of `stopPresentation` (so the host can hide the bar / reset the counter). 4. Add `getCurrentIndex()` and `getTotal()` to the public API. 5. Update the public-API return object to include `setOnChange`, `getCurrentIndex`, `getTotal`. Dependencies: none. #### Step 2: `board.html` — floating control bar + counter wiring Files: `crates/hero_whiteboard_ui/templates/web/board.html` 1. CSS in the existing `{% block head %}` `<style>` block: ```css #presentation-controls { display: none; position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%); z-index: 10001; background: var(--wb-surface); border: 1px solid var(--wb-border); border-radius: 24px; box-shadow: 0 4px 16px rgba(0,0,0,0.20); padding: 4px 8px; align-items: center; gap: 4px; pointer-events: auto; } body.wb-presenting #presentation-controls { display: flex; } #presentation-controls .pres-btn { background: transparent; border: none; color: var(--wb-text); font-size: 14px; padding: 6px 10px; border-radius: 6px; cursor: pointer; } #presentation-controls .pres-btn:hover:not(:disabled) { background: var(--wb-bg); } #presentation-controls .pres-btn:disabled { opacity: 0.4; cursor: default; } #presentation-controls .pres-counter { font-size: 12px; color: var(--wb-text-muted); min-width: 56px; text-align: center; font-variant-numeric: tabular-nums; } #presentation-controls .pres-divider { width: 1px; align-self: stretch; background: var(--wb-border); margin: 4px 4px; } ``` `min-width` on the counter avoids the bar resizing as the index ticks `1 / 9 → 10 / 9`. 2. Markup in `{% block content %}` (next to the existing `frames-empty-toast`): ```html <div id="presentation-controls" role="group" aria-label="Presentation controls"> <button type="button" class="pres-btn" id="pres-prev" title="Previous (Left arrow)" aria-label="Previous slide"><i class="bi bi-chevron-left"></i></button> <span class="pres-counter" id="pres-counter">1 / 1</span> <button type="button" class="pres-btn" id="pres-next" title="Next (Right arrow / Space)" aria-label="Next slide"><i class="bi bi-chevron-right"></i></button> <span class="pres-divider"></span> <button type="button" class="pres-btn" id="pres-exit" title="Exit (Esc)" aria-label="Exit presentation"><i class="bi bi-x-lg"></i></button> </div> ``` 3. JS in `{% block scripts %}` (inside the same `<script>` block that already has the helpers): ```js (function () { function bindPresentation() { if (typeof WhiteboardFrames === 'undefined' || !WhiteboardFrames.setOnChange) return; var prevBtn = document.getElementById('pres-prev'); var nextBtn = document.getElementById('pres-next'); var exitBtn = document.getElementById('pres-exit'); var counter = document.getElementById('pres-counter'); if (!prevBtn || !nextBtn || !exitBtn || !counter) return; prevBtn.addEventListener('click', function () { WhiteboardFrames.prevFrame(); }); nextBtn.addEventListener('click', function () { WhiteboardFrames.nextFrame(); }); exitBtn.addEventListener('click', function () { WhiteboardFrames.stopPresentation(); }); WhiteboardFrames.setOnChange(function (state) { if (!state || state.mode !== 'present') return; counter.textContent = (state.index + 1) + ' / ' + state.total; prevBtn.disabled = state.index <= 0; nextBtn.disabled = state.index >= state.total - 1; }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', bindPresentation); } else { bindPresentation(); } })(); ``` Dependencies: Step 1 (depends on `setOnChange` / `getCurrentIndex` / `getTotal`). ### Acceptance Criteria - [ ] In presentation mode, each frame fills the viewport with no fixed padding (aspect-ratio fit preserved). - [ ] A floating control bar at the bottom-center shows prev / `X / N` counter / next / exit. - [ ] Counter updates when navigating. - [ ] Prev disabled on first frame; next disabled on last. - [ ] Exit button stops presentation. - [ ] Keyboard nav still works alongside the buttons. - [ ] Bar is hidden when not in presentation mode. - [ ] Light + dark themes both look correct. - [ ] No regression in no-frames toast or chrome-hide CSS. - [ ] `cargo check / clippy / fmt --check / test` clean. ### Notes - The bar uses `pointer-events: auto` and lives at `z-index: 10001` so it sits above the canvas and the chrome-hide CSS doesn't need extra rules. The chrome-hide rules in #87 only target `.wb-navbar / .wb-toolbar / .wb-minimap / .wb-zoom / #properties-panel` — `#presentation-controls` is unaffected. - The single `setOnChange` slot is sufficient (only one host listener at a time). Extending to multiple subscribers is unnecessary overhead. - The bar is hidden via `display: none` when not presenting, then `body.wb-presenting #presentation-controls { display: flex; }` flips it on; the CSS already adds the host's body class via `wb-presenting`. - Don't add a slide-thumbnails sidebar in this iteration; the issue's focus is the prev/counter/next/exit affordance + edge-to-edge fit.
Author
Member

Test Results

  • cargo test -p hero_whiteboard_server: 3 passed.
  • cargo clippy --workspace -- -D warnings: clean.
  • cargo fmt --all -- --check: clean.
  • cargo check --workspace: clean.
  • node --check frames.js: parses cleanly.

UI-only change. Manual verification recommended:

  1. Place 3 frames on a board. Click Present — first frame fills the viewport edge-to-edge with no fixed gutter.
  2. Confirm a floating control bar appears at the bottom-center showing 1 / 3, with a left chevron (disabled), 1 / 3, right chevron (enabled), and an exit button.
  3. Click the right chevron — second frame fills viewport, counter reads 2 / 3, both chevrons enabled.
  4. Click the right chevron once more — 3 / 3, right chevron disabled.
  5. Click the left chevron — 2 / 3, both chevrons enabled.
  6. Press Right arrow / Space / PageDown — counter still updates. Press Esc — control bar hides, chrome restored.
  7. Click Exit (the button) — same effect as Esc.
  8. Confirm the no-frames toast still shows when Present is clicked with zero frames on the board.
## Test Results - `cargo test -p hero_whiteboard_server`: 3 passed. - `cargo clippy --workspace -- -D warnings`: clean. - `cargo fmt --all -- --check`: clean. - `cargo check --workspace`: clean. - `node --check frames.js`: parses cleanly. UI-only change. Manual verification recommended: 1. Place 3 frames on a board. Click Present — first frame fills the viewport edge-to-edge with no fixed gutter. 2. Confirm a floating control bar appears at the bottom-center showing `1 / 3`, with a left chevron (disabled), `1 / 3`, right chevron (enabled), and an exit button. 3. Click the right chevron — second frame fills viewport, counter reads `2 / 3`, both chevrons enabled. 4. Click the right chevron once more — `3 / 3`, right chevron disabled. 5. Click the left chevron — `2 / 3`, both chevrons enabled. 6. Press Right arrow / Space / PageDown — counter still updates. Press Esc — control bar hides, chrome restored. 7. Click Exit (the `✕` button) — same effect as Esc. 8. Confirm the no-frames toast still shows when Present is clicked with zero frames on the board.
Author
Member

Implementation Summary

2 files changed, +107 / -3.

frames.js

  • focusFrame: removed the padding = 40 so the frame fills the viewport edge-to-edge. Aspect-ratio fit is preserved; only the forced gutter is gone.
  • New module-scope _onChange slot + _emit() helper that builds { index, total, mode } and calls the host listener. mode is 'present' while active, 'idle' after stopPresentation.
  • _emit() is called at the end of startPresentation, nextFrame, prevFrame, and stopPresentation.
  • New public-API methods: setOnChange(fn), getCurrentIndex(), getTotal().

board.html

  • New CSS in {% block head %} for #presentation-controls: bottom-center floating bar, hidden by default, revealed via body.wb-presenting #presentation-controls { display: flex; }. Pill-shaped, var(--wb-...) themed, tabular-nums counter.
  • New markup next to the no-frames toast: prev / counter (X / N) / next / vertical divider / exit. Buttons use bi-chevron-left, bi-chevron-right, bi-x-lg.
  • New IIFE in the script block that, on DOMContentLoaded, wires the three buttons to WhiteboardFrames.prevFrame() / nextFrame() / stopPresentation() and registers a state listener via setOnChange. The listener updates the counter text and the disabled state of the prev/next buttons.

Verification

  • cargo test -p hero_whiteboard_server: 3 passed.
  • cargo clippy --workspace -- -D warnings: clean.
  • cargo fmt --all -- --check: clean.
  • cargo check --workspace: clean.
  • node --check frames.js: parses cleanly.

Notes / caveats

  • UI-only; no server / SDK / openrpc / DB changes.
  • _onChange is a single-slot listener (the host registers one). Multiple subscribers aren't needed and would add complexity without a use case.
  • The control bar lives at z-index: 10001 (above the board-deleted overlay's 10000 if both ever happened to be active, which they won't because deleted boards close the editor).
  • Keyboard nav is unchanged — the listener fires the same nextFrame / prevFrame / stopPresentation calls that the buttons do, and _emit() runs from inside those, so the counter stays in sync regardless of input source.
## Implementation Summary 2 files changed, +107 / -3. ### `frames.js` - `focusFrame`: removed the `padding = 40` so the frame fills the viewport edge-to-edge. Aspect-ratio fit is preserved; only the forced gutter is gone. - New module-scope `_onChange` slot + `_emit()` helper that builds `{ index, total, mode }` and calls the host listener. `mode` is `'present'` while active, `'idle'` after `stopPresentation`. - `_emit()` is called at the end of `startPresentation`, `nextFrame`, `prevFrame`, and `stopPresentation`. - New public-API methods: `setOnChange(fn)`, `getCurrentIndex()`, `getTotal()`. ### `board.html` - New CSS in `{% block head %}` for `#presentation-controls`: bottom-center floating bar, hidden by default, revealed via `body.wb-presenting #presentation-controls { display: flex; }`. Pill-shaped, `var(--wb-...)` themed, tabular-nums counter. - New markup next to the no-frames toast: `prev` / counter (`X / N`) / `next` / vertical divider / `exit`. Buttons use `bi-chevron-left`, `bi-chevron-right`, `bi-x-lg`. - New IIFE in the script block that, on DOMContentLoaded, wires the three buttons to `WhiteboardFrames.prevFrame() / nextFrame() / stopPresentation()` and registers a state listener via `setOnChange`. The listener updates the counter text and the disabled state of the prev/next buttons. ### Verification - `cargo test -p hero_whiteboard_server`: 3 passed. - `cargo clippy --workspace -- -D warnings`: clean. - `cargo fmt --all -- --check`: clean. - `cargo check --workspace`: clean. - `node --check frames.js`: parses cleanly. ### Notes / caveats - UI-only; no server / SDK / openrpc / DB changes. - `_onChange` is a single-slot listener (the host registers one). Multiple subscribers aren't needed and would add complexity without a use case. - The control bar lives at `z-index: 10001` (above the board-deleted overlay's 10000 if both ever happened to be active, which they won't because deleted boards close the editor). - Keyboard nav is unchanged — the listener fires the same `nextFrame` / `prevFrame` / `stopPresentation` calls that the buttons do, and `_emit()` runs from inside those, so the counter stays in sync regardless of input source.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_whiteboard#93
No description provided.