Ruby, as both products' phonetic guide writes it: one element holding a base and its reading, so the editor keeps it as one inline atom and the browser sets it as a real <ruby>. ODF gets <text:ruby> plus a minted ruby-family style (centred, above — probed: LibreOffice writes that style back unchanged); Word a <w:ruby> run from a post-pack pass, the package having none. Both legs verified through a LibreOffice re-save, our .docx to .odt included.
1110 lines
45 KiB
CSS
1110 lines
45 KiB
CSS
.editor {
|
||
flex: 1;
|
||
/* Top padding clears the toolbar island overlaying this scroll area (height set
|
||
by App.svelte); extra bottom padding clears the fixed status bar. */
|
||
padding: calc(var(--toolbar-overlay-h, 0px) + 1.25rem) 1rem 3rem;
|
||
overflow: auto;
|
||
/* Positioning context for the floating table toolbar (TableToolbar.svelte),
|
||
which is an absolutely-positioned child that scrolls with the content. */
|
||
position: relative;
|
||
}
|
||
|
||
/* Reserves the SCALED footprint of the transformed .paper (a CSS transform takes
|
||
no layout space of its own), so the scrollbars and horizontal centering reflect
|
||
the zoomed page. Its width/height are sized in JS (Editor.svelte). */
|
||
.paper-scaler {
|
||
margin: 0 auto;
|
||
}
|
||
|
||
/* 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));
|
||
|
||
/* The widest section's paper, so a landscape page is not cut at the sheet edge. */
|
||
width: var(--pb-paper-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. */
|
||
transform-origin: top left;
|
||
/* Positioning context for the table page-break overlay (.band-layer in
|
||
Editor.svelte), which lives inside .paper so the same transform scales it
|
||
identically to the .tiptap page background. */
|
||
position: relative;
|
||
/* 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 sheets themselves are one box per page (PageSheetLayer), since a section on its
|
||
own paper makes them differ in size. They sit at z-index -2, under a header's page
|
||
background (.hf-bg-layer, -1) and so under the body text. */
|
||
}
|
||
|
||
/* While a freshly opened document is still finding its pagination (Editor.svelte).
|
||
Hidden, not removed: every pass measures the same boxes either way, and the sheet
|
||
.paper paints stays up, so the page appears once instead of shuffling into place. */
|
||
.paper.settling > * {
|
||
visibility: hidden;
|
||
}
|
||
|
||
.paper .tiptap {
|
||
/* 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);
|
||
|
||
position: relative;
|
||
width: 100%;
|
||
min-height: var(--page-height);
|
||
padding: var(--page-margin-top) var(--page-margin-right) var(--page-margin-bottom) var(--page-margin-left);
|
||
outline: none;
|
||
font-family: var(--font-serif);
|
||
/* 12pt matches odf-kit's Standard style and the toolbar's declared default. */
|
||
font-size: 12pt;
|
||
/* "Single" spacing is the font's natural line height (Liberation Serif ≈ 1.15× em),
|
||
not CSS unitless 1. A paragraph's line spacing multiplies that, so the two ride as
|
||
separate variables and the block rule below is the one place they meet. */
|
||
--natural-line: 1.15;
|
||
--line-factor: 1;
|
||
line-height: 1.15;
|
||
color: var(--color-page-text);
|
||
|
||
/* The page borders ride the sheet boxes (PageSheetLayer). */
|
||
border: none;
|
||
|
||
/* Subtle overall shadow for depth */
|
||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||
}
|
||
|
||
/* Page break spacer widgets (inserted by ProseMirror decorations).
|
||
line-height/font-size are zeroed so the spacer doesn't pick up the
|
||
inherited inline strut when it sits mid-paragraph between text nodes —
|
||
otherwise the browser adds a phantom line of height around the widget
|
||
and the continuation text on the next page lands too low.
|
||
Scoped to div so it never overrides the display of the table-row spacer below. */
|
||
.paper .tiptap div[data-page-break-spacer] {
|
||
display: block;
|
||
width: 100%;
|
||
margin: 0;
|
||
padding: 0;
|
||
border: none;
|
||
background: none;
|
||
line-height: 0;
|
||
font-size: 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* Table page-break spacer: a borderless, empty <tr>/<td> that bridges the page
|
||
gap so the following rows start on the next page (a table can't hold a block
|
||
spacer between its rows). Keeps default table-row/cell display. */
|
||
.paper .tiptap tr[data-page-break-spacer] {
|
||
background: none;
|
||
}
|
||
|
||
.paper .tiptap td[data-page-break-spacer-cell] {
|
||
padding: 0;
|
||
border: none;
|
||
line-height: 0;
|
||
font-size: 0;
|
||
}
|
||
|
||
/* A section whose page setup has side margins of its own insets its top-level blocks by
|
||
the difference — .tiptap's padding can only draw one pair (pageBreaks.ts sets the two
|
||
properties per block). Everything that writes a block margin adds them in, so an indent
|
||
and the inset compose; a descendant clears them, or a nested one would add them twice. */
|
||
.paper .tiptap > * {
|
||
margin-left: var(--sec-inset-left, 0px);
|
||
margin-right: var(--sec-inset-right, 0px);
|
||
}
|
||
.paper .tiptap > * * {
|
||
--sec-inset-left: 0px;
|
||
--sec-inset-right: 0px;
|
||
}
|
||
|
||
/* No paragraph spacing by default, like LibreOffice's Default Paragraph Style and
|
||
Word's implied 0 — an editor default here would show up as extra space in every
|
||
imported document (export/odt.ts zeroes odf-kit's Standard style to match). */
|
||
.paper .tiptap p {
|
||
margin-top: 0;
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
/* One paragraph opting out of the document's automatic hyphenation (pageBreak.ts).
|
||
The host carries `hyphens: auto`, so only the paragraph has to say no. */
|
||
.paper .tiptap [data-no-hyphenation] {
|
||
hyphens: none;
|
||
}
|
||
|
||
/* A page-anchored frame is placed from its page corner and is out of the flow, so the
|
||
paragraph the importer hoists it into reserves no space (image.ts). !important: several
|
||
other paragraph rules below set their own `position: relative` (line-height shift, the
|
||
trailing-break spacer) — any of those would make this paragraph the frame's containing
|
||
block instead of the page, offsetting it by a page margin. */
|
||
.paper .tiptap p:has(> [data-anchor-page]) {
|
||
height: 0;
|
||
margin: 0;
|
||
position: static !important;
|
||
}
|
||
|
||
/* Paragraph "colored field" + rule line (paragraphBox.ts). Background-color and per-side
|
||
borders are inline styles from renderHTML; a bordered block gets a little padding so the
|
||
text doesn't touch the rule (matches LibreOffice's fo:padding on such paragraphs). */
|
||
.paper .tiptap [data-pborder-top],
|
||
.paper .tiptap [data-pborder-bottom],
|
||
.paper .tiptap [data-pborder-left],
|
||
.paper .tiptap [data-pborder-right] {
|
||
padding: 0.04cm 0.08cm;
|
||
}
|
||
|
||
/* Hyperlinks (link.ts). Ctrl/Cmd+click opens them (Editor.svelte). */
|
||
/* The browser's own link colour is off first: a link Word left unstyled is drawn like the
|
||
text around it, since there the blue is a character style, not a property of being a
|
||
link (link.ts). Every other link — ODF paints them all — takes the visual back. */
|
||
.paper .tiptap a {
|
||
color: inherit;
|
||
text-decoration: none;
|
||
cursor: text;
|
||
}
|
||
.paper .tiptap a:not([data-plain]) {
|
||
color: #1a56db;
|
||
text-decoration: underline;
|
||
}
|
||
|
||
/* Find & Replace match highlights (searchReplace.ts); current match is stronger. */
|
||
.paper .tiptap .search-match {
|
||
background: rgba(255, 213, 0, 0.45);
|
||
border-radius: 2px;
|
||
}
|
||
.paper .tiptap .search-match-current {
|
||
background: rgba(255, 145, 0, 0.8);
|
||
}
|
||
|
||
/* A ruby annotation (ruby.ts). The reading sits above its base at half its size,
|
||
which is what both word processors print and what `w:hps` says. */
|
||
.paper .tiptap ruby {
|
||
ruby-position: over;
|
||
}
|
||
.paper .tiptap ruby rt {
|
||
font-size: 0.5em;
|
||
line-height: 1;
|
||
}
|
||
|
||
/* The word being offered (wordCompletion.ts): after the caret, in the text's own
|
||
font, greyed — it is not in the document until Enter takes it. */
|
||
.paper .tiptap .word-completion {
|
||
opacity: 0.45;
|
||
user-select: none;
|
||
}
|
||
|
||
/* Generated table of contents (tableOfContents.ts). A non-editable block listing
|
||
headings with dot leaders to their live page number; entries are clickable. */
|
||
.paper .tiptap .toc {
|
||
margin: 0 0 0.5cm;
|
||
user-select: none;
|
||
cursor: default;
|
||
}
|
||
.paper .tiptap .toc-title {
|
||
font-size: 16pt;
|
||
font-weight: bold;
|
||
margin-bottom: 0.3cm;
|
||
}
|
||
.paper .tiptap .toc-empty {
|
||
font-style: italic;
|
||
opacity: 0.6;
|
||
}
|
||
.paper .tiptap .toc-entry {
|
||
display: flex;
|
||
/* The last one: an entry whose heading carries a hard break is several lines, and the
|
||
leader and page number belong beside the last of them. */
|
||
align-items: last baseline;
|
||
cursor: pointer;
|
||
}
|
||
.paper .tiptap .toc-entry:hover .toc-text {
|
||
text-decoration: underline;
|
||
}
|
||
/* One level = 0.5cm, the indent LibreOffice's own Contents 1…5 styles carry — the
|
||
fallback for an index whose file names no per-level style (that one brings its own). */
|
||
.paper .tiptap .toc-level-2:not([data-style]) { padding-left: 0.5cm; }
|
||
.paper .tiptap .toc-level-3:not([data-style]) { padding-left: 1cm; }
|
||
.paper .tiptap .toc-level-4:not([data-style]) { padding-left: 1.5cm; }
|
||
.paper .tiptap .toc-level-5:not([data-style]) { padding-left: 2cm; }
|
||
.paper .tiptap .toc-text {
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
flex: 0 1 auto;
|
||
}
|
||
/* Leader dots filling the gap: the row grants the span the space that is left and
|
||
clips the dots that do not fit, so the page number stays put. */
|
||
.paper .tiptap .toc-leader {
|
||
flex: 1 1 0; /* basis 0: the dots are decoration, they must not size the row */
|
||
min-width: 1em;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
}
|
||
.paper .tiptap .toc-page {
|
||
flex: 0 0 auto;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* Footnotes and endnotes (notes.ts). Sizes and indents are LibreOffice's own Footnote
|
||
style; the separator values follow its style:footnote-sep — see
|
||
docs/architecture/notes.md, all of them probed. The dialog overrides them on :root. */
|
||
/* Registered, so getComputedStyle resolves them to px: pageBreaks.ts reserves the
|
||
separator's own band out of the page and an unregistered var reads back as "0.1cm". */
|
||
@property --note-sep-above { syntax: '<length>'; inherits: true; initial-value: 0.1cm; }
|
||
@property --note-sep-weight { syntax: '<length>'; inherits: true; initial-value: 0.5pt; }
|
||
@property --note-sep-below { syntax: '<length>'; inherits: true; initial-value: 0.1cm; }
|
||
.paper .tiptap .note-ref {
|
||
/* style:text-position="super 58%" on LibreOffice's Footnote anchor style. */
|
||
vertical-align: super;
|
||
font-size: 58%;
|
||
line-height: 0;
|
||
cursor: pointer;
|
||
}
|
||
/* Padding, not a margin, for the hanging indent: the box then still spans the whole
|
||
text width, which is what the separator's 25% is measured against. LibreOffice's own
|
||
Footnote style, and only for a note whose file names none of its own. */
|
||
.paper .tiptap .note {
|
||
padding-left: 0.6cm;
|
||
text-indent: -0.6cm;
|
||
}
|
||
.paper .tiptap .note:not([data-style]) {
|
||
font-size: 10pt;
|
||
}
|
||
/* The note's own marker is plain text at the baseline — LibreOffice's Footnote Symbol
|
||
style carries nothing, only the anchor is raised. An inline-block the width of the
|
||
hanging indent stands in for the tab LibreOffice puts after it. */
|
||
.paper .tiptap .note-marker {
|
||
display: inline-block;
|
||
min-width: 0.6cm;
|
||
text-indent: 0;
|
||
}
|
||
/* A note style declares its indent as a hanging pair (fo:margin-left with a negative
|
||
fo:text-indent) and the style model has no first-line half, so its margin alone would
|
||
just push the note right. The hanging pair stays LibreOffice's; the font, size and
|
||
colour are the file's. */
|
||
.paper .tiptap .note[data-style] {
|
||
margin-left: 0;
|
||
margin-right: 0;
|
||
}
|
||
/* A footnote is lifted out of the flow to the foot of its anchor's page; pageBreaks.ts
|
||
sets `top` in document px and marks the first note of each page. The margin custom
|
||
properties are .tiptap's own, so the box spans exactly the text width. */
|
||
.paper .tiptap .note[data-note-kind='footnote'] {
|
||
position: absolute;
|
||
left: var(--page-margin-left);
|
||
right: var(--page-margin-right);
|
||
}
|
||
/* Collected at the end of the document instead (noteSettings.ts): the footnotes rejoin
|
||
the flow and open their own page, where the endnote list also starts. */
|
||
:root[data-collect-footnotes] .paper .tiptap .note[data-note-kind='footnote'] {
|
||
position: static;
|
||
}
|
||
/* The separator hangs above the first note of a page, inside the band pageBreaks.ts
|
||
already reserved for it — so it costs the note's own box no height, which is what
|
||
keeps that height out of the reservation that produced it. A gradient, not a
|
||
border-top, because the line is only a quarter of the text width. */
|
||
.paper .tiptap .note[data-note-page-first]::after {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0;
|
||
right: 0;
|
||
top: calc(-1 * (var(--note-sep-weight) + var(--note-sep-below)));
|
||
/* Drawn at least one device pixel: 0.5pt is 0.66px, which Chromium rounds away to
|
||
nothing. The reservation above still uses the true weight, as a hairline border
|
||
does — the line is a pixel wider than it measures, not the space it sits in. */
|
||
height: max(1px, var(--note-sep-weight));
|
||
/* The two non-length values carry their own default: only a registered property
|
||
takes one from @property, and the dialog writes all of them onto :root. */
|
||
background-image: linear-gradient(var(--note-sep-color, #000), var(--note-sep-color, #000));
|
||
background-size: var(--note-sep-rel-width, 25%) 100%;
|
||
background-repeat: no-repeat;
|
||
background-position: var(--note-sep-align, left) top;
|
||
}
|
||
|
||
/* Formulas (formula.ts). MathML needs a font with an OpenType MATH table or Chromium
|
||
drops ∑/ϕ and won't stretch brackets — STIX Two Math is bundled for that. */
|
||
/* The family must be set on <math> itself: the UA stylesheet declares
|
||
`math { font-family: math }` on the element, and an inherited value never beats it. */
|
||
.paper .tiptap .formula math {
|
||
font-family: 'STIX Two Math', math, serif;
|
||
font-style: normal;
|
||
}
|
||
/* Baseline, not middle: an inline formula sits on the text baseline in LibreOffice and
|
||
Word, and MathML already carries the right baseline for its own box. */
|
||
.paper .tiptap span.formula {
|
||
display: inline-block;
|
||
vertical-align: baseline;
|
||
cursor: pointer;
|
||
}
|
||
/* A display formula owns its line, centered, as in LibreOffice and Word. */
|
||
.paper .tiptap span.formula[data-display='true'] {
|
||
display: block;
|
||
text-align: center;
|
||
}
|
||
.paper .tiptap span.formula.selected {
|
||
outline: 2px solid var(--color-primary, #52748b);
|
||
outline-offset: 1px;
|
||
border-radius: 2px;
|
||
}
|
||
|
||
/* Inline, as-character images (image.ts). The wrapper reserves the rotated bounding
|
||
box; the rotor holds the <img>, resize handles and rotate grip, rotating together. */
|
||
.paper .tiptap .image-node {
|
||
position: relative;
|
||
display: inline-block;
|
||
/* An as-char frame stands on the text baseline in LibreOffice and Word alike; the
|
||
file may say otherwise, and image.ts then overrides this per frame. */
|
||
vertical-align: baseline;
|
||
line-height: 0;
|
||
}
|
||
/* Alone on its line (image.ts marks the frame) it sets the line height by itself: no
|
||
descent hangs below it, and every vertical-pos but `offset` is moot (probed). Beats
|
||
the node view's own inline alignment, which has no other way to know. */
|
||
.paper .tiptap .image-node.image-line:not([data-v-align='offset']) {
|
||
vertical-align: bottom !important;
|
||
}
|
||
/* While selected, lift the node (and its handles) above the header/footer layer
|
||
(z-index 22) so a rotate/resize handle poking into a margin stays grabbable. */
|
||
.paper .tiptap .image-node.image-selected {
|
||
z-index: 25;
|
||
}
|
||
/* A block after a float keeps its own box over the float — only its lines move out of
|
||
the way — so the block on top swallows every click meant for the frame. Nothing is
|
||
hidden by lifting it: text flows beside a frame, never across it. A page-anchored
|
||
one is excluded, since it carries its own place in the stack (image.ts). */
|
||
.paper .tiptap .image-node[data-wrap]:not([data-anchor-page]) {
|
||
z-index: 1;
|
||
}
|
||
.paper .tiptap .image-rotor {
|
||
position: absolute;
|
||
left: 50%;
|
||
top: 50%;
|
||
transform: translate(-50%, -50%);
|
||
}
|
||
.paper .tiptap .image-rotor img {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: block;
|
||
}
|
||
.paper .tiptap .image-node.image-selected .image-rotor img {
|
||
outline: 2px solid var(--color-primary);
|
||
outline-offset: 1px;
|
||
}
|
||
.paper .tiptap .image-resize-handle {
|
||
display: none;
|
||
position: absolute;
|
||
width: 10px;
|
||
height: 10px;
|
||
background: var(--color-primary);
|
||
border: 1px solid #fff;
|
||
border-radius: 50%;
|
||
z-index: 30;
|
||
}
|
||
.paper .tiptap .image-node.image-selected .image-resize-handle,
|
||
.paper .tiptap .image-node.image-selected .image-rotate-handle,
|
||
.paper .tiptap .textbox-node.textbox-selected .image-resize-handle,
|
||
.paper .tiptap .textbox-node.textbox-selected .image-rotate-handle,
|
||
.paper .tiptap .textbox-node.textbox-active .image-resize-handle,
|
||
.paper .tiptap .textbox-node.textbox-active .image-rotate-handle {
|
||
display: block;
|
||
}
|
||
.paper .tiptap .image-resize-nw { top: -5px; left: -5px; cursor: nwse-resize; }
|
||
.paper .tiptap .image-resize-n { top: -5px; left: 50%; margin-left: -5px; cursor: ns-resize; }
|
||
.paper .tiptap .image-resize-ne { top: -5px; right: -5px; cursor: nesw-resize; }
|
||
.paper .tiptap .image-resize-e { top: 50%; right: -5px; margin-top: -5px; cursor: ew-resize; }
|
||
.paper .tiptap .image-resize-se { bottom: -5px; right: -5px; cursor: nwse-resize; }
|
||
.paper .tiptap .image-resize-s { bottom: -5px; left: 50%; margin-left: -5px; cursor: ns-resize; }
|
||
.paper .tiptap .image-resize-sw { bottom: -5px; left: -5px; cursor: nesw-resize; }
|
||
.paper .tiptap .image-resize-w { top: 50%; left: -5px; margin-top: -5px; cursor: ew-resize; }
|
||
/* Rotate grip floats above top-centre with a short connector line; the icon is a
|
||
circular rotation arrow. */
|
||
.paper .tiptap .image-rotate-handle {
|
||
display: none;
|
||
position: absolute;
|
||
top: -28px;
|
||
left: 50%;
|
||
width: 16px;
|
||
height: 16px;
|
||
margin-left: -8px;
|
||
background: #fff center / 11px 11px no-repeat
|
||
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2352748b' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 12a9 9 0 1 1-2.6-6.4'/%3E%3Cpath d='M21 4v5h-5'/%3E%3C/svg%3E");
|
||
border: 1px solid var(--color-primary);
|
||
border-radius: 50%;
|
||
cursor: grab;
|
||
z-index: 31;
|
||
}
|
||
.paper .tiptap .image-rotate-handle::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 15px;
|
||
left: 50%;
|
||
width: 1px;
|
||
height: 13px;
|
||
margin-left: -0.5px;
|
||
background: var(--color-primary);
|
||
}
|
||
/* Live size readout while resizing. */
|
||
.paper .tiptap .image-size-badge {
|
||
display: none;
|
||
position: absolute;
|
||
top: 6px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
padding: 2px 7px;
|
||
background: rgba(0, 0, 0, 0.75);
|
||
color: #fff;
|
||
font-size: 11px;
|
||
line-height: 1.4;
|
||
white-space: nowrap;
|
||
border-radius: 4px;
|
||
pointer-events: none;
|
||
z-index: 32;
|
||
}
|
||
|
||
/* Text boxes / shapes (textBox.ts). Same wrapper/rotor split as images: the wrapper
|
||
reserves the rotated bounding box, the rotor carries fill/stroke/rotation and the
|
||
shared image-* handles; .textbox-content is the editable area. */
|
||
.paper .tiptap .textbox-node {
|
||
position: relative;
|
||
/* The anchor paragraph's space above, as padding so it adds to the block above
|
||
rather than collapsing against it (textBox.ts, storage/spacingModel.ts). The
|
||
inline height is the frame's, so the padding has to sit outside it. */
|
||
box-sizing: content-box;
|
||
padding-top: var(--space-before);
|
||
}
|
||
.paper .tiptap .textbox-node.textbox-selected,
|
||
.paper .tiptap .textbox-node.textbox-active {
|
||
z-index: 25;
|
||
}
|
||
.paper .tiptap .textbox-rotor {
|
||
position: absolute;
|
||
left: 50%;
|
||
/* Centred in the padding box, so half the space above has to come back to keep the
|
||
frame itself below it — the *applied* space, which a page top drops. */
|
||
top: calc(50% + var(--space-top, 0px) / 2);
|
||
box-sizing: border-box;
|
||
transform: translate(-50%, -50%);
|
||
}
|
||
.paper .tiptap .textbox-node.textbox-selected .textbox-rotor,
|
||
.paper .tiptap .textbox-node.textbox-active .textbox-rotor {
|
||
outline: 2px solid var(--color-primary);
|
||
outline-offset: 1px;
|
||
}
|
||
/* Grab affordance on the frame edge (outside the text area). */
|
||
.paper .tiptap .textbox-rotor {
|
||
cursor: move;
|
||
}
|
||
/* The outline of a shape CSS can't draw, behind the text. Such a frame carries no
|
||
padding of its own (the text inset moves to .textbox-content), so the padding box
|
||
this covers is the frame. overflow stays visible or the stroke is clipped in half
|
||
at the viewBox edge. */
|
||
.paper .tiptap .textbox-outline {
|
||
position: absolute;
|
||
inset: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
overflow: visible;
|
||
pointer-events: none;
|
||
}
|
||
/* A line is drawn in the frame's own pixels, so the SVG carries no stretching; it
|
||
still overflows, since half the stroke sits outside a frame of no height. */
|
||
.paper .tiptap .textbox-line {
|
||
position: absolute;
|
||
inset: 0;
|
||
overflow: visible;
|
||
pointer-events: none;
|
||
}
|
||
.paper .tiptap .textbox-content {
|
||
cursor: text;
|
||
min-height: 1em;
|
||
position: relative;
|
||
}
|
||
.paper .tiptap .textbox-content > :first-child {
|
||
margin-top: 0;
|
||
padding-top: 0;
|
||
}
|
||
.paper .tiptap .textbox-content > :last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
/* Multi-column section (columns.ts); column-count/gap come from the node's
|
||
inline style. Balanced fill matches ODF/Word section behavior; no visual
|
||
affordance — the section must read as plain flowing text. */
|
||
.paper .tiptap .columns-node {
|
||
column-fill: balance;
|
||
}
|
||
/* Its first block sits flush with the column top — the space above it is dropped there,
|
||
as at a page top (probed: LibreOffice starts both columns level). Zeroing the property
|
||
the two spacing models read beats the block's own inline value, which a margin wouldn't. */
|
||
.paper .tiptap .columns-node > :first-child {
|
||
--space-before: 0 !important;
|
||
margin-top: 0;
|
||
padding-top: 0;
|
||
}
|
||
.paper .tiptap .columns-node > :last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
/* A tab stop's leader fills the gap the tab was given as margin-left. The span is
|
||
zero-width (tab-size:0), so its right padding edge is where the stop sits. Not scoped
|
||
to .tiptap: a static header/footer zone carries the same spans. */
|
||
.paper .tab-leader {
|
||
position: relative;
|
||
}
|
||
.paper .tab-leader::before {
|
||
position: absolute;
|
||
right: 0;
|
||
top: 0;
|
||
width: var(--leader-w, 0);
|
||
overflow: hidden;
|
||
white-space: pre;
|
||
line-height: inherit;
|
||
pointer-events: none;
|
||
/* Long enough to outrun any gap A4 can hold; the box clips the rest. */
|
||
content: '................................................................................................................................................................................................................';
|
||
}
|
||
.paper .tab-leader[data-leader='-']::before {
|
||
content: '----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------';
|
||
}
|
||
.paper .tab-leader[data-leader='_']::before {
|
||
content: '____________________________________________________________________________________________________________________________________________________________________________________________________________';
|
||
}
|
||
.paper .tab-leader[data-leader='·']::before {
|
||
content: '····································································································································································································································';
|
||
}
|
||
|
||
/* The document's own tab interval (storage/tabInterval.ts), LibreOffice's by default. */
|
||
.paper .tiptap {
|
||
tab-size: var(--tab-interval, 1.25cm);
|
||
/* prosemirror-view sets `break-spaces`, which forbids a line-end space from
|
||
hanging into the margin, so lines break one word earlier than in
|
||
Word/LibreOffice. `pre-wrap` lets it hang, as they do. */
|
||
white-space: pre-wrap;
|
||
/* A word too long for its line (a URL, a German compound) is broken by Word and
|
||
LibreOffice rather than run past the margin. `break-word`, not `anywhere`, so a
|
||
table column's min-content width is still the longest word. */
|
||
overflow-wrap: break-word;
|
||
}
|
||
|
||
.paper .tiptap :is(h1, h2, h3, h4, h5, h6, h7, h8, h9, h10) {
|
||
/* Neutral base: font, size and margins come from the named paragraph styles
|
||
(styles/styleSheet.ts → the #document-styles element Editor.svelte writes).
|
||
HTML stops at h6, so 7–10 are unknown elements — inline until told otherwise. */
|
||
display: block;
|
||
font: inherit;
|
||
margin: 0;
|
||
}
|
||
|
||
/* Space above a block. LibreOffice adds it to the space below the block above, which CSS
|
||
margins can't do (they collapse to the larger) — so it renders as padding. A Word
|
||
document takes the larger, LibreOffice included, and gets a margin instead. */
|
||
@property --space-before {
|
||
syntax: '<length>';
|
||
inherits: false;
|
||
initial-value: 0;
|
||
}
|
||
.paper .tiptap :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10, .toc, .toc-title, .toc-entry) {
|
||
padding-top: var(--space-before);
|
||
}
|
||
.paper[data-spacing-model='max'] .tiptap :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10, .toc, .toc-title, .toc-entry) {
|
||
padding-top: 0;
|
||
margin-top: var(--space-before);
|
||
}
|
||
/* LibreOffice adds no space above the first block of a table cell (measured; its
|
||
AddParaTableSpacingAtStart), Word does — so it follows the document's spacing model. */
|
||
.paper:not([data-spacing-model='max']) .tiptap :is(td, th) > :first-child {
|
||
padding-top: 0;
|
||
margin-top: 0;
|
||
}
|
||
|
||
/* The line box: the paragraph's spacing factor times its font's natural line height.
|
||
Either half may come from the style rule or, nearer, from the block's own attrs
|
||
(lineHeight.ts / blockFontSize.ts) — multiplying them here is what keeps both. */
|
||
.paper .tiptap :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10, li, .toc-entry) {
|
||
line-height: calc(var(--line-factor) * var(--natural-line));
|
||
}
|
||
|
||
/* A spacing above single puts all of its extra leading *below* each line (measured in
|
||
LibreOffice); CSS splits it half above. Shifting the block up by that half draws its
|
||
lines where LibreOffice does and leaves the flow — so pagination — untouched. */
|
||
.paper .tiptap :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10) {
|
||
position: relative;
|
||
top: calc((1 - var(--line-factor)) * var(--natural-line) * 0.5em);
|
||
}
|
||
|
||
.paper .tiptap ul,
|
||
.paper .tiptap ol {
|
||
/* One level = LIST_LEFT_STEP_CM (export/docx.ts, and the 1.27cm base the ODT list
|
||
styles use), so the on-screen item text sits where both exports place it. */
|
||
padding-left: 1.27cm;
|
||
margin-top: 0;
|
||
/* Matches the exported List_20_* styles, which inherit Standard's zeroed margin. */
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
/* The marker is drawn by the item block's ::before, not by ::marker, because Word and
|
||
LibreOffice put it at the level's hanging indent (LIST_HANGING_CM 0.635cm, which both
|
||
exports write) and CSS gives ::marker no position — Chromium sets it flush against the
|
||
text, ~4mm to the right. `list-style: none` only drops the marker box; the li stays a
|
||
list-item, so the list-item counter and <ol start> keep working. */
|
||
.paper .tiptap li {
|
||
list-style: none;
|
||
}
|
||
|
||
/* Out of flow, so the marker cannot touch the line it labels — in flow it grew every
|
||
list line by a pixel, which cost the thesis fixture a page. `top: 0` seats it on the
|
||
first line, since the pseudo takes the block's own line-height. */
|
||
.paper .tiptap li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10) {
|
||
position: relative;
|
||
}
|
||
|
||
.paper .tiptap li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before {
|
||
position: absolute;
|
||
top: 0;
|
||
left: -0.635cm;
|
||
white-space: pre;
|
||
line-height: inherit;
|
||
/* The marker follows the item's first text portion (listMarker.ts sets the custom
|
||
properties on the <li>, which the block inherits). */
|
||
font-family: var(--marker-family, inherit);
|
||
font-weight: var(--marker-weight, inherit);
|
||
font-style: var(--marker-style, inherit);
|
||
font-size: var(--marker-size, inherit);
|
||
color: var(--marker-color, inherit);
|
||
}
|
||
|
||
/* A label set against the far end of the hanging indent grows into the margin instead
|
||
of into the text — what keeps Word's Roman numerals off the words they label. */
|
||
.paper .tiptap :is(ol, ul)[data-marker-align="right"] > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before {
|
||
left: auto;
|
||
right: calc(100% + 0.635cm);
|
||
}
|
||
|
||
/* Ordered numbering, keyed by data-eff-list-style — the effective numbering
|
||
orderedList.ts's plugin resolves per <ol> from its attr + ancestor chain. The
|
||
undecorated <ol> is the plain decimal fallback and must come first. */
|
||
.paper .tiptap ol > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before { content: counter(list-item, decimal) ". "; }
|
||
.paper .tiptap ol[data-eff-list-style="decimal"] > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before { content: counter(list-item, decimal) ". "; }
|
||
.paper .tiptap ol[data-eff-list-style="decimal-paren"] > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before { content: counter(list-item, decimal) ") "; }
|
||
.paper .tiptap ol[data-eff-list-style="lower-alpha"] > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before { content: counter(list-item, lower-alpha) ". "; }
|
||
.paper .tiptap ol[data-eff-list-style="lower-alpha-paren"] > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before { content: counter(list-item, lower-alpha) ") "; }
|
||
.paper .tiptap ol[data-eff-list-style="upper-alpha"] > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before { content: counter(list-item, upper-alpha) ". "; }
|
||
.paper .tiptap ol[data-eff-list-style="upper-alpha-paren"] > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before { content: counter(list-item, upper-alpha) ") "; }
|
||
.paper .tiptap ol[data-eff-list-style="lower-roman"] > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before { content: counter(list-item, lower-roman) ". "; }
|
||
.paper .tiptap ol[data-eff-list-style="lower-roman-paren"] > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before { content: counter(list-item, lower-roman) ") "; }
|
||
.paper .tiptap ol[data-eff-list-style="upper-roman"] > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before { content: counter(list-item, upper-roman) ". "; }
|
||
.paper .tiptap ol[data-eff-list-style="upper-roman-paren"] > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before { content: counter(list-item, upper-roman) ") "; }
|
||
|
||
/* Legal/outline numbering (1., 1.1., 1.2.1. …): the plugin tags the multilevel list and
|
||
every attr-less member of its chain with data-eff-list-style="multilevel". */
|
||
.paper .tiptap ol[data-eff-list-style="multilevel"] > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before {
|
||
content: counters(list-item, ".") ". ";
|
||
}
|
||
|
||
/* Default bullet markers per nesting depth — the same cycle the ODT/DOCX export writes
|
||
(DEFAULT_BULLET_CYCLE in bulletListTypes.ts). The bundled symbol shim comes first: it
|
||
holds only the glyphs Liberation Serif lacks (❖ ➢ ⇨ ✓ ▸ ◆), so those stay compact and
|
||
platform-independent while every other char falls through to the text font. */
|
||
.paper .tiptap ul > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before {
|
||
content: '• ';
|
||
font-family: 'EdenText Symbols', var(--marker-family, var(--font-serif)), serif;
|
||
}
|
||
.paper .tiptap ul ul > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before { content: '◦ '; }
|
||
.paper .tiptap ul ul ul > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before { content: '▪ '; }
|
||
.paper .tiptap ul ul ul ul > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before { content: '▸ '; }
|
||
.paper .tiptap ul ul ul ul ul > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before { content: '– '; }
|
||
.paper .tiptap ul ul ul ul ul ul > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before { content: '· '; }
|
||
|
||
/* A list's own `bulletChar` beats the cycle at any depth — an attribute outranks the
|
||
descendant chain. A nested list without one keeps its depth's char, which is what both
|
||
exports write, even though --bullet inherits into it. */
|
||
.paper .tiptap ul[data-bullet] > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before {
|
||
content: var(--bullet) ' ';
|
||
}
|
||
|
||
/* The PDF clone (the .paper itself) replaces every marker with a real span (pdf.ts). */
|
||
.paper.pdf-list-markers li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before {
|
||
content: none;
|
||
}
|
||
|
||
.paper .tiptap li {
|
||
margin-top: 0;
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.paper .tiptap li p {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
/* Tables. The visual border/padding must match the ODT export so the editor
|
||
and the exported .odt look identical (see export/odt.ts CUST_TABLE handler):
|
||
• border → export emits 0.5pt solid #000000; 1px solid renders crisply
|
||
on screen and is the visual equivalent of a thin 0.5pt line.
|
||
• padding → Word's/LibreOffice's cell margins. The fallback below is Word's
|
||
own default; a file's own value rides --cell-pad (tableCellPadding.ts).
|
||
Tables are wrapped in <div class="tableWrapper"> by TipTap's node view. */
|
||
.paper .tiptap .tableWrapper {
|
||
/* Default is flush with the text area; a dragged outer edge sets an inline
|
||
margin-left/-right here (tableView.ts), which is what the export writes. */
|
||
margin: 0;
|
||
overflow-x: auto;
|
||
/* The table's space above adds to the block above it, as a paragraph's does. */
|
||
padding-top: var(--space-before);
|
||
}
|
||
.paper[data-spacing-model='max'] .tiptap .tableWrapper {
|
||
padding-top: 0;
|
||
margin-top: var(--space-before);
|
||
}
|
||
|
||
.paper .tiptap table {
|
||
width: 100%;
|
||
/* fixed layout → the per-column <col> percentage widths from the custom node
|
||
view (tableView.ts) are authoritative; width:100% keeps the table the full
|
||
text width, matching the ODT export (export/odt.ts emits per-column widths
|
||
summing to the text width with table:align="margins"). */
|
||
table-layout: fixed;
|
||
border-collapse: collapse;
|
||
}
|
||
|
||
.paper .tiptap td,
|
||
.paper .tiptap th {
|
||
border: 1px solid #000;
|
||
padding: var(--cell-pad-t, 0) var(--cell-pad-r, 0.19cm) var(--cell-pad-b, 0) var(--cell-pad-l, 0.19cm);
|
||
/* A painted border is a whole pixel wide whatever the file declares, so the excess
|
||
comes off the padding — else a long table grows a line every few rows
|
||
(tableCellBorders.ts). Below first, then what is left of it above. */
|
||
padding-bottom: max(0px, calc(var(--cell-pad-b, 0px) - var(--border-over, 0px)));
|
||
padding-top: max(0px, calc(
|
||
var(--cell-pad-t, 0px) - max(0px, var(--border-over, 0px) - var(--cell-pad-b, 0px))));
|
||
vertical-align: top;
|
||
/* A repeating header row is a <th>, and no word processor centres or bolds its text
|
||
for that reason — drop the two UA defaults so the cell follows its own style. */
|
||
text-align: inherit;
|
||
font-weight: inherit;
|
||
/* Positioning context for the column-resize handle widget (a child div the
|
||
resize plugin places at the cell's right edge). */
|
||
position: relative;
|
||
}
|
||
|
||
/* No space below a cell paragraph that names no style: LibreOffice's Table Contents and
|
||
Word's table styles both zero it, and the space would fill the cell, leaving vertical
|
||
alignment no slack. An inline value (direct formatting) still wins. */
|
||
.paper .tiptap :is(td, th) > p:not([data-style]) {
|
||
margin-bottom: 0;
|
||
}
|
||
/* Only LibreOffice's style zeroes the space above too; Word's table styles set the
|
||
spacing after alone, so a Word document keeps it (as the cell-start rule above does). */
|
||
.paper:not([data-spacing-model='max']) .tiptap :is(td, th) > p:not([data-style]) {
|
||
--space-before: 0;
|
||
}
|
||
|
||
/* Header-row cells (tableHeaderRow.ts toggle): bold rendered presentationally so all
|
||
text — existing, typed, or pasted — is bold. The light fill comes from the cell's
|
||
inline background-color; export bakes the bold for Word/LibreOffice consistency. */
|
||
.paper .tiptap td.cell-header,
|
||
.paper .tiptap th.cell-header {
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* Selected-cell highlight while editing (ProseMirror tables add .selectedCell).
|
||
A translucent overlay via the ::after avoids disturbing the cell border. */
|
||
.paper .tiptap .selectedCell {
|
||
position: relative;
|
||
}
|
||
|
||
.paper .tiptap .selectedCell::after {
|
||
content: '';
|
||
position: absolute;
|
||
inset: 0;
|
||
background: var(--color-primary);
|
||
opacity: 0.12;
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* Column resize (tableColumnResize.ts). The handle is a widget the plugin places
|
||
at a cell's right edge when the pointer is near an internal column border; the
|
||
plugin reads the pointer position itself, so the handle ignores pointer events.
|
||
`.resize-cursor` is toggled on the whole editor while a border is active. */
|
||
.paper .tiptap.resize-cursor {
|
||
cursor: col-resize;
|
||
}
|
||
|
||
.paper .tiptap .column-resize-handle {
|
||
position: absolute;
|
||
right: -2px;
|
||
top: 0;
|
||
bottom: 0;
|
||
width: 4px;
|
||
z-index: 20;
|
||
background: var(--color-primary);
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* The table's outer left edge: same handle, mirrored to the cell's left side. */
|
||
.paper .tiptap .column-resize-handle.left {
|
||
left: -2px;
|
||
right: auto;
|
||
}
|
||
|
||
.paper .tiptap .column-resize-dragging {
|
||
/* Suppress text-selection flicker while dragging a column border. */
|
||
user-select: none;
|
||
}
|
||
|
||
/* Row resize (tableRowResize.ts). Mirrors the column handle but sits at a cell's
|
||
lower edge; one handle per cell forms a continuous line across the active row.
|
||
`.row-resize-cursor` is toggled on the whole editor while a row border is active.
|
||
Column resize takes precedence at a corner, so the two never fight. */
|
||
.paper .tiptap.row-resize-cursor {
|
||
cursor: row-resize;
|
||
}
|
||
|
||
.paper .tiptap .row-resize-handle {
|
||
position: absolute;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: -2px;
|
||
height: 4px;
|
||
z-index: 20;
|
||
background: var(--color-primary);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.paper .tiptap p.is-editor-empty:first-child::before {
|
||
content: attr(data-placeholder);
|
||
float: left;
|
||
color: var(--color-text-muted);
|
||
pointer-events: none;
|
||
height: 0;
|
||
}
|
||
|
||
/* Paragraph mark (¶). Out of flow at its static position — after the last character,
|
||
but invisible to line breaking, so a line-end space still hangs into the margin and
|
||
the mark can never be pushed onto a line of its own. The toggle only recolors it. */
|
||
.paper .tiptap :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::after {
|
||
content: '\00A0¶';
|
||
position: absolute;
|
||
display: inline-block;
|
||
width: 0;
|
||
height: 0;
|
||
/* Out of flow the glyph has no baseline to align to, so it needs a line box of the
|
||
block's own height to sit in — the static position gives it that line's top. */
|
||
line-height: inherit;
|
||
overflow: visible;
|
||
white-space: pre;
|
||
color: transparent;
|
||
opacity: 0.5;
|
||
font-family: var(--font-sans);
|
||
font-weight: 400;
|
||
user-select: none;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.paper.show-formatting-marks .tiptap :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::after {
|
||
color: var(--color-primary);
|
||
}
|
||
|
||
/* After an inline atom ProseMirror's trailing-break BR adds a line the block has not
|
||
got, so a paragraph holding an image renders one too tall. Kept where the block really
|
||
does end in a break — bare, or wrapped in the mark spans of the run that carried it. */
|
||
.paper .tiptap :not(br):not(:has(br:last-child)) + br.ProseMirror-trailingBreak {
|
||
display: none;
|
||
}
|
||
|
||
/* A block ProseMirror ends with a trailing-break BR — an empty one, or one ending in a
|
||
hard break — has its last line before that BR, not after it. */
|
||
.paper .tiptap :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10):has(> br.ProseMirror-trailingBreak),
|
||
.paper .tiptap li > p:has(> br.ProseMirror-trailingBreak) {
|
||
position: relative;
|
||
}
|
||
|
||
.paper .tiptap :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10):has(> br.ProseMirror-trailingBreak)::after,
|
||
.paper .tiptap li > p:has(> br.ProseMirror-trailingBreak)::after {
|
||
/* The static position would land past the BR, a line below the block; name the last
|
||
line instead — for an empty block that is also its first. */
|
||
top: calc(100% - 1lh);
|
||
left: 0;
|
||
margin-left: 0;
|
||
}
|
||
|
||
.paper.show-formatting-marks .tiptap .pm-space-mark {
|
||
position: relative;
|
||
}
|
||
|
||
.paper.show-formatting-marks .tiptap .pm-space-mark::before {
|
||
content: '·';
|
||
position: absolute;
|
||
inset: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: var(--color-primary);
|
||
opacity: 0.55;
|
||
font-weight: 700;
|
||
pointer-events: none;
|
||
user-select: none;
|
||
}
|
||
|
||
/* Tab marks: a → arrow overlaid at the start of the tab's width (the tab char
|
||
itself supplies the gap), mirroring Word/LibreOffice. */
|
||
.paper.show-formatting-marks .tiptap .pm-tab-mark {
|
||
position: relative;
|
||
}
|
||
|
||
.paper.show-formatting-marks .tiptap .pm-tab-mark::before {
|
||
content: '→';
|
||
position: absolute;
|
||
left: 0.1em;
|
||
top: 0;
|
||
color: var(--color-primary);
|
||
opacity: 0.5;
|
||
pointer-events: none;
|
||
user-select: none;
|
||
}
|
||
|
||
/* Spell-check: red wavy underline under misspelled words (drawn by spellCheck.ts).
|
||
skip-ink:none keeps the wave continuous under descenders. */
|
||
.tiptap .pm-spell-error {
|
||
text-decoration: red wavy underline;
|
||
text-decoration-skip-ink: none;
|
||
}
|
||
|
||
/* A commented range, as Word paints it: a tinted band the text still reads through,
|
||
and a heavier tint on the one the pane has selected. A resolved comment keeps only a
|
||
faint underline, the way both word processors grey a handled one out. */
|
||
.tiptap .pm-comment {
|
||
background: rgba(255, 200, 60, 0.28);
|
||
border-bottom: 1px solid rgba(200, 140, 0, 0.7);
|
||
}
|
||
|
||
.tiptap .pm-comment[data-comment-resolved='true'] {
|
||
background: none;
|
||
border-bottom-color: rgba(140, 140, 140, 0.6);
|
||
}
|
||
|
||
/* Recorded revisions (trackChanges.ts). Both word processors underline an insertion
|
||
and strike a deletion, in the reviewer's colour; one colour here, since the editor
|
||
has a single author. */
|
||
/* An index entry is a place, not text: a thin grey bar the way both word processors
|
||
shade a field, so it can be seen and deleted without taking room in the line. */
|
||
.tiptap .pm-index-entry {
|
||
display: inline-block;
|
||
width: 3px;
|
||
height: 0.9em;
|
||
margin: 0 1px;
|
||
vertical-align: -0.05em;
|
||
border-radius: 1px;
|
||
background: var(--color-field-shade, rgba(0, 0, 0, 0.18));
|
||
}
|
||
|
||
/* A cell holding a formula shows its result on the field shade, since that result is
|
||
recomputed and typing over it does not last. Word draws the field the same way. */
|
||
.tiptap :is(td, th)[data-formula] {
|
||
background-image: linear-gradient(var(--color-field-shade, rgba(0, 0, 0, 0.08)) 0 100%);
|
||
}
|
||
|
||
/* A citation is a field: it shows its text on the field shade both word processors use. */
|
||
.tiptap .pm-citation {
|
||
background: var(--color-field-shade, rgba(0, 0, 0, 0.08));
|
||
border-radius: 2px;
|
||
}
|
||
|
||
/* A recorded revision. The mark only tints, so a DOM built without the decoration plugin
|
||
still shows one; the line comes from the decoration span ProseMirror nests INSIDE the
|
||
mark's, or an ancestor would draw it in the fallback colour instead of the author's. */
|
||
.tiptap .pm-insertion,
|
||
.tiptap .pm-deletion {
|
||
color: #c69200;
|
||
}
|
||
|
||
.tiptap .pm-rev-ins { text-decoration: underline; }
|
||
.tiptap .pm-rev-del { text-decoration: line-through; }
|
||
|
||
/* LibreOffice's author palette, handed out in order of first appearance (trackChanges.ts),
|
||
so several reviewers stay apart the way both word processors draw them. */
|
||
.tiptap .pm-rev-a0 { color: #c69200; }
|
||
.tiptap .pm-rev-a1 { color: #0646a2; }
|
||
.tiptap .pm-rev-a2 { color: #579d1c; }
|
||
.tiptap .pm-rev-a3 { color: #692b9d; }
|
||
.tiptap .pm-rev-a4 { color: #c5000b; }
|
||
.tiptap .pm-rev-a5 { color: #008080; }
|
||
.tiptap .pm-rev-a6 { color: #8c8400; }
|
||
.tiptap .pm-rev-a7 { color: #35556b; }
|
||
.tiptap .pm-rev-a8 { color: #d17600; }
|
||
|
||
/* Print styles: proper A4 pagination */
|
||
@media print {
|
||
.editor {
|
||
padding: 0;
|
||
overflow: visible;
|
||
}
|
||
|
||
/* Never scale for print — the real page size comes from @page below. */
|
||
.paper-scaler {
|
||
width: auto;
|
||
height: auto;
|
||
}
|
||
|
||
.paper {
|
||
width: 100%;
|
||
max-width: none;
|
||
transform: none;
|
||
background: white !important;
|
||
}
|
||
|
||
.paper .tiptap {
|
||
background: none !important;
|
||
box-shadow: none;
|
||
border: none;
|
||
padding: 0;
|
||
min-height: auto;
|
||
}
|
||
|
||
/* Squiggles are an editing aid, never printed. */
|
||
.tiptap .pm-spell-error {
|
||
text-decoration: none;
|
||
}
|
||
|
||
.paper .tiptap > *[data-page-break] {
|
||
page-break-before: always;
|
||
}
|
||
|
||
@page {
|
||
size: A4;
|
||
margin: 25.4mm;
|
||
}
|
||
|
||
/* Hide the space/tab glyphs when printing (they're out-of-flow ::before, so removing
|
||
them changes nothing). The ¶ stays reserved but transparent so print pagination
|
||
matches the screen — removing it would reflow paragraphs it shares a full line with. */
|
||
.paper.show-formatting-marks .tiptap .pm-space-mark::before,
|
||
.paper.show-formatting-marks .tiptap .pm-tab-mark::before {
|
||
content: none;
|
||
}
|
||
|
||
.paper.show-formatting-marks .tiptap :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::after {
|
||
color: transparent;
|
||
}
|
||
}
|
||
|
||
/* Fallback channel: drive color from the --font-color custom property so the
|
||
visible color survives even if ProseMirror's incremental DOM update leaves
|
||
a reused span with a stale `style` attribute that no longer contains
|
||
`color:`. The inline `color:` declaration still wins when present. */
|
||
.tiptap [data-color] {
|
||
color: var(--font-color, currentColor);
|
||
}
|
||
|
||
/* allBlack theme: force all explicitly-colored text to the page text color.
|
||
The color is still stored on the textStyle mark, so .odt exports remain faithful. */
|
||
[data-theme="allBlack"] .tiptap [data-color] {
|
||
color: var(--color-page-text) !important;
|
||
}
|