feat: proper print view styling

scarlett 3.0.0-beta.550
NGPixel 5 days ago
parent 50adab2eac
commit 7ea92a982b
No known key found for this signature in database

@ -1597,6 +1597,7 @@
"common.actions.optimize": "Optimize", "common.actions.optimize": "Optimize",
"common.actions.page": "Page", "common.actions.page": "Page",
"common.actions.preview": "Preview", "common.actions.preview": "Preview",
"common.actions.print": "Print",
"common.actions.proceed": "Proceed", "common.actions.proceed": "Proceed",
"common.actions.properties": "Properties", "common.actions.properties": "Properties",
"common.actions.refresh": "Refresh", "common.actions.refresh": "Refresh",
@ -2282,7 +2283,6 @@
"history.downloadFailed": "Failed to download this version.", "history.downloadFailed": "Failed to download this version.",
"history.downloadVersion": "Download Version", "history.downloadVersion": "Download Version",
"history.emptyPage": "Nothing", "history.emptyPage": "Nothing",
"history.exportPdf": "Export to PDF",
"history.inline": "Inline", "history.inline": "Inline",
"history.loadFailed": "Failed to load the page history.", "history.loadFailed": "Failed to load the page history.",
"history.none": "No history has been recorded for this page yet.", "history.none": "No history has been recorded for this page yet.",

@ -1,9 +1,9 @@
key: puppeteer key: puppeteer
title: Puppeteer title: Puppeteer
description: >- description: >-
Headless Chromium browser. Required to export pages as PDF and to render content elements on the Headless Chromium browser. Required to re-render a page from its source on the server: the markdown
server, such as Mermaid or PlantUML diagrams. Installing it downloads a Chromium build of a few renderer lives in the browser, so the server drives one. Installing it downloads a Chromium build of
hundred megabytes, unless the server already provides one through PUPPETEER_EXECUTABLE_PATH. a few hundred megabytes, unless the server already provides one through PUPPETEER_EXECUTABLE_PATH.
website: 'https://pptr.dev' website: 'https://pptr.dev'
detect: detect:
type: module type: module

@ -165,6 +165,43 @@ Content of the second tab.
margin-bottom: 0; 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 { :host {
--tabs-border: #e0e0e0; --tabs-border: #e0e0e0;
--tabs-strip-bg: linear-gradient(to bottom, #fdfdfd, #eeeeee); --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-active-label: var(--tabs-active-fg);
--tabs-panel-bg: #fff; --tabs-panel-bg: #fff;
} }
:host([dark]) { /*
--tabs-border: rgba(255, 255, 255, 0.15); The dark palette, and only where there is a lit screen to read it on.
--tabs-strip-bg: linear-gradient(to bottom, #1b212a, #12161d);
--tabs-inactive-fg: rgba(255, 255, 255, 0.7); The dark attribute comes off the body class, which says nothing about the medium -- so without
/* -> A mix of --q-primary, so a re-themed site's own hue comes with it. See block-index. */ this a page printed from the dark theme printed a near-black panel, and the app prints the text
--tabs-active-label: var(--color-primary-light); inside it in black ink (the article's palette is light on paper, by the same reasoning as here:
--tabs-panel-bg: #1e232a; 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;
}
} }
` `
} }

@ -126,9 +126,9 @@
dense dense
icon="la:print" icon="la:print"
color="grey" color="grey"
aria-label="Print" :aria-label="t(`common.actions.print`)"
@click="printPage"> @click="printPage">
<w-tooltip>Print</w-tooltip> <w-tooltip>{{ t('common.actions.print') }}</w-tooltip>
</w-btn> </w-btn>
<!-- <!--
Only for whoever reviews this page: the server answers `canReview` from the approval rules Only for whoever reviews this page: the server answers `canReview` from the approval rules

@ -21,23 +21,25 @@
<!-- VERSION ACTIONS --> <!-- VERSION ACTIONS -->
<!-- <!--
What this header has in place of Watch / Print / Edit: the three things there are to do with a What this header has in place of Watch / Print / Edit: the three things there are to do with a
snapshot. Two are icons -- taking a copy of it, in either of two forms -- and the third is snapshot, and Print, which is the one button from that row meaning the same thing here. Two of
the three are icons -- taking a copy of the version, in either of two forms -- and the third is
labelled, because it is the one that WRITES, and a button that overwrites the live page should labelled, because it is the one that WRITES, and a button that overwrites the live page should
not be a glyph somebody presses to find out what it does. Its ellipsis is doing the same work: not be a glyph somebody presses to find out what it does. Its ellipsis is doing the same work:
restoring asks first. restoring asks first.
Download, Restore and Branch off each do the same as their entry in the history overlay's version Download, Restore and Branch off each do the same as their entry in the history overlay's version
menu, through the same code. Export to PDF is the one still disabled, because it is not wired up menu, through the same code. Print is `PageHeader`'s own button -- the same icon, the same site
yet -- disabled rather than inert on purpose: a button that silently does nothing when pressed setting behind it and the same one-line call -- because a snapshot goes on paper exactly as a page
reads as a bug, where a dimmed one reads as not-yet. does, and `css/_print.scss` keeps this bar on the sheet so the printout says which version it was.
Restore is the only one that WRITES to the live page, which is why it keeps the orange this app Restore is the only one that WRITES to the live page, which is why it keeps the orange this app
gives an action that changes a page, and why it asks before doing it. Branch off creates a page gives an action that changes a page, and why it asks before doing it. Branch off creates a page
instead of overwriting one, so it sits with the harmless ones. instead of overwriting one, so it sits with the harmless ones.
They stay on a phone, where `PageHeader` drops its whole row: that row is icons for things They stay on a phone, where `PageHeader` drops its whole row: that row is icons for things
reachable elsewhere -- Print is the browser's own menu -- while these three are the only actions reachable elsewhere -- Print is the browser's own menu -- while these are the only actions this
this view offers at all, so hiding them would leave the screen with none. view offers at all, so hiding them would leave the screen with none. Print rides along with them
rather than being kept for its own sake; it is one glyph in a row that has to be there anyway.
What they do instead is take a row of their own, which is what `w-full` at phone widths buys: the What they do instead is take a row of their own, which is what `w-full` at phone widths buys: the
bar already wraps, but this block is `flex-none` and about 230px wide, so beside a 32px icon it bar already wraps, but this block is `flex-none` and about 230px wide, so beside a 32px icon it
@ -65,14 +67,20 @@
@click="emit(`download`)"> @click="emit(`download`)">
<w-tooltip>{{ t('history.downloadVersion') }}</w-tooltip> <w-tooltip>{{ t('history.downloadVersion') }}</w-tooltip>
</w-btn> </w-btn>
<!--
On the site's own Print Button setting, as the page header's is: an administrator who has turned
it off has said the wiki does not offer one, and a version of a page is not the exception to
that.
-->
<w-btn <w-btn
class="acrylic-btn ml-2" class="acrylic-btn ml-2"
v-if="siteStore.theme.showPrintBtn"
flat flat
icon="la:file-pdf" icon="la:print"
color="grey" color="grey"
disable :aria-label="t(`common.actions.print`)"
:aria-label="t(`history.exportPdf`)"> @click="printPage">
<w-tooltip>{{ t('history.exportPdf') }}</w-tooltip> <w-tooltip>{{ t('common.actions.print') }}</w-tooltip>
</w-btn> </w-btn>
<!-- <!--
Branch off before Restore: it reads as the gentler of the two, and Restore stays next to the Branch off before Restore: it reads as the gentler of the two, and Restore stays next to the
@ -80,7 +88,7 @@
Indigo, which is the colour this app gives history -- the Schedule tab's calendar, the version Indigo, which is the colour this app gives history -- the Schedule tab's calendar, the version
timeline's dots, the bar at the top of this very screen. It also tells this button apart from timeline's dots, the bar at the top of this very screen. It also tells this button apart from
the disabled Export to PDF beside it, which grey did not. the grey pair beside it, which a third grey button did not.
Two shades, because one will not do: as a LABEL, `indigo` measures 6.3:1 on the light header Two shades, because one will not do: as a LABEL, `indigo` measures 6.3:1 on the light header
and 2.5:1 on the dark one, while `indigo-4` is 5.0:1 dark and 3.2:1 light. So each theme takes and 2.5:1 on the dark one, while `indigo-4` is 5.0:1 dark and 3.2:1 light. So each theme takes
@ -152,6 +160,8 @@ import { useI18n } from 'vue-i18n'
import { useDark } from '@/composables/dark' import { useDark } from '@/composables/dark'
import { useMinWidth } from '@/composables/screen' import { useMinWidth } from '@/composables/screen'
import { useSiteStore } from '@/stores/site'
const emit = defineEmits(['download', 'restore', 'branch']) const emit = defineEmits(['download', 'restore', 'branch'])
defineProps({ defineProps({
@ -178,6 +188,10 @@ defineProps({
} }
}) })
// STORES
const siteStore = useSiteStore()
// COMPOSABLES // COMPOSABLES
const dark = useDark() const dark = useDark()
@ -195,4 +209,18 @@ const { t } = useI18n()
*/ */
const isAtLeastSm = useMinWidth(600) const isAtLeastSm = useMinWidth(600)
const iconSize = computed(() => (isAtLeastSm.value ? '64px' : '32px')) const iconSize = computed(() => (isAtLeastSm.value ? '64px' : '32px'))
// METHODS
/**
* Hand the snapshot to the browser's print dialog.
*
* Here rather than emitted to `PageVersion.vue`, unlike every other button in this header: those three
* act on the VERSION and go through the API helpers the history overlay uses, so the parent owns them.
* This one acts on the window and knows nothing about what is in it -- the same call `PageHeader` makes
* for a live page, and what it prints is decided entirely by `css/_print.scss`.
*/
function printPage() {
window.print()
}
</script> </script>

@ -191,5 +191,47 @@ async function render() {
font-weight: 600; font-weight: 600;
} }
} }
/*
ON PAPER
--------
The banner survives printing. It is site chrome rather than this page, and the same on every one of
them -- but an administrator raises a banner because it has to be seen, and a printout that quietly
drops "this documentation is superseded" is worse than one that spends the ink. What goes is the
wash, as every other tint does on paper (see `css/_print.scss`); the rule under the notice and its
hue are what carry it.
Here rather than with the rest of the print stylesheet because of the cascade: these styles ship in
the page view's lazily-imported chunk, which the browser adds after `main`'s, so a rule over there
would tie on specificity and lose on order.
The margins are the other half. They are measured off the article column's padding -- see the note
on `--site-banner-pad` above -- and print zeroes that padding, so left alone they would pull the
band a whole rem out past the text on three sides.
*/
@media print {
margin: 0 0 1.5rem;
.site-banner-alert {
background-color: transparent;
/*
The warning triangle is a mask over a background COLOUR, and a print dialog leaves background
graphics off by default -- so the one mark that says this notice is a warning rather than an
aside was the one part of it that did not print. Same pair as a done task item in
`css/_page-contents.scss`, for the same reason.
*/
&::before {
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
}
}
/* -> The dark theme's hue is pitched to be seen against a dark page and prints as a pale smear */
@at-root .body--dark & .site-banner-alert {
--site-banner-hue: #c02636;
}
}
} }
</style> </style>

@ -27,9 +27,15 @@
:style="{ '--w-drawer-width': `${width}px` }"> :style="{ '--w-drawer-width': `${width}px` }">
<teleport to="body"> <teleport to="body">
<transition name="w-drawer-scrim"> <transition name="w-drawer-scrim">
<!--
Carries a class of its own, unlike the utility-styled elements around it, because
`css/_print.scss` has to reach it: this is teleported out of the drawer it belongs to, so
hiding `.w-drawer` for print does not take it along -- and a scrim left on the sheet is a
black first page.
-->
<div <div
v-if="isVisible && isOverlay" v-if="isVisible && isOverlay"
class="fixed inset-0 z-30 bg-black/40" class="w-drawer-scrim fixed inset-0 z-30 bg-black/40"
@click="$emit('update:modelValue', false)" /> @click="$emit('update:modelValue', false)" />
</transition> </transition>
</teleport> </teleport>

@ -219,80 +219,96 @@
*/ */
isolation: isolate; 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 & { @at-root .body--dark & {
--content-ink: rgba(255, 255, 255, 0.87); @media not print {
--content-ink-muted: rgba(255, 255, 255, 0.62); --content-ink: rgba(255, 255, 255, 0.87);
--content-ink-faint: rgba(255, 255, 255, 0.42); --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` */ /* -> The mid-tone brand blue is too dim on a dark surface; see `--color-primary-light` */
--content-link: var(--color-primary-light); --content-link: var(--color-primary-light);
--content-h1: var(--color-primary-light); --content-h1: var(--color-primary-light);
--content-rule: rgba(255, 255, 255, 0.16); --content-rule: rgba(255, 255, 255, 0.16);
--content-rule-strong: rgba(255, 255, 255, 0.32); --content-rule-strong: rgba(255, 255, 255, 0.32);
--content-h2-rule: rgba(255, 255, 255, 0.45); --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 */ /* 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-code: #161b22;
--content-surface-alt: rgba(255, 255, 255, 0.07); --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% 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 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. 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); --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 /* -> 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 */ 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: rgba(255, 255, 255, 0.02);
--content-table-row-alt: rgba(255, 255, 255, 0.055); --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, /* -> 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 */ and the extra step the light theme needs would leave the head sitting above the page */
--content-table-head: #1e232a; --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 */ /* -> 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: rgba(255, 255, 255, 0.05);
--content-links-row-hover: color-mix(in srgb, var(--color-primary-light) 14%, transparent); --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 */ /* -> 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); --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 */ /* -> 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); --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 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. well below the white of the tick on it, which is what makes the tick readable.
*/ */
--content-tick: rgba(255, 255, 255, 0.38); --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 */ /* -> A white box would be a lit square on a dark page; the empty one is just its outline */
--content-tick-empty: transparent; --content-tick-empty: transparent;
--content-info: #6ab0ff; --content-info: #6ab0ff;
--content-info-wash: rgba(106, 176, 255, 0.12); --content-info-wash: rgba(106, 176, 255, 0.12);
--content-success: #4ecf9a; --content-success: #4ecf9a;
--content-success-wash: rgba(78, 207, 154, 0.12); --content-success-wash: rgba(78, 207, 154, 0.12);
--content-warning: #f3b661; --content-warning: #f3b661;
--content-warning-wash: rgba(243, 182, 97, 0.12); --content-warning-wash: rgba(243, 182, 97, 0.12);
--content-danger: #ff8b8b; --content-danger: #ff8b8b;
--content-danger-wash: rgba(255, 139, 139, 0.12); --content-danger-wash: rgba(255, 139, 139, 0.12);
--content-important: #c4a7ff; --content-important: #c4a7ff;
--content-important-wash: rgba(196, 167, 255, 0.12); --content-important-wash: rgba(196, 167, 255, 0.12);
--content-code-comment: #8b949e; --content-code-comment: #8b949e;
--content-code-keyword: #ff7b72; --content-code-keyword: #ff7b72;
--content-code-string: #a5d6ff; --content-code-string: #a5d6ff;
--content-code-number: #79c0ff; --content-code-number: #79c0ff;
--content-code-title: #d2a8ff; --content-code-title: #d2a8ff;
--content-code-type: #ffa657; --content-code-type: #ffa657;
--content-code-attr: #79c0ff; --content-code-attr: #79c0ff;
--content-code-meta: #8b949e; --content-code-meta: #8b949e;
--content-code-addition: #56d364; --content-code-addition: #56d364;
--content-code-addition-wash: rgba(46, 160, 67, 0.15); --content-code-addition-wash: rgba(46, 160, 67, 0.15);
--content-code-deletion: #ffa198; --content-code-deletion: #ffa198;
--content-code-deletion-wash: rgba(248, 81, 73, 0.15); --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 */ /* -> The page's own margins are the container's business; the first and last block give theirs up */
@ -1850,6 +1866,13 @@
// PRINT // 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 { @media print {
/* Washes and tints cost ink and print muddy; the structure has to come from the rules instead */ /* Washes and tints cost ink and print muddy; the structure has to come from the rules instead */
--content-ink: #000; --content-ink: #000;
@ -1869,6 +1892,18 @@
/* -> And the banding, which on paper is grey ink over every other row for no gain */ /* -> And the banding, which on paper is grey ink over every other row for no gain */
--content-table-row: transparent; --content-table-row: transparent;
--content-table-row-alt: 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; font-size: 11pt;
line-height: 1.55; line-height: 1.55;
@ -1892,9 +1927,174 @@
break-after: avoid; 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 { 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;
} }
} }
} }

@ -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.
*/
}

@ -2,4 +2,5 @@
@use 'animation'; @use 'animation';
@use 'page-chrome'; @use 'page-chrome';
@use 'page-contents'; @use 'page-contents';
/* -> Last, so that where a print rule ties with a screen rule on specificity, print wins */
@use 'print';

@ -129,7 +129,15 @@
ref="pageContents" ref="pageContents"
v-html="pageStore.render" v-html="pageStore.render"
@click="onContentClick" /> @click="onContentClick" />
<template v-if="pageStore.relations && pageStore.relations.length > 0"> <!--
A box of its own rather than a bare `<template>`, so that `css/_print.scss` has something
to hide: these are links onward -- the next page, the previous one, a related topic --
which is the one thing a sheet of paper cannot do anything with. The separator goes with
them, since it exists only to divide them from the article above.
-->
<div
class="page-relations"
v-if="pageStore.relations && pageStore.relations.length > 0">
<w-separator class="my-6" /> <w-separator class="my-6" />
<div class="flex flex-wrap"> <div class="flex flex-wrap">
<div class="min-w-0 flex-1 text-left" v-if="relationsLeft.length > 0"> <div class="min-w-0 flex-1 text-left" v-if="relationsLeft.length > 0">
@ -185,7 +193,7 @@
</w-btn> </w-btn>
</div> </div>
</div> </div>
</template> </div>
</div> </div>
<!-- <!--
Inside the scrolling column, and last: this is the bottom of the PAGE, so it is reached by Inside the scrolling column, and last: this is the bottom of the PAGE, so it is reached by

Loading…
Cancel
Save