feat: a header's page background paints behind the page it belongs to
A positioned frame in a header is out of flow — the title-page background or watermark. It keeps its wrap and its offset from the page corner, and the layer paints it per page under the body instead of flowing it inline, where it reserved a page-tall line of body space. The page surface moves from .tiptap to .paper so the image has a layer between the sheet and the text; ODF export writes it back page-anchored.
This commit is contained in:
1 parent
84eb7d6bf3
commit
30b5b7890e
10 files changed
+136
-34
No files matched your search
@@ -47,7 +47,7 @@ Both anchors differ: a wheel zoom holds the **point under the cursor** fixed (`p
|
||||
## Headers & footers (`HeaderFooterLayer.svelte`, `storage/headerFooter.ts`)
|
||||
|
||||
One header and one footer (`HfDoc` = a single-paragraph TipTap doc per zone, persisted to `odf-editor-header`/`-footer`), repeated on every page. Double-clicking a page margin — or the Layout-panel "Edit header/footer" buttons — opens the zone for editing; clicking back into the body (`onFocus`) or Escape ends it. The edge→zone distance (header from top, footer from bottom) is user-configurable in cm via the Layout panel (`HfDistances`, persisted to `odf-editor-hf-distances`, default `HF_DISTANCE_CM` 1.27cm, clamped 0–10 and below the body margin).
|
||||
- **Rendering:** `HeaderFooterLayer.svelte` mounts inside the scaled `.paper` (like `.band-layer`), positioning per-page zone boxes in unscaled doc px. Inactive zones render as static HTML (`generateHTML` + `hfExtensions`); the active zone hosts the single live TipTap editor. `App.svelte`'s `activeEditor`/`activeTick` route the top toolbars to `hfEditor` while a zone is active, so all body formatting works on the header/footer with no toolbar changes.
|
||||
- **Rendering:** `HeaderFooterLayer.svelte` mounts inside the scaled `.paper` (like `.band-layer`), positioning per-page zone boxes in unscaled doc px. A zone's out-of-flow frame (`wrap` ≠ inline) is hidden in the box and painted per page by `.hf-bg-layer` at `z-index: -1` — hence `.paper`, not `.tiptap`, paints the page surface, so the image lands between the sheet and the body text. Inactive zones render as static HTML (`generateHTML` + `hfExtensions`); the active zone hosts the single live TipTap editor. `App.svelte`'s `activeEditor`/`activeTick` route the top toolbars to `hfEditor` while a zone is active, so all body formatting works on the header/footer with no toolbar changes.
|
||||
|
||||
## Debug tooling (dev only)
|
||||
|
||||
|
||||
@@ -74,14 +74,15 @@
|
||||
const HF_LINE_PX = 16 * 1.15;
|
||||
function hfReachPx(doc: HfDoc, distPx: number): number {
|
||||
if (!doc || hfIsEmpty(doc)) return 0;
|
||||
const inline = ((doc.content?.[0] as { content?: { type?: string; attrs?: { height?: number } }[] } | undefined)?.content ?? []);
|
||||
const inline = ((doc.content?.[0] as { content?: { type?: string; attrs?: { height?: number; wrap?: string } }[] } | undefined)?.content ?? []);
|
||||
// Per line, since an as-character image (a letterhead logo) makes its own line
|
||||
// as tall as it is; the others are one text line each.
|
||||
// as tall as it is; the others are one text line each. A positioned frame is out
|
||||
// of flow — a page-sized background would otherwise reserve the whole page.
|
||||
let total = 0;
|
||||
let image = 0;
|
||||
for (const n of inline) {
|
||||
if (n.type === 'hardBreak') { total += Math.max(HF_LINE_PX, image); image = 0; }
|
||||
else if (n.type === 'image' && typeof n.attrs?.height === 'number') image = Math.max(image, n.attrs.height);
|
||||
else if (n.type === 'image' && typeof n.attrs?.height === 'number' && (n.attrs.wrap ?? 'inline') === 'inline') image = Math.max(image, n.attrs.height);
|
||||
}
|
||||
return distPx + total + Math.max(HF_LINE_PX, image);
|
||||
}
|
||||
|
||||
@@ -114,6 +114,25 @@
|
||||
return '';
|
||||
}
|
||||
}
|
||||
// A positioned frame in a zone is out of flow: the page-anchored letterhead or
|
||||
// watermark a title page is made of. It paints once per page from the page corner,
|
||||
// behind the body, and the zone's own box never sees it (editor.css hides it there).
|
||||
type PageBg = { src: string; x: number; y: number; width: number; height: number };
|
||||
function backgrounds(doc: HfDoc): PageBg[] {
|
||||
const inline = ((doc?.content?.[0] as { content?: { type?: string; attrs?: Record<string, unknown> }[] } | undefined)?.content ?? []);
|
||||
const out: PageBg[] = [];
|
||||
for (const n of inline) {
|
||||
if (n.type !== 'image' || typeof n.attrs?.src !== 'string' || (n.attrs.wrap ?? 'inline') === 'inline') continue;
|
||||
out.push({
|
||||
src: n.attrs.src,
|
||||
x: cmToPx(Number(n.attrs.wrapOffset) || 0),
|
||||
y: cmToPx(Number(n.attrs.wrapOffsetY) || 0),
|
||||
width: Number(n.attrs.width) || pageWidthPx,
|
||||
height: Number(n.attrs.height) || pageHeightPx,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
// Section 1 is the app's own editable state; the rest live in extraHfSections.
|
||||
let sets = $derived<HfSet[]>([
|
||||
{
|
||||
@@ -130,6 +149,11 @@
|
||||
headerFirst: staticHtml(s.headerFirst), footerFirst: staticHtml(s.footerFirst),
|
||||
headerEven: staticHtml(s.headerEven), footerEven: staticHtml(s.footerEven),
|
||||
})));
|
||||
let setBg = $derived(sets.map((s) => ({
|
||||
header: backgrounds(s.header), footer: backgrounds(s.footer),
|
||||
headerFirst: backgrounds(s.headerFirst), footerFirst: backgrounds(s.footerFirst),
|
||||
headerEven: backgrounds(s.headerEven), footerEven: backgrounds(s.footerEven),
|
||||
})));
|
||||
|
||||
// Which section a page belongs to: the count of section starts at or before it,
|
||||
// clamped to what the document actually carries.
|
||||
@@ -161,6 +185,13 @@
|
||||
if (zone === 'header') return v === 'first' ? h.headerFirst : v === 'even' ? h.headerEven : h.header;
|
||||
return v === 'first' ? h.footerFirst : v === 'even' ? h.footerEven : h.footer;
|
||||
}
|
||||
function zoneBackgrounds(zone: HfZone, page: number): PageBg[] {
|
||||
const index = sectionOf(page);
|
||||
const v = variantFor(page, index);
|
||||
const b = setBg[index] ?? setBg[0];
|
||||
if (zone === 'header') return v === 'first' ? b.headerFirst : v === 'even' ? b.headerEven : b.header;
|
||||
return v === 'first' ? b.footerFirst : v === 'even' ? b.footerEven : b.footer;
|
||||
}
|
||||
|
||||
// Lay out the zone's tabs: static HTML no ProseMirror plugin reaches. The advances are
|
||||
// layout px, so only a content change invalidates them — not the zoom transform.
|
||||
@@ -299,6 +330,23 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Own layer below the body (z-index -1 against .paper's zoom stacking context), so a
|
||||
full-page background sits under the text the way LibreOffice paints it. -->
|
||||
<div class="hf-bg-layer">
|
||||
{#each pages as p}
|
||||
{#each ['header', 'footer'] as const as zone}
|
||||
{#each zoneBackgrounds(zone, p) as bg}
|
||||
<img
|
||||
class="hf-page-bg"
|
||||
src={bg.src}
|
||||
alt=""
|
||||
style="top: {(p - 1) * cycle + bg.y}px; left: {bg.x}px; width: {bg.width}px; height: {bg.height}px;"
|
||||
/>
|
||||
{/each}
|
||||
{/each}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="hf-layer">
|
||||
{#each pages as p}
|
||||
{#each ['header', 'footer'] as const as zone}
|
||||
@@ -354,6 +402,21 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hf-bg-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
}
|
||||
.hf-page-bg {
|
||||
position: absolute;
|
||||
}
|
||||
/* The frame is painted by .hf-bg-layer; inside the zone it would grow the box and,
|
||||
while editing, flow as a line. Selecting it still works from the keyboard. */
|
||||
.hf-zone :global([data-wrap]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hf-zone {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
|
||||
@@ -305,6 +305,9 @@ class ImageView {
|
||||
private applyWrap(): void {
|
||||
const d = this.dom;
|
||||
const wrap = this.attrWrap();
|
||||
// Mirrors renderHTML's data-wrap, so one selector reaches a floating frame in the
|
||||
// live view and in generated static HTML alike.
|
||||
if (wrap === 'inline') delete d.dataset.wrap; else d.dataset.wrap = wrap;
|
||||
d.style.float = '';
|
||||
d.style.display = '';
|
||||
d.style.clear = '';
|
||||
|
||||
@@ -38,7 +38,7 @@ The filename is derived from the first non-empty heading (max 50 chars, sanitize
|
||||
|
||||
## Header/footer export
|
||||
|
||||
- **Export:** a synthetic `__cust_hf__` node routes to `unknownNodeHandler`, which calls odf-kit's `setHeader`/`setFooter` (`applyHfRuns`); `hardBreak`→`LBR`, `pageNumber`→`addPageNumber`, `pageCount`→`PGC` sentinel. `applyHfPostProcess` (on styles.xml) rewrites the sentinels to `<text:line-break/>`/`<text:page-count>`, applies the paragraph alignment to the `Header`/`Footer` styles, and converts geometry to Word's model: page margin = the zone's edge distance (`headerDistanceCm`/`footerDistanceCm`), header/footer `min-height` = body margin − distance, so the **body** still starts at the editor's margin.
|
||||
- **Export:** a synthetic `__cust_hf__` node routes to `unknownNodeHandler`, which calls odf-kit's `setHeader`/`setFooter` (`applyHfRuns`); `hardBreak`→`LBR`, `pageNumber`→`addPageNumber`, `pageCount`→`PGC` sentinel. A zone image is an as-char `<draw:frame>`, or — floating — a page-anchored one plus a minted `HfBg*` graphic style (`run-through` behind the text, positioned from the page corner). `applyHfPostProcess` (on styles.xml) rewrites the sentinels to `<text:line-break/>`/`<text:page-count>`, applies the paragraph alignment to the `Header`/`Footer` styles, and converts geometry to Word's model: page margin = the zone's edge distance (`headerDistanceCm`/`footerDistanceCm`), header/footer `min-height` = body margin − distance, so the **body** still starts at the editor's margin.
|
||||
|
||||
Sentinel order matters — the passes assume it. Full list of sentinels in use:
|
||||
`LBR`/`TAB` (inline), `IMG`, `SEG` (cell blocks), `TBX` U+E008 (text boxes),
|
||||
|
||||
+27
-3
@@ -2799,19 +2799,38 @@ function imageFrameXml(img: ImageExport, index: number): string {
|
||||
);
|
||||
}
|
||||
|
||||
// As-character <draw:frame> for a header/footer image (always inline; no wrap style or
|
||||
// rotation). Distinct draw:name so it never collides with a body image's frame.
|
||||
// <draw:frame> for a header/footer image. As-character by default; a positioned frame
|
||||
// (a page-anchored background) instead rides the zone paragraph at its page corner
|
||||
// offset, run through behind the text. Distinct draw:name per frame.
|
||||
function hfImageFrameXml(img: ImageExport, index: number): string {
|
||||
const dims =
|
||||
(img.widthCm ? ` svg:width="${img.widthCm}cm"` : '') +
|
||||
(img.heightCm ? ` svg:height="${img.heightCm}cm"` : '');
|
||||
const title = img.alt ? `<svg:title>${escapeXml(img.alt)}</svg:title>` : '';
|
||||
const anchor =
|
||||
img.wrap === 'inline'
|
||||
? `text:anchor-type="as-char"`
|
||||
: `text:anchor-type="paragraph" draw:style-name="${HF_BG_STYLE}${index + 1}"` +
|
||||
` svg:x="${img.wrapOffsetCm ?? 0}cm" svg:y="${img.wrapOffsetYCm ?? 0}cm"`;
|
||||
return (
|
||||
`<draw:frame draw:name="HfImage${index + 1}" text:anchor-type="as-char" draw:z-index="${index}"${dims}>` +
|
||||
`<draw:frame draw:name="HfImage${index + 1}" ${anchor} draw:z-index="${index}"${dims}>` +
|
||||
`<draw:image xlink:href="${img.path}"/>${title}</draw:frame>`
|
||||
);
|
||||
}
|
||||
|
||||
const HF_BG_STYLE = 'HfBg';
|
||||
|
||||
// The graphic style a page-anchored zone frame points at: positioned from the page's
|
||||
// top-left corner and run through behind the text, as LibreOffice writes a watermark.
|
||||
function hfBackgroundStyleXml(index: number): string {
|
||||
return (
|
||||
`<style:style style:name="${HF_BG_STYLE}${index + 1}" style:family="graphic">` +
|
||||
`<style:graphic-properties style:wrap="run-through" style:run-through="background"` +
|
||||
` style:horizontal-rel="page" style:horizontal-pos="from-left"` +
|
||||
` style:vertical-rel="page" style:vertical-pos="from-top"/></style:style>`
|
||||
);
|
||||
}
|
||||
|
||||
// Inject automatic styles, tolerating an empty/self-closed or absent section.
|
||||
function injectAutomaticStyles(content: string, styles: string): string {
|
||||
if (!styles) return content;
|
||||
@@ -3580,6 +3599,11 @@ function applyHfPostProcess(odtBytes: Uint8Array, margins: PageMargins, headerPa
|
||||
const img = hfImages[Number(idx)];
|
||||
return img ? hfImageFrameXml(img, Number(idx)) : '';
|
||||
});
|
||||
const bgStyles = hfImages.map((img, i) => (img.wrap === 'inline' ? '' : hfBackgroundStyleXml(i))).join('');
|
||||
if (bgStyles) {
|
||||
if (styles.includes('</office:automatic-styles>')) styles = styles.replace('</office:automatic-styles>', `${bgStyles}</office:automatic-styles>`);
|
||||
else styles = styles.replace('<office:automatic-styles/>', `<office:automatic-styles>${bgStyles}</office:automatic-styles>`);
|
||||
}
|
||||
for (const img of hfImages) files[img.path] = img.bytes;
|
||||
const manifestBytes = files['META-INF/manifest.xml'];
|
||||
if (manifestBytes) {
|
||||
|
||||
@@ -67,7 +67,8 @@ function buildClone(paper: HTMLElement, pageW: number): { holder: HTMLElement; c
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
[data-pdf-export] .paper, [data-pdf-export] .paper .tiptap { box-shadow:none !important; }
|
||||
[data-pdf-export] .paper .tiptap { background:#fff !important; }
|
||||
[data-pdf-export] .paper { background:#fff !important; }
|
||||
[data-pdf-export] .paper .tiptap { background:none !important; }
|
||||
[data-pdf-export] .column-resize-handle, [data-pdf-export] .row-resize-handle { display:none !important; }
|
||||
[data-pdf-export] .selectedCell::after { display:none !important; }
|
||||
[data-pdf-export] .tiptap .is-editor-empty::before { content:none !important; }
|
||||
@@ -471,8 +472,8 @@ function printCss(o: PrintPdfOptions): string {
|
||||
${o.differentOddEven ? `@page :left {\n ${marginBoxesFirst(o.headerEvenDoc ?? null, o.footerEvenDoc ?? null)}\n}` : ''}
|
||||
${o.differentFirstPage ? `@page :first {\n ${marginBoxesFirst(o.headerFirstDoc ?? null, o.footerFirstDoc ?? null)}\n}` : ''}
|
||||
html, body { margin: 0; padding: 0; background: #fff; }
|
||||
.paper { width: auto !important; transform: none !important; box-shadow: none !important; background: none !important; }
|
||||
.paper .tiptap { padding: 0 !important; min-height: 0 !important; background: #fff !important; box-shadow: none !important; color: #000 !important; }
|
||||
.paper { width: auto !important; transform: none !important; box-shadow: none !important; background: #fff !important; }
|
||||
.paper .tiptap { padding: 0 !important; min-height: 0 !important; background: none !important; box-shadow: none !important; color: #000 !important; }
|
||||
/* generateHTML drops TipTap's .tableWrapper; mirror its (zero) margin. */
|
||||
.paper .tiptap table { margin: 0; }
|
||||
.paper .tiptap tr { break-inside: avoid; }
|
||||
|
||||
@@ -23,7 +23,7 @@ The **Open .odt** button (`App.svelte`) parses an uploaded file with `importOdt(
|
||||
|
||||
## Header/footer import
|
||||
|
||||
- **Import:** `StyleResolver.masterPageHF()` reads `style:header`/`style:footer` + the header/footer-style heights; `pageGeometry()` reconstructs the body margin (page margin + zone height + spacing) and `edgeDistancesCm()` returns the raw page margins as the zone distances. The header↔body spacing is only reserved when `style:dynamic-spacing="false"` — a `true` (LibreOffice's usual) means the gap collapses, so reserving it would shove the body down. `convertHfZone` → single-paragraph doc (extra paragraphs → hard breaks; a text-less zone that carries a background/border rule line is kept, box props merged with the bottom rule from the last paragraph); `convertInline(…, hfFields=true)` keeps `text:page-number`/`-count` as field nodes. A master page whose `style:next-style-name` points at *another* master governs one page only (the "different first page" idiom): its own zones become the first-page variants and the successor's the running ones — read as the running header instead, a title-page master repeats on every page of its section and its height pushes each block onto its own page. As in DOCX, a zone's positioned (non-`as-char`) frames are dropped with a warning: the one-paragraph zone can't place them, and a page-sized background reserves the whole page inline. `hfIsEmpty` counts a rule-/shading-only paragraph as non-empty so it renders and exports.
|
||||
- **Import:** `StyleResolver.masterPageHF()` reads `style:header`/`style:footer` + the header/footer-style heights; `pageGeometry()` reconstructs the body margin (page margin + zone height + spacing) and `edgeDistancesCm()` returns the raw page margins as the zone distances. The header↔body spacing is only reserved when `style:dynamic-spacing="false"` — a `true` (LibreOffice's usual) means the gap collapses, so reserving it would shove the body down. `convertHfZone` → single-paragraph doc (extra paragraphs → hard breaks; a text-less zone that carries a background/border rule line is kept, box props merged with the bottom rule from the last paragraph); `convertInline(…, hfFields=true)` keeps `text:page-number`/`-count` as field nodes. A master page whose `style:next-style-name` points at *another* master governs one page only (the "different first page" idiom): its own zones become the first-page variants and the successor's the running ones — read as the running header instead, a title-page master repeats on every page of its section and its height pushes each block onto its own page. A zone's positioned (non-`as-char`) frame keeps its wrap and its `svg:x`/`svg:y` from the page corner: it is out of flow (a letterhead or full-page title background), so `HeaderFooterLayer` paints it per page behind the body and the zone's reach ignores it — inline it would reserve a page-tall line of body space. Text boxes there are still dropped with a warning. `hfIsEmpty` counts a rule-/shading-only paragraph as non-empty so it renders and exports.
|
||||
|
||||
## Defaults on DOCX import
|
||||
|
||||
|
||||
+10
-6
@@ -1433,13 +1433,17 @@ function convertInline(root: Element, ctx: Ctx, baseProps: PropMap, defaults: Bl
|
||||
}
|
||||
if (e.namespaceURI === NS.draw) {
|
||||
const conv = convertDrawElement(e, ctx);
|
||||
// The one-paragraph zone takes as-char images only — boxes and positioned
|
||||
// frames (a page-sized background reserves the whole page inline) are dropped
|
||||
// with a warning, as the DOCX importer does.
|
||||
// The one-paragraph zone flows as-char images; a positioned frame is out of
|
||||
// flow — the page-anchored letterhead or watermark a title page is made of —
|
||||
// and keeps its wrap plus its position from the page corner. Boxes are dropped.
|
||||
if (hfFields) {
|
||||
const wrap = conv?.inline?.attrs?.wrap;
|
||||
if (conv?.inline?.type === 'image' && (!wrap || wrap === 'inline')) {
|
||||
out.push({ ...conv.inline, attrs: { ...conv.inline.attrs, wrap: 'inline' } });
|
||||
const img = conv?.inline?.type === 'image' ? conv.inline : null;
|
||||
const wrap = img?.attrs?.wrap;
|
||||
if (img && (!wrap || wrap === 'inline')) {
|
||||
out.push({ ...img, attrs: { ...img.attrs, wrap: 'inline' } });
|
||||
} else if (img) {
|
||||
const at = (a: string) => Math.max(0, lengthToCm(e.getAttributeNS(NS.svg, a)) ?? 0);
|
||||
out.push({ ...img, attrs: { ...img.attrs, wrapOffset: at('x'), wrapOffsetY: at('y') } });
|
||||
} else if (conv) ctx.warnings.add('Drawings were removed');
|
||||
continue;
|
||||
}
|
||||
|
||||
+22
-16
@@ -19,6 +19,12 @@
|
||||
/* A4 paper container (794px = 210mm at 96dpi by default; --user-page-width is
|
||||
set from the Layout panel and swaps to 1123px in landscape — pageOrientation.ts) */
|
||||
.paper {
|
||||
/* Driven by --user-page-height (pageOrientation.ts); fallback = A4 portrait. The
|
||||
page geometry lives here, not on .tiptap, so both inherit the same values. */
|
||||
--page-height: var(--user-page-height, 1123px);
|
||||
--page-gap: 20px;
|
||||
--page-cycle: calc(var(--page-height) + var(--page-gap));
|
||||
|
||||
width: var(--user-page-width, 794px);
|
||||
/* Zoom is a `transform: scale()` set inline (Editor.svelte); pin the origin so the
|
||||
page scales down/right from the top-left of its reserved footprint. */
|
||||
@@ -30,19 +36,28 @@
|
||||
/* A frame or a formula wider than the text area is cut at the sheet's edge, as it is
|
||||
in LibreOffice. `clip` rather than `hidden`: it must not become a scroll box. */
|
||||
overflow-x: clip;
|
||||
|
||||
/* The page surface (white) alternating with the gap, where .tiptap paints only the
|
||||
borders on top of it. It sits here so a header's page background (.hf-bg-layer,
|
||||
z-index -1) can paint between the sheet and the body text. */
|
||||
background: repeating-linear-gradient(
|
||||
to bottom,
|
||||
var(--color-page-bg) 0px,
|
||||
var(--color-page-bg) var(--page-height),
|
||||
transparent var(--page-height),
|
||||
transparent var(--page-cycle)
|
||||
);
|
||||
background-size: 100% var(--page-cycle);
|
||||
background-repeat: repeat-y;
|
||||
}
|
||||
|
||||
.paper .tiptap {
|
||||
/* Driven by --user-page-height (pageOrientation.ts); fallback = A4 portrait. */
|
||||
--page-height: var(--user-page-height, 1123px);
|
||||
--page-gap: 20px;
|
||||
/* Driven by --user-margin-* set on :root from the Layout panel (pageMargins.ts).
|
||||
Fallbacks preserve the original hardcoded layout when unset. */
|
||||
--page-margin-top: var(--user-margin-top, 96px);
|
||||
--page-margin-bottom: var(--user-margin-bottom, 96px);
|
||||
--page-margin-left: var(--user-margin-left, 80px);
|
||||
--page-margin-right: var(--user-margin-right, 80px);
|
||||
--page-cycle: calc(var(--page-height) + var(--page-gap));
|
||||
|
||||
position: relative;
|
||||
width: 100%;
|
||||
@@ -60,7 +75,7 @@
|
||||
line-height: 1.15;
|
||||
color: var(--color-page-text);
|
||||
|
||||
/* Multi-page background: page surfaces separated by editor background */
|
||||
/* Page borders, drawn over the sheet .paper paints */
|
||||
background:
|
||||
/* Left border per page */
|
||||
repeating-linear-gradient(
|
||||
@@ -89,24 +104,14 @@
|
||||
var(--color-page-border) var(--page-height),
|
||||
transparent var(--page-height),
|
||||
transparent var(--page-cycle)
|
||||
),
|
||||
/* Page surface (white) alternating with gap (transparent = editor bg shows) */
|
||||
repeating-linear-gradient(
|
||||
to bottom,
|
||||
var(--color-page-bg) 0px,
|
||||
var(--color-page-bg) var(--page-height),
|
||||
transparent var(--page-height),
|
||||
transparent var(--page-cycle)
|
||||
);
|
||||
background-size:
|
||||
1px var(--page-cycle),
|
||||
1px var(--page-cycle),
|
||||
100% var(--page-cycle),
|
||||
100% var(--page-cycle);
|
||||
background-position:
|
||||
left top,
|
||||
right top,
|
||||
left top,
|
||||
left top;
|
||||
background-repeat: repeat-y;
|
||||
|
||||
@@ -826,10 +831,11 @@
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
transform: none;
|
||||
background: white !important;
|
||||
}
|
||||
|
||||
.paper .tiptap {
|
||||
background: white !important;
|
||||
background: none !important;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
|
||||
Reference in new issue
Block a user