From 011657e72442d9be5c765cff4da5080ae865a1ff Mon Sep 17 00:00:00 2001 From: Graham Barber Date: Sun, 17 May 2026 12:25:09 -0700 Subject: [PATCH] feat(core): pronouns + handle + reworked title/outro composition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two improvements landed together because they share a CSS path: 1. Optional pronouns + handle in frontmatter, rendered as a quieter byline beneath the speaker name. Pronouns get Recursive's slnt/CASL axes (italic aside, the same gesture DESIGN.md uses for blockquotes); handle gets the MONO axis (technical identifier). Equal Muted color and equal size so neither piece dominates. 2. Title and outro now have layout-specific section grids and reduced padding (space.md vs the previous space.xl), giving the marquee and colophon room to hug the slide edges and the hero room to breathe. - Title: vertical stack (marquee / hero / colophon). Wordmark spans the full slide width with its natural two-line break; colophon is a horizontal row at the bottom — attribution stacked on the left, socials aligned to the bottom-right baseline of the last context line, hairline above. - Outro: two-column grid ("Thank you" left, colophon stack right, marquee spans the top). The short outro h1 doesn't need full slide width; the right column gets enough space to host attribution + context + socials comfortably with no hairline (the column gap is the separator). Light-DOM grid-area assignments live in slide-styles.eta and cascade into the shadow-DOM grid via the slot's display: contents. data-has-colophon on the host triggers the grid template; without frontmatter the bare title/outro composition is unchanged. 3. Marquee spacing scrubbed of ch units (gap: 2cqi, padding: 1cqmin 0) so it scales strictly with the slide. examples/showcase.md adds pronouns + handle to the frontmatter so the demo deck exercises every field. Co-Authored-By: Claude Opus 4.7 --- examples/showcase.md | 2 + packages/core/renderer.ts | 43 ++++++- .../core/templates/partials/slide-styles.eta | 109 ++++++++++++++---- packages/wc/components/slide.ts | 43 +++++-- 4 files changed, 163 insertions(+), 34 deletions(-) diff --git a/examples/showcase.md b/examples/showcase.md index f9a145d..295f02b 100644 --- a/examples/showcase.md +++ b/examples/showcase.md @@ -3,6 +3,8 @@ event: Bryophyta Society Spring Meeting date: May 2026 location: Portland, Oregon speaker: Graham Barber +pronouns: he/him +handle: "@graham.systems" links: github: gambarber bluesky: graham.bsky.social diff --git a/packages/core/renderer.ts b/packages/core/renderer.ts index 6e9f6a3..90ecf66 100644 --- a/packages/core/renderer.ts +++ b/packages/core/renderer.ts @@ -49,6 +49,15 @@ interface DeckMeta { date?: string; location?: string; speaker?: string; + /** Optional pronouns rendered inline after the speaker name. */ + pronouns?: string; + /** + * Optional handle of the speaker's choice, rendered as mono text + * after the pronouns (e.g. a Bluesky `@graham.systems` or any + * other primary identifier). Display only — to make it linkable, + * also include it under `links`. + */ + handle?: string; links?: Record; } @@ -195,9 +204,37 @@ function renderColophon(meta: DeckMeta): string { let attributionHtml = ""; if (hasAttribution) { - const speakerHtml = meta.speaker - ? `

${escapeHtml(meta.speaker)}

` - : ""; + let speakerHtml = ""; + if (meta.speaker) { + // The name carries its own line, weight 700 + Text color, so + // it reads as the primary identity moment uncrowded. + // Pronouns + handle ride a quieter "byline" line beneath it, + // sharing typographic weight so neither dominates: pronouns + // get Recursive's slnt/CASL axes (italic aside, the gesture + // DESIGN.md uses for blockquotes), handle gets MONO (technical + // identifier). The byline only renders if at least one of + // them is present. + speakerHtml = + `

${escapeHtml(meta.speaker)}

`; + const bylinePieces: string[] = []; + if (meta.pronouns) { + bylinePieces.push( + `${escapeHtml(meta.pronouns)}`, + ); + } + if (meta.handle) { + bylinePieces.push( + `${escapeHtml(meta.handle)}`, + ); + } + if (bylinePieces.length > 0) { + speakerHtml += ``; + } + } const contextHtml = contextParts.length === 0 ? "" : `

${ diff --git a/packages/core/templates/partials/slide-styles.eta b/packages/core/templates/partials/slide-styles.eta index 3effc7d..8dc9782 100644 --- a/packages/core/templates/partials/slide-styles.eta +++ b/packages/core/templates/partials/slide-styles.eta @@ -618,22 +618,40 @@ min-height: 0; } + /* ---- Grid placement of the three blocks ---- + * The shadow-DOM section (slide.ts) sets up a two-column / two-row + * grid template-areas for title/outro slides. Light-DOM children + * (.marquee, .hero-body, .colophon) are slotted in and need to be + * assigned to their grid areas from the document stylesheet — the + * grid container is in the shadow DOM but slotted children + * participate in its layout via display: contents on the slot. */ + morkdeck-slide[data-has-colophon] > .marquee { + grid-area: marquee; + } + morkdeck-slide[data-has-colophon] > .hero-body { + grid-area: hero; + } + morkdeck-slide[data-has-colophon] > .colophon { + grid-area: colophon; + } + /* Marquee. Theatre-marquee tone: mono caps, label tracking, Muted - color, hairline rules above and below. The track is rendered twice - by the renderer so the keyframe shifts -50% for a seamless loop. - Reduced-motion users see a static first pass. */ + color, hairline rules above and below. The track is rendered + twice by the renderer so the keyframe shifts -50% for a + seamless loop. Reduced-motion users see a static first pass. + All metrics in cqi/cqmin so the strip scales with the slide. */ morkdeck-slide .marquee { width: 100%; overflow: hidden; border-top: 1px solid var(--morkdeck-color-highlight-med); border-bottom: 1px solid var(--morkdeck-color-highlight-med); - padding: 1.4cqi 0; + padding: 1cqmin 0; } morkdeck-slide .marquee-track { display: inline-flex; align-items: center; - gap: 1ch; + gap: 2cqi; white-space: nowrap; will-change: transform; animation: morkdeck-marquee-scroll 48s linear infinite; @@ -651,7 +669,6 @@ } morkdeck-slide .marquee-sep { - padding: 0 1ch; color: var(--morkdeck-color-highlight-high); } @@ -666,48 +683,96 @@ } } - /* Colophon row. Two-line attribution on the left (speaker on top, - dot-separated context line beneath) and the socials list on the - right. Baseline-aligned, hairline above, no card chrome. The - whole block hides if the renderer didn't have anything to put - in it. */ - morkdeck-slide .colophon { + /* The .colophon-attribution wrapper holds speaker name, byline, + and event context — a single stacked block of text. The + outer .colophon then arranges this text block against the + socials icons depending on layout (row for title, column for + outro). All metrics in cqi/cqmin so the rhythm scales with + the slide. */ + morkdeck-slide .colophon-attribution { + display: flex; + flex-direction: column; + gap: 0.4em; + min-width: 0; + } + + /* Title colophon: horizontal row spanning the slide. Attribution + block on the left, socials on the right, hairline above to + separate from the hero content. flex-end alignment puts the + icons on the same baseline as the bottom (context) line of + the attribution block so the row reads as one editorial + marginalia. */ + morkdeck-slide[data-layout="title"] .colophon { display: flex; + flex-direction: row; align-items: flex-end; justify-content: space-between; - gap: var(--morkdeck-dimension-space-md); - padding-top: var(--morkdeck-dimension-space-sm); + gap: var(--morkdeck-dimension-space-lg); + padding-top: 1.5cqmin; border-top: 1px solid var(--morkdeck-color-highlight-med); width: 100%; min-width: 0; } - morkdeck-slide .colophon-attribution { + /* Outro colophon: vertical stack in the right grid column, + anchored to the bottom so the icons bottom-align with the + wordmark in the left column. No hairline — the grid's column + gap is the only separator the composition needs. */ + morkdeck-slide[data-layout="outro"] .colophon { display: flex; flex-direction: column; - gap: 0.4em; + align-items: stretch; + justify-content: flex-end; + gap: 3cqmin; min-width: 0; - flex: 1 1 auto; + min-height: 0; } + /* Speaker name owns its line so the bolder weight and full Text + color can carry the identity moment without the secondaries + crowding into it. */ morkdeck-slide .colophon-speaker { margin: 0; - font-size: 2.2cqi; - font-weight: var(--morkdeck-fontWeight-medium); + font-size: 2.4cqi; + font-weight: var(--morkdeck-fontWeight-bold); color: var(--morkdeck-color-text); line-height: 1.1; + letter-spacing: -0.005em; + } + + /* Byline: a quieter sub-line for pronouns + handle. Sits directly + under the name with a thin breathing gap, sized down so it + reads as supporting marginalia, not a second title. Pronouns + and handle share size + weight + color so they're peers; their + only typographic differentiator is the axis that carries + meaning (slnt/CASL for italic aside, MONO for identifier). */ + morkdeck-slide .colophon-byline { + margin: 0; + font-size: 1.7cqi; + font-weight: var(--morkdeck-fontWeight-regular); + color: var(--morkdeck-color-muted); + line-height: 1.3; + } + + morkdeck-slide .colophon-pronouns { + font-variation-settings: "slnt" -8, "CASL" 0.4; + } + + morkdeck-slide .colophon-handle { + font-family: var(--morkdeck-fontFamily-mono); + font-variation-settings: "MONO" 1; } morkdeck-slide .colophon-context { margin: 0; font-family: var(--morkdeck-fontFamily-mono); font-variation-settings: "MONO" 1; - font-size: 1.4cqi; + font-size: 1.5cqi; font-weight: var(--morkdeck-fontWeight-regular); - letter-spacing: 0.12em; + letter-spacing: 0.1em; text-transform: uppercase; color: var(--morkdeck-color-muted); - line-height: 1.4; + line-height: 1.5; } morkdeck-slide .colophon-sep { diff --git a/packages/wc/components/slide.ts b/packages/wc/components/slide.ts index 7631766..44aecfd 100644 --- a/packages/wc/components/slide.ts +++ b/packages/wc/components/slide.ts @@ -75,24 +75,49 @@ export class Slide extends MorkdeckElement { } /* With colophon chrome present (renderer set data-has-colophon - on the host), the section becomes a three-row grid: marquee - strip on top, the wordmark in the middle row (bottom-left - within it), and the colophon row anchored to the bottom. The - middle row stretches to absorb leftover space so the - composition holds regardless of how much frontmatter the - author provided. + on the host), the section reduces its default xl padding so + the marquee and colophon hug the slide edges and leave the + title / wordmark more room to breathe. Title and outro + diverge on grid template: + + - Title: vertical stack (marquee / hero / colophon). The + wordmark spans the full slide width — wrapping it into a + narrow column made it feel cramped. + - Outro: two columns ("Thank you" on the left, attribution + + socials stack on the right). The short outro h1 doesn't + need the full slide width, and the right column has room + to host the colophon comfortably. We trigger on a host attribute rather than :has(.hero-body) on the section, because .hero-body is slotted from light DOM and isn't a tree descendant of the shadow-DOM section, so :has() can't see it from inside the shadow root. */ - :host([data-layout="title"][data-has-colophon]) > section, - :host([data-layout="outro"][data-has-colophon]) > section { + :host([data-layout="title"][data-has-colophon]) > section { + padding: ${dimension("space.md")}; display: grid; grid-template-rows: auto minmax(0, 1fr) auto; + grid-template-columns: minmax(0, 1fr); + grid-template-areas: + "marquee" + "hero" + "colophon"; + row-gap: ${dimension("space.md")}; + justify-content: stretch; + align-items: stretch; + } + + :host([data-layout="outro"][data-has-colophon]) > section { + padding: ${dimension("space.md")}; + display: grid; + grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); + grid-template-rows: auto minmax(0, 1fr); + grid-template-areas: + "marquee marquee" + "hero colophon"; + column-gap: ${dimension("space.xl")}; + row-gap: ${dimension("space.lg")}; justify-content: stretch; align-items: stretch; - gap: ${dimension("space.md")}; } /* Section divider: center the heading both axes, generous breathing room. -- 2.51.2