Skip to content

Commit 80964b5

Browse files
HamptonMakesclaude
andcommitted
Present mode, the directory pattern, and stage media that fills the canvas
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>
1 parent 3881780 commit 80964b5

8 files changed

Lines changed: 559 additions & 11 deletions

File tree

docs/SLIDE_SPEC.md

Lines changed: 130 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ blocks. Classify the same string the renderer renders — in CoPlan that
7878
includes the hoisted definition preamble, so reference-style images
7979
(`![chart][q3]`) resolve to image nodes here exactly as they do on screen.
8080

81-
Four derived terms:
81+
Five derived terms:
8282

8383
- **lead heading** — the first block of the content sequence, if it is a
8484
heading (any level). The **body** is the sequence minus the lead heading.
@@ -96,6 +96,12 @@ Four derived terms:
9696
whitespace and comments, or a code block whose info string's **first
9797
word** is `mermaid` (the rest of the info string is renderer options,
9898
which is also how the rendered `lang` attribute treats it).
99+
- **short entry** — a list item holding exactly one paragraph, with no
100+
images and no hard line breaks, whose plain text is at most 60
101+
characters: one rendered line, an inventory row. An item carrying more
102+
structure than that — a nested list, a second paragraph, a long line —
103+
is an argument, not an entry. Comment-only HTML blocks inside the item
104+
don't count as structure (comments never influence layout).
99105

100106
## The pattern catalog
101107

@@ -111,15 +117,16 @@ body (the sequence after the lead heading, which any pattern may carry).
111117
| 5 | one blockquote ± one adjacent short paragraph | `quote` |
112118
| 6 | one table ± one adjacent short paragraph | `table` |
113119
| 7 | exactly two lists | `columns` |
114-
| 8 | exactly one media block, first or last, plus anything else | `split` |
115-
| 9 | anything else (including an empty sequence) | `content` |
120+
| 8 | one list of at least 15 short entries | `directory` |
121+
| 9 | exactly one media block, first or last, plus anything else | `split` |
122+
| 10 | anything else (including an empty sequence) | `content` |
116123

117124
"± one adjacent short paragraph" means the body is either the block alone,
118125
or the block plus one short paragraph immediately before or after it —
119126
a kicker line or a caption. Anything more is a `content` or `split` slide.
120127

121128
Rule 3 sits above rule 4, so a `mermaid` fence is always media, never code.
122-
Rule 8 requires at least one non-media block, so a lone image lands on
129+
Rule 9 requires at least one non-media block, so a lone image lands on
123130
`stage`, and requires exactly one media block, so two images fall through to
124131
`content` rather than guessing which one gets the pane.
125132

@@ -334,6 +341,122 @@ And a closing thought that keeps this a document section.
334341
pattern: content
335342
```
336343

344+
### `directory` — one long inventory flows into two columns
345+
346+
One list of at least fifteen short entries and nothing else. That shape is
347+
an inventory — every project in flight, the full roster, an API surface —
348+
not an argument, and a single column of it runs off the canvas while half
349+
the slide sits empty. The list flows into two balanced columns; an ordered
350+
list keeps counting down the first column and into the second.
351+
352+
Because the list renders in two columns it also bills at half for the
353+
type scale (see below) — the pattern doesn't just fit the inventory, it
354+
keeps the type readable while doing it:
355+
356+
```conformance
357+
## Every project in flight
358+
359+
- Atlas — payment routing
360+
- Beacon — status page
361+
- Cedar — ledger exports
362+
- Delta — dispute intake
363+
- Ember — fraud scoring
364+
- Flint — invoice search
365+
- Grove — seller onboarding
366+
- Harbor — webhook retries
367+
- Iris — receipt redesign
368+
- Juniper — tax engine
369+
- Keel — capacity planning
370+
- Lumen — audit trails
371+
- Maple — payout scheduling
372+
- Nectar — feedback tagging
373+
- Onyx — rate limiting
374+
- Pine — sandbox reset
375+
.
376+
pattern: directory
377+
step: 2
378+
```
379+
380+
Fourteen entries is a long content slide, not a directory — below the
381+
threshold the list stays one column and bills in full:
382+
383+
```conformance
384+
## Every project in flight
385+
386+
- Atlas — payment routing
387+
- Beacon — status page
388+
- Cedar — ledger exports
389+
- Delta — dispute intake
390+
- Ember — fraud scoring
391+
- Flint — invoice search
392+
- Grove — seller onboarding
393+
- Harbor — webhook retries
394+
- Iris — receipt redesign
395+
- Juniper — tax engine
396+
- Keel — capacity planning
397+
- Lumen — audit trails
398+
- Maple — payout scheduling
399+
- Nectar — feedback tagging
400+
.
401+
pattern: content
402+
step: 4
403+
```
404+
405+
A speaker note tucked inside an entry is still a comment — it neither
406+
breaks the entry's shape nor bills:
407+
408+
```conformance
409+
## Every project in flight
410+
411+
- Atlas — payment routing
412+
- Beacon — status page
413+
- Cedar — ledger exports
414+
- Delta — dispute intake
415+
416+
<!-- double-check the owner with Maya -->
417+
418+
- Ember — fraud scoring
419+
- Flint — invoice search
420+
- Grove — seller onboarding
421+
- Harbor — webhook retries
422+
- Iris — receipt redesign
423+
- Juniper — tax engine
424+
- Keel — capacity planning
425+
- Lumen — audit trails
426+
- Maple — payout scheduling
427+
- Nectar — feedback tagging
428+
- Onyx — rate limiting
429+
.
430+
pattern: directory
431+
step: 2
432+
```
433+
434+
Every item must be a short entry. One entry carrying real prose means the
435+
list is an argument, and arguments read top to bottom:
436+
437+
```conformance
438+
## Every project in flight
439+
440+
- Atlas — payment routing
441+
- Beacon — status page
442+
- Cedar — ledger exports
443+
- Delta — dispute intake
444+
- Ember — fraud scoring
445+
- Flint — invoice search
446+
- Grove — seller onboarding
447+
- Harbor — webhook retries
448+
- Iris — receipt redesign
449+
- Juniper — tax engine
450+
- Keel — capacity planning
451+
- Lumen — audit trails
452+
- Maple — payout scheduling
453+
- Nectar — feedback tagging
454+
- Meridian — the cross-region failover rehearsal program we keep deferring
455+
.
456+
pattern: content
457+
step: 4
458+
```
459+
337460
### `split` — media pane beside content
338461

339462
Exactly one media block at the body's edge, with real content beside it.
@@ -419,7 +542,9 @@ code renders at 0.8 em with 1.55 line-height — about ⅚ of a body line per
419542
code line, not half of one. A mermaid fence renders as a fit-to-box
420543
diagram, so it bills like media, not like its source line count.
421544

422-
The slide's units are the sum over its content sequence. The step:
545+
The slide's units are the sum over its content sequence, with one
546+
pattern-aware adjustment: a `directory` slide's list renders across two
547+
columns, so it bills at ⌈its units / 2⌉. The step:
423548

424549
| Units | Step |
425550
|---|---|

engine/app/assets/stylesheets/coplan/application.css

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2956,6 +2956,43 @@ img.avatar {
29562956
display: block;
29572957
}
29582958

2959+
/* The Present control above a slideshow's deck. The show itself is
2960+
deck.css territory (.deck--presenting); this is host chrome. */
2961+
.deck-toolbar {
2962+
display: flex;
2963+
justify-content: flex-end;
2964+
margin-bottom: var(--space-sm);
2965+
}
2966+
2967+
.deck-toolbar__present {
2968+
display: inline-flex;
2969+
align-items: center;
2970+
gap: 6px;
2971+
background: var(--color-surface);
2972+
border: 1px solid var(--color-border);
2973+
border-radius: var(--radius);
2974+
padding: var(--space-xs) var(--space-sm);
2975+
cursor: pointer;
2976+
color: var(--color-text-muted);
2977+
font-size: var(--text-sm);
2978+
transition: color 0.15s, box-shadow 0.15s;
2979+
}
2980+
2981+
.deck-toolbar__present:hover {
2982+
color: var(--color-text);
2983+
box-shadow: var(--shadow-pop);
2984+
}
2985+
2986+
.deck-toolbar__present kbd {
2987+
font-family: var(--font-mono);
2988+
font-size: 0.7rem;
2989+
background: var(--color-bg);
2990+
border: 1px solid var(--color-border);
2991+
border-radius: 3px;
2992+
padding: 0 4px;
2993+
color: var(--color-text-muted);
2994+
}
2995+
29592996
/* Add scroll-margin to headings — and the plan's own masthead header — so
29602997
they clear the sticky nav. The nav-title control scrolls to the very top
29612998
in JS; this keeps a no-JS #plan-header jump landing below the bar too. */

engine/app/assets/stylesheets/coplan/deck.css

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,12 +252,18 @@
252252

253253
/* ---- stage — one visual gets the whole canvas ------------------------ */
254254

255+
.deck-slide--stage {
256+
/* The visual is the slide; the frame is a thin margin, not a stage
257+
apron. */
258+
padding: 2.5cqi 3.5cqi;
259+
}
260+
255261
.deck-slide--stage .deck-content {
256262
display: flex;
257263
flex-direction: column;
258264
align-items: center;
259265
text-align: center;
260-
gap: 0.9em;
266+
gap: 0.7em;
261267
}
262268

263269
.deck-slide--stage .deck-content > h1,
@@ -277,7 +283,21 @@
277283
}
278284

279285
.deck-slide--stage .deck-content img {
280-
max-height: 36cqi;
286+
max-height: 44cqi;
287+
}
288+
289+
/* Mermaid inlines a natural-size max-width on its SVGs, which pins a
290+
small diagram at thumbnail size; the stage overrides it so the diagram
291+
scales up to the canvas (preserveAspectRatio keeps it undistorted). */
292+
.deck-slide--stage .deck-content .mermaid-diagram {
293+
width: 100%;
294+
}
295+
296+
.deck-slide--stage .deck-content .mermaid-diagram svg {
297+
width: 100%;
298+
height: 44cqi;
299+
max-width: none !important;
300+
max-height: none;
281301
}
282302

283303
.deck-slide--stage .deck-content > p:not(:has(img)) {
@@ -384,6 +404,23 @@
384404
margin: 0;
385405
}
386406

407+
/* ---- directory — one long inventory flows into two columns ------------
408+
The list is the slide's only body block, so CSS multi-column balances
409+
it; an ordered list keeps continuous numbering down-then-across. */
410+
411+
.deck-slide--directory .deck-content > ul,
412+
.deck-slide--directory .deck-content > ol {
413+
columns: 2;
414+
column-gap: 2.6em;
415+
margin: 0;
416+
}
417+
418+
.deck-slide--directory .deck-content li {
419+
break-inside: avoid;
420+
margin-bottom: 0.3em;
421+
line-height: 1.4;
422+
}
423+
387424
/* ---- split — media pane beside content --------------------------------
388425
DOM order is source order (media leading → left pane, trailing → right),
389426
so grid auto-placement does the layout with zero reordering. Guarded on
@@ -413,3 +450,60 @@
413450
max-height: 40cqi;
414451
object-fit: contain;
415452
}
453+
454+
/* ---- presenting — the deck takes the screen ---------------------------
455+
Present mode (coplan--deck-presenter) shows the deck as a top-layer
456+
popover sized to the largest 16:9 canvas that fits the viewport — the
457+
deck stays the size container, so every cqi measurement above scales to
458+
the screen with no presentation-specific typography. Top layer matters:
459+
fixed positioning alone resolves against any backdrop-filter ancestor
460+
(the host's glass card), while top-layer elements always position
461+
against the viewport. The spread shadow blacks out the letterbox bars
462+
without extra DOM; ::backdrop does the same when the top layer is in
463+
charge. The border/padding/overflow resets neutralize the UA's popover
464+
styles. */
465+
466+
.deck--presenting {
467+
position: fixed;
468+
inset: 0;
469+
margin: auto;
470+
/* For the no-popover-API fallback: above every piece of app chrome
471+
(the host tops out around 300). Irrelevant in the top layer. */
472+
z-index: 1000;
473+
width: min(100vw, calc(100vh * 16 / 9));
474+
height: min(100vh, calc(100vw * 9 / 16));
475+
gap: 0;
476+
border: none;
477+
padding: 0;
478+
overflow: hidden;
479+
background: #000;
480+
box-shadow: 0 0 0 100vmax #000;
481+
}
482+
483+
.deck--presenting::backdrop {
484+
background: #000;
485+
}
486+
487+
/* Fullscreen is requested on the presenter wrapper (it survives
488+
live-update swaps of the deck inside it); paint it black so nothing of
489+
the page shows around the canvas. */
490+
.deck-presenter:fullscreen {
491+
background: #000;
492+
}
493+
494+
.deck--presenting .deck-slide {
495+
display: none;
496+
}
497+
498+
.deck--presenting .deck-slide--current {
499+
display: flex;
500+
height: 100%;
501+
border: none;
502+
border-radius: 0;
503+
box-shadow: none;
504+
overscroll-behavior: contain;
505+
}
506+
507+
.deck--presenting .deck-slide--current::after {
508+
content: attr(data-slide) " / " attr(data-slide-total);
509+
}

engine/app/helpers/coplan/markdown_helper.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ module MarkdownHelper
3434
# version. Bump it whenever the rendering pipeline changes output for the
3535
# same input (new tags, attribute changes, checkbox wiring, etc.), or
3636
# stale HTML will be served from cache.
37-
RENDER_CACHE_VERSION = 10
37+
RENDER_CACHE_VERSION = 11
3838

3939
# Matches `[@username](mention:username)` where the bracket text and link
4040
# target encode the same username. Username allows letters, digits, dots,

0 commit comments

Comments
 (0)