Commit Graph
77 Commits
Author SHA1 Message Date
Steffen Becker 5034ac7d6f docs: add an editor screenshot to the README
docs/screenshot.png sits right before the Features section, linked to
the app. Plain Markdown image syntax, so the VS Code preview renders
it too; the title bar's file name is painted out.
2026-08-20 18:59:53 +02:00
Steffen Becker 6bbf4286c3 style: the floating-island chrome is dev-only in the menu
The appearance menu's "Modern (beta)" entry sits behind
import.meta.env.DEV, so a production build offers no way into the
island chrome until it catches up with the ribbon. The chrome itself
and a stored 'modern' pref keep working.
2026-08-20 14:56:48 +02:00
Steffen Becker 10b650c64c refactor: the floating island's mode id is 'modern', matching its label
The id said 'classic' while the UI called the ribbon Classic and the
island Modern (beta). Type, i18n keys, comments and docs now agree;
a stored legacy 'classic' still selects the island.
2026-08-17 20:05:15 +02:00
Steffen Becker 501bfe501f fix: a first visit opens the ribbon, not the floating island
The tabbed chrome is the layout users know; the stored choice still wins.
2026-08-17 20:03:06 +02:00
Steffen Becker 847fe59d3d feat: a frame the text runs through reserves no space at all
Word's in-front-of / behind-text (wp:wrapNone + behindDoc) and ODF's
style:wrap="run-through" both read as a side float, so a near-column-wide
picture reserved its whole height and pushed the text 23-52mm down. They
are a wrap mode of their own now: out of flow at the static position it
was anchored at, z-order from the file (inFront). Both exports carry it
back; tests/unit/wrap-through.test.ts covers the round trip.
The image fixture 72 -> 38 over both formats and 7 pages -> 6, matching
the reference; corpus 1108 -> 1074, no other fixture moved.
2026-08-17 13:45:39 +02:00
Steffen Becker a62fa11cc1 feat: the index options menu switches its page numbers
References ▸ Options, where the depth already lives: Word's "Show page numbers"
as a checkbox, over the pageNumbers attr both formats now carry. The menu is no
longer only levels, so its button says Options.
Its node view repainted on the entries alone, so nothing an attr changes ever
reached the rows — leader, title and level styles included. Keyed on the look
as well now, minus the cached entries the pass writes back itself.
2026-08-17 00:09:48 +02:00
Steffen Becker 6c731b9233 feat: insert caption is in the picture and table tabs too
References keeps it, as in both products, but that is a tab away from the
object being captioned. The contextual tabs mount their own CaptionDialog —
it reads the caret for category and position, so the copies take no props.
2026-08-16 22:22:29 +02:00
Steffen Becker 2d3395137f style: the status bar follows the chrome above it
Under the ribbon it takes the same --w-* remap .ribbon gives its pickers: the
app's dark neutrals are bluer than Word's, so the two ends of the window did
not match. Classic keeps the themed set. --color-primary is the brand slate
now, so both chromes share one accent.
2026-08-16 14:19:17 +02:00
Steffen Becker b3e5a1c8a9 style: the ribbon takes the logo's colours
--w-accent is the logo's slate instead of Word's blue (5.1:1 on the chrome,
carries white), and the clay marks the contextual tabs — the pair meets where
those sit beside the normal ones. Both defined once from favicon.svg; the
label tone is darkened, the clay itself only 3.3:1 on white.
2026-08-16 14:12:30 +02:00
Steffen Becker bc98446198 docs: table-style import re-derives regions and unbakes the region text 2026-08-16 13:43:08 +02:00
Steffen Becker 6397594d35 docs: how to hunt the bugs the suite cannot see
Coverage names the untested surface, a pageerror collector reports what
nobody predicted, and semantics beat crash-detection — plus the false
alarms the probes themselves produce.
2026-08-16 07:12:59 +02:00
Steffen Becker 41ab0ef06c feat: two panes on one document, scrolled apart
View ▸ Split (Ctrl+Alt+S): a second live view of the editor's state, stacked
below the first and editable the same way. The panes share a width, so the
second renders the pagination the first measures — pageBreaks, columnsFlow
and tabStops skip the pass on a split pane rather than run it twice.

Every widget decoration now builds its DOM in a function: two views render
one decoration set, and a shared node is taken back and forth between the
two documents until the tab locks up (measured on a two-page document).
2026-08-15 13:15:37 +02:00
Steffen Becker 503c9821d6 feat: a freeform drawing keeps its own outline
A polygon, a polyline, a curve or a connector's elbow used to be dropped
with a warning; it now rides the box as an SVG path in the 0…100 box a
preset's points use, and the editor draws it. Read from all four dialects
the two products write (ODF points and svg:d, a non-primitive
enhanced-path, DrawingML custGeom, VML's reversed-case path), written
back as enhanced-path and custGeom — LibreOffice renders both exactly as
the editor draws them. An open outline is stroked, never filled.
2026-08-15 12:09:12 +02:00
Steffen Becker 9be1578b97 feat: a formula cell prints a currency and a date too
Both come from the document's language, read off Intl: the symbol's side
and spacing, the short date's order and padding. Each minted data style
names that language — without it LibreOffice prints its own locale's
separators (probed: a German document's 1.234,00 € came out $1,234.00).
A date is a serial day from LibreOffice's day 0, 1899-12-30. Word's
formula dialog offers no date, so the DOCX leg uses the field's \@
picture. Also drops a document's name from three older comments.
2026-08-15 11:44:43 +02:00
Steffen Becker c800618299 feat: a text box can run its text top-to-bottom
Word's Text Direction and LibreOffice's, on the frame's own toolbar: the
browser lays the vertical flow out, so the box needs nothing but the mode.

ODF keeps it in the frame style's *paragraph* properties — probed: in the
graphic properties LibreOffice drops it, there it keeps it and lays the box
out — and Word in the shape body's `vert`, the VML fallback's
`layout-flow:vertical` read too. Both survive a LibreOffice re-save.
2026-08-15 11:13:46 +02:00
Steffen Becker eda134f162 feat: a formula cell prints its result in its own number format
The Formula dialog gains the number format Word's has: whole numbers, two
decimals, grouped thousands and the two percentages beside the general
one.

ODF keeps it on the cell *style* as a reference to a data style — odf-kit
shares one cell style across every cell, so a formatted cell gets a copy
of it — and LibreOffice writes both back unchanged. Word keeps it on the
field as its \# switch; LibreOffice's DOCX filter neither reads nor writes
that switch, so that leg holds through our own round trip.
2026-08-15 10:53:30 +02:00
Steffen Becker dab1d662c9 feat: the table sort gets its three keys and a sort type
Both dialogs offer three sort keys and let you pick the type; only the
first key was offered, and the type was always the detected one. A later
key decides only where the ones before tie, and the sort stays stable.

Alphanumeric is the plain collation ("10" before "2") — that is what
choosing it over the detected type is for, so the two collators differ in
`numeric`. Sorted numerically, a cell that is no number sorts last either
way, as an empty cell does in a spreadsheet.
2026-08-15 10:12:24 +02:00
Steffen Becker 0ec35b0168 feat: each page is centred, and a section can pick its own paper
A section narrower than the reserved sheet now splits the difference
between its insets instead of hanging it all on the right, and every
per-page layer draws from the box's own left — sheet, header/footer, page
border and line numbers. The ribbon's Size menu gains the same per-section
block Orientation already had.
2026-08-15 09:52:43 +02:00
Steffen Becker bf564fbab4 fix: footnote counting per page/chapter and collecting at the end really work
Both were selectable in the dialog and changed nothing — they only rode the
file formats, so an imported document numbered and placed its notes wrong.
noteLabels now counts within a bucket (class, chapter, or the page pageBreaks
publishes through setNoteAnchorPages); a collected footnote leaves position:
absolute and opens its own page, as LibreOffice does. Counting and position
move to the footnote tab, the only place LibreOffice offers them.

The separator's own alignment moves the line alone — probed, notes stay left.
2026-08-14 23:11:10 +02:00
Steffen Becker ba827b4ce9 fix: a footnote keeps its place at the page foot after an edit above it
The placements key watched each note's top but not its doc position, so an
edit that shifted the note section without moving a note left the decoration
set naming stale positions — ProseMirror dropped it and every footnote fell
back to its static spot, stacked on top of the next one. The position now
rides the key, resolved once beside the top.
2026-08-14 22:40:42 +02:00
Steffen Becker 46165b82f7 fix: a caption is boxed to its frame, and the frame stays clickable
Two faults on one picture. A block after a float keeps its box over
the float — only its lines move — so the caption on top swallowed
every click meant for the picture; the frame is lifted above the
in-flow boxes. And a caption can only sit under a side-wrapped frame
if it is boxed to it, so its span in the column is measured and
becomes the caption's indent, which also drops it clear of the float.
Measured at 100% and 150%: caption under the picture, its width, in
all four wrap modes.
2026-08-14 22:40:42 +02:00
Steffen Becker 8d866e4f1a fix: a table popover opens where it fits, in both chromes
The three dialogs placed themselves, so the ribbon's viewport
coordinates met CSS that hangs the box above its anchor: the
formula popover landed under the band, clipped. Each caller now
owns the placement — `anchored` in the ribbon, like every other
menu there, and a wrapper in the editor that clamps a tall one
into the scroll area instead of letting the overflow cut it.
2026-08-14 22:11:49 +02:00
Steffen Becker 3b24b14470 feat: lines and arrows, which both importers used to drop
Two endpoints rather than a box: no text, no fill, and the diagonal of
the frame with `flipV` picking which one. The three kinds share their
preset names, since the heads a file declares are what tell them apart.

ODF gives a line its own element and its heads a named marker, which
needs the draw: prefix declared in styles.xml — undeclared, the file
does not parse at all. Word keeps the frame and flips it.
2026-08-14 22:11:49 +02:00
Steffen Becker 147342655d feat: sort a table, sum a column, recognise a number
The formula rides the cell, as ODF writes it; Word's field syntax is what
is stored, since it is the wider language. `ooow:` in the ODF attribute
value is a namespace prefix — undeclared, LibreOffice reads it as part of
the formula and prints "Expression is faulty".

Sort takes one key and detects the type per cell where both dialogs make
you pick. Number recognition is off by default, as it is in LibreOffice,
and a formula parses a cell's text either way — exactly as there.
2026-08-14 22:11:49 +02:00
Steffen Becker 639fac90fd feat: a section can have its own paper, landscape amid portrait
The page grid was one repeating cycle, so every page had to be the same
size. It is now a table of height runs the placement pass grows as each
section's first page becomes known, and the sheets are one box per page
instead of a repeating gradient — .paper reserves the widest section's
paper, a narrower one takes the difference as a right inset.

Layout ▸ Orientation offers "this section"; the size stays document-wide
in the ribbon but round-trips per section from a file.
2026-08-14 10:36:52 +02:00
Steffen Becker 4cda289272 feat: a shape can be a triangle, a star or a block arrow
Twelve presets beside the three rectangular ones. Each outline is one
polygon in utils/shapes.ts, given once: the node view draws it as an SVG
path, the ODF export scales the same points into draw:enhanced-path, and
Word gets the preset's name and draws its own geometry.

A gallery in the box toolbar and the ribbon renders its tiles from that
same table, so a new shape needs no icon. Verified through LibreOffice
from both formats; a preset we can't draw still imports as a warning.
2026-08-14 08:02:08 +02:00
Steffen Becker cb1c61c900 feat: a table's header row repeats on every page
The structural header, beside the styling one: pageBreaks grows the
between-rows spacer by the first row's height and draws a copy of that
row — colgroup included, so the columns line up — at the foot of the
gap, which is the top of the continuation. Round-trips as ODF
table:table-header-rows and Word's w:tblHeader; LibreOffice keeps both.
Word's "don't allow row to break" needs nothing: a row that fits a page
is already an atomic leaf here.
2026-08-14 01:31:02 +02:00
Steffen Becker 06c5f2b12a fix: a cell paragraph drops the default style's space below it
Neither word processor passes it into a cell — LibreOffice puts cell text in
its Table Contents style, Word's table styles carry w:spacing after=0 — so the
space filled the cell and vertical alignment had no slack left to move the text
in, which read as the alignment being ignored. The space above is LibreOffice's
alone, so only the add model zeroes that too. Both exporters write the zeros
onto the paragraph, and only when the default style has spacing at all.
2026-08-13 23:39:31 +02:00
Steffen Becker 0da75ed456 refactor: the project is called edentext
Renames the product in the docs, the page title and the DOCX creator, and moves
the whole storage namespace from odf-editor- to edentext-. The prefix is a
per-origin namespace, so it stays the product name rather than a generic one.
2026-08-13 21:43:12 +02:00
Steffen Becker c469759e69 fix: the ribbon reserved height for air above every button
The band was a round 108px while its tallest control measured 65, so each tab
carried a strip of empty chrome above its buttons. Padding, gaps and the caret
slot come in, the band is cut to what the controls actually need, and the style
gallery stops short of the group label it was sitting on: 144px of chrome to
118px. Line-heights are pinned in px — as ratios they hand the band's height to
whatever font resolves, and the group footers hang out of it.
2026-08-13 18:32:24 +00:00
Steffen Becker bbbe72bd22 feat: a note renders in the file's own style, marker included
The marker moves from a CSS ::before to a widget decoration: generated
content is no text node, so it reached neither the PDF nor a measurement.
The note also carries the paragraph style its file names, so font, size and
colour are the document's; only the hanging indent stays the editor's,
because the style model has no first-line half to pair with the margin.
Adds a unit test for the numbering and the settings, plus the architecture
doc and the two open parity findings.
2026-08-13 12:17:45 +00:00
Steffen Becker 04d1fed5f0 fix: banded rows start on the first body row
The stripe began one row late: regionsAt counted the second body row as the
first band. Probed on a five-row fixture with a red band1Horz — LibreOffice
shades row 0 where the style paints no header region, and the row under the
header where it does.

The corpus cannot see this (no fixture uses the editor's own registry), so it
stayed at 739; checked by eye and by the unit test's inverted expectations. A
document with a table style assigned re-bands on its next open.
2026-08-13 07:06:13 +00:00
Steffen Becker c7e1f91d46 fix: a rule line a paragraph style declares is drawn
Only direct w:pPr/w:pBdr was read, so a Title style's line went missing and
everything under it sat 1.9mm high. The box now goes to the style registry,
where ParaProps' borders already render and round-trip.

Two thirds of that gap is the file's own w:pBdr w:space, carried as
ParaProps.borderPadding (ODF fo:padding) and applied to the ruled sides only —
padding the top as well pushes the paragraph's first line down.
Corpus 761 -> 739.
2026-08-13 00:39:10 +00:00
Steffen Becker 0656b606fb fix: a table's indent is measured to its text, not its edge
w:tblInd means the distance to the cell's text up to Word 2010, so a table
indented 0 hangs its left cell margin into the page margin and its first column
lines up with the body. We drew every such table 2.2mm right of LibreOffice.
Import subtracts it, marginLeft may now be negative, and both exports carry the
edge as it stands.

Word 2013 (compatibilityMode 15) redefined it as the edge — probed both ways
against soffice on one file, 29.6mm against 31.75mm. Corpus 795 -> 761.
2026-08-12 22:57:16 +00:00
Steffen Becker 9d6aedb6ef feat: a Word table style paints the areas its file declares
w:tblStylePr — header row, banded rows, first/last column — was never read, so a
table styled in Word arrived as a bare grid. Import bakes each area into the cells
now (fill, borders, run marks): the file's own style has no registry entry to
re-derive regions from, and the bake is what a re-export carries.

A band's box is the whole banded region, so its insideH is the line between two
band rows; banding counts from the first body row, as in Word and LibreOffice.
Corpus 803 -> 795, page counts unchanged.
2026-08-12 22:30:02 +00:00
Steffen Becker 096de84b82 feat: mirrored page margins, so an even page is the left-hand one
ODF style:page-usage="mirrored" and Word's w:mirrorMargins make the
declared left/right the inner/outer pair, which an even page swaps.
Read, rendered and written back in both formats; the body rides the
per-block inset a section's own margins already use, the header and
footer band moves with it. Measured on a mirrored two-column file:
its page 2 went from 12.7-184.1mm to 31.8-203.2, which is LibreOffice's
to 0.1mm, and its running head with it.
2026-08-12 21:50:25 +00:00
Steffen Becker efd4e9c8a7 feat: the table's four row/column inserts are big buttons
Rows & Columns follows Word: four big buttons carrying a 28px glyph and a
two-line label, with the three deletions folded into one Entfernen dropdown
beside them so the group keeps its width.

Two lines cost the band 12px (--w-ribbon-h 96 -> 108). The labels size to
min-content, not max-width, which a wrapped box ignores; and the inserts drop
the reserved caret slot, without which the group's foot label sits 8px below
every other group's.
2026-08-12 23:44:22 +02:00
Steffen Becker 35f3c50541 fix: a column break lands on the line it was measured to
Three measurement bugs in columnsFlow, each worth a page on its own:
rects became lines by their top, so a raised run counted as a line of
its own (192 for 111); the split probed `left + 1`, which is a line's
*end* right-to-left, so it kept a word too many and the flow then shaved
one line per pass; and one median advance under-measured a paragraph
whose lines differ in height (2834px real against 2453 estimated), by
enough to budget 74 lines into room for 64. The chain used to flip
between one fragment and two forever; it now settles after one split.
2026-08-12 20:57:49 +00:00
Steffen Becker c334b5368e feat: collapse the ribbon to its tab strip
A chevron in the tab strip drops the band, leaving the strip; persisted, so
it survives a reload. Clicking a tab expands again, since a collapsed strip
whose tabs do nothing is a trap.

Word puts that chevron in the band's corner. It rides the strip so the band
keeps its full width: a flex row can only reserve a column, never a corner.

Skipped: Word floats the band over the document until the next click lands
in the text.
2026-08-12 19:51:35 +02:00
Steffen Becker 357fdd86fe feat: the ribbon in dark mode
Word's dark palette on the same metrics: Fluent's dark neutrals, chrome
#1f1f1f, and the accent lightened because #185abd on a dark chrome is
unreadable. AllBlack goes on to Word's Black theme. The lightened accent
cannot carry white text, so the File pill and the pickers' --color-primary
take a second, filled accent — identical to the first in light mode.
2026-08-12 19:51:35 +02:00
Steffen Becker 7db9cc96b3 fix: unclip the ribbon's picker panels and size their triggers
Every reused picker anchors its panel to its own trigger, so the band clipped
it: the table grid showed one row of eight. `pinPanels`, one action on the
band, pins whatever panel mounts inside it, and panels now drop below the whole
band instead of over the controls under them.

Their triggers also kept the classic toolbar's compact box, so Table, Symbol
and Date & time were half the size of the buttons beside them. `.rb-captioned`
moves to global.css, replacing four copies, and gives the trigger the big
button's 34px box, a 28px glyph and the ribbon's own hover and open tints.
2026-08-12 13:35:07 +02:00
Steffen Becker 04eeba3432 docs: why the ribbon has no Font dialog
Character spacing and kerning live on a named style, not on a run — setting them
as direct formatting would need a new mark attribute and export work on both
formats. Everything else that dialog would hold is already on the Home tab.
2026-08-12 02:00:20 +02:00
Steffen Becker 0d8f396c23 docs: where the ribbon lives and why it is built that way
The palette outside the theme system, the token remap that lets a reused picker
adopt it, and the two things the fixed-height band forces — a menu that leaves
the flow, and a wrapper that names the menu it owns.

Also the deletion the ribbon would unlock if classic were ever retired, so the
option stays visible without anything being removed now.
2026-08-12 01:55:48 +02:00
Steffen Becker 2803e6196e fix: a table split across pages closes its box on both
Collapsed borders paint a shared edge once, so the spacer <tr> bridging
the gap took the row separator with it and left one fragment open —
visible in a fixture as tables missing their frame on a page. The spacer now
draws the line LibreOffice draws there: the separator the break falls on,
or the table's own box where the rows carry none. Absolutely positioned,
since a collapsed border on the spacer shifts every row below by half.
2026-08-11 20:24:56 +00:00
Steffen Becker baf0865256 fix: a section keeps its own side margins too
.tiptap's padding draws one pair of side margins for the whole document, so a
title page declaring 3.18cm left sat 3.85mm off. The difference now rides a
--sec-inset-left/-right node decoration on the section's top-level blocks, and
everything that writes a block margin adds it in — an indent, a named style's
and a table's alignment all compose with it instead of losing to it.
2026-08-11 19:03:58 +00:00
Steffen Becker 853762a774 fix: a float keeps the gap its file declares, not one of ours
`wrapDist` carries Word's distL/distR and ODF's fo:margin-left/-right onto
the frame; a file that declares none wraps flush, which is what LibreOffice
draws (probed: text runs to the frame edge, and 0.5cm of declared margin
stops it exactly 0.5cm short). The hardcoded 14px was 2px more than Word's
own 0.32cm and broke a line one word early.
2026-08-11 16:40:34 +00:00
Steffen Becker a4bcaa9ded fix: a frame keeps the size its file states, to the fraction
Whole pixels cost a page-wide picture 0.3mm of height, and the column cap
then scaled that loss into the width too. Sizes stay fractional (snapped
back to a whole pixel inside the export's own 0.001cm quantization), and a
sub-pixel overhang trims the width without touching the height — which is
what LibreOffice draws.
2026-08-11 08:03:02 +00:00
Steffen Becker b825e483ac fix: an ODF cell that declares no padding has none
fo:padding defaults to 0 in ODF, but an undeclared side was left open and
fell through to Word's implicit 0.19cm, indenting every cell of a table
whose file names no cell style — a fixture's figure captions by 1.9mm.
2026-08-11 07:09:17 +00:00
Steffen Becker 0830fff86f fix: a frame is alone on its line, not only on its block
The no-descent rule matched a block holding nothing but frames, so the
image-break-caption idiom kept a text descent under its picture — 0.9mm
per figure, and LibreOffice's own guides stack dozens of them in tables.
Test the run between two hard breaks instead and mark the frame itself.
2026-08-11 06:58:40 +00:00
Steffen Becker db8f4439b1 feat: a table that may not break between rows moves whole
ODF style:may-break-between-rows="false" — probed: LibreOffice leaves four
lines of a page empty rather than split such a table, and splits the identical
one when the attribute says "true". pageBreaks now emits a single atomic leaf
for the wrapper unless the table is taller than a page, where the rule cannot
be met. Imported, exported and round-tripped; Word has no equivalent.
2026-08-11 00:02:31 +00:00