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