Commit Graph
139 Commits
Author SHA1 Message Date
Steffen Becker 6abc7f7704 refactor: the kept versions are offered at a failed load, not in the menu
A list of timestamps in both file menus paid for itself only on the day
something went wrong, and it was in the way on every other. The entries
are gone; loadDocument now offers the versions where the browser copy has
actually failed the reader — newest first, one question each, and the one
taken up starts under the boot flag like any other document. The store
loses its reactive singleton with them.
2026-09-04 19:30:13 +02:00
Steffen Becker ea327174e8 fix: the unsaved state says so in words instead of a dot
A mark has to be known to be read, so the name now carries "• Not saved"
("Nicht gespeichert") instead of a bare dot with a tooltip. In the ribbon
the label sits beside the name box rather than inside it: that box is
capped at 30% of the strip, which would take the width off the name.
2026-09-04 19:30:13 +02:00
Steffen Becker e7d458aca5 fix: the unsaved dot counts a rename too
The name decides which file the next save writes, so it belongs in the
checksum beside the text and the page setup. The DOM run renames a
freshly opened document and expects the dot.
2026-09-04 19:30:13 +02:00
Steffen Becker 616851f62f fix: the unsaved dot counts the page setup and the styles too
It is now an FNV-1a checksum over the text plus everything exportArgs()
hands the exporter, so a margin preset marks the document as much as a
keystroke does and an undo back to the saved state clears it again. The
page count stays out — a late-loading font must not mark anything. The
beat is throttled, not debounced, and the baseline is taken on the spot
instead of in the timer, which a repagination can push past a first edit.
The DOM run checks all four states.
2026-09-04 19:30:13 +02:00
Steffen Becker ef8d219cdd feat: the autosave keeps the last three versions of the document
One version every five minutes goes into an IndexedDB store, pictures
inline, and both file menus list them by time; a click puts that text
back and leaves the page setup, the styles and the zones alone. The
browser copy is one document deep, so a bad edit had nothing behind it.
2026-09-04 19:30:13 +02:00
Steffen Becker 2d2b321bad feat: the document name carries a dot while the file is behind
The dot shows in both chromes as soon as the text differs from what was
last written to a file, and goes when a save, an open or a new document
makes them agree again. Leaving the page warns only where the document
has a file to lose the changes from.
2026-09-04 19:30:12 +02:00
Steffen Becker b8ff3d764f feat: Save As offers both document formats in one picker
The picker lists .odt and .docx, and the chosen extension decides which
exporter runs and which format the document keeps — no silent switch to
.odt for a file that came in as .docx. Both save paths now build their
bytes through one helper instead of repeating the argument list.
2026-09-04 19:30:12 +02:00
Steffen Becker cb0db7dd96 fix: a reload keeps the document protected, and the first save asks for the password
The password itself is never stored, so a reload left the document open and
the next save wrote the protected file unencrypted without a word. The fact
of the protection now survives; the save dialog asks for the password again
or lets the protection go, and backing out cancels the save.
2026-09-04 19:30:12 +02:00
Steffen Becker 87743fdd57 perf: the word count and the name placeholder no longer walk the document per transaction
A caret move recounted every word; a blank title serialized the whole
document to JSON on every transaction. The count now runs once per changed
document, deferred off the keystroke, and the placeholder reads the first
heading straight off the document.
2026-09-04 19:30:12 +02:00
Steffen Becker 066737463d perf: the ODT exporter and both importers load on first use
The heading constants move to styles/headings.ts and the filename logic to
storage/documentName.ts, which were all the editor needed of the exporter
at startup. Main chunk 2.1 MB → 1.37 MB (gzip 615 KB → 411 KB).
2026-09-04 19:30:12 +02:00
Steffen Becker 9335f392ce fix: the document's format survives a reload
A .docx document came back as .odt in the title and the save picker after
a reload, since only the session knew its format.
2026-09-04 19:30:12 +02:00
Steffen Becker b2cb216af0 fix: a save that fails falls back to a new file only for a lost handle
Any error but an encryption one went to Save As, which hid exporter bugs
and silently turned a .docx document into .odt. Only a handle that lost
its permission or its file prompts again now, in the document's format.
2026-09-04 19:30:12 +02:00
Steffen Becker 57b543806c fix: an opened file's password becomes the document's only once the file is loaded
Declining to replace the current document, or an import that throws, left
the foreign password on the document that stayed open, and an unprotected
file kept the previous document's password. Both now travel with the load.
2026-09-04 19:30:12 +02:00
Steffen Becker 80f68f1883 feat: a document can turn off the spacing at the start of a page
LibreOffice's compatibility option (ODF AddParaTableSpacingAtStart, Word's
w:suppressSpBfAfterPgBrk read the other way round). Off, the block that opens a
page loses its space above however the page broke — probed by flipping the flag
in one 63-page document, whose chapter heading then moved from 25.4mm to 20.0mm,
which is where the editor had been drawing every one of them. On is the default
both products write, and both formats carry the flag back out. Fixture 244 → 160.
2026-09-04 19:30:09 +02:00
Steffen Becker 5856de6c2d feat: a password can be set for a document, and one is asked for on opening
The File menu of both chromes protects the document; the password reaches the
two write primitives in saveFile.ts, so .odt, .docx and both template kinds are
encrypted after every post-processing pass. Opening a protected file asks until
it fits or the user gives up, before any importer reads the archive. The
password is session state and never persisted; the autosaved copy stays plain.
2026-09-04 19:30:04 +02:00
Steffen Becker de71830922 feat: a comment or a change gets a balloon beside its own line
ReviewMarginLayer draws the cards in a strip right of the page, at the height
of the line each belongs to, with a dashed leader running through the gap
under it. A balloon is pushed down by the one above (balloonStack, measured
per card, unit-tested) and the column belongs to its page, so it never runs
past the sheet. The strip is only reserved where the margin has something to
show; the smoke test guards a card's content against its box.
2026-09-04 19:30:03 +02:00
Steffen Becker 4b66631b56 feat: the Review tab picks how much markup is shown and where
One button per kind shows or hides it and its caret says where its cards go —
balloons or the pane, which is why the two pane toggles are gone. Display for
review carries Word's four modes beside them, and the accept / reject / walk
pairs sit under each kind. The PDF path takes the mode as page attributes, so
a printout of a page in no markup reads as the page does.
2026-09-04 19:30:03 +02:00
Steffen Becker f51e3377e6 fix: a printed comment says which page it is anchored on
The list quoted its text but named no place, so on a five-page printout the
entry was a needle. It now leads with the page, the format LibreOffice prints
its own end-of-document comments in. Only the raster paths can say it — they
print the editor's own pagination, the vector one re-paginates in the browser.

commentsHeading gives way to commentLabels, since the page needs a word too.
2026-09-04 19:30:02 +02:00
Steffen Becker 29433e1b1d feat: a switch decides whether a printout carries the review markup
The Review tab's "Print markup", on by default and stored only when off
(edentext-print-markup). Off, the raster paths drop the bar layer from the
clone and skip the comment pages, the vector path emits neither the markup CSS
nor the list.

printPdf rebuilds its options field by field, so it never saw commentsHeading
either — the printed list was headed in English whatever the UI language.
2026-09-04 19:30:02 +02:00
Steffen Becker 2aecb1a1b3 feat: a printed document carries its comments and changes
The bar in the margin beside every changed or commented block, and the comment
bodies as a list after the document — a comment's text is nowhere on the page,
so a bar alone could only say that one exists. Resolved comments print nowhere,
and a document with no markup prints exactly as before.

The print engine clips to the page area (measured), so the vector path buys the
bar's strip out of the left @page margin and gives it back as padding. The
raster paths clone the editor's own layer and append the list as whole pages.
2026-09-04 19:30:02 +02:00
Steffen Becker eb7f3c731d feat: a comment or change says which line it belongs to
Three pairings, the way both word processors draw them: a bar in the margin
beside every line a change or an open comment covers (ChangeBarLayer), the
reviewing pane's card marked and scrolled into view, and a dashed leader from
the text to that card (ConnectorLayer). The leader is drawn for the one at the
caret only, and dropped where a second pane sits between text and card.

commentIdAt/revisionIdAt compare ranges, not the marks at the caret: both marks
are non-inclusive, so the range a click on a card selects would match nothing.
2026-09-04 19:30:02 +02:00
Steffen Becker 060dcd7380 feat: a text box sits in the line of text
textBox becomes an inline node carrying block content, like the frame both
formats write, so "in line with text" really is in the line and a box reaches
a table cell or a list item through their paragraphs. Both exports write it
where it sits (an as-char draw:frame / a wp:inline drawing) and both imports
leave it there, which drops the hoisting machinery on either side.

Two guards the schema cannot express: no box inside a box, and a selection
reaching into a box grows to the box's bounds rather than dissolving it.
2026-09-04 19:30:01 +02:00
Steffen Becker 0f8bca31cd feat: built-in template gallery with a DIN 5008 letter
lib/templates/: registry + one module per template, localized via t()
at build time. applyTemplate (App.svelte) reuses the extracted
resetDocumentState() from handleNew, then assigns the template's
margins, styles and fold marks; no file handle, so Save asks where.
Gallery dialog reachable from the ribbon File menu and the modern
chrome; first template is a DIN 5008 Form B business letter built from
placeholder fields, with the address field at 45mm and a 10cm info tab.
2026-08-24 16:54:03 +00:00
Steffen Becker a5c072db23 feat: fold and punch marks (Falzmarken, DIN 5008)
edentext-fold-marks flag: FoldMarkLayer draws the 87/192mm folds and the
148.5mm punch mark per page inside .paper, so print and PDF include them.
Toggled in the Layout tab. Exported as named EdenFoldMark* lines in the
master page's header (ODF page-relative draw:line, DOCX VML v:line);
both importers read the names back into the flag. Roundtrip Leg 32.
2026-08-24 16:27:27 +00:00
Steffen Becker ca29ff780b fix: make the recent-files list only offer files it can reopen
A file opened without a File System Access handle (no API — Firefox,
Safari, Brave — or a template) is no longer recorded: nothing could
ever reopen it, so every click failed and dropped the entry. A startup
prune clears such stale entries. Reopening now asks for 'read' instead
of 'readwrite' (Save re-prompts on its own), and a declined permission
prompt keeps the entry — only a moved or deleted file drops it.
2026-08-23 18:54:52 +00:00
Steffen Becker fc20e0a89c fix: improve saving where the File System Access API is unavailable
The fallback download shows a one-time hint about the browser's
ask-where-to-save setting, keeps its anchor in the DOM and the blob
URL alive past the click (Gecko). Failed saves and exports now report
the underlying error, with a clear message when a script blocker
prevented a lazy chunk from loading. A full or blocked localStorage
no longer fails the save when recording the recent-files entry.
2026-08-18 19:56:32 +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 3ae8d27b26 fix: the build runs under a subpath, not only at the domain root
base './' emits relative asset URLs; the manifest, the service-worker
registration and sw.js's precache resolve against their own location;
the four hardcoded /image literals turn relative. Verified headless
under a simulated /edentext/ subpath: boots, types, SW scope correct.
2026-08-17 18:59:48 +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 273c533fce fix: open a file whose extension names the wrong format
Which importer ran was decided by the extension alone, so a .docx saved or
renamed as .odt (and the reverse) was reported as a broken file. A failed
read is retried as the other format; a file that is neither still reports
the error for the extension it carries.
2026-08-16 03:00:27 +02:00
Steffen Becker d78181fdd9 fix: Save round-trips a .docx-opened document as .docx
Ctrl+S always built .odt bytes and wrote them into whatever handle was
open — including a handle obtained by opening a .docx, silently
overwriting it with ODF content under its old name. Track the loaded
format and route Save through the matching exporter and handle; the
title's extension label now reflects it instead of a hardcoded .odt.
Save As stays .odt-only, so it switches the format back.
2026-08-15 22:40:32 +02:00
Steffen Becker 0a91e573b4 refactor: the thesaurus is called Synonyms in the UI
Both products name the tool after what it is, which says nothing to a
reader who does not know the word. Every label is "Synonyms" now — ribbon,
context menu, tools menu, dialog — and "thesaurus" stays in the tooltip so
it is still findable by name. Keys and code keep the technical name.
2026-08-15 21:12:46 +02:00
Steffen Becker 44b351e865 feat: a thesaurus, LibreOffice's own synonyms
Ctrl+F7, Review ▸ Thesaurus and the context menu open one dialog: the
word at the caret, the sense groups it is in, click one to replace it,
and a box to look up any other word. The data is MyThes re-packed to one
group per line (de 3.0MB/37k groups, en 6.7MB/143k) — the format repeats
every group once per member, ten times the bytes. Loaded on first use for
the document's language and scanned, not indexed: ~4ms a lookup, where a
word→group index measured ~100MB of memory against the text's ~8MB.
2026-08-15 20:23:32 +02:00
Steffen Becker cbb1805910 feat: multi-page view
View ▸ Multiple Pages (1–4): rows of pages filled left to right, scrolling
as one canvas, every page editable. Neither reference product stops at two —
LibreOffice takes a column count, Word fits what the window allows — and both
re-zoom for it, so a whole row is fitted when the count changes. A pane is a
grid cell clipped to one page, a view showing only a contiguous slice; two
rows of cells take turns as the upper one. Cost: columns x 2 live views, each
holding the whole document (measured: +94k nodes and +16ms per keystroke per
view at 177 pages).
2026-08-15 19:19:33 +02:00
Steffen Becker ad2a01ba75 feat: recording revisions belongs to the document, not the editor
A new document starts with it off and an import adopts the file's own
flag, so an editor left recording no longer marks the next document it
opens. Both formats carry it, probed against soffice: ODF on the change
registry (text:track-changes, written for the flag alone where nothing
is recorded yet), Word as w:trackRevisions in settings.xml — LibreOffice
writes and reads exactly those two, and reading w:trackChanges instead
would have found nothing.
2026-08-15 17:07:06 +02:00
Steffen Becker c771193c83 fix: an opened document is nobody's tracked insertion
With revision recording on, setContent inserted the whole file in one
transaction, so the recorder marked every run as the reader's own
insertion — the document then rendered underlined in the author colour
(#c69200) from the first character. Measured: 19 843 of 19 843 characters
on an 11-page import, 436 revision spans.

Both loads (new document, import) now carry the RECORDING meta; what is
typed after the open is still recorded, as in both word processors.
2026-08-15 16:42:28 +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 9892377ee3 feat: Find & Replace also searches font, size and colour
The three attrs LibreOffice's Format… dialog is mostly about, on both
sides of the bar. They live together on one TextStyle mark and a mark of
one type replaces that type whole, so each run's own attrs are the base of
the new one — recolouring leaves the font alone.

F3 was bound twice from here on: it now expands an AutoText shortcut where
the caret has one, as it does in both word processors, and finds the next
match otherwise.
2026-08-15 11:19:02 +02:00
Steffen Becker 405b175e49 feat: AutoText keeps a block of text under a shortcut
LibreOffice's Tools ▸ AutoText, in the ribbon where Word keeps its Quick
Parts: store the selection under a name and a shortcut, insert it from the
library or by typing the shortcut and pressing F3 — LibreOffice's key for
it.

An entry is HTML parsed back through the schema, so it keeps its marks and
blocks; the library is app-level, as it is in both products, so no file
carries it.
2026-08-15 10:33:16 +02:00
Steffen Becker 1c4dec49f3 feat: a reviewing pane, and a colour per reviewer
Every recorded change in document order beside the comments pane: author,
date, the text, accept or reject in place. A change split by a paragraph
boundary is one row, so acceptRevision/rejectRevision take an id. The
author colour is a decoration, since a mark cannot know the document's
other authors; it nests inside the mark's span and carries the under/
strikethrough with it. shortcutHint no longer throws on an id nothing
binds, which heading 7-10 had just made reachable.
2026-08-15 09:40:26 +02:00
Steffen Becker e1b2fefa55 feat: the OS can hand a document to the installed app
The manifest registers .odt/.ott/.docx/.dotx as file handlers and a
launchQueue consumer opens what the OS passes, keeping the handle so the
first Save writes that file. A .dotx now reads like an .ott: Word's
importer, but never bound as the file to save over.
2026-08-15 01:00:20 +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 61ddf69d54 fix: a caption follows the frame's place, not the paragraph's
A picture with "above and below" wrap is a frame centred in the
column while its paragraph carries no alignment at all, so the
caption still went to the left margin. framePlacement reads the
place off the frame: centred, the side it is set against, or its
offset as the caption's indent. The debug dump gains a frames
section — every other one measures the flow downwards, and this
is a sideways question.
2026-08-14 22:40:42 +02:00
Steffen Becker 03384c3da4 chore: the gap list is current again, and the check is clean
The Navigator was still listed as missing; only split view is. Formula
recalculation stops the walk at the blocks a table cannot be in, or it
visits every run of the document per keystroke. One dead CSS rule goes,
so svelte-check reports nothing at all.
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 c97a38ec77 feat: a Navigator pane lists the chapters and moves them
F5 (LibreOffice's key) opens an outline of the document's headings:
click one to jump, and move it up or down or promote/demote its level,
each carrying everything under the heading. Below it the tables,
pictures and bookmarks LibreOffice's Navigator also lists.

A move swaps with the adjacent chapter at the same level and stops at a
heading above it, so a subheading never reparents itself. The pane docks
right beside the comments one — the classic chrome's Tools chip owns the
left edge.
2026-08-14 08:32:30 +02:00
Steffen Becker 49e485038e feat: the document saves as a template, and the File menu remembers
A .ott/.dotx is the built document with one label changed — ODF's
package media type, Word's content type for the document part — so the
picker offers both and the chosen extension decides which exporter runs.

Opening or saving also records the file in a recent list. The name is in
localStorage so the menu draws at once; the handle that reopens it is in
IndexedDB beside it. A file that has moved, or whose permission the
browser drops on reload, is reported and dropped from the list.
2026-08-14 08:23:09 +02:00
Steffen Becker da438a7655 feat: line numbering in the left margin
Continuous or per page, every nth line, empty lines counted or not —
LibreOffice's Tools ▸ Line Numbering, round-tripping as its
text:linenumbering-configuration and Word's w:lnNumType. CSS exposes no line
boxes, so the layer Range-selects each block and takes the distinct tops of
its client rects as its lines; LibreOffice renders the exported file with the
numbers in the same places.
2026-08-14 06:56:20 +02:00
Steffen Becker 96d827b2c7 feat: page background, page border and watermark
LibreOffice's Format ▸ Page Style ▸ Area/Borders and its Format ▸ Watermark,
all three round-tripping. Probed placements that are not guessable: ODF reads
a watermark only out of the master page's header (and only with xmlns:draw
declared), Word's w:sectPr wants pgBorders after pgMar or it loses the header
references, and the VML shapetype has to travel or LibreOffice cannot resolve
it. On screen the watermark is an SVG textLength, since a fontwork shape
stretches its text to its box rather than setting a font size.
2026-08-14 06:50:25 +02:00