Skip to content

Markdown slideshows: the deck design system and present mode - #193

Merged
HamptonMakes merged 6 commits into
mainfrom
claude/coplan-slideshows-deck-and-presenter
Aug 23, 2026
Merged

Markdown slideshows: the deck design system and present mode#193
HamptonMakes merged 6 commits into
mainfrom
claude/coplan-slideshows-deck-and-presenter

Conversation

@HamptonMakes

Copy link
Copy Markdown
Collaborator

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.md is the product boundary, and its fenced conformance examples ARE the test suite: spec/services/slideshows/conformance_spec.rb executes 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 by data-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-content name and, on split slides, the .deck-media/.deck-body panes — 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. p or 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 glass backdrop-filter makes it the containing block for position: 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-N resumes; the "N / total" counter is CSS attr() 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 on turbo:before-cache (a cached snapshot would otherwise restore a closed-popover, display: none deck), and the Present toolbar lives outside the text-selection content target so its label never enters the comment-anchor text model.

directory pattern. 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-width overridden so small diagrams scale up to the slide instead of rendering as thumbnails.

RENDER_CACHE_VERSION → 11.

Notes for reviewers

  • Deck-specific code is deliberately isolated (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.
  • Commonmarker quirk worth knowing: comment boundaries follow the HTML parser in both malformed directions (<!--> 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.
  • Verified with three adversarial multi-agent review rounds (spec-drift, deck/document parity, and step-sanity attackers with in-browser geometry measurement; then classifier/presenter/CSS lenses with two independent refuters per finding for present mode). All confirmed findings are fixed and pinned as conformance fixtures or specs — among them the backdrop-filter containing-block trap, the Turbo snapshot restoring a mid-show deck as display: none, toolbar text entering the comment-anchor occurrence model, and a nested speaker note flipping a directory slide to single-column.
  • These commits were originally built on the pre-Enforce RuboCop in CI, and clear the 895 offenses first #190 base and are cherry-picked onto current main; the seeds conflict with Browsable URLs: people at the root, one address per plan, live libraries #191's showcase entries is resolved by keeping both.

Still to come (separate PRs): deck review loop (slide rail, changed-slides, theme picker), speaker-notes drawer + /present deep link, agent fit report.

🤖 Generated with Claude Code

HamptonMakes and others added 3 commits August 23, 2026 09:37
…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>
@HamptonMakes

Copy link
Copy Markdown
Collaborator Author

🤖 @codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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.

Comment on lines +144 to +145
case " ":
this._navigate(event, this.index + 1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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.

HamptonMakes and others added 2 commits August 23, 2026 10:12
…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>
@HamptonMakes

Copy link
Copy Markdown
Collaborator Author

🤖 @codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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>
@HamptonMakes
HamptonMakes merged commit 058797d into main Aug 23, 2026
4 checks passed
@HamptonMakes
HamptonMakes deleted the claude/coplan-slideshows-deck-and-presenter branch August 23, 2026 18:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant