| 1 | {{/* viewer — GET /d/{sqid} and GET /s/{token}, the paged viewer. |
| 2 | |
| 3 | One file for all three audiences (viewer.go's modeOwner, |
| 4 | modeMember, modeLink). The mode decides what the handler PUTS in |
| 5 | the view data — the notes, the owner's buttons, the filmstrip's |
| 6 | links — and this template only ever asks whether a field is there. |
| 7 | That is the whole reason it is not two templates: a second file is |
| 8 | a second place for "does this reader get the speaker notes?" to be |
| 9 | answered, and the two answers would drift. |
| 10 | |
| 11 | render.go builds it twice, as "viewer" (with the account menu) and |
| 12 | as "viewer_link" (with a sign-in link instead), because nobody |
| 13 | reading a deck over a share link has an account to show. |
| 14 | |
| 15 | Present is task 6's surface in its scriptless form: one slide, two |
| 16 | links, nothing else on the page. §8's floor, and the thing the |
| 17 | fullscreen presenter will be an enhancement OF rather than a |
| 18 | replacement for. */}} |
| 19 | {{/* The slide's own stylesheet, on the screens that render a slide and |
| 20 | nowhere else: it is a document's look, not the app's chrome, and |
| 21 | every other screen would pay for it without using it. */}} |
| 22 | {{define "page-head"}}<link rel="stylesheet" href="{{asset "static/slides.css"}}"> |
| 23 | <style> |
| 24 | /* The chrome around the slide. Everything a slide itself looks like |
| 25 | is slides.css; these five rules only place the viewer's own parts, |
| 26 | and each one is an app attribute (slides-*) so nothing here can |
| 27 | collide with the rst-* vocabulary. */ |
| 28 | [slides-stage] { |
| 29 | margin-block: var(--rst-sp-4); |
| 30 | } |
| 31 | [slides-tools] { |
| 32 | display: flex; |
| 33 | flex-wrap: wrap; |
| 34 | gap: var(--rst-sp-2); |
| 35 | } |
| 36 | [slides-pager] { |
| 37 | align-items: center; |
| 38 | display: flex; |
| 39 | flex-wrap: wrap; |
| 40 | gap: var(--rst-sp-3); |
| 41 | } |
| 42 | /* A pager end — the previous link on slide one — is plain text, not |
| 43 | a disabled control: there is nowhere to go, so there is nothing to |
| 44 | put in the tab order. */ |
| 45 | [slides-pager-end], |
| 46 | [slides-counter] { |
| 47 | color: var(--rst-text-muted); |
| 48 | margin: 0; |
| 49 | } |
| 50 | [slides-film] { |
| 51 | display: flex; |
| 52 | flex-wrap: wrap; |
| 53 | gap: var(--rst-sp-2); |
| 54 | list-style: none; |
| 55 | margin: 0; |
| 56 | padding: 0; |
| 57 | } |
| 58 | [slides-film] a { |
| 59 | display: inline-block; |
| 60 | /* 24 by 24 is the floor a pointer target has to meet (WCAG 2.2 AA); |
| 61 | the padding is what gets a one-word slide name there. */ |
| 62 | min-height: 24px; |
| 63 | padding: var(--rst-sp-1) var(--rst-sp-2); |
| 64 | } |
| 65 | [slides-film] a[aria-current="true"] { |
| 66 | background: var(--rst-accent-soft); |
| 67 | border-radius: var(--rst-radius-sm); |
| 68 | font-weight: 600; |
| 69 | } |
| 70 | </style>{{end}} |
| 71 | {{define "content"}} |
| 72 | {{if .Present}} |
| 73 | <div slides-present data-vp="{{.VP}}"> |
| 74 | {{.Slide}} |
| 75 | <nav slides-pager aria-label="{{.PagerLabel}}"> |
| 76 | {{if .PrevHref}}<a rst-btn href="{{.PrevHref}}" rel="prev">{{.PrevLabel}}</a> |
| 77 | {{else}}<span slides-pager-end>{{.PrevLabel}}</span>{{end}} |
| 78 | <p slides-counter>{{.Counter}}</p> |
| 79 | {{if .NextHref}}<a rst-btn href="{{.NextHref}}" rel="next">{{.NextLabel}}</a> |
| 80 | {{else}}<span slides-pager-end>{{.NextLabel}}</span>{{end}} |
| 81 | </nav> |
| 82 | </div> |
| 83 | {{else}} |
| 84 | <div slides-viewer data-vp="{{.VP}}"> |
| 85 | {{if .BackHref}}{{template "back-nav" dict "Href" .BackHref "Label" .BackLabel}}{{end}} |
| 86 | {{template "page-header" dict "Title" .Title}} |
| 87 | {{if or .EditHref .SettingsHref .ShareHref .NotesHref .ExportHref .PresentHref .PresentAction .FollowHref}} |
| 88 | <nav slides-tools aria-label="{{.ToolsLabel}}"> |
| 89 | {{if .EditHref}}<a rst-btn="primary" href="{{.EditHref}}">{{.EditLabel}}</a>{{end}} |
| 90 | {{if .SettingsHref}}<a rst-btn href="{{.SettingsHref}}">{{.SettingsLabel}}</a>{{end}} |
| 91 | {{if .ShareHref}}<a rst-btn href="{{.ShareHref}}">{{.ShareLabel}}</a>{{end}} |
| 92 | {{if .NotesHref}}<a rst-btn href="{{.NotesHref}}">{{.NotesPageLabel}}</a>{{end}} |
| 93 | {{/* Export is a LINK to a screen of its own, not a form button: it |
| 94 | asks whether the speaker notes go in the file before it starts |
| 95 | anything (exports.go). */}} |
| 96 | {{if .ExportHref}}<a rst-btn href="{{.ExportHref}}">{{.ExportLabel}}</a>{{end}} |
| 97 | {{/* Present is a FORM on the signed-in screens, because pressing it |
| 98 | writes: it opens a session and mints the remote's capability |
| 99 | (presentation.go). Over a share link it stays a link to the same |
| 100 | capability with ?present=1 (spec §5) — a reader may present the |
| 101 | deck on their own screen, and may not start a session on |
| 102 | somebody else's deck. */}} |
| 103 | {{if .PresentAction}} |
| 104 | <form method="post" action="{{.PresentAction}}"> |
| 105 | <button rst-btn="primary" type="submit">{{.PresentLabel}}</button> |
| 106 | </form> |
| 107 | {{else if .PresentHref}}<a rst-btn href="{{.PresentHref}}">{{.PresentLabel}}</a>{{end}} |
| 108 | {{/* Somebody is presenting this deck right now, and this is the |
| 109 | offer to watch along (spec §3.4). Only ever on a share link. */}} |
| 110 | {{if .FollowHref}}<a rst-btn="primary" href="{{.FollowHref}}">{{.FollowLabel}}</a>{{end}} |
| 111 | </nav> |
| 112 | {{end}} |
| 113 | |
| 114 | {{if .HasSlides}} |
| 115 | <div slides-stage>{{.Slide}}</div> |
| 116 | |
| 117 | <nav slides-pager aria-label="{{.PagerLabel}}"> |
| 118 | {{if .PrevHref}}<a rst-btn href="{{.PrevHref}}" rel="prev">{{.PrevLabel}}</a> |
| 119 | {{else}}<span slides-pager-end>{{.PrevLabel}}</span>{{end}} |
| 120 | <p slides-counter>{{.Counter}}</p> |
| 121 | {{if .NextHref}}<a rst-btn href="{{.NextHref}}" rel="next">{{.NextLabel}}</a> |
| 122 | {{else}}<span slides-pager-end>{{.NextLabel}}</span>{{end}} |
| 123 | </nav> |
| 124 | |
| 125 | {{if .HasNotes}} |
| 126 | <details slides-notes> |
| 127 | <summary>{{.NotesLabel}}</summary> |
| 128 | {{.Notes}} |
| 129 | </details> |
| 130 | {{end}} |
| 131 | |
| 132 | <nav slides-film-nav aria-label="{{.FilmLabel}}"> |
| 133 | <ol slides-film> |
| 134 | {{range .Film}} |
| 135 | <li><a href="{{.Href}}"{{if .Current}} aria-current="true"{{end}}>{{.Label}}</a></li> |
| 136 | {{end}} |
| 137 | </ol> |
| 138 | </nav> |
| 139 | {{else}} |
| 140 | <div rst-box> |
| 141 | {{template "empty-state" dict "Title" .EmptyTitle "Body" .EmptyBody "ActionHref" .EditHref "ActionLabel" .EditLabel}} |
| 142 | </div> |
| 143 | {{end}} |
| 144 | </div> |
| 145 | {{end}} |
| 146 | {{end}} |
| 147 | |