From 0bb0bfbcb8060c7c343d9b223364c5fc56fed5c3 Mon Sep 17 00:00:00 2001 From: Graham Barber Date: Sun, 17 May 2026 10:06:49 -0700 Subject: [PATCH] feat(core): full-bleed image/code/diagram layouts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A new family of layouts for slides whose entire purpose is a single media block. Structurally detected: a slide with no heading, exactly one media element (image, code fence, or mermaid block), and an optional trailing blockquote becomes image-full / code-full / diagram-full. The focus layouts still trigger when a heading is present, so authors pick the variant by what they write, not by a DSL. - core/layout.ts: detect the no-heading branch off the existing focus-layout path and route to the new layout names. - wc/slide.ts: drop section padding to zero for these variants and set position: relative so the figure can size against the section. - templates/partials/slide-styles.eta: figure becomes a flex column (media flex: 1, caption flex: 0); media types each fill their box with sensible padding (xl for code, md for diagrams, full cover for images). - Caption styled as typographic marginalia per DESIGN.md: no card, no backdrop blur, no border. Recursive slnt axis at -8° + CASL 0.4, Muted reading color, centered under the media. The earlier frosted-glass treatment was the SaaS-card cliché PRODUCT.md explicitly rejects. - examples/showcase.md: add one demo of each variant at natural pauses in the existing field-guide narrative. Co-Authored-By: Claude Opus 4.7 --- examples/showcase.md | 39 ++++++++ packages/core/layout.ts | 16 +++ .../core/templates/partials/slide-styles.eta | 99 +++++++++++++++++++ packages/wc/components/slide.ts | 19 ++++ 4 files changed, 173 insertions(+) diff --git a/examples/showcase.md b/examples/showcase.md index a0b0f7d..e12f7e9 100644 --- a/examples/showcase.md +++ b/examples/showcase.md @@ -39,6 +39,12 @@ Time check: should be at 04:00 by end of this slide. --- +![Sunlight filtering through a stand of moss-wrapped trees, the trunks furred in green from the forest floor up.](https://images.unsplash.com/photo-1503435824048-a799a3a84bf7?w=1920&q=80) + +> A wet thing, an alive thing, an old thing. + +--- + ## Part two: how to look --- @@ -75,6 +81,23 @@ flowchart LR --- +```mermaid +flowchart TB + L["Land plants"] + L --> B["Bryophytes
~20,000 species"] + L --> V["Vascular plants
~300,000 species"] + B --> M["Mosses"] + B --> H["Hornworts"] + B --> LW["Liverworts"] + M --> M1["Sphagnopsida
peat mosses"] + M --> M2["Polytrichopsida
hair-caps"] + M --> M3["Bryopsida
~95% of all moss species"] +``` + +> Three classes of moss inside a much larger family. + +--- + ## Two genera to know :::columns @@ -144,6 +167,22 @@ A short list of reliable spots: --- +```python +# How long to undo a drained peatland? +# Rough field rule: about 250 years per centimeter of +# restored peat depth, under ideal hydrology. +def peat_recovery_years(centimeters_lost: float) -> int: + base_rate = 250 + drought_penalty = climate.drought_index * 1.4 + return int(centimeters_lost * base_rate * drought_penalty) + +print(peat_recovery_years(45)) # ~16,000 years +``` + +> What we drain in a season, we can't restore in a lifetime. + +--- + ## Where to look next - Crum, *Mosses of the Great Lakes Forest* diff --git a/packages/core/layout.ts b/packages/core/layout.ts index 3b5e642..827f97a 100644 --- a/packages/core/layout.ts +++ b/packages/core/layout.ts @@ -5,8 +5,11 @@ export type Layout = | "content" | "section" | "code-focus" + | "code-full" | "diagram-focus" + | "diagram-full" | "image-focus" + | "image-full" | "two-column"; const HEADINGS = new Set(["h1", "h2", "h3", "h4", "h5", "h6"]); @@ -132,14 +135,27 @@ export function detectLayout( // After stripping headings + caption, focus layouts require exactly one // dominant media block. Anything else is regular content flow. + // + // If the slide also has *no* heading, we upgrade to the full-bleed + // variant — the media fills the slide edge to edge with no chrome, + // and the optional blockquote becomes an overlay caption instead of + // sitting under the figure. if (mid.length === 1) { const only = mid[0]; + const noHeading = heads.length === 0; if (isMediaBlock(only)) { + if (noHeading) { + const layout: Layout = isMermaid(only) ? "diagram-full" : "code-full"; + return { layout, children: [figureFor(only, caption)] }; + } const layout: Layout = isMermaid(only) ? "diagram-focus" : "code-focus"; return { layout, children: [...heads, figureFor(only, caption)] }; } if (isImageOnlyParagraph(only)) { const img = childrenOf(only as ComarkElement)[0] as ComarkElement; + if (noHeading) { + return { layout: "image-full", children: [figureFor(img, caption)] }; + } return { layout: "image-focus", children: [...heads, figureFor(img, caption)] }; } if (isElement(only) && only[0] === "columns") { diff --git a/packages/core/templates/partials/slide-styles.eta b/packages/core/templates/partials/slide-styles.eta index 27d1513..489254e 100644 --- a/packages/core/templates/partials/slide-styles.eta +++ b/packages/core/templates/partials/slide-styles.eta @@ -315,6 +315,105 @@ align-self: center; } + /* ---- Full-bleed layouts (image-full / code-full / diagram-full) ---- + * Triggered when a slide has no heading and exactly one media block — + * authored simply as a slide containing nothing but the image / code + * fence / mermaid block (plus an optional trailing blockquote that + * becomes the caption beneath it). + * + * The figure is a flex column: media grows to fill, caption sits + * underneath at its intrinsic height. No card, no chrome — DESIGN.md + * is explicit that captions are typographic marginalia (Recursive's + * slnt axis at -8°, Muted reading color), not the frosted-glass + * cliché. The base-color section behind the caption gives a single + * uniform reading surface for all three media types. + */ + morkdeck-slide[data-layout$="-full"] figure { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + margin: 0; + padding: 0; + gap: 0; + flex: none; + min-height: 0; + min-width: 0; + max-width: none; + max-height: none; + } + + morkdeck-slide[data-layout$="-full"] figure > :is(img, pre, .mermaid) { + flex: 1 1 auto; + min-height: 0; + width: 100%; + margin: 0; + } + + /* Image: cover the slide. Some cropping of off-axis content is the + price of the full-bleed look; users who want letterboxing instead + should use the image-focus layout (heading present). */ + morkdeck-slide[data-layout="image-full"] figure > img { + object-fit: cover; + display: block; + height: 100%; + } + + /* Code: pre fills the available height with generous padding so the + code itself doesn't kiss the slide edges. Background matches the + section base for visual continuity; long files scroll within the + pre rather than blowing up the layout. */ + morkdeck-slide[data-layout="code-full"] figure > pre { + padding: var(--morkdeck-dimension-space-xl); + border-radius: 0; + background: var(--morkdeck-color-base); + overflow: auto; + font-size: var(--morkdeck-dimension-size-code); + line-height: var(--morkdeck-number-leading-relaxed); + } + + /* Diagram: SVG fills the available space with a small margin so + labels at the viewBox edges aren't clipped by the slide letterbox. */ + morkdeck-slide[data-layout="diagram-full"] figure > .mermaid { + display: flex; + align-items: center; + justify-content: center; + padding: var(--morkdeck-dimension-space-md); + background: transparent; + } + + morkdeck-slide[data-layout="diagram-full"] figure > .mermaid > svg { + width: 100%; + height: 100%; + max-width: 100% !important; + max-height: 100%; + } + + /* Caption: bare typographic marginalia. No background, no border, + no blur, no card — the Recursive slnt axis carries the affect, the + Muted color carries the quietness. Mirrors the blockquote rule in + DESIGN.md (slnt -8°, typographic indent). */ + morkdeck-slide[data-layout$="-full"] figcaption { + flex: 0 0 auto; + margin: 0; + padding: + var(--morkdeck-dimension-space-sm) + var(--morkdeck-dimension-space-lg) + var(--morkdeck-dimension-space-md); + background: transparent; + border: none; + border-radius: 0; + color: var(--morkdeck-color-muted); + font-size: var(--morkdeck-dimension-size-caption); + font-weight: 400; + font-variation-settings: "slnt" -8, "CASL" 0.4; + letter-spacing: var(--morkdeck-dimension-tracking-wide); + line-height: var(--morkdeck-number-leading-normal); + text-align: center; + max-width: none; + align-self: stretch; + } + /* ---- Columns -------------------------------------------------------- * Two even columns with a hairline divider centered in the gap. The * divider stretches with the columns so the line reads as a rule, not diff --git a/packages/wc/components/slide.ts b/packages/wc/components/slide.ts index 1027afb..680fb88 100644 --- a/packages/wc/components/slide.ts +++ b/packages/wc/components/slide.ts @@ -112,6 +112,25 @@ export class Slide extends MorkdeckElement { content: none; } + /* Full-bleed variants: no padding, the media reaches every edge of + the 16:9 letterbox. position: relative anchors the optional + caption overlay defined in slide-styles.eta. The slide-number + marginalia is suppressed — there's no breathing room around the + media to host it. */ + :host([data-layout="image-full"]) > section, + :host([data-layout="code-full"]) > section, + :host([data-layout="diagram-full"]) > section { + padding: 0; + justify-content: stretch; + align-items: stretch; + position: relative; + } + :host([data-layout="image-full"]) > section::after, + :host([data-layout="code-full"]) > section::after, + :host([data-layout="diagram-full"]) > section::after { + content: none; + } + /* Two-column: stretch content across columns. */ :host([data-layout="two-column"]) > section { justify-content: center; -- 2.51.2