An as-char figure frame sits in a paragraph of its own; the importer lifts the box to top level and dropped that paragraph whole, so its space above and below went with it — every figure in a fixture sat 2.1mm too tight. The box now carries them (ParagraphSpacing covers textBox), space above as padding so it adds to the block above, and the export mints an anchor style to write both back. One fixture: 142 → 100 parity issues.
943 lines
35 KiB
CSS
943 lines
35 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));
|
||
|
||
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 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-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);
|
||
|
||
/* Page borders, drawn over the sheet .paper paints */
|
||
background:
|
||
/* Left border per page */
|
||
repeating-linear-gradient(
|
||
to bottom,
|
||
var(--color-page-border) 0px,
|
||
var(--color-page-border) var(--page-height),
|
||
transparent var(--page-height),
|
||
transparent var(--page-cycle)
|
||
),
|
||
/* Right border per page */
|
||
repeating-linear-gradient(
|
||
to bottom,
|
||
var(--color-page-border) 0px,
|
||
var(--color-page-border) var(--page-height),
|
||
transparent var(--page-height),
|
||
transparent var(--page-cycle)
|
||
),
|
||
/* Top/bottom border lines of each page */
|
||
repeating-linear-gradient(
|
||
to bottom,
|
||
var(--color-page-border) 0px,
|
||
var(--color-page-border) 1px,
|
||
transparent 1px,
|
||
transparent calc(var(--page-height) - 1px),
|
||
var(--color-page-border) calc(var(--page-height) - 1px),
|
||
var(--color-page-border) 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);
|
||
background-position:
|
||
left top,
|
||
right top,
|
||
left top;
|
||
background-repeat: repeat-y;
|
||
|
||
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;
|
||
}
|
||
|
||
/* 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;
|
||
}
|
||
|
||
/* 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). */
|
||
.paper .tiptap p:has(> [data-anchor-page]) {
|
||
height: 0;
|
||
margin: 0;
|
||
}
|
||
|
||
/* 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). */
|
||
.paper .tiptap a {
|
||
color: #1a56db;
|
||
text-decoration: underline;
|
||
cursor: text;
|
||
}
|
||
|
||
/* 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);
|
||
}
|
||
|
||
/* 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;
|
||
align-items: 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;
|
||
}
|
||
|
||
/* 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;
|
||
vertical-align: bottom;
|
||
line-height: 0;
|
||
}
|
||
/* 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;
|
||
}
|
||
.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(--accent, #3b82f6);
|
||
outline-offset: 1px;
|
||
}
|
||
.paper .tiptap .image-resize-handle {
|
||
display: none;
|
||
position: absolute;
|
||
width: 10px;
|
||
height: 10px;
|
||
background: var(--accent, #3b82f6);
|
||
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='%233b82f6' 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(--accent, #3b82f6);
|
||
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(--accent, #3b82f6);
|
||
}
|
||
/* 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. */
|
||
top: calc(50% + var(--space-before) / 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(--accent, #3b82f6);
|
||
outline-offset: 1px;
|
||
}
|
||
/* Grab affordance on the frame edge (outside the text area). */
|
||
.paper .tiptap .textbox-rotor {
|
||
cursor: move;
|
||
}
|
||
.paper .tiptap .textbox-content {
|
||
cursor: text;
|
||
min-height: 1em;
|
||
}
|
||
.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;
|
||
}
|
||
.paper .tiptap .columns-node > :first-child {
|
||
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 h1,
|
||
.paper .tiptap h2,
|
||
.paper .tiptap h3,
|
||
.paper .tiptap h4,
|
||
.paper .tiptap h5 {
|
||
/* Neutral base: font, size and margins come from the named paragraph styles
|
||
(styles/styleSheet.ts → the #document-styles element Editor.svelte writes). */
|
||
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, .toc, .toc-title, .toc-entry) {
|
||
padding-top: var(--space-before);
|
||
}
|
||
.paper[data-spacing-model='max'] .tiptap :is(p, h1, h2, h3, h4, h5, .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, 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) {
|
||
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) {
|
||
position: relative;
|
||
}
|
||
|
||
.paper .tiptap li > :is(p, h1, h2, h3, h4, h5)::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)::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)::before { content: counter(list-item, decimal) ". "; }
|
||
.paper .tiptap ol[data-eff-list-style="decimal"] > li > :is(p, h1, h2, h3, h4, h5)::before { content: counter(list-item, decimal) ". "; }
|
||
.paper .tiptap ol[data-eff-list-style="decimal-paren"] > li > :is(p, h1, h2, h3, h4, h5)::before { content: counter(list-item, decimal) ") "; }
|
||
.paper .tiptap ol[data-eff-list-style="lower-alpha"] > li > :is(p, h1, h2, h3, h4, h5)::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)::before { content: counter(list-item, lower-alpha) ") "; }
|
||
.paper .tiptap ol[data-eff-list-style="upper-alpha"] > li > :is(p, h1, h2, h3, h4, h5)::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)::before { content: counter(list-item, upper-alpha) ") "; }
|
||
.paper .tiptap ol[data-eff-list-style="lower-roman"] > li > :is(p, h1, h2, h3, h4, h5)::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)::before { content: counter(list-item, lower-roman) ") "; }
|
||
.paper .tiptap ol[data-eff-list-style="upper-roman"] > li > :is(p, h1, h2, h3, h4, h5)::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)::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)::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)::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)::before { content: '◦ '; }
|
||
.paper .tiptap ul ul ul > li > :is(p, h1, h2, h3, h4, h5)::before { content: '▪ '; }
|
||
.paper .tiptap ul ul ul ul > li > :is(p, h1, h2, h3, h4, h5)::before { content: '▸ '; }
|
||
.paper .tiptap ul ul ul ul ul > li > :is(p, h1, h2, h3, h4, h5)::before { content: '– '; }
|
||
.paper .tiptap ul ul ul ul ul ul > li > :is(p, h1, h2, h3, h4, h5)::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)::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)::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 here — else a long table grows a line every few rows
|
||
(tableCellBorders.ts). */
|
||
padding-bottom: max(0px, calc(var(--cell-pad-b, 0px) - var(--border-over, 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;
|
||
/* Cells hold paragraphs; suppress their bottom margin so a single-line cell
|
||
isn't taller than its text (matches the list-item paragraph treatment). */
|
||
}
|
||
|
||
.paper .tiptap td > p,
|
||
.paper .tiptap th > p {
|
||
margin-bottom: 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 (¶). Reserved in the layout at all times, so the toggle
|
||
only recolors it and text never reflows. Zero-size inline-block, glyph overflows
|
||
visibly: width/height/line-height 0 drop both footprint and line-box strut. */
|
||
.paper .tiptap p::after,
|
||
.paper .tiptap h1::after,
|
||
.paper .tiptap h2::after,
|
||
.paper .tiptap h3::after,
|
||
.paper .tiptap h4::after,
|
||
.paper .tiptap h5::after {
|
||
content: '\00A0¶';
|
||
display: inline-block;
|
||
width: 0;
|
||
height: 0;
|
||
line-height: 0;
|
||
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 p::after,
|
||
.paper.show-formatting-marks .tiptap h1::after,
|
||
.paper.show-formatting-marks .tiptap h2::after,
|
||
.paper.show-formatting-marks .tiptap h3::after,
|
||
.paper.show-formatting-marks .tiptap h4::after,
|
||
.paper.show-formatting-marks .tiptap h5::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 after a real BR
|
||
(it carries the caret onto the empty line) and in an empty block (its only line). */
|
||
.paper .tiptap :not(br) + br.ProseMirror-trailingBreak {
|
||
display: none;
|
||
}
|
||
|
||
/* A block ProseMirror ends with a trailing-break BR — an empty one, or one ending in a
|
||
hard break — puts the inline ¶ on a line of its own past that BR, so the block renders
|
||
one line too tall. Pull ::after out of flow, onto the last line. */
|
||
.paper .tiptap p:has(> br.ProseMirror-trailingBreak),
|
||
.paper .tiptap h1:has(> br.ProseMirror-trailingBreak),
|
||
.paper .tiptap h2:has(> br.ProseMirror-trailingBreak),
|
||
.paper .tiptap h3:has(> br.ProseMirror-trailingBreak),
|
||
.paper .tiptap h4:has(> br.ProseMirror-trailingBreak),
|
||
.paper .tiptap h5:has(> br.ProseMirror-trailingBreak),
|
||
.paper .tiptap li > p:has(> br.ProseMirror-trailingBreak) {
|
||
position: relative;
|
||
}
|
||
|
||
.paper .tiptap p:has(> br.ProseMirror-trailingBreak)::after,
|
||
.paper .tiptap h1:has(> br.ProseMirror-trailingBreak)::after,
|
||
.paper .tiptap h2:has(> br.ProseMirror-trailingBreak)::after,
|
||
.paper .tiptap h3:has(> br.ProseMirror-trailingBreak)::after,
|
||
.paper .tiptap h4:has(> br.ProseMirror-trailingBreak)::after,
|
||
.paper .tiptap h5:has(> br.ProseMirror-trailingBreak)::after,
|
||
.paper .tiptap li > p:has(> br.ProseMirror-trailingBreak)::after {
|
||
position: absolute;
|
||
/* Out of flow, so restore the paragraph's line-height (the base rule zeroes it) to
|
||
seat the glyph on a line instead of the collapsed top edge — the last one, which
|
||
for an empty block is also the first. */
|
||
line-height: inherit;
|
||
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;
|
||
}
|
||
|
||
/* 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 p::after,
|
||
.paper.show-formatting-marks .tiptap h1::after,
|
||
.paper.show-formatting-marks .tiptap h2::after,
|
||
.paper.show-formatting-marks .tiptap h3::after,
|
||
.paper.show-formatting-marks .tiptap h4::after,
|
||
.paper.show-formatting-marks .tiptap h5::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;
|
||
}
|