feat(slides): image beside text, slide numbers, and presenter font scaling - #550
Draft
vpavlin wants to merge 6 commits into
Draft
feat(slides): image beside text, slide numbers, and presenter font scaling#550vpavlin wants to merge 6 commits into
vpavlin wants to merge 6 commits into
Conversation
Slides were derived by serialising the document to Markdown and parsing it back: doc -> HTML -> Markdown -> HTML. Everything Markdown cannot express was silently dropped on the way to the stage, most visibly multi-column blocks and paragraph font sizes, so an image beside text was impossible on a slide. Split the ProseMirror document directly instead. Slides are still `string[]`, rendered back through the editor's own schema, so the preview panel, PDF export and share links are untouched. Breaks are decided by measuring against the real 1080x608 stage rather than by counting characters, which split slides that visibly had room to spare. The character heuristics remain as a fallback for environments without layout. Lists paginate between their items; columns and tables are never divided, and a heading is never left stranded without its content. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The windowed presentation stylesheet set type at roughly 2.2% of stage width (82px headings, 24px body, 24px paragraph gaps on a 608px-tall slide) while the fullscreen stylesheet already used ~1.6%. Slides therefore held barely a few lines, split with obvious space to spare, and the preview looked nothing like the deck being presented. Match the windowed block to fullscreen's proportions. Also: - thread --slide-font-scale through both stylesheets, including the 14 viewport-relative sizes in the fullscreen block that previously ignored it, so the font-size control works in fullscreen as well as windowed - stretch .ProseMirror inside .fullscreen; the editor wraps slide content in a single element, which as a lone flex item under `align-items: start` shrank to its content width and collapsed column grids to one character per line - let images inside a column fill it at their natural aspect instead of being capped at 32rem and letterboxed into 16/9 - use a unitless line-height so spacing tracks the font scale Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fullscreen injected slide HTML with dangerouslySetInnerHTML while the windowed view rendered through the editor, so custom nodes could appear in one and not the other. Both now render through the editor. The wrapper is animated via controls rather than remounted, because EditorContent owns the editor's DOM node and re-parenting it on every slide change is what let the paths drift. - slide numbers now render in both windowed and fullscreen; the counter was previously mobile-fullscreen only - presenter font scaling via the toolbar or +/-/0, applied as a CSS custom property so elements keep their relative proportions - the presentation editor is read-only: left editable, `f` toggled fullscreen *and* typed an "f" into the slide - release focus on open, so navigation keys no longer reach the document editor behind the overlay and edit the document while presenting - surface build failures instead of leaving the loader spinning silently Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Presentation mode was reachable only by mouse, despite being keyboard-driven once open. Matches on event.code rather than event.key, since Option-P emits "π" on macOS and the shortcut would never fire there. Registered in the capture phase so the browser's own Ctrl/Cmd-P print binding is suppressed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@vpavlin is attempting to deploy a commit to the fileverse Team on Vercel. A member of the Team first needs to authorize it. |
Bullets differed between windowed and fullscreen: the fullscreen block styled tight lists with a hollow `circle` while the windowed block had no list rules at all and fell back to the browser's `disc`. `data-tight` describes spacing, not nesting depth, so a top-level tight list was reading as a nested one. List rules now live once in the shared .ProseMirror block; the fullscreen block only adjusts spacing. Also zero the margin on a list item's paragraph. TipTap wraps item text in a <p>, which took the block-level paragraph margin while the marker stayed anchored to the top of the item, dropping the text below its own bullet — pronounced in fullscreen, where that margin is viewport-relative. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two problems with text carrying its own size, both newly reachable because this branch stops destroying inline sizes on the way to a slide. An inline `font-size` beats any stylesheet rule, so explicitly-sized text ignored the presenter font scale entirely while everything around it grew and shrank. Those values are now routed through the same multiplication the stylesheet uses, so they respond to the control and are accounted for when slides are measured. A native list marker is sized by its <li>, but an explicit size lives on a textStyle mark or paragraph attribute inside it, so a resized item kept a base-size bullet sitting off its own baseline — widening as the scale went up. listItem can now carry a fontSize, applied when slides are built. The attribute defaults to null and nothing sets it during editing, so document output is unchanged outside presentation mode. Both transforms are pure functions over HTML and the document tree, so unlike the measurement path they are covered by unit tests (14 added, 25 total). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Slides were derived by serialising the document to Markdown and parsing it back:
Everything Markdown cannot express was silently dropped on the way to the stage.
multi-columnandfont-sizeextensions already exist in the editor, but no Turndown rule carries them, andmd-to-html.tscontains the stringcolumnzero times — so an image beside text was impossible on a slide, and author-set font sizes never arrived.This builds the deck straight from the ProseMirror document instead. Slides remain
string[], rendered back through the editor's own schema, so the preview panel, PDF export and share links are untouched.Three user-facing additions: image beside text, slide numbers, and presenter font scaling.
Screenshots
1. Image beside text (the headline change)
2. Slide numbers and font-size control
4. Windowed vs fullscreen parity
What changed
package/utils/doc-to-slides.ts(new) — splits the document at structural boundaries (page breaks, headings), then decides breaks by measuring against the real 1080×608 stage rather than counting characters. The old heuristics (7 lines / 1000 chars / 250 words) split slides that visibly had room to spare. Character counting survives as a fallback for environments without layout.Fitting rules:
<li>items, withstartcarried over so ordered numbering stays continuouspresentation-mode.tsx— fullscreen useddangerouslySetInnerHTMLwhile windowed rendered through the editor, so custom nodes could appear in one and not the other. Both now render through the editor. The wrapper is animated via controls rather than remounted, becauseEditorContentowns the editor's DOM node and re-parenting it per slide is what let the two paths drift apart.editor.css— windowed presentation type was set at ~2.2% of stage width while fullscreen already used ~1.6%. Aligned to fullscreen's proportions, and--slide-font-scalenow threads through both.ddoc-editor.tsx— Mod+Alt+P opens the deck.Bugs found along the way
Not part of the original scope, but worth flagging:
f,+,-,0, arrows and space were typed into the document while presenting. Fixed by releasing focus on open and making the presentation editor read-only.processMarkdownContentinmd-to-slides.ts(~200 lines) is exported but never imported, and disagrees with the live path (maxLines8 vs 7). Left alone here, but it's dead code.Behaviour changes reviewers should weigh
Testing
npm run buildpasses. 11 unit tests added, covering the structural splitter against the real schema viagetHeadlessExtensions. Lint unchanged frommain(8 pre-existing warnings, 0 errors).Known coverage gap, stated plainly: the measurement path is unreachable in unit tests — jsdom parses markup but reports every height as
0, sofitSlidesToStagefalls back to heuristics there. Everything in that path was verified by hand in the demo app. The rules most worth protecting (columns never split, headings never stranded, lists paginate) regressed at least once during development and would each have been caught instantly by a browser test. A Playwright test asserting real geometry is the natural follow-up.Follow-up work, deliberately not in this PR
handle-print.tsis a separate path with its own stylesheet.🤖 Generated with Claude Code