Markdown slideshows: the deck design system and present mode - #193
Conversation
…howcase The Phase 2 centerpiece is docs/SLIDE_SPEC.md — the product boundary of the deck design system. Its conformance examples ARE the test suite (spec/services/slideshows/conformance_spec.rb executes every fenced fixture), so the spec cannot drift from the implementation, and a future JS renderer proves itself against the same fixtures. - Slideshows::Classify: a pure function assigning each slide a layout pattern (title, stage, code, quote, table, columns, split, content) and a type-scale step from content shape alone — no layout syntax, no measurement. - deck.css + three token themes (coplan, graphite, poster): 16:9 cards sized in container units, per-pattern art direction, discrete type steps instead of shrink-to-fit. Themes are validated tokens on data-deck-theme; user CSS is never accepted. - The renderer decorates rendered slides per the spec's markup contract (deck-content, split panes) after the deck/document parity passes, and wrapping is skipped on any classified-shape/DOM mismatch. - A pattern-showcase seed deck exercises every pattern. An adversarial review round (spec-drift, parity, and step-sanity attackers) then hardened the rules, each fix landing in the spec, the classifier, and a conformance fixture together: - Image-bearing paragraphs are never "short" (a two-image comparison no longer classifies as a title subtitle), and image alt text counts for nothing — accessible descriptions no longer shrink type or flip patterns. - Comment boundaries follow the HTML parser (<!--> is an empty comment, an unclosed comment swallows its block), and inline comments beside an image are speaker notes, so what renders as nothing classifies as nothing — in both directions. - Split-pane wrapping never reorders visible text (loose text nodes left by sanitize skip the wrappers) and takes lead-heading identity from the classifier, not the DOM, so a raw-HTML heading stays body content. - The unit model matches what the stylesheet renders: hard line breaks bill as rendered lines, code bills at ~5/6 of a body line (divisor 1.2, not 2), and mermaid bills as fit-to-box media rather than source lines. - Overflowing slides scroll from the top: content centers via safe auto margins instead of justify-content: center, which pushed the start of the content unreachably above the card. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ed diagrams Review feedback from the seeded decks: the readout deck's code block was invisible in dark mode — the host flips its --code-* variables with the app scheme, which painted deck-ink text on a near-black panel inside the fixed light slide. A deck is a fixed artifact; it must render the same on every screen. - deck.css pins one code treatment per slide: a fixed dark panel and a fixed One Dark highlight palette (every variable the host's code chrome reads — panel, header, hairlines, tokens), identical in every deck theme and both app schemes. - Mermaid diagrams inside a deck now take their theme from the deck (graphite renders dark, everything else light) instead of the reader's scheme; diagrams outside decks follow the app exactly as before. - SLIDE_SPEC.md's theme section states the rule: code panels and diagrams color from the deck, never from the reader. Also from review: two new pattern-showcase slides — "When a slide is mostly words" (three paragraphs of prose landing on the floor step and fitting) and "Every project in flight" (forty bold-name entries, the deliberately-past-the-floor slide that scrolls and that the fit report will exist to flag). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…anvas Presenting is the point of a slideshow, so it stops waiting for a phase: `p` or the Present button runs the deck as a fullscreen 16:9 show — exactly what a Zoom or Meet screen-share needs. The deck is promoted to the top layer (popover API + native fullscreen on the wrapper), which is what actually escapes the plan card's glass backdrop-filter: a merely-fixed overlay would be trapped at the card's size and stacking level. Arrows, space, page keys, Backspace, Home/End, and clicks navigate; Escape or leaving fullscreen ends the show; #present-N in the URL resumes; a collaborator's edit landing mid-show swaps the deck under the presenter without ending it (the wrapper sits outside the live-update swap target, and a childList observer re-applies the show to the fresh deck). The toolbar lives outside the text-selection content target so its label never enters the comment-anchor text model, and the show tears down on turbo:before-cache so a cached snapshot can't restore a closed-popover (display: none) deck. The directory pattern: one list of at least fifteen short entries is an inventory, not an argument — it flows into two balanced columns and bills at half for the type scale, so forty projects land two columns at a readable step instead of one column running off the canvas. Comment-only blocks inside an entry don't break its shape (comments never influence layout — an adversarial-review catch, pinned as a conformance fixture). Stage slides give the visual the whole canvas: thinner padding, taller media, and mermaid's inline natural-size max-width overridden so small diagrams scale up instead of rendering as thumbnails. RENDER_CACHE_VERSION 11 (new pattern class changes cached deck HTML). Spec now carries 31 conformance fixtures; suite green; presenter flow verified live against the real glass-card ancestor chain. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
🤖 @codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 80964b5d8a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // Links, checkboxes, and buttons on a slide still work; everything | ||
| // else — canvas, letterbox, the page hidden behind the overlay — | ||
| // advances (and is swallowed so hidden controls can't be hit). | ||
| if (onCanvas && event.target.closest("a, button, input, textarea, summary, [contenteditable]")) return |
There was a problem hiding this comment.
Navigate to the slide containing an internal link target
When a presentation contains a same-document link whose target is on another slide, this exemption lets reference_preview_controller#follow update the hash and call scrollIntoView(), but the target remains inside a display: none non-current slide. The click therefore appears to do nothing and also replaces the #present-N resume hash; resolve the target's owning slide and show it before performing the fragment navigation.
AGENTS.md reference: AGENTS.md:L76-L80
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
🤖 Fixed in eb91fa2. The click handler now intercepts fragment links before the interactive pass-through: it resolves the target's owning slide and navigates the show there (_show keeps the hash as #present-N, then scrollIntoView({block: "nearest"}) brings the target into view within the slide). Fragment targets outside the deck — the footnote back matter, hidden behind the overlay — are swallowed as no-ops instead of clobbering the resume hash. Verified in a browser harness: a slide-1 link to a slide-5 heading shows slide 5 with hash #present-5; an outside-deck fragment link leaves index and hash untouched.
| case " ": | ||
| this._navigate(event, this.index + 1) |
There was a problem hiding this comment.
Preserve Space activation for focused slide controls
When a task checkbox or other Space-activated control has focus during present mode, this capture-phase branch calls _navigate, prevents the default activation, and advances the deck instead. The click handler deliberately preserves interactive slide controls, but the keydown path lacks the equivalent target guard, so keyboard users cannot toggle an interactive checkbox without leaving the slide.
AGENTS.md reference: AGENTS.md:L68-L71
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
🤖 Fixed in eb91fa2. The presenting keydown branch now mirrors the click pass-through: when the event target is a typing target (input/textarea/select/contenteditable), a button, or a summary, the key passes through untouched — so Space toggles the checkbox it sits on instead of advancing. Escape stays the exit everywhere, including with a control focused. Verified in a browser harness: Space on a focused checkbox is not default-prevented and the index doesn't move; Space with no control focused still advances; Escape with the checkbox focused still ends the show.
…keys Two Codex review findings on the presenter, both real: A same-document link mid-show pointed at a display: none slide — the browser's fragment jump showed nothing and clobbered the #present-N resume hash. The click handler now resolves the target's owning slide and navigates the show there; fragment targets outside the deck (the footnote back matter, hidden behind the overlay) are swallowed as no-ops instead of corrupting state. Space with a task checkbox focused advanced the deck and prevented the toggle. The keydown path now mirrors the click pass-through: a focused control (typing targets, buttons, summaries) owns its keys, with Escape remaining the exit everywhere. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Clicking Present left the button focused, and the old control guard let it soak every navigation key — arrows fell through to comment navigation and Escape ended the show while visibly aimed at a popover. Focus now moves into the deck at start, and a focused control claims only the keys it actually responds to: text entry owns everything wherever it lives, checkboxes own Space and Enter, buttons qualify only on the canvas. Arrows and paging always drive the show, and unclaimed keys stop propagating so the hidden page's shortcuts (comment j/k, section jumps) can't fire underneath the presentation. Presenting also hides the review layer: comment marks on slides lose their tint, underline, and hit-testing for the duration, mouseup is starved so text-selection can't offer its affordance over the deck, and a thread popover left open on the page is dismissed at start. All of it comes back on exit. Exit seams, from an adversarial review round: - Escape that peels a popover under native fullscreen no longer ends the show — the browser's fullscreen exit on that keypress is uncancelable, so it is forgiven once and the presentation continues on the top-layer fallback. - Clicks inside an open foreign popover (a reference preview, a pinned thread) belong to the popover instead of advancing the deck. - A fast Escape during the fullscreen grant hands the screen back instead of leaving fullscreen orphaned on a stopped show. - Teardown restores focus and page scroll even when a live update swapped the deck away mid-show. - A pre-existing URL fragment (a heading deep link) is restored on exit instead of being stripped along with the #present-N hash. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
🤖 @codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8478e6b5d1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } | ||
|
|
||
| _handleKeydown(event) { | ||
| if (event.metaKey || event.ctrlKey || event.altKey) return |
There was a problem hiding this comment.
Block modifier voice hotkeys during presentation
With the default Ctrl+Space voice hotkey—or the Alt option—this early return allows the page-level keydown@document->coplan--voice#keyDown handler to start recording while the deck is presenting. Because the voice control and status remain behind the fullscreen overlay, the presenter gets no visible recording feedback and releasing the key can submit an unintended comment; stop these application hotkeys during the show or surface their status within the presentation.
AGENTS.md reference: AGENTS.md:L81-L82
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
🤖 Fixed in 321afce. Mid-show, modifier chords are starved at the capture-phase document handler just like the plain-key shortcuts, so Ctrl+Space / held Alt never reach the voice controller while the deck is presenting. Two carve-outs: text entry above the show keeps its shortcuts (Cmd+Enter still submits a popover reply — that handler is element-level and would otherwise be starved too), and the dialog guard moved above the modifier branch so the mermaid lightbox keeps owning its keys. Browser chords (reload, find, copy) are unaffected since stopPropagation doesn't touch defaults. Verified headless: chord starved with deck focus, heard again outside the show, text-entry and dialog exemptions, bare Shift (voice's third hotkey) already starved by the default case.
The modifier early-return let Ctrl+Space (and a held Alt) bubble to the voice controller's document-level hotkeys, starting a recording with its status hidden behind the overlay — releasing the chord could then submit an unintended voice comment. Mid-show, modifier chords are now starved like every other page shortcut. Text entry above the show keeps its shortcuts (Cmd+Enter submits a reply), a modal dialog keeps its keys — the dialog guard moved above the modifier branch for that — and browser chords (reload, find, copy) ignore propagation entirely. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow-up to #189, which shipped the slideshow foundation (plan-type behavior, the deck split, per-slide rendering). This PR ships the layout engine and the way to actually give the talk: the deck design system and present mode. A deck stays a rendering convention over the plan's single markdown blob — nothing new is persisted, so versioning, diffs, and comment anchoring keep working unchanged. Design doc: CoPlan plan
01a01696-aad6-762b-ad66-7681bbadd98c(rev 6).The deck design system
docs/SLIDE_SPEC.mdis the product boundary, and its fencedconformanceexamples ARE the test suite:spec/services/slideshows/conformance_spec.rbexecutes every fixture in the document, so the spec cannot drift from the implementation, and a future implementation in another language proves itself against the same fixtures.Slideshows::Classify. A pure function assigning each slide a layout pattern (title,stage,code,quote,table,columns,directory,split,content) and one of four type-scale steps, from content shape alone — no layout syntax, no measurement, no AI in the layout path. Authors and agents move an image in the source to move it on the slide.deck.css+ three token themes (coplan,graphite,poster): 16:9 cards sized in container-query units so a slide is the same design at any width, per-pattern art direction (title canvas + accent bar, staged media, oversized pull quotes, side-by-side panes via grid with zero DOM reordering), and discrete type steps instead of shrink-to-fit. Themes are validated custom-property sets applied bydata-deck-theme; user CSS is never accepted. Slide chrome (numbers, kickers, quote marks) is CSS-generated, invisible to the visible-text counts comment anchoring relies on. Decks ignore the reader's color scheme — a theme is one design, not two — so code panels are pinned to the theme and mermaid diagrams render deck-themed.Markup decoration. After the deck/document parity passes, the renderer applies the spec's markup contract: the neutral
.deck-contentname and, on split slides, the.deck-media/.deck-bodypanes — skipped entirely on any classified-shape/DOM mismatch (sanitized-away raw HTML), so the stylesheet degrades to the content layout rather than wrap the wrong node.Present mode, the directory pattern, and full-canvas stage media
Present mode.
por the Present button runs the deck as a fullscreen 16:9 show — what a Zoom/Meet screen-share needs. The deck itself is promoted to the top layer (popover API, plus native fullscreen on the wrapper), which is the load-bearing choice: the plan card's glassbackdrop-filtermakes it the containing block forposition: fixed, so a merely-fixed overlay would render at card size inside the card's stacking context. Top-layer elements always position against the viewport. Arrows/space/page keys/Backspace/Home/End/clicks navigate (links and checkboxes on slides still work); Escape or leaving fullscreen ends the show;#present-Nresumes; the "N / total" counter is CSSattr()chrome. The presenter wrapper sits outside the live-update swap target and re-applies show state via a childList observer, so a collaborator's edit landing mid-show swaps slides under the presenter without ending it. The show tears down onturbo:before-cache(a cached snapshot would otherwise restore a closed-popover,display: nonedeck), and the Present toolbar lives outside the text-selection content target so its label never enters the comment-anchor text model.directorypattern. One list of ≥15 short entries (one paragraph, ≤60 chars, no images/breaks — comment-only blocks inside an entry don't count, comments never influence layout) is an inventory, not an argument: it flows into two balanced CSS columns and bills at ⌈units/2⌉ for the type scale, so a forty-project list lands as two readable columns instead of one column running off the canvas.Stage sizing. Stage slides now give the visual the whole canvas: thinner padding, media up to 44cqi, and mermaid's inline natural-size
max-widthoverridden so small diagrams scale up to the slide instead of rendering as thumbnails.RENDER_CACHE_VERSION→ 11.Notes for reviewers
Slideshows::services, own helper,deck-*class namespace, spec + stylesheets with no host-CSS reliance) so the layout engine can be extracted as a standalone package later.<!-->is a complete empty comment; an unclosed<!--swallows the rest of its block), and image alt text counts for nothing in the unit model — accessible descriptions never shrink type.display: none, toolbar text entering the comment-anchor occurrence model, and a nested speaker note flipping a directory slide to single-column.Still to come (separate PRs): deck review loop (slide rail, changed-slides, theme picker), speaker-notes drawer +
/presentdeep link, agent fit report.🤖 Generated with Claude Code