vfrag
Version:
Paginate a document by breaking one or more containers vertically into multiple fragments.
194 lines (161 loc) • 3.9 kB
CSS
/* Base breaking styles */
/* The unbreakables */
h1, h2, h3, h4, h5, h6,
summary, legend, caption, figcaption,
figure, img, video, audio, iframe, embed, object,
dt, dd {
page-break-inside: avoid;
}
/* Breaking inside these should be possible, but only when really worth it */
pre, code, article, blockquote, aside, details, table {
orphans: 3;
widows: 4;
}
h1, h2, h3, h4, h5, h6,
[role="heading"][aria-level],
summary, legend, caption,
figcaption:where(:first-child) {
/* Avoid breaking after headings and heading-like elements */
page-break-after: avoid;
}
figcaption:where(:last-child) {
page-break-before: avoid;
}
/* Utility classes to be used in post-production */
.force-break-before {
page-break-before: always;
}
.force-break-after {
page-break-after: always;
}
/* To be used when printing from the paginated view.
We have already fragmented the content, so there should naturally be enough space,
so this prevents issues where a tiny amount of overflow causes the UA to create a new page.
*/
@page paginated {
margin-block: 0;
}
:where(.pagination-root) {
page: paginated;
--pages-total: calc(var(--pages, 1) + var(--pages-left, 0));
counter-reset: page var(--first-page, 1)
pages var(--pages-total, 1);
}
.page,
.vfrag-page {
position: relative;
counter-increment: page;
counter-reset: page var(--page-number);
& + & {
page-break-before: always;
}
&:has(+ &) {
page-break-after: always;
}
> .running-header,
> .running-footer {
position: absolute;
line-height: 1;
width: max-content;
max-width: calc(100% - var(--running-margin, 2lh));
&.start {
inset-inline-start: 0;
}
&.end {
inset-inline-end: 0;
}
&:not(.start, .end) {
inset-inline-start: 50%;
transform: translateX(-50%);
}
&:where([data-page="1"] > *) {
/* No running headers and footers on the very first page */
display: none;
}
a&, a {
text-decoration: none;
color: inherit;
}
}
> .running-header {
inset-block-start: var(--running-header-offset, 3lh);
&:where([data-fragment="1"] > *) {
/* No running header on a chapter first page */
display: none;
}
}
> .running-footer {
inset-block-end: var(--running-footer-offset, 1.2lh);
}
> .page-number {
font-variant-numeric: diagonal-fractions;
&::before {
counter-reset: page var(--page-number);
content: counter(page);
font-size: 125%;
font-weight: bold;
letter-spacing: .1em;
}
&::after {
content: "/" counter(pages);
opacity: .6;
font-weight: 300;
}
}
&:is(.debug-pagination, .debug-pagination *) {
@media print {
outline: 1px solid red;
}
@media screen {
/* Highlight areas that exceed page dimensions */
background: var(--page-aspect-ratio-image,) no-repeat hsl(0 50% 95%);
&:where(.empty-space-m, .empty-space-l) {
/* Highlight empty space */
&::after {
content: var(--empty-lines-text) " empty lines";
display: block;
height: calc(var(--empty-lines) * 1lh);
flex: 1;
box-sizing: border-box;
padding: .5em;
background:
linear-gradient(to top, hsl(0 50% 50% / 50%) 1px, transparent 0) 0 0 / 100% 1lh,
hsl(0 90% 50% / 10%);
}
}
}
}
&:not(.debug-pagination, .debug-pagination *) {
&[data-page="1"] {
> .page-first {
margin-block-start: auto;
}
}
&.fragment-last {
/* Last page */
> .page-last {
margin-block-end: auto;
}
}
&:not([data-page="1"], .fragment-last) {
&:not(h1, h2, h3, h4, h5, h6, header):where(:has(+ :is(h1, h2))) {
/* Distribute empty space between before headings and after last child */
margin-bottom: auto;
}
.footnote:nth-child(1 of .footnote) {
margin-block: auto 0;
}
}
}
}
/* Fragment styles */
.fragment:not([data-fragment="1"]) {
&:is(li) {
list-style: none;
}
&:is(details) {
> summary {
opacity: .5;
}
}
}