Presentation: fit frames edge-to-edge and add a Miro-style control bar (prev / N of M / next / exit) #93
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_whiteboard#93
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problems
Followup to issue #87. Two gaps in the current presentation experience:
frames.js::focusFrameadds a hardcodedpadding = 40on 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.Miro / FigJam / Figma all solve this with a small floating control bar at the bottom-center of the viewport with: previous arrow,
X of Nslide counter, next arrow, and an exit button.Reproduction
1 / 3and 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; addgetCurrentIndex()/getTotal()accessors.crates/hero_whiteboard_ui/templates/web/board.html-- add the floating control bar + the slide counter element; toggle them viabody.wb-presenting; wire buttons toWhiteboardFrames.prevFrame()/nextFrame()/stopPresentation(); subscribe to the notifier so the counter updates when the user advances.No server / SDK / openrpc / DB changes.
Expected behavior
X / N(e.g.2 / 5).X) at the right edge of the bar.var(--wb-...)).WhiteboardFrames.prevFrame() / nextFrame()already does (zoom-to-fit the new frame); clicking exit callsstopPresentation().var(--wb-...).Acceptance criteria
X / Nand updates when navigating.stopPresentation().var(--wb-...)tokens; works in light + dark.frames.jsandboard.html.cargo check / clippy / fmt --check / testclean.Notes
WhiteboardFrames.onChange(fn)), whichWhiteboardFramescalls insidestartPresentation,nextFrame,prevFrame, andstopPresentation. The host's listener updates the counter element and toggles the prev/next disabled state.WhiteboardFrames.isPresentationMode()is enough to gate the control bar visibility through CSS (body.wb-presenting).pointer-events: autoon the bar andpointer-events: noneon its wrapping container if a wrapper is needed, so the canvas stays interactive (e.g. for two-finger zoom on touchpads).<button type="button">is fine.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
focusFrameno 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).X / Ncounter, next arrow, exit (close✕) button. Bottom-center, ~16 px from the bottom edge.start / next / prev / stopruns.WhiteboardFrames.stopPresentation().body.wb-presenting).var(--wb-...)CSS variables for light + dark.frames.jsandboard.html.cargo check / clippy / fmt --check / testclean.Files to Modify
crates/hero_whiteboard_ui/static/web/js/whiteboard/frames.js— drop the 40 px padding infocusFrame; add asetOnChange(fn)API + invoke the registered callback insidestartPresentation,nextFrame,prevFrame, andstopPresentation; exposegetCurrentIndex()andgetTotal()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 + accessorsFiles:
crates/hero_whiteboard_ui/static/web/js/whiteboard/frames.jsfocusFrame, drop the gutter: Remove thepadding = 40constant and the+ padding * 2math._onChange = null;and asetOnChange(fn)setter on the public API. Define a small_emit()helper that calls_onChange({ index, total, mode })if set, wheremodeis'present'while presentation is active and'idle'afterstopPresentation._emit()at the end ofstartPresentation(afterfocusFrame(frames[0])), at the end ofnextFrameandprevFrame(afterfocusFrame(...)), and at the end ofstopPresentation(so the host can hide the bar / reset the counter).getCurrentIndex()andgetTotal()to the public API.setOnChange,getCurrentIndex,getTotal.Dependencies: none.
Step 2:
board.html— floating control bar + counter wiringFiles:
crates/hero_whiteboard_ui/templates/web/board.html{% block head %}<style>block:min-widthon the counter avoids the bar resizing as the index ticks1 / 9 → 10 / 9.{% block content %}(next to the existingframes-empty-toast):{% block scripts %}(inside the same<script>block that already has the helpers):Dependencies: Step 1 (depends on
setOnChange/getCurrentIndex/getTotal).Acceptance Criteria
X / Ncounter / next / exit.cargo check / clippy / fmt --check / testclean.Notes
pointer-events: autoand lives atz-index: 10001so 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-controlsis unaffected.setOnChangeslot is sufficient (only one host listener at a time). Extending to multiple subscribers is unnecessary overhead.display: nonewhen not presenting, thenbody.wb-presenting #presentation-controls { display: flex; }flips it on; the CSS already adds the host's body class viawb-presenting.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 / 3, with a left chevron (disabled),1 / 3, right chevron (enabled), and an exit button.2 / 3, both chevrons enabled.3 / 3, right chevron disabled.2 / 3, both chevrons enabled.✕button) — same effect as Esc.Implementation Summary
2 files changed, +107 / -3.
frames.jsfocusFrame: removed thepadding = 40so the frame fills the viewport edge-to-edge. Aspect-ratio fit is preserved; only the forced gutter is gone._onChangeslot +_emit()helper that builds{ index, total, mode }and calls the host listener.modeis'present'while active,'idle'afterstopPresentation._emit()is called at the end ofstartPresentation,nextFrame,prevFrame, andstopPresentation.setOnChange(fn),getCurrentIndex(),getTotal().board.html{% block head %}for#presentation-controls: bottom-center floating bar, hidden by default, revealed viabody.wb-presenting #presentation-controls { display: flex; }. Pill-shaped,var(--wb-...)themed, tabular-nums counter.prev/ counter (X / N) /next/ vertical divider /exit. Buttons usebi-chevron-left,bi-chevron-right,bi-x-lg.WhiteboardFrames.prevFrame() / nextFrame() / stopPresentation()and registers a state listener viasetOnChange. 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
_onChangeis a single-slot listener (the host registers one). Multiple subscribers aren't needed and would add complexity without a use case.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).nextFrame/prevFrame/stopPresentationcalls that the buttons do, and_emit()runs from inside those, so the counter stays in sync regardless of input source.