| index 53c6055..acfcec0 100644 |
| --- a/internal/docs/views/show.html |
| +++ b/internal/docs/views/show.html |
| @@ -35,35 +35,86 @@ |
| render_test.go's TestOurTemplatesUseTheAttributeGrammar, which |
| holds it against this file too. */}} |
| {{define "title"}}{{.Title}}{{end}} |
| +{{/* brand and account fill two of the topbar shell's own slots |
| + (render.go's shellLayout, rastrillo ui/layouts/topbar.html). |
| + Defined here rather than in views/head.html — which is the one |
| + definition all three pages share — because these two differ per |
| + screen: this page's brand carries a document's name beside the |
| + link home, and sign-in must offer no account menu at all. |
| + |
| + brand emits two elements, not one. The block sits directly inside |
| + [rst-shell-bar], which is a flex row, so the link and the title |
| + become two flex items with the bar's own --rst-sp-4 gap between |
| + them; there is no wrapper because a wrapper would need a layout |
| + rule of its own to reproduce what the bar already does. |
| + |
| + The title is printed as-is, the same way list.html's row titles |
| + are — it is the document's own data, not authored copy, so it |
| + never goes through rastrillo.T. */}} |
| +{{define "brand"}}<a rst-shell-brand href="/">{{.BackLabel}}</a><span docs-shell-title>{{.Title}}</span>{{end}} |
| +{{/* account is the dropdown's MENU BODY, not the whole menu: the |
| + shell owns the <details>/<summary> and this block supplies only |
| + what sits inside [rst-dropdown-menu]. A <form> rather than a |
| + link, because ending a session is a POST (auth.go), and |
| + [rst-dropdown-menu] button already styles a bare button as a |
| + full-width menu row, so it needs no rst-btn of its own — which |
| + is the point of putting it here instead of leaving it as a ghost |
| + button in a page header. */}} |
| +{{define "account"}}<form method="post" action="/signout"><button type="submit">{{.SignOutLabel}}</button></form>{{end}} |
| {{define "content"}}<style> |
| - /* [docs-back]: the route to the document list, above the title. |
| - Chrome, so it takes the app's theme tokens rather than the |
| - canvas's literals. Muted at rest and full-strength on hover and |
| - focus, so it reads as secondary to the document's own title |
| - without dropping under the 4.5:1 body-text floor at rest. |
| - min-block-size states the 24×24 floor (WCAG 2.2 AA SC 2.5.8) |
| - outright rather than hoping padding adds up to it: measured, the |
| - first version of this rule — --rst-fs-sm plus a --rst-sp-1 |
| - padding either side — came to 22px, because --rst-fs-sm is |
| - 12.5px here and not the 14px it was assumed to be. A target size |
| - derived from two tokens is a target size that changes when either |
| - token does, silently. inline-flex is what makes min-block-size |
| - apply to a link at all and centres the label inside it. */ |
| - [docs-back] { |
| - display: inline-flex; |
| - align-items: center; |
| - min-block-size: 24px; |
| - padding-block: var(--rst-sp-1); |
| - font-size: var(--rst-fs-sm); |
| - line-height: 1.15; |
| - color: var(--rst-text-muted); |
| - text-decoration: none; |
| - } |
| - [docs-back]:hover, |
| - [docs-back]:focus-visible { |
| + /* This screen is the one that is not a column of content. It is a |
| + document on a desk, and in both of the applications this layout |
| + language comes from the desk runs to the window's edges. |
| + |
| + [rst-page] — rastrillo's own <main>, written by the topbar shell |
| + outside any block this app can override — is a 64rem centred |
| + column with --rst-sp-5/--rst-sp-4/--rst-sp-6 of padding. That is |
| + right for the list and wrong here, in two separate ways: it |
| + leaves the grey tray as a rectangle floating in white with about |
| + 85px of tray showing either side of the sheet, and it holds the |
| + formatting bar to 64rem when the bar belongs to the window the |
| + way the shell's own bar above it does. |
| + |
| + Cancelling the column here rather than reaching for |
| + `width: 100vw` is the same choice, for the same reason, that the |
| + narrow-screen block below already records: 100vw measures the |
| + viewport INCLUDING a classic scrollbar, so it overhangs by the |
| + scrollbar's width and starts exactly the horizontal scroll that |
| + SC 1.4.10 forbids. A container told to stop padding itself |
| + cannot overhang. |
| + |
| + Scoped by delivery rather than by selector: this stylesheet is |
| + inside show.html's own "content" and ships only with this page, |
| + so the rule reaches neither the list nor sign-in. It is written |
| + bare — no `[docs-canvas] ~` dance — because a page-wide rule |
| + that pretends to be scoped is worse than one that says what it |
| + does. */ |
| + [rst-page] { |
| + max-inline-size: none; |
| + padding: 0; |
| + } |
| + /* [docs-shell-title] is the document's own name in the shell bar, |
| + beside the brand link back to the list — the pattern both |
| + reference applications use, and the reason this page no longer |
| + carries a [rst-page-header] of its own (see render.go's |
| + shellLayout comment for the whole argument). |
| + |
| + It is a <span>, not a heading: the document's title already IS a |
| + heading in the only place that counts, <title>, and a visible h1 |
| + that is not the document's first line was the thing making the |
| + old header read as a screen heading rather than a document's |
| + name. Truncation over wrapping — flex: 0 1 auto with a zero |
| + min-inline-size, which is what lets a flex item shrink below its |
| + content at all — so a long title gives way to the account |
| + control rather than pushing it off the bar. */ |
| + [docs-shell-title] { |
| + flex: 0 1 auto; |
| + min-inline-size: 0; |
| + overflow: hidden; |
| + white-space: nowrap; |
| + text-overflow: ellipsis; |
| color: var(--rst-text); |
| - text-decoration: underline; |
| - text-underline-offset: 0.2em; |
| + font-size: var(--rst-fs-sm); |
| } |
| /* [docs-canvas] is the tray the page sits in — it is chrome around |
| paper, so it stays close to the surrounding theme's tone, but it |
| @@ -75,7 +126,15 @@ |
| color-scheme: light; |
| display: flex; |
| justify-content: center; |
| - padding: var(--rst-sp-6) var(--rst-sp-4); |
| + /* Less air above the sheet than below it. The tray used to carry |
| + --rst-sp-6 on both block edges and sat under a page header that |
| + had already spent --rst-sp-5 of its own below the rule; with the |
| + header gone and the bar sitting directly on the tray, the same |
| + --rst-sp-6 read as a gap rather than as a margin. --rst-sp-5 |
| + above puts the top of the sheet where the eye expects it from |
| + the bar; --rst-sp-6 below is scroll run-out, which wants to stay |
| + generous. */ |
| + padding: var(--rst-sp-5) var(--rst-sp-4) var(--rst-sp-6); |
| background: #d8d8dc; |
| } |
| /* [docs-page]: white, centred, with margins — literal colours per |
| @@ -100,46 +159,51 @@ |
| /* Narrow screens stop simulating paper, because at phone widths the |
| simulation costs more than it says. |
| |
| - Above this breakpoint a reader sees THREE nested insets, and the |
| - first one is easy to miss because it belongs to the framework: the |
| - shell this template renders into (rastrillo's [rst-page]) pads |
| - --rst-sp-4 either side, then [docs-canvas] adds its own 1rem tray |
| - padding, then [docs-page] adds 1in. That inner inch is a real |
| - measurement — it stands for the document's page margin, which page |
| - setup and the layout engine will own properly later — and on a |
| - desktop it is worth every pixel. On a 375px phone the three together |
| - (375 - 32 - 32 - 192) left a 119px column, and at 320px, which this |
| - app supports without exemption (WCAG 2.2 AA SC 1.4.10), 64px: about |
| - five characters of 12pt body text per line. A page margin that |
| - squeezes the page is no longer describing the page. (The character |
| - counts here and below were measured against 12pt Georgia, the |
| - fallback for the serif this canvas used to default to. The default |
| - is Docs Sans now, whose characters run a little wider at the same |
| - size, so the counts are if anything generous — the breakpoint the |
| - arithmetic argues for does not move.) |
| - |
| - (An earlier version of this comment counted two insets and said |
| - 151px and 96px. It was wrong in the direction that flatters the |
| - code, and it was wrong while the paragraph below it described |
| - cancelling the very inset it had forgotten. The numbers here are |
| - measured, not derived.) |
| - |
| - So below 36rem the tray's side padding goes to nothing, its block |
| - padding drops from --rst-sp-6 to --rst-sp-4, the sheet runs edge to |
| - edge, and ONE margin survives inside it. Text goes to 343px at 375px |
| - wide and 288px at 320px. The 1in is untouched at every width above |
| - the breakpoint, so where the print margin can be shown honestly it |
| - still is. |
| + Above this breakpoint a reader sees TWO nested insets: |
| + [docs-canvas]'s own 1rem tray padding, then [docs-page]'s 1in. |
| + That inner inch is a real measurement — it stands for the |
| + document's page margin, which page setup and the layout engine |
| + will own properly later — and on a desktop it is worth every |
| + pixel. On a 375px phone the two together (375 - 32 - 192) leave a |
| + 151px column, and at 320px, which this app supports without |
| + exemption (WCAG 2.2 AA SC 1.4.10), 96px: about seven characters |
| + of 12pt body text per line. A page margin that squeezes the page |
| + is no longer describing the page. (The character counts here and |
| + below were measured against 12pt Georgia, the fallback for the |
| + serif this canvas used to default to. The default is Docs Sans |
| + now, whose characters run a little wider at the same size, so the |
| + counts are if anything generous — the breakpoint the arithmetic |
| + argues for does not move.) |
| + |
| + (The count and the numbers in this paragraph have now been wrong |
| + in both directions, which is worth leaving on the record. They |
| + first said two insets and 151px/96px, forgetting that [rst-page] |
| + pads --rst-sp-4 either side; that was corrected to three and |
| + 119px/64px. Both were true of the tree they were written against. |
| + [rst-page] no longer pads this screen at all — the rule at the top |
| + of this stylesheet zeroes it, so that the tray can reach the |
| + window's edges — which puts the arithmetic back on two insets and |
| + back on the original figures. Re-measure rather than trusting any |
| + of the three the next time something above this changes.) |
| + |
| + So below 36rem the tray's side padding goes to nothing, the sheet |
| + runs edge to edge, its block padding drops from --rst-sp-5/-6 to |
| + --rst-sp-4, and ONE margin survives inside it. Text goes to 343px |
| + at 375px wide and 288px at 320px. The 1in is untouched at every |
| + width above the breakpoint, so where the print margin can be shown |
| + honestly it still is. |
| |
| 36rem is a judgement rather than a formula, and the honest version |
| - of the arithmetic says so: at 576px the three insets leave a 320px |
| - column, about 40 characters of this face, and holding out for the |
| - 45 characters that make a comfortable measure would put the |
| - breakpoint nearer 38rem. 36rem is the choice because reflow only |
| - promises 320px viewports, and between 577 and 600px the paper still |
| - yields 306-330px — cramped, readable, and still recognisably a |
| - page. Phones in portrait are all well below it; a tablet or a small |
| - window keeps the paper. |
| + of the arithmetic says so: at 576px the two insets leave a 352px |
| + column, about 44 characters of this face — a hair under the 45 |
| + that make a comfortable measure, where before the shell's padding |
| + came off it was 320px and about 40. The breakpoint has not been |
| + moved to follow that, deliberately: 36rem was already the |
| + conservative end of its own argument, the change only ever gives |
| + the sheet more room, and moving a breakpoint because a number |
| + improved is how a breakpoint stops meaning anything. Phones in |
| + portrait are all well below it; a tablet or a small window keeps |
| + the paper. |
| |
| The shadow goes with the tray — it exists to lift the sheet off the |
| grey, and once there is no grey beside it, it is a dark smear on the |
| @@ -155,34 +219,26 @@ |
| measured in CSS pixels like any other: US Letter and A4 come out |
| around 700-800px and stay above this breakpoint, but A5 portrait is |
| about 559px and crosses it. Printing a document on A5 would then |
| - drop the 1in and apply a negative margin meant for a phone screen — |
| - an edge-bleeding sheet with 1rem margins, in the one medium where |
| - the page margin is not a metaphor at all. `screen` is what keeps |
| - the paragraph above true. */ |
| + drop the 1in for the 1rem meant for a phone screen — an |
| + edge-bleeding sheet, in the one medium where the page margin is |
| + not a metaphor at all. `screen` is what keeps the paragraph above |
| + true. */ |
| @media screen and (max-width: 36rem) { |
| [docs-canvas] { |
| + /* Zeroing the tray's own padding is now all it takes to reach |
| + the screen edge. It used to take a second rule as well — |
| + `margin-inline: calc(var(--rst-sp-4) * -1)`, cancelling the |
| + --rst-sp-4 that [rst-page] pads on every side — and that rule |
| + is gone because the inset it cancelled is gone: the [rst-page] |
| + override at the top of this stylesheet zeroes that padding at |
| + every width, so there is nothing left here to pull back. |
| + The reasoning it carried has not been lost, only moved: the |
| + argument for cancelling a known padding instead of reaching |
| + for `width: 100vw`, which overhangs by a classic scrollbar's |
| + width and starts the horizontal scroll SC 1.4.10 forbids, is |
| + written out where that override lives. */ |
| padding-inline: 0; |
| padding-block: var(--rst-sp-4); |
| - /* The tray's own padding is not the only inset in the way: the |
| - shell this template renders into ([rst-page], rastrillo's own |
| - chrome) carries --rst-sp-4 either side, measured. Cancelling it |
| - here is what actually reaches the screen edge, and it buys more |
| - than the 32px: with the sheet at 0 and its own 1rem inside it, |
| - the document's first character lines up exactly with the page |
| - header's title above it, which an inset sheet does not do. |
| - |
| - A negative margin in the shell's own token rather than the |
| - 100vw trick on purpose. `width: 100vw` measures the viewport |
| - INCLUDING a classic scrollbar, so on a desktop browser narrowed |
| - under this breakpoint — which is exactly how anyone tests the |
| - 320px reflow this app promises — it overhangs by the scrollbar's |
| - width and starts a horizontal scroll: the same WCAG 1.4.10 |
| - failure this whole block exists to fix, reintroduced by the fix. |
| - Cancelling a known padding cannot overhang. It does couple this |
| - rule to [rst-page]'s padding staying --rst-sp-4; if that ever |
| - changes, the sheet sits slightly inset or slightly proud, which |
| - is visible and harmless rather than silent. */ |
| - margin-inline: calc(var(--rst-sp-4) * -1); |
| } |
| [docs-page] { |
| inline-size: 100%; |
| @@ -447,29 +503,88 @@ |
| outline: none; |
| } |
| |
| - /* [docs-toolbar] is chrome, not paper, so — unlike [docs-page] and |
| - [docs-text] above — it uses the app's own theme tokens and |
| - repaints with the reader's light/dark preference, exactly as the |
| - page header does. It is a sibling of [docs-canvas], never inside |
| - [docs-editor]: ProseMirror's {mount: el} takes over everything |
| - inside that element, so a toolbar rendered in there would be |
| - replaced by the document on the first mount. |
| - Sticky, because a formatting bar that scrolls away with the first |
| - page is not there when a person is writing on the third one. */ |
| - [docs-toolbar] { |
| + /* [docs-bar] is the strip between the shell's own bar and the |
| + paper: formatting on the left, the document's own actions on the |
| + right. Both reference applications put exactly this row in |
| + exactly this place, and the right-hand end is why it exists as a |
| + container at all — the formatting controls fill about half the |
| + width, and before the exports moved down here the other half was |
| + empty while three ghost buttons crowded the header above. |
| + |
| + It carries what [docs-toolbar] used to carry itself: chrome, not |
| + paper, so unlike [docs-page] and [docs-text] it takes the app's |
| + theme tokens and repaints with the reader's light/dark |
| + preference. Sticky, because a formatting bar that scrolls away |
| + with the first page is not there when a person is writing on the |
| + third one — and sticky on the container rather than on the |
| + toolbar so the exports stay put with it instead of scrolling out |
| + from beside it. |
| + |
| + It wraps, and the order matters when it does: at 320px the |
| + formatting groups take the first line or two and the exports drop |
| + to their own, which is why [docs-doc-actions] uses an auto margin |
| + rather than `justify-content: space-between` — an auto margin |
| + collapses on a wrapped line and leaves the exports flush left |
| + under the toolbar, where space-between would strand them apart |
| + from each other at opposite screen edges. */ |
| + [docs-bar] { |
| position: sticky; |
| inset-block-start: 0; |
| z-index: 2; |
| display: flex; |
| flex-wrap: wrap; |
| align-items: center; |
| - gap: var(--rst-sp-1); |
| + gap: var(--rst-sp-2); |
| padding: var(--rst-sp-2) var(--rst-sp-4); |
| background: var(--rst-surface); |
| border-block-end: 1px solid var(--rst-line); |
| } |
| + /* [docs-toolbar] is a sibling of [docs-canvas], never inside |
| + [docs-editor]: ProseMirror's {mount: el} takes over everything |
| + inside that element, so a toolbar rendered in there would be |
| + replaced by the document on the first mount. */ |
| + [docs-toolbar] { |
| + display: flex; |
| + flex-wrap: wrap; |
| + align-items: center; |
| + gap: var(--rst-sp-1); |
| + } |
| + /* The export links, at the bar's trailing end. A sibling of |
| + [docs-toolbar] and deliberately outside it: [docs-toolbar] is a |
| + real role="toolbar" whose controls are one roving-tabindex widget |
| + (fixtures/toolbar.mjs, which reads [data-docs-command] within its |
| + own root), and a link that is not a formatting control has no |
| + business joining that widget's arrow-key order. Outside it, the |
| + exports are two ordinary tab stops, which is what they should be. */ |
| + [docs-doc-actions] { |
| + display: flex; |
| + flex: none; |
| + align-items: center; |
| + gap: var(--rst-sp-2); |
| + margin-inline-start: auto; |
| + } |
| + /* A group wraps inside itself, and that is not the same claim as |
| + the gap/rule one below. The three <select>s — Style, Font, Size — |
| + are one group, and a <select>'s automatic minimum size is its |
| + widest option, so at 320px (§10.3's reflow width, which this app |
| + supports without exemption) the three of them measure wider than |
| + the bar and the page grows a horizontal scrollbar: SC 1.4.10, |
| + failed. Letting the group wrap and letting each control shrink |
| + below its content — min-inline-size: 0, which is what actually |
| + lifts that automatic minimum — is what keeps the row inside the |
| + viewport. The rule between groups survives a wrap, so the |
| + grouping still reads once it has happened. |
| + |
| + This predates the bar being restructured around it; it was not |
| + introduced by that work and is, if anything, slightly less bad |
| + than it was. The toolbar used to sit inside [rst-page] and get |
| + 320 - 32 (the shell's padding) - 32 (its own) = 256px at this |
| + width; it now gets 320 - 32 = 288px, because the shell no longer |
| + pads this screen. Both overflow. Measured at 320px in Chromium, |
| + before and after. */ |
| [docs-toolbar-group] { |
| display: flex; |
| + flex-wrap: wrap; |
| align-items: center; |
| gap: 2px; |
| } |
| @@ -560,6 +675,15 @@ |
| } |
| [docs-tool-select] { |
| min-block-size: 28px; |
| + /* min-inline-size: 0 lifts the automatic minimum size, which for |
| + a <select> is its widest option — see [docs-toolbar-group]'s |
| + own comment for what that overflows and where. Wrapping is what |
| + actually resolves the 320px case, and it is the better of the |
| + two outcomes: these three controls carry words, and a control |
| + shrunk under its widest word is a control you cannot read. This |
| + only takes over if a viewport is narrow enough that even one of |
| + them will not fit on a line of its own. */ |
| + min-inline-size: 0; |
| max-inline-size: 11rem; |
| padding: 0 var(--rst-sp-1); |
| border: 1px solid var(--rst-line); |
| @@ -568,13 +692,6 @@ |
| color: var(--rst-text); |
| font-size: var(--rst-fs-sm); |
| } |
| - /* The export links, in the page header beside sign-out. */ |
| - [docs-header-actions] { |
| - display: flex; |
| - align-items: center; |
| - gap: var(--rst-sp-2); |
| - } |
| - |
| /* [docs-save-status] (fix round 1, F2) is chrome, not paper — it |
| reports on the save itself, not the document's own content — so |
| unlike [docs-page]/[docs-text] above it uses the app's own theme |
| @@ -627,49 +744,39 @@ |
| display: inline; |
| } |
| </style> |
| -<header rst-page-header> |
| - <div rst-page-header-titles> |
| - {{/* [docs-back] is this page's only in-app navigation, and until |
| - it landed there was none: the header below held sign-out and |
| - nothing else, so arriving here by a bookmark or a mailed link |
| - left a person with no route to their own list at all. It sits |
| - inside [rst-page-header-titles] rather than beside it because |
| - [rst-page-header]'s space-between rule wants at most two |
| - top-level children (titles, one action area) — the same |
| - constraint list.html's [docs-header-actions] comment records |
| - from the other direction. |
| - |
| - A breadcrumb, not an arrow: the label is the destination |
| - screen's own name (showPage.BackLabel, which *is* |
| - docs.list.title), so it needs no glyph to be understood and |
| - nothing about it has to be mirrored for the RTL locales in |
| - this app's twelve. */}} |
| - <a docs-back href="/">{{.BackLabel}}</a> |
| - <h1>{{.Title}}</h1> |
| - </div> |
| - {{/* Sign-out control (POST /signout, auth.go) — same reasoning as |
| - list.html's own [docs-header-actions] comment: a session lasts |
| - 30 days and this page needs a working way to end one on |
| - purpose, not just list.html. |
| - |
| - The two export links sit beside it, in a wrapper this header |
| - now needs (it used to have a single form as its one flex |
| - child). Ordinary links to ordinary URLs, so both work with no |
| - JavaScript at all — spec §6.2 — and both are GETs of a document |
| - the signed-in person already owns, so neither needs a form or a |
| - CSRF token. download tells the browser to save rather than |
| - navigate; the filename it uses comes from the server's |
| - Content-Disposition, not from this attribute, because a title |
| - is arbitrary text a person typed and sanitising it belongs |
| - server-side (handlers.go's exportFilename). */}} |
| - <div docs-header-actions> |
| - <a rst-btn="ghost" href="/d/{{.Sqid}}/export.pdf" aria-label="{{.ExportPDFLabel}}" download>{{.ExportPDFShort}}</a> |
| - <a rst-btn="ghost" href="/d/{{.Sqid}}/export.md" aria-label="{{.ExportMarkdownLabel}}" download>{{.ExportMarkdownShort}}</a> |
| - <form method="post" action="/signout"> |
| - <button rst-btn="ghost" type="submit">{{.SignOutLabel}}</button> |
| - </form> |
| - </div> |
| -</header> |
| +{{/* This page has no [rst-page-header]. It used to, and what it held |
| + is not gone — every piece of it moved to where its equivalent |
| + lives in the two applications this layout language comes from. |
| + The argument for the move is in render.go's shellLayout comment; |
| + what follows is only the map. |
| + |
| + The route back to the list was [docs-back], a breadcrumb above |
| + the title, and is now the shell bar's brand link — same |
| + destination, same label (showPage.BackLabel, which *is* |
| + docs.list.title, so it needs no glyph to be understood and |
| + nothing about it has to be mirrored for the RTL locales in this |
| + app's twelve), one row up. Until [docs-back] landed this page had |
| + no in-app navigation at all, so arriving by a bookmark or a mailed |
| + link left a person with no route to their own list; promoting it |
| + to the brand slot keeps that fixed and makes it the same control |
| + on every screen rather than one this page draws for itself. |
| + |
| + The title is [docs-shell-title], beside that brand link. |
| + |
| + Sign-out (POST /signout, auth.go) is in the bar's account menu, |
| + which is where a 30-day session belongs and is now the one place |
| + both screens end one. |
| + |
| + The exports are in [docs-doc-actions] below, at the formatting |
| + bar's trailing end. Ordinary links to ordinary URLs, so both work |
| + with no JavaScript at all — spec §6.2 — and both are GETs of a |
| + document the signed-in person already owns, so neither needs a |
| + form or a CSRF token. download tells the browser to save rather |
| + than navigate; the filename it uses comes from the server's |
| + Content-Disposition, not from this attribute, because a title is |
| + arbitrary text a person typed and sanitising it belongs |
| + server-side (handlers.go's exportFilename). */}} |
| +<div docs-bar> |
| {{/* The formatting bar (fixtures/toolbar.mjs). Rendered server-side, |
| with every label resolved from the catalog (render.go's |
| docs.editor.toolbar.*) — the bundle carries no copy of its own, |
| @@ -756,6 +863,11 @@ |
| </button> |
| </div> |
| </div> |
| +<div docs-doc-actions> |
| + <a rst-btn="ghost" href="/d/{{.Sqid}}/export.pdf" aria-label="{{.ExportPDFLabel}}" download>{{.ExportPDFShort}}</a> |
| + <a rst-btn="ghost" href="/d/{{.Sqid}}/export.md" aria-label="{{.ExportMarkdownLabel}}" download>{{.ExportMarkdownShort}}</a> |
| +</div> |
| +</div> |
| <div docs-canvas> |
| <div docs-page> |
| {{/* docs-editor is the ProseMirror mount point (fixtures/editor-entry.js, |