441 lines
15 KiB
CSS
441 lines
15 KiB
CSS
.editor {
|
||
flex: 1;
|
||
padding: 2rem 1rem 3rem; /* extra bottom padding clears the fixed status bar */
|
||
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 {
|
||
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;
|
||
}
|
||
|
||
.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%;
|
||
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;
|
||
/* ODF/LibreOffice "single" spacing = the font's natural line height
|
||
(Liberation Serif ≈ 1.15× em), NOT 1.0× the font size like CSS unitless 1.
|
||
Keep this in sync with LINE_HEIGHT_RATIO in lineHeight.ts. */
|
||
line-height: 1.15;
|
||
color: var(--color-page-text);
|
||
|
||
/* Multi-page background: page surfaces separated by editor background */
|
||
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)
|
||
),
|
||
/* 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;
|
||
|
||
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;
|
||
}
|
||
|
||
.paper .tiptap p {
|
||
margin-top: 0;
|
||
/* matches odf-kit Standard style fo:margin-bottom (0.212cm ≈ 6pt) */
|
||
margin-bottom: 0.212cm;
|
||
}
|
||
|
||
.paper .tiptap h1,
|
||
.paper .tiptap h2,
|
||
.paper .tiptap h3 {
|
||
font-weight: 700;
|
||
line-height: 1.15;
|
||
margin-top: 1.5em;
|
||
margin-bottom: 0.5em;
|
||
}
|
||
|
||
|
||
.paper .tiptap h1 {
|
||
font-size: 20pt;
|
||
}
|
||
|
||
.paper .tiptap h2 {
|
||
font-size: 16pt;
|
||
}
|
||
|
||
.paper .tiptap h3 {
|
||
font-size: 14pt;
|
||
}
|
||
|
||
.paper .tiptap ul,
|
||
.paper .tiptap ol {
|
||
padding-left: 1.5rem;
|
||
margin-top: 0;
|
||
margin-bottom: 0.212cm;
|
||
}
|
||
|
||
/* Ordered-list numbering styles (Toolbar split-button dropdown → orderedList
|
||
`listStyleType` attr, rendered as data-list-style). The dot variants reuse the
|
||
built-in counter styles (which already render with a trailing "."); the bracket
|
||
variants override the suffix via @counter-style … system: extends. `decimal`
|
||
needs no rule (the <ol> default already renders "1."). Keep in sync with
|
||
src/lib/editor/orderedListTypes.ts and export/odt.ts. */
|
||
@counter-style oledit-decimal-paren { system: extends decimal; suffix: ") "; }
|
||
@counter-style oledit-lower-alpha-paren { system: extends lower-alpha; suffix: ") "; }
|
||
@counter-style oledit-upper-alpha-paren { system: extends upper-alpha; suffix: ") "; }
|
||
@counter-style oledit-lower-roman-paren { system: extends lower-roman; suffix: ") "; }
|
||
@counter-style oledit-upper-roman-paren { system: extends upper-roman; suffix: ") "; }
|
||
|
||
.paper .tiptap ol[data-list-style="decimal-paren"] { list-style-type: oledit-decimal-paren; }
|
||
.paper .tiptap ol[data-list-style="lower-alpha"] { list-style-type: lower-alpha; }
|
||
.paper .tiptap ol[data-list-style="lower-alpha-paren"] { list-style-type: oledit-lower-alpha-paren; }
|
||
.paper .tiptap ol[data-list-style="upper-alpha"] { list-style-type: upper-alpha; }
|
||
.paper .tiptap ol[data-list-style="upper-alpha-paren"] { list-style-type: oledit-upper-alpha-paren; }
|
||
.paper .tiptap ol[data-list-style="lower-roman"] { list-style-type: lower-roman; }
|
||
.paper .tiptap ol[data-list-style="lower-roman-paren"] { list-style-type: oledit-lower-roman-paren; }
|
||
.paper .tiptap ol[data-list-style="upper-roman"] { list-style-type: upper-roman; }
|
||
.paper .tiptap ol[data-list-style="upper-roman-paren"] { list-style-type: oledit-upper-roman-paren; }
|
||
|
||
.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 → export emits fo:padding 0.1cm per cell (CELL_PADDING).
|
||
Tables are wrapped in <div class="tableWrapper"> by TipTap's node view. */
|
||
.paper .tiptap .tableWrapper {
|
||
margin: 0 0 0.212cm;
|
||
overflow-x: auto;
|
||
}
|
||
|
||
.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: 0.1cm;
|
||
vertical-align: top;
|
||
/* 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;
|
||
}
|
||
|
||
/* 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;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
/* Formatting marks (Toggle in extended toolbar).
|
||
Zero-width inline-block with visible overflow: the ¶ glyph renders just
|
||
past the last character without consuming line width — so right/center/
|
||
justify alignment of the actual text is preserved. The leading NBSP in
|
||
the content gives a small visual gap from the text. */
|
||
.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 {
|
||
content: '\00A0¶';
|
||
display: inline-block;
|
||
width: 0;
|
||
overflow: visible;
|
||
white-space: pre;
|
||
color: var(--color-primary);
|
||
opacity: 0.5;
|
||
font-family: var(--font-sans);
|
||
font-weight: 400;
|
||
user-select: none;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.paper.show-formatting-marks .tiptap li > p::after {
|
||
content: '\00A0¶';
|
||
}
|
||
|
||
/* Empty block fix: when a paragraph/heading/li-paragraph contains only the
|
||
ProseMirror trailing-break BR, the inline ::after would otherwise render
|
||
on a second line below the BR, doubling the block's height and shifting
|
||
the page-break calculation. Pull ::after out of flow via absolute positioning. */
|
||
.paper.show-formatting-marks .tiptap p:has(> br.ProseMirror-trailingBreak:only-child),
|
||
.paper.show-formatting-marks .tiptap h1:has(> br.ProseMirror-trailingBreak:only-child),
|
||
.paper.show-formatting-marks .tiptap h2:has(> br.ProseMirror-trailingBreak:only-child),
|
||
.paper.show-formatting-marks .tiptap h3:has(> br.ProseMirror-trailingBreak:only-child),
|
||
.paper.show-formatting-marks .tiptap li > p:has(> br.ProseMirror-trailingBreak:only-child) {
|
||
position: relative;
|
||
}
|
||
|
||
.paper.show-formatting-marks .tiptap p:has(> br.ProseMirror-trailingBreak:only-child)::after,
|
||
.paper.show-formatting-marks .tiptap h1:has(> br.ProseMirror-trailingBreak:only-child)::after,
|
||
.paper.show-formatting-marks .tiptap h2:has(> br.ProseMirror-trailingBreak:only-child)::after,
|
||
.paper.show-formatting-marks .tiptap h3:has(> br.ProseMirror-trailingBreak:only-child)::after,
|
||
.paper.show-formatting-marks .tiptap li > p:has(> br.ProseMirror-trailingBreak:only-child)::after {
|
||
position: absolute;
|
||
top: 0;
|
||
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;
|
||
}
|
||
|
||
/* 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;
|
||
}
|
||
|
||
.paper .tiptap {
|
||
background: white !important;
|
||
box-shadow: none;
|
||
border: none;
|
||
padding: 0;
|
||
min-height: auto;
|
||
}
|
||
|
||
.paper .tiptap > *[data-page-break] {
|
||
margin-top: 0 !important;
|
||
page-break-before: always;
|
||
}
|
||
|
||
@page {
|
||
size: A4;
|
||
margin: 25.4mm;
|
||
}
|
||
|
||
.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 li > p::after,
|
||
.paper.show-formatting-marks .tiptap .pm-space-mark::before {
|
||
content: none;
|
||
}
|
||
}
|
||
|
||
/* 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;
|
||
}
|