From 8f9399a426b901838f3102e2ee9ddbc29f626919 Mon Sep 17 00:00:00 2001 From: Graham Barber Date: Sun, 17 May 2026 22:24:44 -0700 Subject: [PATCH] refactor(wc): rebuild the presenter chrome around labeled time cells The chrome was a single strip with grid-template-columns: 1fr auto 1fr, which sorted items by horizontal position rather than by meaning. The big 1.8rem elapsed timer pinned the far left, navigation sat in the center, and timer controls plus target plus clock plus exit jostled on the right. Four unlabeled time displays (elapsed, countdown, target, wall clock) read as an undifferentiated row of MM:SS digits; a presenter had to watch them tick to identify which was which. The slide counter wrapped to two lines and the exit button clipped at the viewport edge. Rebuild the strip as three hairline-separated groups echoing the audience toolbar's toolset language: Group A (Time): elapsed, countdown ("Left"), play/pause + reset, target. Each clock renders as a two-line dashboard cell with a small mono uppercase label above a tabular-numeral value. Reading color sits on elapsed (the primary glance); countdown fades to subtle, target to muted (or danger when over time), so the visual hierarchy reflects the read order. Group B (Navigate): prev/counter/next + overview + jumpback. The jumpback now lives inside a fixed-width slot so its appearance no longer reflows the row. The counter is white-space: nowrap with a 5ch min-width so it stays on one line at every slide count. Group C (Status): wall clock + exit. Clock gets the same labeled dashboard cell treatment as the timer values; exit reads as a distinct affordance through the group hairline rather than its own border. Target becomes click-to-edit. Default state: a small button showing the current value with cursor: pointer, hover-color shift, and a "Click to set talk duration" tooltip. Click flips targetEditing to true; updated() fires an entry-edge focus + select on the input. Blur or Enter commits; Escape discards. Always-visible form chrome disappears in the common case. Elapsed drops from 1.8rem to 1.3rem so it sits in proportion with the other clocks. Differentiation now comes from the labels, not from outsized type. The overall chrome strip ends up shorter (~53px vs ~60px) despite carrying more legible information. Co-Authored-By: Claude Opus 4.7 --- packages/wc/components/presentation/wc.ts | 333 +++++++++++++++------- 1 file changed, 235 insertions(+), 98 deletions(-) diff --git a/packages/wc/components/presentation/wc.ts b/packages/wc/components/presentation/wc.ts index 35e227e..7037730 100644 --- a/packages/wc/components/presentation/wc.ts +++ b/packages/wc/components/presentation/wc.ts @@ -106,6 +106,18 @@ 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 @@ -114,6 +126,11 @@ 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 = ""; @@ -420,71 +437,127 @@ 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. */ .chrome { grid-column: 1 / -1; grid-row: 2; - display: grid; - grid-template-columns: 1fr auto 1fr; - align-items: center; - gap: ${dimension("space.md")}; - padding: ${dimension("space.sm")} ${dimension("space.md")}; + display: flex; + align-items: stretch; + padding: ${dimension("space.xs")} 0; background: ${color("overlay")}; border-radius: ${dimension("radius.md")}; font-family: ${fontFamily("mono")}; font-variation-settings: "MONO" 1; + overflow: hidden; } - .chrome-left, - .chrome-center, - .chrome-right { + .chrome-group { display: flex; align-items: center; gap: ${dimension("space.md")}; + padding: 0 ${dimension("space.md")}; + min-width: 0; } - .chrome-right { + .chrome-group + .chrome-group { + border-left: 1px solid ${color("highlight.med")}; + } + + .chrome-group.time { + flex: 1 1 auto; + justify-content: flex-start; + } + + .chrome-group.nav { + flex: 0 0 auto; + justify-content: center; + } + + .chrome-group.status { + flex: 1 1 auto; justify-content: flex-end; - color: ${color("subtle")}; } - .elapsed { - font-size: 1.8rem; - color: ${color("text")}; + /* Two-line dashboard cell: small uppercase mono label above a + larger tabular-numeral value. Labels disambiguate the four time + displays so a presenter can identify each at a glance instead + of having to watch them tick. Reading-color stays Text on the + primary value (Elapsed); supporting values fade to Subtle and + Muted, in order of relevance. */ + .time-cell { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.1em; + line-height: 1; + } + + .time-label { + font-size: 0.65rem; + letter-spacing: ${dimension("tracking.wide")}; + text-transform: uppercase; + color: ${color("muted")}; + font-weight: 500; + } + + .time-value { + font-size: 1.3rem; + font-feature-settings: "tnum"; letter-spacing: 0.04em; + color: ${color("text")}; } - .countdown { - font-size: 1.4rem; + .time-value.muted { color: ${color("subtle")}; - letter-spacing: 0.04em; } - .countdown.over { + .time-value.danger { color: ${color("danger")}; } - .countdown-input { + /* 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; - width: 5ch; + 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")}; - background: transparent; - border-bottom: 1px solid ${color("highlight.med")}; - padding: 0 0.25em; - text-align: center; + padding: 0; + transition: color ${duration("quick")} ${cubicBezier("out-quart")}; } - .countdown-input:focus { - border-bottom-color: ${color("link")}; + 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.25em 0.5em; + padding: 0.35em 0.5em; border-radius: ${dimension("radius.sm")}; display: inline-flex; align-items: center; @@ -496,32 +569,26 @@ export class PresentationWC extends LitElement { color: ${color("text")}; } - /* Exit button: separated from the timer controls by a hairline rule - so it reads as a distinct affordance, not part of the timer group. */ - button.chrome-button.exit { - margin-left: ${dimension("space.xs")}; - padding: 0.35em 0.45em; - border-left: 1px solid ${color("highlight.med")}; - border-radius: 0; - padding-left: ${dimension("space.sm")}; - } - - button.chrome-button.exit:hover { - background: transparent; - color: ${color("text")}; - } - - button.chrome-button.exit svg { - 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. */ .slide-counter { color: ${color("text")}; font-size: 1rem; + font-feature-settings: "tnum"; + white-space: nowrap; + min-width: 5ch; + text-align: center; } - .clock { - font-size: 1rem; + /* 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; } /* In presenter mode, slides are projected into named slots as @@ -902,6 +969,22 @@ 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(); } @@ -1163,10 +1246,17 @@ export class PresentationWC extends LitElement { } /** - * Presenter chrome strip: timer on the left, navigation in the - * middle, target/clock/exit on the right. Reused between the - * normal presenter layout and presenter-overview mode so the - * overview keeps the navigation strip the presenter is used to. + * Presenter chrome strip: three hairline-separated groups echoing + * the audience toolbar's toolset language. Time on the left + * (elapsed, countdown, target, controls), navigation centered + * (prev/counter/next plus overview and a reserved-space jumpback), + * status + exit on the right (wall clock, exit). Each clock cell + * carries a small uppercase label so the four numbers don't read + * as an undifferentiated row of digits. + * + * Reused between the normal presenter layout and presenter- + * overview mode so the overview keeps the navigation strip the + * presenter is used to. */ #renderChrome() { const total = this.slides.length; @@ -1180,18 +1270,57 @@ export class PresentationWC extends LitElement { const returnLabel = returnSlideIndex >= 0 ? String(returnSlideIndex + 1) : ""; + const targetText = this.#targetAsString(); return html`
-
- ${this.#formatMs(elapsed)} +
+
+ Elapsed + ${this.#formatMs(elapsed)} +
${ remaining !== null - ? html`${this.#formatMs(remaining)}` + ? html`
+ Left + ${this.#formatMs(remaining)} +
` : nothing } + + +
+ Target + ${ + this.targetEditing + ? html`` + : html`` + } +
-
+ + -
- - - - ${this.#formatClock()} + +
+
+ Clock + ${this.#formatClock()} +