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()} +