| 1 | {{define "head"}} |
| 2 | <link rel="stylesheet" href="{{asset "static/vendor/jsuites.css"}}"> |
| 3 | <link rel="stylesheet" href="{{asset "static/vendor/jspreadsheet.css"}}"> |
| 4 | {{/* Vendored, in dependency order, and deferred so neither blocks the |
| 5 | first paint. defer preserves document order, so each module below |
| 6 | is defined before the one that calls it: live.js registers |
| 7 | window.SheetsLive and paint.js window.SheetsPaint; grid.js calls |
| 8 | both once the window has loaded; menubar.js and commands.js wire |
| 9 | the chrome and read window.SheetsGrid, which grid.js publishes. */}} |
| 10 | <script defer src="{{asset "static/vendor/jsuites.js"}}"></script> |
| 11 | <script defer src="{{asset "static/vendor/jspreadsheet.js"}}"></script> |
| 12 | <script defer src="{{asset "static/live.js"}}"></script> |
| 13 | <script defer src="{{asset "static/paint.js"}}"></script> |
| 14 | <script defer src="{{asset "static/sizes.js"}}"></script> |
| 15 | <script defer src="{{asset "static/autofit.js"}}"></script> |
| 16 | <script defer src="{{asset "static/grid.js"}}"></script> |
| 17 | <script defer src="{{asset "static/menubar.js"}}"></script> |
| 18 | <script defer src="{{asset "static/formulabar.js"}}"></script> |
| 19 | <script defer src="{{asset "static/formulapick.js"}}"></script> |
| 20 | <script defer src="{{asset "static/note.js"}}"></script> |
| 21 | <script defer src="{{asset "static/tsv.js"}}"></script> |
| 22 | <script defer src="{{asset "static/cellqueue.js"}}"></script> |
| 23 | <script defer src="{{asset "static/clipboard.js"}}"></script> |
| 24 | <script defer src="{{asset "static/commands.js"}}"></script> |
| 25 | {{end}} |
| 26 | |
| 27 | {{/* The open sheet's title, and for anyone who may edit it the rename, |
| 28 | are the bar's (estilo's editable title slot, chrome.TitleEdit, built |
| 29 | in handleSheetPage). It is still a real form posting to |
| 30 | /sheets/{id}/rename, so Enter renames with scripting off, and the |
| 31 | Rename button is visually hidden until the field has focus. Escape |
| 32 | restores the title and leaves the field (estilo chrome.js), which |
| 33 | fixes the sticky focus look the audit found. A reader sees the |
| 34 | title read-only. */}} |
| 35 | |
| 36 | {{define "main"}} |
| 37 | {{/* The grid gets its own <main>, without rst-page's padding and max |
| 38 | width: a spreadsheet is the one screen in this app that should fill |
| 39 | the viewport. Everything else uses the layout's default. */}} |
| 40 | <main data-sheet-surface id="main" |
| 41 | data-sheet-id="{{.Data.Sheet.ID}}" |
| 42 | data-tab-id="{{.Data.Sheet.TabID}}" |
| 43 | data-rows="{{.Data.Rows}}" |
| 44 | data-cols="{{.Data.Cols}}" |
| 45 | {{/* Set when the sheet reaches past the widest grid this build |
| 46 | can paint. grid.js reads it and says so in the status line |
| 47 | rather than the page shouting at somebody who may never |
| 48 | scroll that far — but it is said, which is the point: the |
| 49 | window used to be a constant 100 rows and a cell on row 151 |
| 50 | was simply not on this screen, with nothing anywhere |
| 51 | admitting a window existed. */}} |
| 52 | {{if .Data.Truncated}}data-window-truncated="1"{{end}} |
| 53 | {{if .Data.MayWrite}}data-may-write="1"{{end}} |
| 54 | {{/* The chords clipboard.js names when a browser refuses a menu |
| 55 | copy, cut or paste ("Press ⌘C instead"), in this request's |
| 56 | platform's spelling, so its status line agrees with the menu. */}} |
| 57 | data-key-copy="{{.Frame.Keys.copy}}" data-key-cut="{{.Frame.Keys.cut}}" data-key-paste="{{.Frame.Keys.paste}}"> |
| 58 | {{template "content" .}} |
| 59 | </main> |
| 60 | {{end}} |
| 61 | |
| 62 | {{/* --- Glyphs ------------------------------------------------------- |
| 63 | |
| 64 | Written here rather than fetched with {{icon}}. rastrillo vendors |
| 65 | twelve Lucide slugs — alert-triangle, check, check-circle, |
| 66 | chevron-down, help-circle, info, kebab, menu, plus, search, x, |
| 67 | x-circle — and rastrillo.Icon returns an EMPTY STRING for a slug it |
| 68 | does not have, with no error. So {{icon "bold"}} is not a missing |
| 69 | icon, it is a button with no label and no complaint, which is the |
| 70 | same silent-failure shape as an invented CSS token. |
| 71 | |
| 72 | Bold and italic are letters, because that is what they are on every |
| 73 | spreadsheet anybody has used. The three alignments are rules, |
| 74 | because "left" as a word is longer than the button. */}} |
| 75 | |
| 76 | {{define "glyph-align-left"}}<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M2 3.5h12M2 6.5h7M2 9.5h12M2 12.5h7"/></svg>{{end}} |
| 77 | {{define "glyph-align-center"}}<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M2 3.5h12M4.5 6.5h7M2 9.5h12M4.5 12.5h7"/></svg>{{end}} |
| 78 | {{define "glyph-align-right"}}<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M2 3.5h12M7 6.5h7M2 9.5h12M7 12.5h7"/></svg>{{end}} |
| 79 | {{define "glyph-undo"}}<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M3 8h7a3 3 0 0 1 0 6H6M3 8l3-3M3 8l3 3"/></svg>{{end}} |
| 80 | |
| 81 | {{define "content"}} |
| 82 | |
| 83 | {{/* Row B — the menu bar and the toolbar share one row. |
| 84 | Six short words are about 300px; a row holding only them wastes |
| 85 | two-thirds of itself. They wrap onto separate rows below ~900px, |
| 86 | which is also what keeps the chrome inside the 320px reflow |
| 87 | budget. */}} |
| 88 | <div data-chrome> |
| 89 | |
| 90 | <nav estilo-menubar data-menubar aria-label="Sheet menu"> |
| 91 | |
| 92 | {{/* estilo's menu bar ([estilo-menubar], estilo menu.css): square titles |
| 93 | in the page flow, 9px floating panels with inset rows, a shortcut |
| 94 | column, and the platform's own labels (⌘Z on a Mac, Ctrl+Z |
| 95 | elsewhere) from estilo's menu.Shortcut, formatted server-side. |
| 96 | |
| 97 | Every top-level menu is in the one group, name="estilo-menus", with |
| 98 | the bar's Menu, switcher and account: opening File closes the |
| 99 | account menu and the other way round, which is the behaviour this |
| 100 | app chose on purpose and TestTheMenusShareOneGroup pins. estilo's |
| 101 | chrome.js closes the group on Escape and an outside click. |
| 102 | |
| 103 | There are no submenus. A <details> nested inside a grouped one |
| 104 | closes its parent when it opens (TestNoNestedExclusiveDetails), and |
| 105 | inline accordions made Format a panel that scrolled with its |
| 106 | swatches cut off. Format lost Number, Fill colour and Text colour, |
| 107 | which the toolbar beside it already carries; Insert's functions |
| 108 | are rows under a heading. Hidden items are hidden until |
| 109 | commands.js can run them: an item appears only if it works. */}} |
| 110 | |
| 111 | <details estilo-menubar-menu name="estilo-menus"> |
| 112 | <summary estilo-menubar-title>File</summary> |
| 113 | <div estilo-menu> |
| 114 | <form method="post" action="/sheets"><button estilo-menu-item type="submit"><span estilo-menu-label>New sheet</span></button></form> |
| 115 | {{/* A page, not a file input in this menu. A hidden input behind a |
| 116 | menu-shaped label never submits itself, so the item would have |
| 117 | needed script to import anything at all — see handleImportPage. */}} |
| 118 | <a estilo-menu-item href="/sheets/import"><span estilo-menu-label>Import a spreadsheet…</span></a> |
| 119 | <a estilo-menu-item href="/sheets/{{.Data.Sheet.ID}}/export.xlsx"><span estilo-menu-label>Download as .xlsx</span></a> |
| 120 | <hr> |
| 121 | <a estilo-menu-item href="/sheets/{{.Data.Sheet.ID}}/table"><span estilo-menu-label>Open as table</span></a> |
| 122 | <a estilo-menu-item href="/sheets/{{.Data.Sheet.ID}}/history"><span estilo-menu-label>Version history</span></a> |
| 123 | {{if .Data.MayShare}}<a estilo-menu-item href="/sheets/{{.Data.Sheet.ID}}/share"><span estilo-menu-label>Share…</span></a>{{end}} |
| 124 | </div> |
| 125 | </details> |
| 126 | |
| 127 | {{if .Data.MayWrite}} |
| 128 | <details estilo-menubar-menu name="estilo-menus"> |
| 129 | <summary estilo-menubar-title>Edit</summary> |
| 130 | <div estilo-menu> |
| 131 | {{/* Undo is a form, not a button, so it works with scripting off — |
| 132 | it posts to the same route the keyboard shortcut fetches, so |
| 133 | there is one undo and not two that can disagree. */}} |
| 134 | <form method="post" action="/sheets/{{.Data.Sheet.ID}}/undo"><button estilo-menu-item type="submit"><span estilo-menu-label>Undo</span><kbd estilo-menu-key>{{.Frame.Keys.undo}}</kbd></button></form> |
| 135 | {{/* The separators around the script-only groups are script-only too: |
| 136 | with scripting off Undo stands alone, not above two empty rules. */}} |
| 137 | <hr data-needs-js hidden> |
| 138 | <button estilo-menu-item type="button" data-cmd="cut" hidden><span estilo-menu-label>Cut</span><kbd estilo-menu-key>{{.Frame.Keys.cut}}</kbd></button> |
| 139 | <button estilo-menu-item type="button" data-cmd="copy" hidden><span estilo-menu-label>Copy</span><kbd estilo-menu-key>{{.Frame.Keys.copy}}</kbd></button> |
| 140 | {{/* Paste is here despite the browser being allowed to refuse a menu |
| 141 | read of the clipboard. It is not a dead control: clipboard.js |
| 142 | asks, and on a refusal says so in the status line below the grid |
| 143 | — which is more than the app could do for a person who does not |
| 144 | know the keyboard is the only way. The keyboard path needs no |
| 145 | permission at all, because a paste EVENT carries its own data. */}} |
| 146 | <button estilo-menu-item type="button" data-cmd="paste" hidden><span estilo-menu-label>Paste</span><kbd estilo-menu-key>{{.Frame.Keys.paste}}</kbd></button> |
| 147 | <button estilo-menu-item type="button" data-cmd="clear" hidden><span estilo-menu-label>Clear contents</span><kbd estilo-menu-key>{{.Frame.Keys.delete}}</kbd></button> |
| 148 | <hr data-needs-js hidden> |
| 149 | {{/* Fill down and fill right are the menu equivalents of the grid's |
| 150 | drag handle — AGENTS.md requires one for every drag-only |
| 151 | affordance. Ctrl+R is reload in every browser, so fill right is |
| 152 | deliberately shortcut-free. */}} |
| 153 | <button estilo-menu-item type="button" data-cmd="fill-down" hidden><span estilo-menu-label>Fill down</span><kbd estilo-menu-key>{{.Frame.Keys.filldown}}</kbd></button> |
| 154 | <button estilo-menu-item type="button" data-cmd="fill-right" hidden><span estilo-menu-label>Fill right</span></button> |
| 155 | <button estilo-menu-item type="button" data-cmd="select-all" hidden><span estilo-menu-label>Select all</span><kbd estilo-menu-key>{{.Frame.Keys.selectall}}</kbd></button> |
| 156 | </div> |
| 157 | </details> |
| 158 | {{end}} |
| 159 | |
| 160 | {{if or .Data.MayWrite .Data.MayComment}} |
| 161 | <details estilo-menubar-menu name="estilo-menus"> |
| 162 | <summary estilo-menubar-title>Insert</summary> |
| 163 | <div estilo-menu> |
| 164 | {{if .Data.MayWrite}} |
| 165 | {{/* Exactly the aggregates the engine registers and that a one-click |
| 166 | insert can guess the arguments for. VLOOKUP, INDEX and MATCH are |
| 167 | registered too and are deliberately absent: nothing here can guess |
| 168 | their second and third arguments. Rows under a heading, not a |
| 169 | submenu, and hidden until commands.js can insert them. */}} |
| 170 | <p estilo-menu-heading data-needs-js hidden>Function</p> |
| 171 | <button estilo-menu-item type="button" data-cmd="fn" data-fn="SUM" hidden><span estilo-menu-label>SUM</span></button> |
| 172 | <button estilo-menu-item type="button" data-cmd="fn" data-fn="AVERAGE" hidden><span estilo-menu-label>AVERAGE</span></button> |
| 173 | <button estilo-menu-item type="button" data-cmd="fn" data-fn="COUNT" hidden><span estilo-menu-label>COUNT</span></button> |
| 174 | <button estilo-menu-item type="button" data-cmd="fn" data-fn="MIN" hidden><span estilo-menu-label>MIN</span></button> |
| 175 | <button estilo-menu-item type="button" data-cmd="fn" data-fn="MAX" hidden><span estilo-menu-label>MAX</span></button> |
| 176 | {{if .Data.MayComment}}<hr data-needs-js hidden>{{end}} |
| 177 | {{end}} |
| 178 | {{if .Data.MayComment}}<a estilo-menu-item href="/sheets/{{.Data.Sheet.ID}}/comments"><span estilo-menu-label>Comment…</span></a>{{end}} |
| 179 | </div> |
| 180 | </details> |
| 181 | {{end}} |
| 182 | |
| 183 | {{if .Data.MayWrite}} |
| 184 | {{/* The Format menu is entirely script-driven, so the whole menu is |
| 185 | hidden until commands.js reveals it. A menu of items that do |
| 186 | nothing is worse than no menu. */}} |
| 187 | <details estilo-menubar-menu name="estilo-menus" hidden data-needs-js> |
| 188 | <summary estilo-menubar-title>Format</summary> |
| 189 | <div estilo-menu> |
| 190 | <button estilo-menu-item type="button" data-cmd="bold"><span estilo-menu-label>Bold</span><kbd estilo-menu-key>{{.Frame.Keys.bold}}</kbd></button> |
| 191 | <button estilo-menu-item type="button" data-cmd="italic"><span estilo-menu-label>Italic</span><kbd estilo-menu-key>{{.Frame.Keys.italic}}</kbd></button> |
| 192 | <hr> |
| 193 | <button estilo-menu-item type="button" data-cmd="align" data-value="left"><span estilo-menu-label>Align left</span></button> |
| 194 | <button estilo-menu-item type="button" data-cmd="align" data-value="center"><span estilo-menu-label>Align centre</span></button> |
| 195 | <button estilo-menu-item type="button" data-cmd="align" data-value="right"><span estilo-menu-label>Align right</span></button> |
| 196 | <hr> |
| 197 | {{/* The menu equivalent of dragging a column edge (AGENTS.md: every |
| 198 | drag-only affordance has one). A number field rather than a set of |
| 199 | buttons, because the size a drag produces is the one thing here you |
| 200 | cannot pick from a list. Real forms rather than a prompt(); they |
| 201 | post through commands.js because the size route takes JSON. */}} |
| 202 | <button estilo-menu-item type="button" data-cmd="autofit-columns"><span estilo-menu-label>Auto-fit columns</span></button> |
| 203 | <form data-size-form data-axis="col"> |
| 204 | <label for="colw">Column width</label> |
| 205 | <input rst-input id="colw" name="size" type="number" min="16" max="2000" step="1" placeholder="px"> |
| 206 | <button rst-btn="ghost" type="submit">Set</button> |
| 207 | </form> |
| 208 | <form data-size-form data-axis="row"> |
| 209 | <label for="rowh">Row height</label> |
| 210 | <input rst-input id="rowh" name="size" type="number" min="16" max="2000" step="1" placeholder="px"> |
| 211 | <button rst-btn="ghost" type="submit">Set</button> |
| 212 | </form> |
| 213 | <hr> |
| 214 | <button estilo-menu-item type="button" data-cmd="clear-format"><span estilo-menu-label>Clear formatting</span></button> |
| 215 | </div> |
| 216 | </details> |
| 217 | {{end}} |
| 218 | |
| 219 | <details estilo-menubar-menu name="estilo-menus"> |
| 220 | <summary estilo-menubar-title>Data</summary> |
| 221 | <div estilo-menu> |
| 222 | {{/* Both items lead to the table view, and the labels say so. The |
| 223 | grid does not apply a saved sort or filter yet — snapshot never |
| 224 | reads the ViewSpec, only the table does — and a "Sort A→Z" here |
| 225 | that silently moved you to another page would be worse than a |
| 226 | link that admits where it goes. */}} |
| 227 | <a estilo-menu-item href="/sheets/{{.Data.Sheet.ID}}/table"><span estilo-menu-label>Sort and filter, in the table view…</span></a> |
| 228 | {{if .Data.MayWrite}} |
| 229 | <form method="post" action="/sheets/{{.Data.Sheet.ID}}/view"> |
| 230 | <input type="hidden" name="sortCol" value="-1"> |
| 231 | <input type="hidden" name="filterCol" value="-1"> |
| 232 | <button estilo-menu-item type="submit"><span estilo-menu-label>Clear sort and filter</span></button> |
| 233 | </form> |
| 234 | {{end}} |
| 235 | </div> |
| 236 | </details> |
| 237 | |
| 238 | {{/* No Help menu here. The shortcut list moved into the account menu's |
| 239 | Help, as "Keyboard shortcuts…" opening a dialog, where every app's |
| 240 | help lives (spec §8.2). */}} |
| 241 | |
| 242 | </nav> |
| 243 | |
| 244 | {{if .Data.MayWrite}} |
| 245 | {{/* The toolbar. Hidden until commands.js wires it: every control |
| 246 | posts, so an unwired one is a button that does nothing. |
| 247 | |
| 248 | role="toolbar" with a roving tabindex, so the whole strip is one |
| 249 | tab stop rather than fourteen — the same shape the Docs app uses, |
| 250 | deliberately, so the two apps behave alike. */}} |
| 251 | <div data-toolbar role="toolbar" aria-label="Formatting" aria-controls="main" hidden data-needs-js> |
| 252 | |
| 253 | <button type="button" data-tool rst-tip data-cmd="undo" aria-label="Undo" data-tip="Undo ({{.Frame.Keys.undo}})">{{template "glyph-undo"}}</button> |
| 254 | |
| 255 | <span data-tool-rule aria-hidden="true"></span> |
| 256 | |
| 257 | <details rst-dropdown name="rst-tools" data-tool-menu> |
| 258 | <summary data-tool rst-tip aria-label="Number format" data-tip="Number format">123<span rst-caret aria-hidden="true">{{icon "chevron-down"}}</span></summary> |
| 259 | <div rst-dropdown-menu> |
| 260 | {{range .Data.NumberFormats}} |
| 261 | <button type="button" data-cmd="numfmt" data-value="{{.Code}}">{{.Label}}</button> |
| 262 | {{end}} |
| 263 | </div> |
| 264 | </details> |
| 265 | |
| 266 | <span data-tool-rule aria-hidden="true"></span> |
| 267 | |
| 268 | <button type="button" data-tool rst-tip data-cmd="bold" aria-pressed="false" aria-label="Bold" data-tip="Bold ({{.Frame.Keys.bold}})"><b>B</b></button> |
| 269 | <button type="button" data-tool rst-tip data-cmd="italic" aria-pressed="false" aria-label="Italic" data-tip="Italic ({{.Frame.Keys.italic}})"><i>I</i></button> |
| 270 | |
| 271 | <span data-tool-rule aria-hidden="true"></span> |
| 272 | |
| 273 | <details rst-dropdown name="rst-tools" data-tool-menu> |
| 274 | <summary data-tool rst-tip aria-label="Fill colour" data-tip="Fill colour"><span data-fill-chip aria-hidden="true"></span><span rst-caret aria-hidden="true">{{icon "chevron-down"}}</span></summary> |
| 275 | <div rst-dropdown-menu data-swatches> |
| 276 | <button type="button" data-cmd="fill" data-value="none">No fill</button> |
| 277 | {{range .Data.FillSwatches}} |
| 278 | <button type="button" data-cmd="fill" data-value="{{.Name}}" aria-label="Fill {{.Label}}" |
| 279 | data-swatch style="--sheet-h:{{.H}};--sheet-c:{{.C}}">{{.Label}}</button> |
| 280 | {{end}} |
| 281 | </div> |
| 282 | </details> |
| 283 | |
| 284 | <details rst-dropdown name="rst-tools" data-tool-menu> |
| 285 | <summary data-tool rst-tip aria-label="Text colour" data-tip="Text colour"><span data-ink-chip aria-hidden="true">A</span><span rst-caret aria-hidden="true">{{icon "chevron-down"}}</span></summary> |
| 286 | <div rst-dropdown-menu data-swatches> |
| 287 | <button type="button" data-cmd="ink" data-value="none">Default</button> |
| 288 | {{range .Data.InkSwatches}} |
| 289 | <button type="button" data-cmd="ink" data-value="{{.Name}}" aria-label="Text {{.Label}}" |
| 290 | data-swatch data-ink-swatch style="--sheet-kh:{{.H}};--sheet-kc:{{.C}}">{{.Label}}</button> |
| 291 | {{end}} |
| 292 | </div> |
| 293 | </details> |
| 294 | |
| 295 | <span data-tool-rule aria-hidden="true"></span> |
| 296 | |
| 297 | <button type="button" data-tool rst-tip data-cmd="align" data-value="left" aria-pressed="false" aria-label="Align left" data-tip="Align left">{{template "glyph-align-left"}}</button> |
| 298 | <button type="button" data-tool rst-tip data-cmd="align" data-value="center" aria-pressed="false" aria-label="Align centre" data-tip="Align centre">{{template "glyph-align-center"}}</button> |
| 299 | <button type="button" data-tool rst-tip data-cmd="align" data-value="right" aria-pressed="false" aria-label="Align right" data-tip="Align right">{{template "glyph-align-right"}}</button> |
| 300 | |
| 301 | </div> |
| 302 | {{end}} |
| 303 | |
| 304 | </div> |
| 305 | |
| 306 | {{/* Row C — the name box and the formula bar, full width. |
| 307 | The formula bar shows the selected cell's INPUT while the grid |
| 308 | shows its computed value, which is the difference between a |
| 309 | spreadsheet and a table of numbers. It is also the reliable way to |
| 310 | see a formula: the in-cell editor swap is best effort. */}} |
| 311 | <div data-formula-row> |
| 312 | <label class="rst-sr-only" for="namebox">Cell reference</label> |
| 313 | <input rst-input id="namebox" class="rst-mono" data-name-box type="text" |
| 314 | autocomplete="off" spellcheck="false" value="A1"> |
| 315 | <label for="formula" data-fx aria-hidden="true"><i>fx</i></label> |
| 316 | <span class="rst-sr-only" id="formula-label">Selected cell contents</span> |
| 317 | <input rst-input id="formula" class="rst-mono" data-formula-input type="text" |
| 318 | aria-labelledby="formula-label" autocomplete="off" spellcheck="false" |
| 319 | placeholder="Select a cell"{{if not .Data.MayWrite}} readonly{{end}}> |
| 320 | </div> |
| 321 | |
| 322 | <noscript> |
| 323 | {{/* Spec §6 names the grid editor, drag-fill and live sync as the |
| 324 | script-required parts, and everything else as scriptless. A visitor |
| 325 | with scripting off should land somewhere useful rather than on an |
| 326 | empty div, so this is a link and not an apology. */}} |
| 327 | <div rst-callout rst-tone="info"> |
| 328 | <span rst-callout-ic aria-hidden="true">{{icon "info"}}</span> |
| 329 | <div rst-callout-body> |
| 330 | <p>The grid needs JavaScript. Everything it does, the |
| 331 | <a href="/sheets/{{.Data.Sheet.ID}}/table">table view</a> also does — |
| 332 | read every cell, edit any of them, one page at a time.</p> |
| 333 | </div> |
| 334 | </div> |
| 335 | </noscript> |
| 336 | |
| 337 | {{template "contrast-notice" .Data.Warned}} |
| 338 | |
| 339 | <div data-sheet-grid></div> |
| 340 | |
| 341 | {{/* Row D — the tabs along the bottom, and the status that used to sit |
| 342 | in the strip above the grid. |
| 343 | |
| 344 | One tab today, rendered as a link to the page it is already on, so |
| 345 | the strip is real markup rather than a picture of tabs. There is |
| 346 | deliberately no "+": nothing in this build creates, names or |
| 347 | switches a tab, and spec §14 makes "a second tab is reachable in |
| 348 | the UI" the trigger for multi-worksheet import. A + that did |
| 349 | nothing would fake that trigger. */}} |
| 350 | <div data-tabstrip> |
| 351 | <nav data-sheet-tabs aria-label="Sheets in this document"> |
| 352 | <a href="/sheets/{{.Data.Sheet.ID}}" aria-current="page">{{.Data.Sheet.TabName}}</a> |
| 353 | </nav> |
| 354 | {{/* Deliberately NOT role="status". It changes on every arrow key, and |
| 355 | a live region announcing "3 cells" after each one talks over the |
| 356 | cell content a screen reader is trying to read. It is a glance, |
| 357 | and the selection itself is what announces. */}} |
| 358 | <span data-selection-size></span> |
| 359 | {{/* The one place this app can say something transient. role="status" |
| 360 | is right here and wrong on the selection size beside it: this |
| 361 | changes when you ask for something and it did not happen, which |
| 362 | is exactly what a person needs read out, where the selection |
| 363 | size changes on every arrow key and would talk over the cell. */}} |
| 364 | <span data-sheet-note role="status"></span> |
| 365 | <span data-sheet-live data-state="off" role="status">Connecting</span> |
| 366 | </div> |
| 367 | {{end}} |
| 368 | |