Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/SLIDE_SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,8 @@ Every theme defines all of:
| `--deck-title-bg` | title-slide canvas (lets a theme art-direct openers) |
| `--deck-title-ink` | text on the title canvas |
| `--deck-title-accent` | accent on the title canvas (a theme whose title canvas is the accent color must pick a visible one) |
| `--deck-pen` | the presenter's ink: strokes drawn over a slide during a show, and never part of the artifact. Pick something the theme's own accent can't be mistaken for |
| `--deck-title-pen` | ink on the title canvas — optional, falls back to `--deck-pen`. Required only of a theme whose title canvas would swallow it |
| `--deck-font-display` | headings |
| `--deck-font-text` | body |
| `--deck-font-mono` | code |
Expand Down
4 changes: 3 additions & 1 deletion engine/app/assets/stylesheets/coplan/application.css
Original file line number Diff line number Diff line change
Expand Up @@ -1874,7 +1874,9 @@ img.avatar {
color: var(--color-text);
}

.markdown-rendered .mermaid-diagram svg {
/* The diagram's own SVG only — the expand button's icon is an svg inside
the same container, and it must keep its own 16px geometry. */
.markdown-rendered .mermaid-diagram > svg {
max-width: 100%;
height: auto;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
--deck-title-bg: #f6f8fc;
--deck-title-ink: #16181f;
--deck-title-accent: #2563eb;
--deck-pen: #e11d48;
--deck-font-display: "Lexend", system-ui, sans-serif;
--deck-font-text: "Lexend", system-ui, sans-serif;
--deck-font-mono: "Hack", ui-monospace, "SF Mono", Menlo, monospace;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
--deck-title-bg: #101218;
--deck-title-ink: #f2f4f9;
--deck-title-accent: #6ea8fe;
--deck-pen: #ffc857;
--deck-font-display: "Lexend", system-ui, sans-serif;
--deck-font-text: "Lexend", system-ui, sans-serif;
--deck-font-mono: "Hack", ui-monospace, "SF Mono", Menlo, monospace;
Expand Down
4 changes: 4 additions & 0 deletions engine/app/assets/stylesheets/coplan/deck-theme-poster.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
--deck-title-bg: #d94f30;
--deck-title-ink: #fff6ee;
--deck-title-accent: #fff6ee;
/* Teal against vermilion — the pen must not read as the accent. The
title canvas IS the accent, so the pen there is the paper color. */
--deck-pen: #0e6f68;
--deck-title-pen: #fff6ee;
--deck-font-display: "Lexend", system-ui, sans-serif;
--deck-font-text: "Lexend", system-ui, sans-serif;
--deck-font-mono: "Hack", ui-monospace, "SF Mono", Menlo, monospace;
Expand Down
147 changes: 145 additions & 2 deletions engine/app/assets/stylesheets/coplan/deck.css
Original file line number Diff line number Diff line change
Expand Up @@ -193,12 +193,34 @@
height: 0.85em;
}

/* On a slide the diagram IS the content, so it loses the document's framed
panel. That framing is painted from the host's --color-* tokens, which
flip with the reader's light/dark mode — the same leak the code panel
above pins shut — and it put an app-dark box behind a light deck's
diagram. The expand chip is deck chrome for the same reason. */
.deck-slide .deck-content .mermaid-diagram {
display: flex;
justify-content: center;
margin: 0.8em 0;
padding: 0;
background: none;
border: none;
}

.deck-slide .deck-content .mermaid-diagram svg {
.deck-slide .deck-content .mermaid-diagram__expand {
background: var(--deck-bg);
border-color: var(--deck-rule);
color: var(--deck-muted);
}

.deck-slide--title .deck-content .mermaid-diagram__expand {
background: var(--deck-title-bg);
}

/* The diagram's own SVG only — a .mermaid-diagram also holds the expand
button, whose icon is an svg too, and sizing that to the canvas puts a
full-slide glyph over the diagram. */
.deck-slide .deck-content .mermaid-diagram > svg {
max-width: 100%;
max-height: 34cqi;
}
Expand Down Expand Up @@ -293,7 +315,7 @@
width: 100%;
}

.deck-slide--stage .deck-content .mermaid-diagram svg {
.deck-slide--stage .deck-content .mermaid-diagram > svg {
width: 100%;
height: 44cqi;
max-width: none !important;
Expand Down Expand Up @@ -498,6 +520,127 @@
pointer-events: none;
}

/* Selecting text mid-show is the presenter's marker: drag across a line to
put the room's eyes on it (the presenter swallows the drag so it doesn't
also page forward, and starves the comment affordance). Inverse video in
the deck's own accent, not the OS selection tint — this has to read from
the back of a room, on a projector, in every theme. Title slides invert
against their own canvas instead: poster's title background IS its
accent, so an accent block would vanish there. */
.deck--presenting .deck-slide::selection,
.deck--presenting .deck-slide ::selection {
background: var(--deck-accent);
color: var(--deck-accent-ink);
}

.deck--presenting .deck-slide--title::selection,
.deck--presenting .deck-slide--title ::selection {
background: var(--deck-title-accent, var(--deck-accent));
color: var(--deck-title-bg);
}

/* ---- the pen — ephemeral ink over the canvas --------------------------
`d` arms the pen mid-show (DeckInk): a drag paints instead of selecting.
Strokes are a gesture in the room, so each one fades on its own timer and
nothing is stored. The layer is a child of .deck because the presented
deck is a top-layer popover — a sibling on the page would paint
underneath it — and it is sized to the canvas rather than to a slide, so
the mark stays put while the pointer roams. */

.deck-ink {
position: absolute;
inset: 0;
z-index: 2;
/* Ink never eats the click: navigation, links, and checkboxes keep
working under it. Strokes come from listeners on the document. */
pointer-events: none;
}

.deck-ink__stroke {
fill: none;
/* Themes registered before the pen existed still get a visible mark. */
stroke: var(--deck-pen, var(--deck-accent));
stroke-linecap: round;
stroke-linejoin: round;
/* Lifts the stroke off a canvas close to its own hue without a second
path underneath it. */
filter: drop-shadow(0 0.15cqi 0.3cqi rgb(0 0 0 / 0.35));
}

/* A theme may art-direct its title canvas into the pen's own color
(poster's title slide IS the accent), so the pen gets the same
title-slide escape hatch the accent does. :has() rather than
inheritance: the layer belongs to the canvas, not to a slide. */
.deck--presenting:has(.deck-slide--current.deck-slide--title) .deck-ink__stroke {
stroke: var(--deck-title-pen, var(--deck-pen, var(--deck-accent)));
}

/* Held at full strength long enough to make the point, then gone. The
stroke gets this only on release — a slow deliberate line must not start
fading while it is still being drawn. */
.deck-ink__stroke--done {
animation: deck-ink-fade 2.6s ease-in forwards;
}

@keyframes deck-ink-fade {
0%, 45% { opacity: 1; }
100% { opacity: 0; }
}

/* With the pen out the canvas is a drawing surface: no text selection to
fight the stroke, no touch scrolling to swallow it, and a crosshair so
the presenter can see the mode. */
.deck--inking,
.deck--inking .deck-slide {
cursor: crosshair;
user-select: none;
touch-action: none;
}

/* Pen-out indicator, mirroring the slide number's inset on the other
corner. The key hint is pseudo-element content: real text here would be
counted as slide text by comment anchoring. */
.deck-ink-badge {
position: absolute;
left: 1.6cqi;
bottom: 1.1cqi;
z-index: 3;
display: flex;
align-items: center;
gap: 0.6cqi;
padding: 0.4cqi 0.9cqi;
border: 1px solid var(--deck-rule);
border-radius: 999px;
background: var(--deck-bg);
color: var(--deck-pen, var(--deck-accent));
font-family: var(--deck-font-text);
font-size: 1.5cqi;
line-height: 1;
}

.deck-ink-badge svg {
width: 1.8cqi;
height: 1.8cqi;
}

.deck-ink-badge::after {
content: "d";
color: var(--deck-muted);
}

/* The badge's shell follows the canvas it sits on, and only the glyph is
the pen. A theme's --deck-muted is a color for its body canvas: on an
art-directed title canvas the hint has to come from that canvas's own
ink instead, or it disappears into the background. */
.deck--presenting:has(.deck-slide--current.deck-slide--title) .deck-ink-badge {
background: var(--deck-title-bg);
color: var(--deck-title-pen, var(--deck-pen, var(--deck-accent)));
}

.deck--presenting:has(.deck-slide--current.deck-slide--title) .deck-ink-badge::after {
color: var(--deck-title-ink);
}

/* Fullscreen is requested on the presenter wrapper (it survives
live-update swaps of the deck inside it); paint it black so nothing of
the page shows around the canvas. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,16 @@ export default class extends Controller {
}

buildToc() {
const rendered = this.contentTarget.querySelector(".markdown-rendered")
if (!rendered) return
if (!this.contentTarget.querySelector(".markdown-rendered")) return

this.listTarget.innerHTML = ""
this._itemsById = new Map()
this._headings = Array.from(rendered.querySelectorAll("h1, h2, h3"))
// A document renders one .markdown-rendered; a deck renders one per
// slide. Scan the whole content column either way — scanning the first
// rendered block gave a deck an outline of slide 1 and nothing else.
this._headings = Array.from(
this.contentTarget.querySelectorAll(".markdown-rendered h1, .markdown-rendered h2, .markdown-rendered h3")
)

if (this._headings.length === 0) {
this.sidebarTarget.style.display = "none"
Expand Down Expand Up @@ -143,6 +147,24 @@ export default class extends Controller {
heading.scrollIntoView({ behavior: "smooth", block: "start" })
}

// The back-matter links (References, Attachments) jump the same way the
// outline above does. Turbo counts a same-page fragment link as a full
// visit — it refetches and re-renders the page — so the bare anchor read
// as a reload, and the scroll it performed landed against a layout that
// async rendering (Mermaid, highlighting, deck slides) had not settled
// yet. Scrolling in place needs neither.
jumpToSection(event) {
const id = event.currentTarget.getAttribute("href")?.replace(/^#/, "")
const target = id && document.getElementById(id)
if (!target) return

event.preventDefault()
history.replaceState(null, "", `#${id}`)

this._ignoreScroll = true
target.scrollIntoView({ behavior: "smooth", block: "start" })
}

_setActiveLink(id) {
this.listTarget.querySelectorAll(".content-nav__link").forEach(link => {
link.classList.remove("content-nav__link--active")
Expand Down Expand Up @@ -194,12 +216,14 @@ export default class extends Controller {
updateCommentBadges() {
if (!this._headings || this._headings.length === 0) return

const rendered = this.contentTarget.querySelector(".markdown-rendered")
if (!rendered) return
if (!this.contentTarget.querySelector(".markdown-rendered")) return

// Walk the whole content column: on a deck, consecutive outline
// headings live in different slides, so the span between two of them
// crosses .markdown-rendered blocks.
this._headings.forEach((heading, index) => {
const nextHeading = this._headings[index + 1]
const threads = this.collectThreadsBetween(heading, nextHeading, rendered)
const threads = this.collectThreadsBetween(heading, nextHeading, this.contentTarget)

let pendingCount = 0
let todoCount = 0
Expand Down
Loading
Loading