diff --git a/backend/locales/en.json b/backend/locales/en.json index 8563f493e..160cb7e40 100644 --- a/backend/locales/en.json +++ b/backend/locales/en.json @@ -1597,6 +1597,7 @@ "common.actions.optimize": "Optimize", "common.actions.page": "Page", "common.actions.preview": "Preview", + "common.actions.print": "Print", "common.actions.proceed": "Proceed", "common.actions.properties": "Properties", "common.actions.refresh": "Refresh", @@ -2282,7 +2283,6 @@ "history.downloadFailed": "Failed to download this version.", "history.downloadVersion": "Download Version", "history.emptyPage": "Nothing", - "history.exportPdf": "Export to PDF", "history.inline": "Inline", "history.loadFailed": "Failed to load the page history.", "history.none": "No history has been recorded for this page yet.", diff --git a/backend/modules/extensions/puppeteer/definition.yml b/backend/modules/extensions/puppeteer/definition.yml index e012a2c47..e8ee6c1c6 100644 --- a/backend/modules/extensions/puppeteer/definition.yml +++ b/backend/modules/extensions/puppeteer/definition.yml @@ -1,9 +1,9 @@ key: puppeteer title: Puppeteer description: >- - Headless Chromium browser. Required to export pages as PDF and to render content elements on the - server, such as Mermaid or PlantUML diagrams. Installing it downloads a Chromium build of a few - hundred megabytes, unless the server already provides one through PUPPETEER_EXECUTABLE_PATH. + Headless Chromium browser. Required to re-render a page from its source on the server: the markdown + renderer lives in the browser, so the server drives one. Installing it downloads a Chromium build of + a few hundred megabytes, unless the server already provides one through PUPPETEER_EXECUTABLE_PATH. website: 'https://pptr.dev' detect: type: module diff --git a/blocks/block-tabs/component.js b/blocks/block-tabs/component.js index 37b387d72..688dcf422 100644 --- a/blocks/block-tabs/component.js +++ b/blocks/block-tabs/component.js @@ -165,6 +165,43 @@ Content of the second tab. margin-bottom: 0; } + /* + ON PAPER + -------- + + The strip goes. A tabset shows one panel at a time because a screen has one place to put it, + and on paper every panel prints, one after another, each under a bar carrying its own label -- + so a row of buttons naming panels that are all right there is a control with nothing left to + control. + + Only this half is here. Showing the panels and drawing those bars belongs to the app's print + stylesheet, because the panels are slotted light DOM and which one is showing is an inline + display this block writes onto them: nothing in here reaches either. See the block-tab rules in + the print section of the app's css/_page-contents.scss. + + The frame stays, and it is the reason for the one thing that does change here: with the strip + gone it is all that says these sections were one set, so it keeps its border and its radius and + gives up only the shadow, which prints as a grey smudge along two edges and lifts nothing. + + What the panel gives up is its padding, down from 16px/20px to 8px all round. On screen that + inset is what holds a panel's content off the frame around it; on paper it is 40px of the + measure spent on white space that the label bar and the spine inside it already mark out -- + and it is spent twice, since each tab pads itself off its own spine as well. Eight is what + keeps the content from touching the frame, and no more than that. + */ + @media print { + .strip { + display: none; + } + .tabs, + :host([dark]) .tabs { + box-shadow: none; + } + .panel { + padding: 8px; + } + } + :host { --tabs-border: #e0e0e0; --tabs-strip-bg: linear-gradient(to bottom, #fdfdfd, #eeeeee); @@ -179,13 +216,27 @@ Content of the second tab. --tabs-active-label: var(--tabs-active-fg); --tabs-panel-bg: #fff; } - :host([dark]) { - --tabs-border: rgba(255, 255, 255, 0.15); - --tabs-strip-bg: linear-gradient(to bottom, #1b212a, #12161d); - --tabs-inactive-fg: rgba(255, 255, 255, 0.7); - /* -> A mix of --q-primary, so a re-themed site's own hue comes with it. See block-index. */ - --tabs-active-label: var(--color-primary-light); - --tabs-panel-bg: #1e232a; + /* + The dark palette, and only where there is a lit screen to read it on. + + The dark attribute comes off the body class, which says nothing about the medium -- so without + this a page printed from the dark theme printed a near-black panel, and the app prints the text + inside it in black ink (the article's palette is light on paper, by the same reasoning as here: + see the note on the dark block in css/_page-contents.scss). Black on #1e232a is a panel of + content that cannot be read at all. + + Stated as a media query rather than unpicked token by token in the print block above, so that a + token added here later cannot quietly miss it. + */ + @media not print { + :host([dark]) { + --tabs-border: rgba(255, 255, 255, 0.15); + --tabs-strip-bg: linear-gradient(to bottom, #1b212a, #12161d); + --tabs-inactive-fg: rgba(255, 255, 255, 0.7); + /* -> A mix of --q-primary, so a re-themed site's own hue comes with it. See block-index. */ + --tabs-active-label: var(--color-primary-light); + --tabs-panel-bg: #1e232a; + } } ` } diff --git a/frontend/src/components/PageHeader.vue b/frontend/src/components/PageHeader.vue index 887abdfb8..2a0e6ab92 100644 --- a/frontend/src/components/PageHeader.vue +++ b/frontend/src/components/PageHeader.vue @@ -126,9 +126,9 @@ dense icon="la:print" color="grey" - aria-label="Print" + :aria-label="t(`common.actions.print`)" @click="printPage"> - Print + {{ t('common.actions.print') }} - {{ t('history.exportPdf') }} + :aria-label="t(`common.actions.print`)" + @click="printPage"> + {{ t('common.actions.print') }}
diff --git a/frontend/src/css/_page-contents.scss b/frontend/src/css/_page-contents.scss index 367f9bbb4..ef669ed05 100644 --- a/frontend/src/css/_page-contents.scss +++ b/frontend/src/css/_page-contents.scss @@ -219,80 +219,96 @@ */ isolation: isolate; + /* + The dark palette, and only where there is a lit screen to read it on. + + `@media not print` rather than a print block that puts each of these back one at a time: this is + two dozen tokens and it grows, and the print block below would have to shadow every one of them -- + at a higher specificity, since `.body--dark .page-contents` outranks this block -- or quietly print + whichever it had missed. It missed most of them: a page printed from the dark theme came out with + invisible table and code rules (white at 16% opacity, on white paper), the lighter link blue, and + the GitHub-dark syntax colours. + + So the condition is stated once, here, where it is also the true one: this palette is for a screen + that emits light. Paper does not, and what it falls back to is the light palette declared above, + which is the one the print block below was written against. + */ @at-root .body--dark & { - --content-ink: rgba(255, 255, 255, 0.87); - --content-ink-muted: rgba(255, 255, 255, 0.62); - --content-ink-faint: rgba(255, 255, 255, 0.42); + @media not print { + --content-ink: rgba(255, 255, 255, 0.87); + --content-ink-muted: rgba(255, 255, 255, 0.62); + --content-ink-faint: rgba(255, 255, 255, 0.42); - /* -> The mid-tone brand blue is too dim on a dark surface; see `--color-primary-light` */ - --content-link: var(--color-primary-light); - --content-h1: var(--color-primary-light); + /* -> The mid-tone brand blue is too dim on a dark surface; see `--color-primary-light` */ + --content-link: var(--color-primary-light); + --content-h1: var(--color-primary-light); - --content-rule: rgba(255, 255, 255, 0.16); - --content-rule-strong: rgba(255, 255, 255, 0.32); - --content-h2-rule: rgba(255, 255, 255, 0.45); + --content-rule: rgba(255, 255, 255, 0.16); + --content-rule-strong: rgba(255, 255, 255, 0.32); + --content-h2-rule: rgba(255, 255, 255, 0.45); - /* Matches the app's own `dark-4` panel, so code sits a step above the page rather than below it */ - --content-surface-code: #161b22; - --content-surface-alt: rgba(255, 255, 255, 0.07); + /* Matches the app's own `dark-4` panel, so code sits a step above the page rather than below it */ + --content-surface-code: #161b22; + --content-surface-alt: rgba(255, 255, 255, 0.07); - /* - The head keeps its colours -- see the light block -- but the shadow under the table cannot: a 5% - black drop is invisible on a dark page. Deepened, and no wider, so it still reads as the panel - being lifted rather than as a glow around it. - */ - --content-table-shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 3px 10px rgba(0, 0, 0, 0.32); + /* + The head keeps its colours -- see the light block -- but the shadow under the table cannot: a 5% + black drop is invisible on a dark page. Deepened, and no wider, so it still reads as the panel + being lifted rather than as a glow around it. + */ + --content-table-shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 3px 10px rgba(0, 0, 0, 0.32); - /* -> Lifted off the page rather than sunk into it, which is the same intent as the light theme's - pair: the dark page is already darker than anything a row could be tinted towards */ - --content-table-row: rgba(255, 255, 255, 0.02); - --content-table-row-alt: rgba(255, 255, 255, 0.055); + /* -> Lifted off the page rather than sunk into it, which is the same intent as the light theme's + pair: the dark page is already darker than anything a row could be tinted towards */ + --content-table-row: rgba(255, 255, 255, 0.02); + --content-table-row-alt: rgba(255, 255, 255, 0.055); - /* -> Back to `.card-header`'s own near end: on a dark page the bar reads as dark as it measures, - and the extra step the light theme needs would leave the head sitting above the page */ - --content-table-head: #1e232a; + /* -> Back to `.card-header`'s own near end: on a dark page the bar reads as dark as it measures, + and the extra step the light theme needs would leave the head sitting above the page */ + --content-table-head: #1e232a; - --content-mark: #6b5d13; + --content-mark: #6b5d13; - /* -> Lifted off the dark page, and mixed from the lighter blue for the same reason links are */ - --content-links-row: rgba(255, 255, 255, 0.05); - --content-links-row-hover: color-mix(in srgb, var(--color-primary-light) 14%, transparent); - /* -> Barely there: on a dark page the same light that reads as a rounded top edge reads as a bar */ - --content-links-sheen: rgba(255, 255, 255, 0.045); - /* -> Deepened, and no wider, for the reason the table's is: a 7% black drop is invisible here */ - --content-links-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); + /* -> Lifted off the dark page, and mixed from the lighter blue for the same reason links are */ + --content-links-row: rgba(255, 255, 255, 0.05); + --content-links-row-hover: color-mix(in srgb, var(--color-primary-light) 14%, transparent); + /* -> Barely there: on a dark page the same light that reads as a rounded top edge reads as a bar */ + --content-links-sheen: rgba(255, 255, 255, 0.045); + /* -> Deepened, and no wider, for the reason the table's is: a 7% black drop is invisible here */ + --content-links-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); - /* - Lighter than the light theme's, because the box has to be seen against a dark page -- but held - well below the white of the tick on it, which is what makes the tick readable. - */ - --content-tick: rgba(255, 255, 255, 0.38); - /* -> A white box would be a lit square on a dark page; the empty one is just its outline */ - --content-tick-empty: transparent; - - --content-info: #6ab0ff; - --content-info-wash: rgba(106, 176, 255, 0.12); - --content-success: #4ecf9a; - --content-success-wash: rgba(78, 207, 154, 0.12); - --content-warning: #f3b661; - --content-warning-wash: rgba(243, 182, 97, 0.12); - --content-danger: #ff8b8b; - --content-danger-wash: rgba(255, 139, 139, 0.12); - --content-important: #c4a7ff; - --content-important-wash: rgba(196, 167, 255, 0.12); - - --content-code-comment: #8b949e; - --content-code-keyword: #ff7b72; - --content-code-string: #a5d6ff; - --content-code-number: #79c0ff; - --content-code-title: #d2a8ff; - --content-code-type: #ffa657; - --content-code-attr: #79c0ff; - --content-code-meta: #8b949e; - --content-code-addition: #56d364; - --content-code-addition-wash: rgba(46, 160, 67, 0.15); - --content-code-deletion: #ffa198; - --content-code-deletion-wash: rgba(248, 81, 73, 0.15); + /* + Lighter than the light theme's, because the box has to be seen against a dark page -- but held + well below the white of the tick on it, which is what makes the tick readable. + */ + --content-tick: rgba(255, 255, 255, 0.38); + /* -> A white box would be a lit square on a dark page; the empty one is just its outline */ + --content-tick-empty: transparent; + + --content-info: #6ab0ff; + --content-info-wash: rgba(106, 176, 255, 0.12); + --content-success: #4ecf9a; + --content-success-wash: rgba(78, 207, 154, 0.12); + --content-warning: #f3b661; + --content-warning-wash: rgba(243, 182, 97, 0.12); + --content-danger: #ff8b8b; + --content-danger-wash: rgba(255, 139, 139, 0.12); + --content-important: #c4a7ff; + --content-important-wash: rgba(196, 167, 255, 0.12); + + --content-code-comment: #8b949e; + --content-code-keyword: #ff7b72; + --content-code-string: #a5d6ff; + --content-code-number: #79c0ff; + --content-code-title: #d2a8ff; + --content-code-type: #ffa657; + --content-code-attr: #79c0ff; + --content-code-meta: #8b949e; + --content-code-addition: #56d364; + --content-code-addition-wash: rgba(46, 160, 67, 0.15); + --content-code-deletion: #ffa198; + --content-code-deletion-wash: rgba(248, 81, 73, 0.15); + } } /* -> The page's own margins are the container's business; the first and last block give theirs up */ @@ -1850,6 +1866,13 @@ // PRINT // --------------------------------------------------------------------------- + /* + Written against the light palette above and reached from either theme, because the dark one is + screen-only -- see the note on it. + + The chrome around the article is dealt with in `css/_print.scss`, which is the rest of this story: + what is hidden, and what is left of the shell once it stops being a viewport. + */ @media print { /* Washes and tints cost ink and print muddy; the structure has to come from the rules instead */ --content-ink: #000; @@ -1869,6 +1892,18 @@ /* -> And the banding, which on paper is grey ink over every other row for no gain */ --content-table-row: transparent; --content-table-row-alt: transparent; + /* + The article column prints with no padding of its own -- the sheet's own margin is the margin + now, see `_print.scss` -- so the rule under the page title has nothing left to reach back + through. Left at 1rem it would overhang the text by exactly the padding that is gone. + */ + /* + `0px` and not `0`: the title's rule positions itself with `calc(-1 * var(--content-bleed))`, and + a unitless zero makes that a NUMBER rather than a length. The declaration is then invalid, `left` + falls back to `auto` against a `right: 0`, and a rule with no width to shrink to disappears -- + which is exactly how the page title lost the rule under it on paper. + */ + --content-bleed: 0px; font-size: 11pt; line-height: 1.55; @@ -1892,9 +1927,174 @@ break-after: avoid; } - /* -> On paper there is no "leaves the wiki": every link is equally unfollowable */ + /* + The rule under a title and a section heading is a gradient, which is to say a BACKGROUND -- and a + print dialog leaves background graphics off by default, so the one device that makes the sections + of a long page findable was the one thing that did not survive being printed. Same pair as a done + task item further up, for the same reason. + */ + h1::after, + h2::after { + print-color-adjust: exact; + -webkit-print-color-adjust: exact; + } + + /* -> A single line of a paragraph stranded on either side of a break; three is the usual floor */ + p, + li { + orphans: 3; + widows: 3; + } + + /* + Code wraps. + + On screen a long line scrolls sideways inside its panel and nothing is lost. On paper the panel is + as wide as the sheet and everything past that edge is simply gone -- silently, which is the worst + way for a code sample to be wrong. So the rule the screen keeps a few hundred lines up ("code does + not wrap: a wrapped line is a different line, and indentation stops meaning anything") is the + wrong trade here and is inverted: a wrapped line is still readable, an absent one is not. + + `overflow-wrap: break-word` breaks only where a single unbroken run is wider than the panel by + itself -- a URL in a comment, a base64 literal, a long path -- so ordinary code still gives way at + its spaces and its indentation survives. + + The `width: max-content` the code carries has to go with it: it sizes the block to its longest + line, which leaves `pre-wrap` nothing to wrap against. + */ + pre { + overflow-x: visible; + white-space: pre-wrap; + overflow-wrap: break-word; + } + pre.codeblock > code { + width: auto; + } + + /* + And the line-number gutter goes with it. + + Neither the digits nor a highlighted line's wash is part of the text: they are an absolutely + positioned layer of empty rows, each exactly `1lh` tall, that lands on its line by geometry alone. + A wrapped line is taller than one row, so from the first wrap onwards every number sits against + the wrong line -- and a code sample whose numbers lie is worse than one with no numbers at all. + + The padding the gutter had reserved goes back to the panel's own, so a numbered block does not + print with an empty 3.6rem margin down its left side. + */ + pre.codeblock .line-numbers-rows { + display: none; + } + pre.codeblock.line-numbers { + --code-pad-left: var(--code-pad-x); + } + + /* + Every panel of a tabset, one after another. + + A tabset shows one panel at a time because a screen has one place to put it. Paper has as many + places as it needs, and a panel that does not print is content the reader is never told exists -- + so they all print, each under a bar carrying its own label. `block-tabs` hides its strip for the + same reason, in its own stylesheet, where the strip lives. + + Here rather than in that block because of where the panels live. `block-tab` has no shadow root at + all -- its content is page content, styled by this file, which is the whole point of it -- and + which panel is showing is an inline `display` its parent writes onto the element. So this is at + once the only stylesheet that reaches them and the only weight that beats an inline style. + + `[label]` guards the bar: the label is an attribute, and a tab written without one would otherwise + print an empty rule. + */ + block-tab { + display: block !important; + } + block-tab + block-tab { + margin-top: 1.5em; + } + + /* + A solid bar and a spine down the side, and they are the same colour on purpose: at the top of a + panel the two meet and read as one shape turning the corner, which is what marks out where a + panel starts AND how far down the sheet it runs. That second half is the harder one -- a rule + under a label says where a panel begins and nothing at all about where it ends, and on paper + there is no strip left above to hold the set together. + + This is the one place here that spends solid ink, and it is a deliberate departure from the note + a few rules up about a table's head: there, the bold text and the ruled grid already say "head", + so the near-black bar was ink buying nothing. Here the bar is the ONLY thing separating one + panel's content from the next one's, and drawn as a hairline and small caps it was too quiet to + do it. + + Dark grey rather than the near-black the table's head uses on screen: it is enough for white to + read on (about 9:1) and costs a third less ink over a bar that repeats once per panel. + + The bar's negative left margin is what takes it back over the padding so that it meets the spine + rather than starting inboard of it. + + `print-color-adjust` is what makes any of it appear, for the reason a done task item carries the + same pair: a print dialog leaves background graphics OFF by default, so the fill would print as + nothing and the white label as white on white -- the panel losing its heading altogether, which + is worse than the quiet version this replaces. The property is inherited, so declaring it here + covers the bar as well as the spine. + */ + block-tab[label] { + --content-tab-bar: #3a4049; + /* + Held as a property, and in `rem`, because the bar's negative margin has to cancel this padding + EXACTLY. Written as `0.9em` in both places it did not: an `em` here resolves against the + panel's font size and the same `em` on the bar against the bar's own smaller one, so the two + came out 13.2px and 10.7px and left a 2px seam of white between the spine and the bar. + */ + --content-tab-pad: 0.75rem; + + border-left: 4px solid var(--content-tab-bar); + padding-left: var(--content-tab-pad); + print-color-adjust: exact; + -webkit-print-color-adjust: exact; + } + block-tab[label]::before { + content: attr(label); + display: block; + margin: 0 0 0.9em calc(-1 * var(--content-tab-pad)); + padding: 0.35em var(--content-tab-pad); + background-color: var(--content-tab-bar); + color: #fff; + font-size: 0.8125em; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + } + + /* + The marker becomes the address. + + On screen an external link only has to SAY that it leaves the wiki -- where it goes is in the + status bar, one hover away, and a click away after that. On paper there is neither: a link is + worth exactly the address printed beside it, so the arrow gives way to the href itself. + + Still the renderer's class rather than a selector on the href, for the reason the screen rule + gives: whether an address is off-site is a question about its host, and that is settled at + render time. A relative link spelled out in full would be a path nobody can type. + + Everything the screen rule set up for a masked glyph has to be undone, since this is the same + pseudo-element carrying text now rather than a shape -- the mask would otherwise cut the + address down to a 0.8em square of nothing. + + A link whose text is already its own URL prints it twice. That is the cost of the rule, and it + is a good deal cheaper than a printed reference nobody can follow. + */ a.is-external-link::after { - content: none; + content: ' (' attr(href) ')'; + display: inline; + width: auto; + height: auto; + margin-left: 0; + background-color: transparent; + mask-image: none; + opacity: 1; + font-size: 0.85em; + /* -> A URL has no spaces to break at and would otherwise run off the edge of the sheet */ + word-break: break-all; } } } diff --git a/frontend/src/css/_print.scss b/frontend/src/css/_print.scss new file mode 100644 index 000000000..f0c6d7796 --- /dev/null +++ b/frontend/src/css/_print.scss @@ -0,0 +1,300 @@ +/* + THE PRINTED PAGE + ================ + + What the page view and the version view become on paper. This file is the app SHELL's half of it -- + the chrome a page is drawn in. Two other places hold the rest, each because the rules there have to + sit next to the ones they override: `_page-contents.scss` typesets the article, over the content + palette declared at the top of that same file, and `SiteBanner.vue` handles the banner, for the + cascade reason set out at the foot of this file. + + Two problems, and they are different ones. + + The first is that the shell IS a viewport. `WLayout` is `height: 100vh; overflow: hidden`, and the + article scrolls inside `.page-container-scrl` rather than the window -- which is what keeps the + header, the sidebar and the action rail still while a reader scrolls. On paper that means exactly one + screenful is printed and the rest of the page is clipped away with nothing to say it happened. So + every fixed height and every scroll container has to be unwound back into normal flow before + anything else here is worth doing. + + The second is that most of what is on screen is not the document. Someone printing a page wants the + page: its title, where it sits, when it was last written to, and its contents. The navigation, the + contents column, the action rail, the footer and the floating buttons are how they GOT here, not + what they are taking away. + + `!important` throughout, deliberately, and it is not laziness: what these rules overrule is inline + styles (both views write `height: 100%` on the article column in the markup) and scoped rules from + single-file components, which carry a data attribute and so outrank a plain class whatever the order. + A print stylesheet that loses those fights prints a blank sheet and gives no clue why. + + Colours are the exception -- there `!important` would be the wrong tool, because the rules being + beaten are per-theme ones written as `.body--dark .page-header-title`. Naming both theme classes + matches that specificity, and `app.scss` loads this file last so the tie goes to print. +*/ + +@media print { + /* --------------------------------------------------------------------------- + THE SHEET + --------------------------------------------------------------------------- */ + + /* + The margin the content gets. + + Nothing here draws a running header or a page number: the browser prints its own -- title, URL, + date, page count -- outside this margin, and they are the reader's to keep or turn off in the print + dialog. CSS could only compete with them, and not well, since no engine this app targets fills a + page margin box with the document's own URL. + */ + @page { + margin: 16mm 14mm; + } + + /* --------------------------------------------------------------------------- + THE SHELL STOPS BEING A VIEWPORT + --------------------------------------------------------------------------- */ + + html, + body { + height: auto !important; + overflow: visible !important; + /* -> Every layout paints `body` in the dark theme; on paper the sheet is the ground */ + background: #fff !important; + } + + /* + The shell's grid becomes a stack. + + Its three columns exist to seat the drawers beside the page, and both drawers are gone below -- but + that is not enough on its own: a grid item is stretched to its track, and the track is sized off a + viewport height that means nothing here. + */ + .w-layout--page, + .w-layout--container { + display: block !important; + height: auto !important; + overflow: visible !important; + } + + /* + And every box between the shell and the article gives up whatever it was doing to make one screenful + fit -- the flex column, the claimed height, the scroll container. What is left is blocks in normal + flow, which is the only arrangement that can run over a page break. + */ + .w-page-container, + .w-page, + .page-container, + .page-container-scrl, + .page-container-body, + .w-scroll-area { + display: block !important; + height: auto !important; + min-height: 0 !important; + max-height: none !important; + overflow: visible !important; + flex: none !important; + } + + /* + The article column's own padding goes: `@page` above is the margin now, and a second one inside it + only narrows the measure. `--content-bleed` is the padding this one is reaching back through (see + `_page-contents.scss`), so the print block there zeroes it to match -- left at 1rem the rule under + the page title would overhang the text by exactly the padding that is no longer there. + */ + .page-container-body { + /* + Sides only, so the article keeps the full measure -- but not the top. + + `.page-contents > :first-child` has its top margin zeroed, since on screen the column's own + padding is the space above the article. With that padding gone the first block sat against the + header's rule, and a page opening on an h1 -- which is most of them -- put 22pt of display type + hard against a line drawn 2px thick. This is the margin that block would otherwise have brought + with it. + */ + padding: 1.25rem 0 0 !important; + } + + /* --------------------------------------------------------------------------- + WHAT DOES NOT GO ON PAPER + --------------------------------------------------------------------------- */ + + /* + Navigation, the contents column, the action rail, the relation buttons under the article, the + footer, and every button that floats over the page -- all of them ways to go somewhere else, which + is the one thing a sheet of paper cannot do. The relations are the one that reads as content rather + than chrome, and they are not: they are the next page and the previous one, drawn as buttons. + + The last five are the transient overlays, and they are here because printing is not a moment anyone + prepares for: a tooltip is on screen the instant the Print button is clicked, and Ctrl+P works just + as well with a dialog open. Each would otherwise print over the first page. + */ + .w-header, + .w-drawer, + .w-drawer-scrim, + .page-sidebar, + .page-sidebar-scrim, + .page-actions, + .page-header-actions, + .page-relations, + .w-footer, + .w-page-scroller, + .corner-btn, + .w-tooltip, + .w-menu, + .w-notifications, + .w-loading, + .w-dialog-backdrop, + .w-dialog-viewport { + display: none !important; + } + + /* --------------------------------------------------------------------------- + WHAT DOES, FLATTENED + --------------------------------------------------------------------------- */ + + /* + The title bar keeps the title, the description and the page's icon, and loses the gradient it sits + in. Its 95px is a measurement for a bar in a window; here it is a heading, so it takes the height of + what is in it and stays with the page it names. + */ + .page-header { + height: auto; + break-after: avoid; + } + + /* + And the words sit closer to the rule under them. + + The 16px below the title column is pitched for a bar in a window, where the gradient it sits on is + what ends the header and the space is how the title is given room inside it. On paper the rule ends + the header, so the space under the words is doing nothing but pushing the article down the sheet. + Halved rather than dropped: the description still needs to clear the line under it. + + Reached through `:has()` because the column is a stack of layout utilities and the two things in it + are the only stable names in the markup -- the same column, class for class, in both headers. + */ + .page-header > div:has(> .page-header-title) { + padding-bottom: 0.5rem; + } + + /* -> Same: the trail says where this page lives, which is no use on the sheet before it */ + .page-breadcrumbs { + break-after: avoid; + } + + /* + The home crumb becomes the site's own mark. + + On screen that first crumb is a house because it is a BUTTON -- the way back to the root of the + wiki. Nothing on a sheet of paper goes anywhere, so what is left is a house standing at the head of + a path for no reason. The favicon in its place answers the question a printout actually raises, + which is which wiki this page was printed from, and it does it in the width the house was already + taking. + + Matched on `data-icon` rather than on the crumb's position: `WIcon` writes the reference it drew + onto every branch it renders, so this says "the home icon" instead of "whatever is first", and it + stops matching by itself if that crumb ever stops being a house. + + Drawn as a background on the link rather than swapped in the markup, because the swap is a + statement about paper and nothing else -- `WBreadcrumbs` is a shared component and this is not a + behaviour worth giving it a prop for. `print-color-adjust` because a background is exactly what a + print dialog drops by default, and the whole point here is that the mark appears. + + 1.25em is the house's own size: `.w-breadcrumbs__el-icon` sets 125%, so the glyph draws at 1.25em + of the bar's text. The crumb carries no label, so it needs no margin either. + */ + .page-breadcrumbs [data-icon='la:home'] { + display: none; + } + .page-breadcrumbs .w-breadcrumbs__el:has([data-icon='la:home'])::before { + content: ''; + display: inline-block; + width: 1.25em; + height: 1.25em; + background-image: url('/_site/current/favicon'); + background-repeat: no-repeat; + background-position: center; + background-size: contain; + print-color-adjust: exact; + -webkit-print-color-adjust: exact; + } + + /* + The indigo frame the version view draws down both sides and along the bottom goes. + + On screen it is a surround sized to the window and drawn once, and it is how that view says a + snapshot is being read. On paper there is no window to frame: it becomes a 5px band of colour down + the edge of every sheet, saying what the bar at the top of the first one has already said in words + -- which is why that bar keeps a heavier rule here than the page view's. + */ + .page-version { + border-width: 0; + } + + /* + Ink on paper, whichever theme the screen was in. + + Without this a page printed from the dark theme comes out with a white title on white paper -- the + header and the bars paint their own foregrounds per theme, and hiding their backgrounds is what + leaves the foreground stranded. Both theme classes are named because that is the specificity those + rules were written at. + */ + .body--light, + .body--dark { + .page-breadcrumbs { + padding-left: 0; + padding-right: 0; + background: none; + border-bottom: 1px solid #ccc; + color: #000; + } + + /* + The version view's bar keeps a heavier rule than the page view's, which is the one piece of + emphasis worth the ink here: on screen it is indigo against grey chrome and unmistakable, and + printed flat it would say "this is a record of a page and not the page" in the same voice as a + breadcrumb trail. A printout that reads as current when it is not is the failure this view has to + avoid. + */ + .page-breadcrumbs--version { + border-bottom: 2px solid #000; + } + + /* + A rule closes the header, in place of the gradient and the hairline it wears on screen. + + Without one the title, the description and the article's first paragraph were three blocks of + text with nothing but space between them, and the space alone did not say which of them was the + page's name -- the very thing the gradient does on screen. + + A hairline, and the trail's own grey: the same line the trail draws, top and bottom of one + masthead. It was 2px, on the reasoning that twice the weight would say which of the two closes + the block -- but a rule the full width of the sheet is the widest mark on it, and weight there + buys emphasis nobody asked for. The space around it does that work instead. + + 1px is also the only width that survives the trip. On paper it is an absolute 1/96in whatever the + printer's resolution, but a print PREVIEW is rasterised at the display's scale factor, and on a + fractionally-scaled screen 2px lands on 2.5 or 3 device pixels and reads as a bar rather than a + rule. A hairline is the one weight both media agree on. + */ + .page-header { + background: none; + border-top: 0; + border-bottom: 1px solid #ccc; + } + + .page-header-title { + color: #000; + } + .page-header-subtitle { + color: #333; + } + } + + /* + The site banner is the third place with print rules of its own, and the reason is the cascade + rather than tidiness: `SiteBanner.vue` ships its styles in the page view's own lazily-imported + chunk, which the browser adds AFTER this stylesheet. A rule here would tie with it on specificity + and lose on order, so the banner's print treatment sits in that component -- and what it does + there, in one line, is keep the notice and drop the ink. + */ +} diff --git a/frontend/src/css/app.scss b/frontend/src/css/app.scss index f23cb7a4d..431e56661 100644 --- a/frontend/src/css/app.scss +++ b/frontend/src/css/app.scss @@ -2,4 +2,5 @@ @use 'animation'; @use 'page-chrome'; @use 'page-contents'; - +/* -> Last, so that where a print rule ties with a screen rule on specificity, print wins */ +@use 'print'; diff --git a/frontend/src/pages/Index.vue b/frontend/src/pages/Index.vue index a535a6866..f35b78191 100644 --- a/frontend/src/pages/Index.vue +++ b/frontend/src/pages/Index.vue @@ -129,7 +129,15 @@ ref="pageContents" v-html="pageStore.render" @click="onContentClick" /> - +