Commit Graph
427 Commits
Author SHA1 Message Date
Steffen Becker b35551acd0 fix: a document opens on the page it settles on
A pagination pass measures the DOM the last one changed, so a fresh
document searched for its layout in public — 12 of them over 550ms on a
two-column file, a phantom third page among them. Two real causes: the
flow's overflow test had no headroom, so it read its own last answer
(880px fits undecorated, 913px overflows once the decoration pins the
column height), and a split dropped the height decorations without
clearing their cache key. The rest is inherent to measuring rendered
DOM, so the page stays hidden until its layout repeats: one visible
layout instead of 12, and 21-139ms where there are no columns.
2026-08-12 21:50:15 +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 2f3e48ecf9 docs: note mirrored page margins as unimplemented
The inner and outer margins are drawn as left and right on every page,
so an even page's text block sits 19mm off where LibreOffice puts it.
2026-08-12 20:57:56 +00: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 16ce22d4b8 docs: note the hidden heading a running head still needs drawn
A paragraph a style hides is dropped on import now; a hidden heading is not,
because it is the outline the chapter field reads. Records what dropping it
measured and what a real fix would need.
2026-08-12 19:52:49 +00:00
Steffen Becker 9bfd579d65 fix: hidden text stays off the page, except the heading a running head reads
text:display="none" now drops the paragraph its style hides and the stored
value of a field that carries the attribute — LibreOffice lays out neither.
A hidden heading is the exception and still renders: it is the outline the
running head's chapter field reads, and dropping it blanked every head that
quotes one. The editor has no way to hold a block it does not draw.
2026-08-12 19:49:33 +00:00
Steffen Becker 99a14a1a7f fix: Hebrew and Arabic take the size their style declares for them
ODF carries western, asian and complex-script font properties side by side and
a word processor picks by the character's script; the importer read only the
western pair, so a complex-script run came out at the document default.
Decided per run — the finest split the model has — so a document with no
complex script is untouched, and our own export (all three written alike)
round-trips. Measured: a two-column page's line pitch 7.5mm -> 5.9 against
LibreOffice's 6.0.
2026-08-12 19:49:15 +00:00
Steffen Becker 06343103e4 merge: the Word-style ribbon lands on main
Brings the ribbon chrome, its tabs and controls, the Paragraph/Tabs dialogs
and the shared selection reader over from word_ui. No file overlap with
main's cover-graphic fix, so the merge is clean.
2026-08-12 19:56:19 +02:00
Steffen Becker b4b47351c3 fix: Layout and Insert icons that show what they do
Orientation is one page turned, overlapping so both copies keep full size
and the upright one is traced only where the other leaves it visible. Size
measures a page with dimension arrows; Columns is a page carrying two
columns of text, matching the previews in its own menu; Header and Footer
are a whole page with the zone filled in.

Measured painted boxes: every glyph now centres on 8, 8 and spans 12.9 to
14 units, against 15.16 x 8.56 for the old Orientation.
2026-08-12 19:51:36 +02:00
Steffen Becker e0e31d3a70 feat: page previews in the Columns menu
Word shows the layout rather than naming it. The page is 20 units of text
width with a 2-unit gutter, so the columns narrow as their count rises;
surface and border come from the palette, so it follows the theme.

Only equal widths exist here — Word's Left and Right would need a per-column
width on the node.
2026-08-12 19:51:36 +02:00
Steffen Becker 7b3c1da64e fix: style tiles wide enough for the whole name
'Überschrift 4' measures 58.4px against 56px of inner width, so the two
characters that got cut were the ones telling the five headings apart.
Six pixels wider, and no name in either locale truncates.
2026-08-12 19:51:36 +02:00
Steffen Becker 4b84e2efac fix: one shape and one baseline for the ribbon's buttons
Ten of the fifteen caret buttons ran the same toggleMenu from both halves,
so they were split for nothing. They are single buttons now, with the caret
under the label rather than beside it — 13px narrower each, and Word's own
Paste button.

The tint takes the whole button, label included: a row mixes buttons whose
label is inside the button with pickers whose caption is a sibling, so half
of them lit up under the words and half did not. `captionClicks` gives the
caption the click to match, and the picker's trigger takes the icon box's
34px so glyphs and words land on one line.
2026-08-12 19:51:36 +02:00
Steffen Becker a78e21b524 feat: paste options on the ribbon's Paste button
The big split button already stacked, so the caret only needed a menu:
paste, and paste without formatting. Both call the reader the context
menu has always used.

Word offers a third, Merge Formatting. The engine reads the clipboard as
HTML or as text, so there is no third thing to offer.
2026-08-12 19:51:35 +02:00
Steffen Becker 0fce9e01c8 fix: the ribbon on a narrow window
Three failures from one cause — nothing had a floor. The Styles group is
the only one that may shrink, so it absorbed everything down to 8px: an
empty gallery frame whose label reached across the next group's. It now
stops at two tiles, the label truncates, and the tab strip wraps rather
than pushing the page into a horizontal scroll that left the band ending
short of its own tabs.
2026-08-12 19:51:35 +02: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 33784b239f fix: Open reaches the file input in ribbon mode
The hidden input sat inside the classic header, so where the File System
Access API is missing handleOpen clicked nothing at all. It moves out of
the chrome fork, which both paths route through.

The label drops the extension while it is there: the input has always
taken .odt, .ott and .docx.
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 e770d3317f fix: the style gallery fills the band
The group centres its controls, so the gallery sat at content height — 36px
of tile in a 67px row. The wrap opts out with align-self, and the sample
takes the height the name leaves over.

Two letters fewer buy the preview room to scale: 1px per pt with 21 as the
tile's ceiling, measured against its inner width. Heading 1 now reads a
third larger than Heading 5 instead of a pixel.
2026-08-12 19:51:35 +02:00
Steffen Becker fa2726a1df fix: Word's pi for the Equation button
Word's Equation button is a pi, as its Symbol button beside it is an omega.
Same canvas, baseline and weight as that omega, at font-size 20 rather than
its 14: pi is lowercase, so at one size its x-height measures 1.44x smaller.
2026-08-12 19:51:35 +02:00
Steffen Becker ab7d8ddb01 fix: a page number that reads as a 1, and Word's stacked split button
The glyph was a stem on a base with no flag, so it read as a down-tack rather
than the digit it stands for.

A caret welded to the right of a big button floated at icon height, beside the
label instead of with it, on every menu button of the Layout tab. Big split
buttons stack now, as in Word: the icon runs the command, the label and its
caret open the menu. Measured: every label in a group still tops at 87px.
2026-08-12 13:40:12 +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 ba99f20cfc fix: one baseline for the Font group's letter buttons
Sub- and superscript were SVG paths whose X sat on its own baseline, two
units apart from each other and a size larger than the B, I, U and S beside
them. They are text now, in the same font and size, with the digit on a
relative offset because a flex item ignores vertical-align. Clear formatting
stays a path, redrawn upright with its T on the measured letter baseline.

Ink top and baseline agree within 0.15px, measured in the browser.
2026-08-12 13:13:32 +02:00
Steffen Becker c60cb4e0de fix: one button height across the ribbon, and the Paragraph dialog's grid
The shared pickers frame their trigger for the classic toolbar's island; in a
ribbon row of borderless buttons that reads as a different control, and at
27-28px it broke the row's line. Every trigger is 30px there now. Redrew the
change-case and highlighter glyphs, which were malformed and off-centre.

The dialog sized each row on its own, so a long label wrapped and "pt" vs "cm"
shifted the field beside it - fixed unit width, no wrap, one 2x3 grid.
2026-08-12 13:03:59 +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 23f234a237 feat: the Paragraph and Tabs dialogs, and a picture's alt text
Word puts the deep paragraph settings behind a dialog, and so do we — it is the
only place the four text-flow attrs can be set: widow/orphan control, keep with
next, keep lines together, page break before. All four already round-tripped
with nothing to change them.

The Tabs dialog types a stop exactly where the ruler can only drag one, and is
the only way to give it a leader. Alt text becomes editable rather than just
inherited from the file name.
2026-08-12 02:00:11 +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 ad3f0507fd feat: the ribbon's contextual tabs for a table, a picture and a shape
They appear while the caret is in the object and go with it, handing the strip
back to Home. Table Design carries the style gallery, shading, borders and the
style options; Table Layout the rows, columns, merge, split, alignment — and
cell margins, which round-tripped with nothing to set them.

The floating toolbars stay: a mini-toolbar beside the object and a contextual
tab above the document are both Word's, and both reuse the same pickers.
2026-08-12 01:54:49 +02:00
Steffen Becker e58e8a180e fix: a page-anchored cover graphic sits in front, at the page's own corner
Two compounding bugs on a title/back-page background image:
- its host paragraph picks up position:relative from the line-height
  rule, becoming the frame's containing block instead of the page and
  offsetting it by a full page margin — now forced back to static.
- the frame always painted behind text (z-index -1); a graphic that
  declares ODF's style:run-through="foreground" now paints in front,
  as LibreOffice draws it, via a new inFront attr round-tripped through
  ODF export/import.

Confirmed against LibreOffice's own render: the duplicated, ~2cm
displaced cover graphic now matches exactly.
2026-08-11 23:48:52 +00:00
Steffen Becker 0f53571b1f feat: the ribbon's References, Review and View tabs
References inserts the table of contents and sets how deep it goes — the depth
attribute round-tripped with no way to change it. Review holds the statistics
panel and the spell-check language, View the ruler, formatting marks and zoom;
each of those had lived only in the status bar or the secondary toolbar.

That is all seven fixed tabs.
2026-08-12 01:48:32 +02:00
Steffen Becker 5600e84605 feat: the ribbon's Layout tab
Margins, orientation, page size, columns and breaks as menus; indent and spacing
as typed fields. Three of these had no control at all: a section break, and the
absolute left and right indent the ruler could only drag.

Each margin preset is labelled with the value it sets, so none of them hides a
default — and Normal is LibreOffice's 2 cm, the one this editor already uses.
2026-08-12 01:45:24 +02:00
Steffen Becker b0ae6f09af feat: the ribbon's Insert tab
Page break, table, picture, text box, link, bookmark, cross-reference, header,
footer, page number, date, symbol and equation. The page break had a key and no
button; the rest moved up from the secondary toolbar.

The ribbon mounts its own link, bookmark, cross-reference and formula dialogs,
so the classic path keeps the ones it has and its re-fire shim untouched. Only
one chrome is mounted, so the two sets never coexist.
2026-08-12 01:40:07 +02:00
Steffen Becker ac93ba8651 feat: the ribbon's Home tab carries every formatting control
Clipboard, Font, Paragraph, a style gallery of live tiles, and Find/Replace —
which had no button in either chrome before. So did change case, underline and
strikethrough line styles: the engine has carried them all along.

Two things the fixed-height band forced. Its own overflow clips a dropdown, so
a menu leaves the flow and pins itself under its trigger. And with one shared
open-menu id, each wrapper must name the menu it owns, or every sibling closes
it on mousedown and the click on a row never lands.
2026-08-12 01:34:03 +02:00
Steffen Becker f7ee9546a4 refactor: what the selection formats becomes a shared reader
Font, size, mark colour and block attributes all answer the same question — the
one value the selection agrees on, blank when it mixes. One `uniform` walk now
serves all four, framework-free, so any chrome can read it inside its own
reactive wrapper.

The classic toolbar keeps its own copies for now: it is the baseline the ribbon
is compared against, and it should adopt these only once that comparison ends.
2026-08-12 01:22:52 +02:00
Steffen Becker 2aeac7a574 feat: a Word-style ribbon can take the toolbar's place
A second chrome, chosen by odf-editor-chrome and switchable from either side so
the two can be compared. The shell only — tab strip, File menu, quick-access
save/undo/redo; the tab bodies follow. The classic island stays untouched.

Word's palette lives in --w-* outside the theme blocks, and .ribbon remaps the
app's own tokens onto it, so a reused picker adopts the ribbon's look without
an edit of its own.
2026-08-12 01:20:47 +02:00
Steffen Becker b98cc550c3 fix: the suite finds a localStorage again
Node 26 defines its own `localStorage` global — undefined unless the process is
given --localstorage-file — and it shadows the one jsdom installs. Every module
that reads storage at import time threw before a test could run, so 37 of 65
files failed to collect. A setup file defines the property when it is missing.
2026-08-12 01:20:40 +02:00
Steffen Becker 64cc010f01 docs: a real-world document's name stays out of the repo
The parity findings named the six files the corpus is built from, so every
measurement carried a document title into the history. They move to a
gitignored FINDINGS.md beside the corpus itself; the README keeps the harness.

A test that needs a document builds one, so nothing under src/ or tests/
reads the gitignored corpus — CI greps for exactly that.
2026-08-11 22:25:33 +00:00
Steffen Becker fe55aec95f fix: the page draws its text black, as a word processor does
The sheet took the app's slate (#1e293b), so every uncoloured run
differed from LibreOffice — and from a run the same document colours
#000000 explicitly. Light and dark themes now paint the page black on
white; allBlack still inverts it, and the PDF export no longer has to
override anything.
2026-08-11 21:27:39 +00:00
Steffen Becker f055fc4850 docs: every comment back inside the three-line rule
A sweep of the whole tree: 27 blocks ran to four lines or more. Trimmed
to what the code can't say, with two of them split back onto the
function each was really about.
2026-08-11 21:22:13 +00:00
Steffen Becker 2a8f0675bc fix: Word's hollow bullet is the letter it is
A Courier New "o" was mapped to ◦ for looks. LibreOffice and Word both
draw the glyph the numbering names, so the nested lists of the gravity
summary now read like theirs.
2026-08-11 21:22:13 +00:00
Steffen Becker 5969795d04 fix: a header or footer takes the document's font, not the editor's
The zone was pinned to Liberation Serif 12pt, so a run whose font the
file leaves to the default — every page-number field in a fixture
fixture — rendered serif inside an Arial footer. Both formats base the
Header/Footer style on the default paragraph style, so the zone now
takes its text half.
2026-08-11 20:58:29 +00:00
Steffen Becker 95d174501d fix: a Word hyperlink keeps the look its file gives it
Word paints a link through its Hyperlink character style, never for
being one, so the editor's own blue overwrote both the file's #0000FF
and the plain black of a link carrying no style at all. The DOCX
importer marks those `plain` and the CSS leaves them alone; LibreOffice
paints every ODF text:a, so those keep the visual.
2026-08-11 20:52:12 +00:00
Steffen Becker 8db6214c11 fix: a CMYK image keeps the colours its file draws
Chromium colour-manages a CMYK JPEG through its embedded ICC profile
where LibreOffice and Word convert naively, so a fixture's
logo arrived dull teal on grey instead of vivid blue on black. The
importer drops the profile — measured back onto LibreOffice's own
values, and 655 KB smaller for that one logo.
2026-08-11 20:42:00 +00:00
Steffen Becker 1122f6d0a6 fix: an index entry's dots reach its page number
The dot advance was probed once, on the first row, and reused for every
level below it — so a level with a smaller style ran ~9% short, 11mm of
bare gap before the page number. Probed per font now (a handful of
measurements, not one per row). A fixture's contents: 122/84/72 dots per row,
exactly what LibreOffice draws.
2026-08-11 20:31:34 +00: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 40d58463c7 docs: per-section margins ship, and what the corpus reports
Also records the harness change that made the longest fixture measurable at all.
2026-08-11 19:52:39 +00:00
Steffen Becker 3cf1d34035 fix: an index entry keeps the hard break its heading has
A book's chapter heading is "Chapter 1" and its title on two lines, and both
LibreOffice and Word copy that break into the index. We joined it into one line,
so a fixture's contents came out 18 rows short. The leader and page
number ride the last of an entry's lines (align-items: last baseline).
2026-08-11 19:52:02 +00:00
Steffen Becker 860e25d8c8 fix: saving keeps a section's own page margins
Both exports wrote one geometry for the whole document, so a title page's
margins survived import and were lost on the next save. Each ODF section master
now names a page layout of its own — the document's, shifted by the section's
difference, so what the header/footer pass folded into it survives — and every
w:sectPr carries its section's w:pgMar. DOCX import takes the document pair from
the first section, the one .tiptap's padding draws.

The parity harness spread a 458-page document's word list into Math.max and blew
the stack; the longest fixture was never measured.
2026-08-11 19:35:55 +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 9bce48f6e0 fix: a section keeps the top and bottom margins its own page setup gives it
Page geometry was document-wide, so a title page declaring 1cm started 1cm too
low. w:pgMar / the section's ODF page layout now ride its HfSet and feed the
per-page content area; an ODF master handing over (style:next-style-name) gives
its own layout to the section's first page and the successor's to the rest.

A spacer can only push down, so a smaller margin than the document's needs the
opposite: the section's first block gets a lifting spacer where nothing was
pushed onto its page ahead of it.
2026-08-11 18:51:12 +00:00
Steffen Becker 71cf17f47a docs: what the corpus still reports, and why it stays
Three probed findings written down rather than fixed: an outside ::marker has
no metric influence in CSS (a bulleted line is 0.12mm short of LibreOffice's),
an unbreakable run longer than the line moves whole instead of breaking at the
margin, and a typeset formula keeps its own baseline rather than its frame's —
honouring the frame measured worse. Plus the live-vs-cached index numbers.
2026-08-11 18:06:42 +00:00