Commit Graph
111 Commits
Author SHA1 Message Date
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 1018380f88 fix: the collapsed strip ends on the active tab's underline
The underline hangs 2.5px below its tab, onto the band. Collapsed there is no
band to hang onto, so it landed on the ribbon's bottom border with 1px of chrome
under it against 5.5px above. The strip lends it the room while it is alone.
2026-08-13 18:34:19 +00: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 1aa74c0a9f feat: a footnotes and endnotes options dialog
Numbering format, start value, restart, position, prefix/suffix and the two
styles per class, plus the separator's length, thickness, spacing, alignment
and colour. The separator rides :root custom properties, so editor.css draws
the line and pageBreaks.ts reserves its band from the same values. A settings
change is no document change, so it carries a resync meta — without it the
numbering kept the format it was typed under.
2026-08-13 11:50:53 +00:00
Steffen Becker 0c74fe94b1 feat: footnotes and endnotes can be inserted and edited
The anchor is an inline atom, the note text a block in the one noteSection
at the document end; a sync plugin keeps notes in anchor order, renumbers
them per class and drops the ones whose anchor is gone. Numbering, indent,
size and separator defaults are LibreOffice's, read out of a document it
saved. Endnotes still render in flow at the document end — lifting a
footnote to the foot of its page is the next step.
2026-08-13 09:22:17 +00:00
Steffen Becker 29667a6f0d feat: a right-to-left page fills its columns from the right
ODF style:writing-mode="rl-tb" and Word's section-level w:bidi were not
read, so a Hebrew two-column page put column 1 on the left where
LibreOffice puts it on the right — checked against pdftoppm of its own
PDF, since the harness bands both columns into one line and never gets
that far. It is a document setting now, applied as `dir` on the body
host, which is all CSS multi-column needs; it also gives bidi the base
direction the file means. Round-trips through both formats. The
header/footer band keeps its own direction, as LibreOffice lays it out.
2026-08-12 22:02:36 +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 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 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 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 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 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 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 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 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 e2ee55cf35 fix: a header/footer band is as tall as its own font, not the body's
The zone is one paragraph, so its mark sizes every line of the band. Runs
that agree on a font now set that strut (applyUniformRunFont, as the body
already does), the zone schema carries blockFontSize so it renders, and
hfReachPx measures a line by the family's natural height instead of a flat
1.15. A 10pt footer reserved 12pt lines and sat 1.6mm high on every page.
2026-08-11 17:10:08 +00:00
Steffen Becker c41dce79cb fix: a zone reserves the band its own type needs
hfReachPx floored the zone's line at the 12pt body default and only ever grew
it, so a footer set throughout in 10pt still reserved a 12pt line and pushed
the body ~0.8mm too far up. The default now stands in per run, for one that
declares no size of its own.
2026-08-10 22:20:27 +00:00
Steffen Becker 0fe1a8395d fix: the header/footer band grows to hold the zone it carries
fo:min-height is a floor, not the band: LibreOffice grows it for the zone's
own lines and, in a footer, for the first paragraph's space above (probed —
a header's space below only hangs into the body). The zone paragraph now
carries those margins, so the reach counts them at its real font size and a
page with no zone of its own keeps the floor.
2026-08-10 21:22:56 +00:00
Steffen Becker d407febd4b feat: insert a chapter field, and set where a cell's content sits
Both round-tripped through save/load already but had no way in: the
header/footer bar gains a Chapter button next to page number/count, and the
table toolbar three buttons for the cell's vertical alignment.
2026-08-10 17:01:48 +00:00
Steffen Becker 3153c2f7cc feat: the running head names the chapter the page is in
A manual's footer carries text:chapter, which we imported as the string the
producer happened to cache — so every page of a fixture claimed the last chapter
of the book. It is now a live field: the layer resolves the last heading at
or above its outline level to have started by that page. Round-trips to ODF
text:chapter and Word STYLEREF; PDF keeps the cached name (Chrome's print
engine has no named strings).
2026-08-10 13:04:25 +00:00
Steffen Becker d6bc656c83 feat: the space between two blocks follows the document's own model
LibreOffice adds the upper block's space-below to the lower one's
space-above, and drops the space-above where a page break put the block
at a page top — but for a Word document it takes only the larger of the
two (all probed against soffice). CSS margins can only collapse, so
space-before rides --space-before and editor.css renders it as padding
('add') or margin ('max'); the model is read from settings.xml, assumed
for DOCX, and written back on ODF export.
2026-08-09 16:53:13 +00:00
Steffen Becker 30b5b7890e feat: a header's page background paints behind the page it belongs to
A positioned frame in a header is out of flow — the title-page background or
watermark. It keeps its wrap and its offset from the page corner, and the
layer paints it per page under the body instead of flowing it inline, where
it reserved a page-tall line of body space.

The page surface moves from .tiptap to .paper so the image has a layer
between the sheet and the text; ODF export writes it back page-anchored.
2026-08-09 15:13:46 +00:00
Steffen Becker 28a39e017a feat: a document that blocks the start is not loaded twice
An autosaved document that hangs or throws while the editor comes up was
reloaded from localStorage on every refresh, leaving no way back into the app.
Loading now raises a boot flag that only a completed startup clears; finding it
still set means the last attempt never finished, so the document moves to
odf-editor-doc-broken and the app starts empty with a message.
2026-08-09 14:13:17 +00:00
Steffen Becker 78e35cef2c feat: a reference finds the text it points at, and follows it
Bookmarks are a mark, not a point: both formats store a range, and only a
range lets a REF recompute its text. A cross-reference caches its display
like a Word field and resolves live on each pagination settle — the
bookmark's text, or the page it renders on — keeping the cached value when
the target is gone. Ctrl+click jumps to it, internal #name links too.

ODF splices its sentinels into the run text so cells and custom paragraphs
need no separate emitter; DOCX needs no post-pass at all.
2026-08-09 13:56:22 +00:00
Steffen Becker ee56a348ee feat: formulas can be inserted and edited, and survive both formats
A formula node stores only its LaTeX source; the MathML on screen, the ODF
formula object and Word's OMML are derived from it, and both importers parse
foreign markup back into that source, so an imported formula stays editable.

Word documents were losing their formulas outright: m:oMath sits beside the w:r
runs in its own namespace, past convertInline's `namespaceURI !== W` guard.
2026-08-08 21:50:36 +00:00
Steffen Becker aad4e7a3e4 feat: a later section's header and footer can be edited
The zones already imported, rendered and exported per section, but only
section 1 was editable state — the rest were marked read-only.

Editing now targets the section of the page it starts on: a double-click
edits the zone under the pointer, the Layout-panel buttons the current
page's. zoneKey/writeZone route the single live editor's read and its
write-back to either the six per-zone props or the extraHfSections entry.
2026-08-08 17:00:49 +00:00
Steffen Becker ae2290e22c feat: a header or footer keeps its own tab stops
The left⇥centre⇥right layout every letterhead uses rides on them, and a
zone had none: Word keeps them on the Header style, ODF on the zone
paragraph's, and neither was read or written.

An inactive zone is static generateHTML output no plugin reaches, so it
is measured straight on the DOM. Two bugs surfaced doing it: a segment
its own advance had pushed onto the next line measured as wrapped, and
the alignment then had no way back — it takes the sum of the segment's
client rects now. And the scale came from offsetHeight, which rounds
half a percent away on a zone one line tall.
2026-08-08 15:13:58 +00:00
Steffen Becker ea39dde58b feat: a tab lands on the file's own grid, and can draw its leader
The default tab interval was fixed at 1.25cm, so a file's own
w:defaultTabStop / style:tab-stop-distance was ignored and the DOCX
export wrote 1.25 back over Word's 1.27. It is a document setting now.

A stop can also carry the character it repeats across its gap. odf-kit
has no leader option and mints one automatic style per option set, so
the leader rides style:type behind a sentinel — matching on position
and alignment alone gave every stop at 6cm the same leader, which
LibreOffice drew back plain as day.
2026-08-08 14:40:26 +00:00
Steffen Becker 6accc7268f feat: every section keeps its own header and footer
A document had one set for all of it, taken from whichever section
declared one last — so a fixture lost its letterhead entirely
and page 1 got the empty header its final section asked for.

A body block now carries `sectionBreak`, ordinal rather than indexed so
editing cannot desync it. Import resolves Word's "Link to Previous" per
reference type; export writes a set per sectPr, and on the ODF side each
section past the first gets its own master page, which the block switches
to. The layer picks a page's set from where pageBreaks reports the
sections starting, and each section's zone reaches ride
`--pb-section-reach` so a later section's letterhead makes room for
itself: page 10 of a fixture was 24.9mm out, now 1.1mm.

Editing still reaches section 1 only; the rest render read-only.
2026-08-08 14:02:36 +00:00