| 1 | {{/* present — GET /p/{session}, the fullscreen surface, and the |
| 2 | audience's follow view (GET /s/{token}?follow=1), which is the |
| 3 | same page with nothing to press. |
| 4 | |
| 5 | One template for both because they ARE the same screen: a slide |
| 6 | filling the viewport, where in the deck it is, and a way to move — |
| 7 | forms for whoever holds the remote, links for whoever is only |
| 8 | watching, nothing at all for an audience following along. |
| 9 | |
| 10 | Nothing here needs the script. With it, static/present.js swaps |
| 11 | the slide when the session moves; without it, the follow view has |
| 12 | a meta refresh and the presenter has next and previous. */}} |
| 13 | {{define "page-head"}}<link rel="stylesheet" href="{{asset "static/slides.css"}}"> |
| 14 | {{if .Refresh}}{{/* The scriptless heartbeat: an audience with no |
| 15 | JavaScript still moves with the presenter, five seconds behind. |
| 16 | Only on the follow view — a refresh on the presenter's own screen |
| 17 | would blink the projector mid-sentence. */}} |
| 18 | <meta http-equiv="refresh" content="5">{{end}} |
| 19 | <script defer src="{{asset "static/present.js"}}"></script> |
| 20 | <style> |
| 21 | /* The projector is a bounded black stage, never a document that can |
| 22 | push its remote controls underneath the fold. The controls live on |
| 23 | the stage so the slide can use every pixel behind them. */ |
| 24 | [slides-surface] { |
| 25 | background: #000; |
| 26 | block-size: 100dvh; |
| 27 | box-sizing: border-box; |
| 28 | display: flex; |
| 29 | inline-size: 100%; |
| 30 | overflow: hidden; |
| 31 | position: relative; |
| 32 | } |
| 33 | /* The projector is its own full-screen stage, not a document inside |
| 34 | the ordinary constrained rst-page wrapper. Its capability is what |
| 35 | makes it safe to omit the app bar and account menu. */ |
| 36 | [slides-present-stage] { |
| 37 | box-sizing: border-box; |
| 38 | inline-size: 100%; |
| 39 | max-inline-size: none; |
| 40 | min-block-size: 100dvh; |
| 41 | padding: 0; |
| 42 | } |
| 43 | [slides-stage] { |
| 44 | align-items: center; |
| 45 | display: flex; |
| 46 | flex: 1 1 auto; |
| 47 | inline-size: 100%; |
| 48 | justify-content: center; |
| 49 | margin: 0; |
| 50 | min-block-size: 0; |
| 51 | min-inline-size: 0; |
| 52 | } |
| 53 | [slides-stage] > [slides-slide] { |
| 54 | block-size: min(100dvh, calc(100vw * 9 / 16)); |
| 55 | inline-size: min(100vw, calc(100dvh * 16 / 9)); |
| 56 | max-block-size: 100%; |
| 57 | max-inline-size: 100%; |
| 58 | } |
| 59 | [slides-bar] { |
| 60 | align-items: center; |
| 61 | background: color-mix(in srgb, #000 72%, transparent); |
| 62 | bottom: 0; |
| 63 | display: flex; |
| 64 | flex-wrap: wrap; |
| 65 | gap: var(--rst-sp-3); |
| 66 | inset-inline: 0; |
| 67 | justify-content: center; |
| 68 | padding: var(--rst-sp-3); |
| 69 | position: absolute; |
| 70 | } |
| 71 | [slides-bar] form { |
| 72 | display: inline; |
| 73 | margin: 0; |
| 74 | } |
| 75 | [slides-counter] { |
| 76 | color: var(--rst-text-muted); |
| 77 | margin: 0; |
| 78 | } |
| 79 | [slides-progress] { |
| 80 | block-size: var(--rst-sp-1); |
| 81 | inline-size: min(100%, 24rem); |
| 82 | } |
| 83 | /* Blacking out hides the slide and says so in words. Never colour |
| 84 | alone: the sentence below the stage is what tells somebody using a |
| 85 | screen reader — or looking at a projector that is simply off — |
| 86 | that this is deliberate. */ |
| 87 | [slides-surface][data-blackout="true"] [slides-stage] { |
| 88 | visibility: hidden; |
| 89 | } |
| 90 | [slides-blackout] { |
| 91 | align-self: center; |
| 92 | color: #fff; |
| 93 | font-weight: 600; |
| 94 | margin: 0; |
| 95 | position: absolute; |
| 96 | text-align: center; |
| 97 | z-index: 1; |
| 98 | } |
| 99 | </style>{{end}} |
| 100 | {{define "main"}}<main id="main" slides-present-stage>{{template "content" .}}</main>{{end}} |
| 101 | {{define "content"}} |
| 102 | {{/* The two URLs the script reads come DOWN rather than being built up |
| 103 | there: which capability this surface reads under is the handler's |
| 104 | decision (presentation.go's surfaceOptions), and the audience's |
| 105 | follow view deliberately carries no session sqid at all. */}} |
| 106 | <div slides-surface |
| 107 | {{- if .Session}} data-session="{{.Session}}"{{end}} |
| 108 | {{- if .StreamHref}} data-stream="{{.StreamHref}}"{{end}} |
| 109 | {{- if .FragmentHref}} data-fragment="{{.FragmentHref}}"{{end}} |
| 110 | data-slide="{{.Now}}" |
| 111 | data-blackout="{{if .Blackout}}true{{else}}false{{end}}" |
| 112 | {{- if .Token}} data-remote-token="{{.Token}}"{{end}} |
| 113 | {{- if .NotesHref}} data-notes="{{.NotesHref}}"{{end}}> |
| 114 | {{if .HasSlide}} |
| 115 | <div slides-stage>{{.Slide}}</div> |
| 116 | <p slides-blackout{{if not .Blackout}} hidden{{end}}>{{.BlackoutLabel}}</p> |
| 117 | <div slides-bar> |
| 118 | {{if .Controls}}{{template "present-controls" .Controls}}{{end}} |
| 119 | {{if .PrevHref}}<a rst-btn href="{{.PrevHref}}" rel="prev">{{.PrevLabel}}</a>{{end}} |
| 120 | {{if .NextHref}}<a rst-btn href="{{.NextHref}}" rel="next">{{.NextLabel}}</a>{{end}} |
| 121 | {{/* "3 / 12" alone is terse read aloud, so the counter carries what |
| 122 | the number means in a line only a screen reader gets. The bar |
| 123 | beside it is the same fact as a picture, and is hidden from |
| 124 | assistive tech rather than announced twice. */}} |
| 125 | <p slides-counter><span class="rst-sr-only">{{.ProgressLabel}}</span> {{.Counter}}</p> |
| 126 | <progress slides-progress value="{{.Now}}" max="{{.Total}}" aria-hidden="true"></progress> |
| 127 | {{if .NotesHref}}<a rst-btn href="{{.NotesHref}}">{{.NotesLabel}}</a>{{end}} |
| 128 | {{if .BackHref}}<a rst-btn href="{{.BackHref}}">{{.BackLabel}}</a>{{end}} |
| 129 | </div> |
| 130 | {{else}} |
| 131 | <div rst-box> |
| 132 | {{template "empty-state" dict "Title" .EmptyTitle "Body" .EmptyBody}} |
| 133 | </div> |
| 134 | {{end}} |
| 135 | </div> |
| 136 | {{end}} |
| 137 | |