From 39c3d27eadf8379dfbebdbe08f4ed8c929d64961 Mon Sep 17 00:00:00 2001 From: Graham Barber Date: Mon, 18 May 2026 22:19:10 -0700 Subject: [PATCH] refactor(wc): redesign the presenter chrome end to end The previous chrome was a single flex strip with `grid-template-columns: 1fr auto 1fr`, which sorted items by horizontal position rather than by meaning. Four unlabeled time displays (elapsed, countdown, target, clock) read as an undifferentiated row of MM:SS digits. The slide counter wrapped to two lines. The exit button clipped at the right edge. Icons were a mix of Unicode glyphs and Tabler SVGs. This rebuild lands the chrome in its final shape: Structure. Three free-floating overlay panels (Time, Navigate, Status) on the presenter's base canvas, sharing the visual language of the drawing toolbar (overlay background, hairline border, radius.md). The Overlay tonal tier reads as "interactive control surface"; the Surface tier is reserved for informative content (notes, stage, sidebar). The chrome strip wraps the panels with horizontal padding to redistribute the inter-panel space. Cell grid within each panel. Adjacent children get a `border-left: 1px solid highlight.med` (the audience toolbar's `.toolset > * + *` pattern). The slide counter is exempt; its text-only natural height makes its border shorter than the bordered buttons around it. Labeled time cells. ELAPSED in the Time panel and CLOCK in the Status panel render as two-line dashboard cells (small mono uppercase label + tabular-numeral value). Cell width is locked to `calc(5ch + 0.2em)` so the panel never reflows as digits tick. Square icon buttons. `align-self: stretch` ties button height to the panel content; `aspect-ratio: 1 / 1` locks width to height. Icons sit centered at 1.2em. Tabler icons everywhere. The remaining Unicode glyph buttons (prev/next arrows, play/pause, refresh) are now proper Tabler SVGs in the same outline-or-filled language as the existing overview/exit/draw-tools icons. Floating jumpback callout. Pulled out of the navigation cluster entirely and anchored to the bottom-left of the stage when a return slide is stored. Renders at stage scale in the link tier for prominence, so the affordance actually captures attention instead of getting lost as a tiny icon in a row of controls. Layout in the chrome stays stable regardless of whether a jumpback is set. Countdown timer removed. The elapsed timer covers what most presenters need; the countdown added permanent chrome noise without enough function to justify it. The actor state machine's countdown fields stay in place so it can be reintroduced later without re-plumbing the runtime, but the UI and click-to-edit target machinery are gone. Co-Authored-By: Claude Opus 4.7 --- packages/wc/components/presentation/wc.ts | 464 ++++++++++------------ 1 file changed, 213 insertions(+), 251 deletions(-) diff --git a/packages/wc/components/presentation/wc.ts b/packages/wc/components/presentation/wc.ts index 74ee2ac..3d2295c 100644 --- a/packages/wc/components/presentation/wc.ts +++ b/packages/wc/components/presentation/wc.ts @@ -54,9 +54,6 @@ export class PresentationWC extends LitElement { @state() accessor startedAt: number | null = null; - @state() - accessor countdownTargetMs: number | null = null; - @state() accessor timerRunning = false; @@ -106,18 +103,6 @@ export class PresentationWC extends LitElement { @state() accessor jumpReturnSlideId: string | null = null; - /** - * Target-input click-to-edit state. The target value is set once at - * the start of a talk and rarely touched after; rendering an always- - * visible form field for it added permanent chrome clutter. When - * false the target reads as a small button showing the current - * value; click flips this to true and the button is replaced by an - * autofocused input. Blur or Enter commits and flips back; Escape - * discards. - */ - @state() - accessor targetEditing = false; - /** * Previous overview state, used in updated() to detect transitions * and re-anchor the scroll position to the current slide (the @@ -126,11 +111,6 @@ export class PresentationWC extends LitElement { */ #prevOverview = false; - /** Ref for the target-edit input so we can focus on flip-to-edit. */ - #targetInputRef = createRef(); - /** Previous targetEditing state, used to fire focus on the entry edge. */ - #prevTargetEditing = false; - @property({ type: String }) accessor uuid: string = ""; @@ -437,62 +417,56 @@ export class PresentationWC extends LitElement { font-style: italic; } - /* Three deliberate groups separated by hairline borders, mirroring - the audience toolbar's toolset language. Time on the left, - navigation centered ergonomically and visually, status + exit on - the right. */ + /* Three free-floating overlay panels on the presenter's base + canvas, sharing the visual language of the drawing toolbar + (overlay background + hairline border + radius.md). Overlay + reads as "interactive control surface" in the tonal-layering + grammar: the surface tier carries informative content (notes, + stage, sidebar), the overlay tier carries controls. The + presenter chrome is the latter, so its panels live at the + overlay tier directly on base. No enclosing wrap. */ .chrome { grid-column: 1 / -1; grid-row: 2; display: flex; align-items: stretch; - padding: ${dimension("space.xs")} 0; - background: ${color("overlay")}; - border-radius: ${dimension("radius.md")}; + justify-content: space-between; + gap: ${dimension("space.sm")}; font-family: ${fontFamily("mono")}; font-variation-settings: "MONO" 1; - overflow: hidden; } - .chrome-group { + .chrome-panel { display: flex; + flex: 0 0 auto; align-items: center; - gap: ${dimension("space.md")}; - padding: 0 ${dimension("space.md")}; + padding: ${dimension("space.xs")}; min-width: 0; - position: relative; + gap: ${dimension("space.xs")}; + background: ${color("overlay")}; + border: 1px solid ${color("highlight.med")}; + border-radius: ${dimension("radius.md")}; + overflow: hidden; } - /* Inset section-break rule rather than a full-height border. A - top-to-bottom border on a tall content strip reads as the edge - of a control (a tall button or a bordered cell), not as a - group separator. Shortening the rule to the middle ~50% of the - chrome lets it read as editorial punctuation, the visual - equivalent of the middot the colophon and marquee use to - separate items. */ - .chrome-group + .chrome-group::before { - content: ""; - position: absolute; - left: 0; - top: 25%; - bottom: 25%; - width: 1px; - background-color: ${color("highlight.med")}; - pointer-events: none; + /* Inter-cell hairlines within each panel, matching the audience + toolbar's "toolset > * + * { border-left }" pattern. Adjacent + icon buttons read as a row of deliberate controls and the + labeled time cells pick up the same cell-grid framing as the + buttons sitting next to them. */ + .chrome-panel > * + * { + border-left: 1px solid ${color("highlight.med")}; } - .chrome-group.time { - flex: 1 1 auto; + .chrome-panel.time { justify-content: flex-start; } - .chrome-group.nav { - flex: 0 0 auto; + .chrome-panel.nav { justify-content: center; } - .chrome-group.status { - flex: 1 1 auto; + .chrome-panel.status { justify-content: flex-end; } @@ -506,12 +480,13 @@ export class PresentationWC extends LitElement { display: flex; flex-direction: column; align-items: flex-start; - gap: 0.1em; + gap: 0.2em; line-height: 1; + padding: 0 ${dimension("space.xs")}; } .time-label { - font-size: 0.65rem; + font-size: 0.7rem; letter-spacing: ${dimension("tracking.wide")}; text-transform: uppercase; color: ${color("muted")}; @@ -523,60 +498,33 @@ export class PresentationWC extends LitElement { font-feature-settings: "tnum"; letter-spacing: 0.04em; color: ${color("text")}; + /* Lock the value column to its widest natural form (MM:SS) so + the time panel doesn't reflow as digits tick. 5ch covers + digit width; the +0.2em accounts for the 0.04em letter- + spacing across 5 chars so the locked column matches the + actual rendered width. */ + min-width: calc(5ch + 0.2em); } .time-value.muted { color: ${color("subtle")}; } - .time-value.danger { - color: ${color("danger")}; - } - - /* Click-to-edit target affordance. The default state reads as a - value that happens to be interactive; pointer cursor and hover - state signal editability, the tooltip names the action. */ - button.time-target-button { - all: unset; - cursor: pointer; - font-size: 1.3rem; - font-family: inherit; - font-variation-settings: "MONO" 1; - font-feature-settings: "tnum"; - letter-spacing: 0.04em; - color: ${color("subtle")}; - padding: 0; - transition: color ${duration("quick")} ${cubicBezier("out-quart")}; - } - - button.time-target-button:hover, - button.time-target-button:focus-visible { - color: ${color("text")}; - } - - .time-target-input { - all: unset; - width: 5ch; - font-family: inherit; - font-variation-settings: "MONO" 1; - font-feature-settings: "tnum"; - font-size: 1.3rem; - color: ${color("text")}; - background: transparent; - border-bottom: 1px solid ${color("link")}; - padding: 0 0.1em; - letter-spacing: 0.04em; - } - button.chrome-button { all: unset; cursor: pointer; color: ${color("subtle")}; - padding: 0.35em 0.5em; - border-radius: ${dimension("radius.sm")}; display: inline-flex; align-items: center; justify-content: center; + align-self: stretch; + /* Square hit area: height comes from align-self: stretch + matching the panel content, width is locked to the same + dimension via aspect-ratio. Icons sit centered in a + consistent square cell, matching the audience toolbar's + uniform button language. */ + aspect-ratio: 1 / 1; + border-radius: ${dimension("radius.sm")}; } button.chrome-button:hover { @@ -584,9 +532,20 @@ export class PresentationWC extends LitElement { color: ${color("text")}; } + button.chrome-button svg { + width: 1.2em; + height: 1.2em; + display: block; + } + /* Counter is single-line and width-stable, so navigation doesn't reflow mid-talk and the eye locks onto a fixed position when - scanning slide progress. */ + scanning slide progress. The cell-grid hairline is removed on + this element specifically because the counter is text-only (no + align-self stretch + no padding), so its border-left renders + shorter than the buttons around it. The bordered button on its + right side still separates it visually from the next-slide + button. */ .slide-counter { color: ${color("text")}; font-size: 1rem; @@ -594,16 +553,56 @@ export class PresentationWC extends LitElement { white-space: nowrap; min-width: 5ch; text-align: center; - } + border-left: none; + } + + /* Floating jumpback callout, anchored to the bottom-left of the + stage when a return slide is stored. Lives outside the chrome + so its appearance never reflows the navigation row, and so + the affordance reads as a stage-level "you can go back" cue + instead of a tiny icon lost in a row of controls. Uses the + link tier so it captures the presenter's attention; this is + the single moment of brand color in the presenter UI, which + the One Iris Rule allows since presenter chrome is not slide + content. */ + .jumpback-float { + position: absolute; + bottom: ${dimension("space.sm")}; + left: ${dimension("space.sm")}; + z-index: 4; - /* Reserve a fixed width for jumpback so the nav row doesn't - reflow when a return slide is set or consumed. The button - appears in place rather than pushing siblings sideways. */ - .jumpback-slot { display: inline-flex; align-items: center; - justify-content: center; - min-width: 3em; + gap: 0.4em; + + background: ${color("overlay")}; + border: 1px solid ${color("highlight.med")}; + border-radius: ${dimension("radius.md")}; + padding: 0.45em 0.75em; + + color: ${color("link")}; + font-family: ${fontFamily("mono")}; + font-variation-settings: "MONO" 1; + font-size: 0.85rem; + font-weight: 500; + + cursor: pointer; + transition: + background-color ${duration("quick")} ${cubicBezier("out-quart")}, + color ${duration("quick")} ${cubicBezier("out-quart")}; + } + + .jumpback-float:hover, + .jumpback-float:focus-visible { + background: ${color("highlight.med")}; + color: ${color("text")}; + outline: none; + } + + .jumpback-float svg { + width: 1.2em; + height: 1.2em; + flex: none; } /* In presenter mode, slides are projected into named slots as @@ -875,9 +874,6 @@ export class PresentationWC extends LitElement { this.currentIndex = ctx.currentIndex; } if (ctx.startedAt !== this.startedAt) this.startedAt = ctx.startedAt; - if (ctx.countdownTargetMs !== this.countdownTargetMs) { - this.countdownTargetMs = ctx.countdownTargetMs; - } if (ctx.timerRunning !== this.timerRunning) { this.timerRunning = ctx.timerRunning; } @@ -984,22 +980,6 @@ export class PresentationWC extends LitElement { } }); } - // Target click-to-edit: focus and select the input on the entry - // edge so the user can start typing immediately. autofocus is - // unreliable for elements added to the DOM after page load, so - // drive it explicitly off the state transition. - if (this.#prevTargetEditing !== this.targetEditing) { - this.#prevTargetEditing = this.targetEditing; - if (this.targetEditing) { - requestAnimationFrame(() => { - const input = this.#targetInputRef.value; - if (input) { - input.focus(); - input.select(); - } - }); - } - } this.#syncSlots(); this.#syncNotesContent(); } @@ -1123,14 +1103,6 @@ export class PresentationWC extends LitElement { }`; } - #targetAsString(): string { - const target = this.countdownTargetMs; - if (target === null) return ""; - const m = Math.floor(target / 60000); - const s = Math.floor((target / 1000) % 60); - return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`; - } - override render() { if (this.role !== "presenter") { // Audience: no canvas overlay at this level. Each slide hosts its @@ -1245,6 +1217,7 @@ export class PresentationWC extends LitElement { + ${this.#renderJumpbackFloat()}