diff --git a/CHANGELOG.md b/CHANGELOG.md index b31d46d..40fe444 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,7 +48,7 @@ - Bulleted and ordered lists — nested, with multiple numbering styles (decimal, alpha, roman; `.` or `)` suffix) and whole-list indent; Tab / Shift-Tab to nest / un-nest - Word-style nesting defaults for ordered lists: an indented level numbers 1. → a. → i. (repeating) instead of restarting at "1." everywhere; explicit styles per level still override. Plus legal/outline numbering (1., 1.1., 1.2.1. …) as a list type — rendered via CSS counters, round-trips to ODF `text:display-levels` and DOCX `%1.%2.` lvlText chains - Nested ordered levels advance the cycle relative to the level above and inherit its suffix (a level-1 `a)` gives `i)` then `1)`); the numbering style chosen for a level is reused when you nest into it again -- Customizable bullet symbols per list level (Word-style picker on the bullet-list split button: • ◦ ▪ ❖ ➢ ⇨ ✓ – >); round-trips to ODF `text:bullet-char` and DOCX `w:lvlText`, and the DOCX/ODT import maps Wingdings/Symbol bullets (arrows, diamonds, checkmarks …) to their Unicode equivalents instead of flattening them to plain dots. Symbols Liberation Serif lacks render from a bundled 2 KB DejaVu Sans subset (`EdenSymbols.woff2`), so markers look compact and identical on every platform instead of a stretched OS fallback +- Customizable bullet symbols per list level (picker on the bullet-list split button: • ◦ ▪ ❖ ➢ ⇨ ✓ – >); round-trips to ODF `text:bullet-char` and DOCX `w:lvlText`, and the DOCX/ODT import maps Wingdings/Symbol bullets (arrows, diamonds, checkmarks …) to their Unicode equivalents instead of flattening them to plain dots. Symbols Liberation Serif lacks render from a bundled 2 KB DejaVu Sans subset (`EdenSymbols.woff2`), so markers look compact and identical on every platform instead of a stretched OS fallback - Tab stops per paragraph: left, centre, right and decimal, honoured in the rendered text (CSS only has a fixed tab grid, so each tab is measured and placed); a hanging indent implies a stop at the text position. Set them on the ruler; round-trips to ODF `style:tab-stops` and DOCX `w:tabs` - Manual line breaks (Shift+Enter) @@ -67,7 +67,7 @@ - Page margins (cm) and page orientation (portrait / landscape) - Page format picker (Layout panel): 15 Word/LibreOffice sizes — A3–A6, ISO B4–B6, JIS B4/B5, Letter, Legal, Tabloid, Executive, Folio, Statement — each showing its cm dimensions; drives the on-screen page, pagination, and ODT/DOCX/PDF export, and is detected & adopted on import - Multi-column (newspaper) layout: 1–3 columns with adjustable gap, applied to the whole document (no selection) or to selected paragraphs, Word-style — text fills column 1 to the page bottom, then column 2, and flows across pages with mid-paragraph line breaks; a section followed by other content balances its columns. Round-trips to ODF `text:section`/`style:columns` and DOCX continuous sections with `w:cols` -- Headers & footers: page-number / page-count fields, configurable edge distances, and Word-style variants — different first page and different odd & even pages (edit a page's zone directly; blank first/even zones supported). Press Enter to add blank lines that grow a zone into the page while body text reflows so the page break sits above the footer / below the header, never overlapping. Inline images/logos can be inserted in a zone. Round-trips to ODF (`style:header-first`/`-left`, `draw:frame`) and DOCX (`w:titlePg`/`w:evenAndOddHeaders`, `ImageRun`) +- Headers & footers: page-number / page-count fields, configurable edge distances, and variants — different first page and different odd & even pages (edit a page's zone directly; blank first/even zones supported). Press Enter to add blank lines that grow a zone into the page while body text reflows so the page break sits above the footer / below the header, never overlapping. Inline images/logos can be inserted in a zone. Round-trips to ODF (`style:header-first`/`-left`, `draw:frame`) and DOCX (`w:titlePg`/`w:evenAndOddHeaders`, `ImageRun`) - Zoom (20–300 %) - Horizontal ruler above the page: click to place a tab stop of the selected type, drag to move it, drag it off to remove it; two markers set the paragraph's first-line and left indent. Toggle in the extended toolbar - Show formatting marks (spaces, tabs, paragraph marks) @@ -76,7 +76,7 @@ - Word/LibreOffice keyboard shortcuts throughout: Ctrl+L/E/R/J alignment, Ctrl+Alt+1–5 headings and Ctrl+Shift+N default style, Ctrl+1/2/5 line spacing, Ctrl+Shift+P / Ctrl+Shift+B super/subscript, Ctrl+Shift+. / Ctrl+Shift+, grow/shrink font, Ctrl+Space clear formatting, Ctrl+Shift+Space non-breaking space, Ctrl+Shift+- soft hyphen, Alt+Shift+D / Alt+Shift+T date & time field, Ctrl+O open, F3 / Shift+F3 find next/previous, Ctrl+F10 formatting marks — alongside the existing Ctrl+B/I/U, Ctrl+M, Ctrl+K, Ctrl+Enter, Ctrl+F/H, Ctrl+S/P and Tab/Shift+Tab. All bindings live in one table, so tooltips stay in sync and they can be remapped later - Undo / Redo with a labelled history dropdown - Search & Replace (Ctrl+F / Ctrl+H): live match highlighting, match count, next / previous, match-case and whole-word options, replace current / replace all -- Right-click context menu for text (Word-style): cut / copy / paste / paste without formatting, link insert-edit-remove, clear formatting — with the spelling suggestions merged in on top. Shift+right-click keeps the browser's own menu; images, text boxes and header/footer stay with their existing UI +- Right-click context menu for text: cut / copy / paste / paste without formatting, link insert-edit-remove, clear formatting — with the spelling suggestions merged in on top. Shift+right-click keeps the browser's own menu; images, text boxes and header/footer stay with their existing UI - Spell check in English and German with squiggles and suggestions in the context menu (add / ignore word); selectable document language. Powered by Hunspell (WASM), so German compound words (Fußgänger, Krankenversicherung …) are recognised; dictionaries are lazy-loaded per language on demand - Word / character count statistics (whole document and selection) diff --git a/src/lib/components/Editor.svelte b/src/lib/components/Editor.svelte index 9f36949..ad0d892 100644 --- a/src/lib/components/Editor.svelte +++ b/src/lib/components/Editor.svelte @@ -61,8 +61,8 @@ }); // A header/footer that reaches past the body's margin (its distance from the edge plus - // its line count) pushes the body's content area in so text doesn't overlap it — Word - // grows the margin to fit the zone. ~18.4px = one 12pt line. + // its line count) pushes the body's content area in so text doesn't overlap it: the + // margin grows to fit the zone. ~18.4px = one 12pt line. const HF_LINE_PX = 16 * 1.15; function hfReachPx(doc: HfDoc, distPx: number): number { if (!doc || hfIsEmpty(doc)) return 0; @@ -193,7 +193,7 @@ return (Number.isFinite(ph) ? ph : 1123) + 20; } - // --- Right-click context menu (Word's text menu, spelling suggestions merged in) --- + // --- Right-click context menu (text menu, spelling suggestions merged in) --- let ctxMenu = $state<{ top: number; left: number; items: MenuEntry[] } | null>(null); // The misspelled range the open menu's suggestions belong to. let spellTarget: { from: number; to: number; word: string } | null = null; @@ -212,7 +212,7 @@ const view = ed.view; const coords = view.posAtCoords({ left: event.clientX, top: event.clientY }); const sel = view.state.selection; - // Word keeps a selection the click lands inside, otherwise it moves the caret. + // A selection the click lands inside is kept, otherwise the caret moves. if (coords && (sel.empty || coords.pos < sel.from || coords.pos > sel.to)) { view.dispatch(view.state.tr.setSelection(TextSelection.near(view.state.doc.resolve(coords.pos)))); } @@ -294,7 +294,7 @@ // Shown when the selection is inside a table; positioned just above that table. let tableUi = $state<{ visible: boolean; top: number; left: number }>({ visible: false, top: 0, left: 0 }); let tableUiRaf = 0; - // Word-style "Split Cells…" popover, opened from the table toolbar. + // "Split Cells…" popover, opened from the table toolbar. let splitDialogOpen = $state(false); // Drop the dialog if the selection leaves the table (toolbar hidden). $effect(() => { @@ -738,7 +738,7 @@ saveDocument(e.getJSON()); }, onFocus: () => { - // Clicking back into the body ends header/footer editing (Word behaviour). + // Clicking back into the body ends header/footer editing. hfActive = null; }, }); diff --git a/src/lib/components/HeaderFooterLayer.svelte b/src/lib/components/HeaderFooterLayer.svelte index af9a0f3..5566318 100644 --- a/src/lib/components/HeaderFooterLayer.svelte +++ b/src/lib/components/HeaderFooterLayer.svelte @@ -307,7 +307,7 @@ flex-direction: column; pointer-events: auto; /* Content taller than the margin band spills into the margin (footer up, header - down), like Word's auto-growing zones; the anchored edge stays put. */ + down) — the zone auto-grows; the anchored edge stays put. */ overflow: visible; font-family: var(--font-serif); font-size: 12pt; diff --git a/src/lib/components/ParagraphBorderPicker.svelte b/src/lib/components/ParagraphBorderPicker.svelte index bfdd7b6..85defe4 100644 --- a/src/lib/components/ParagraphBorderPicker.svelte +++ b/src/lib/components/ParagraphBorderPicker.svelte @@ -6,7 +6,7 @@ let { editor, tick }: { editor: Editor | null; tick: number } = $props(); let open = $state(false); - // Pen settings (Word-style): presets apply with the currently chosen width/color. + // Pen settings: presets apply with the currently chosen width/color. let widthPt = $state(0.5); let color = $state('#000000'); diff --git a/src/lib/components/TableBorderPicker.svelte b/src/lib/components/TableBorderPicker.svelte index 8a63d7e..3763e95 100644 --- a/src/lib/components/TableBorderPicker.svelte +++ b/src/lib/components/TableBorderPicker.svelte @@ -6,7 +6,7 @@ let { editor, tick }: { editor: Editor | null; tick: number } = $props(); let open = $state(false); - // Pen settings (Word-style): presets apply with the currently chosen width/color. + // Pen settings: presets apply with the currently chosen width/color. let widthPt = $state(0.5); let color = $state('#000000'); @@ -45,7 +45,7 @@ ); // Clicking an inactive preset applies the pen; clicking an active one toggles those - // borders off (Word behavior). The panel stays open so the states visibly update. + // borders off. The panel stays open so the states visibly update. function apply(id: BorderPreset | 'none') { if (!editor) return; if (id === 'none') editor.chain().focus().setTableBorders('all', null).run(); diff --git a/src/lib/components/TableStylePicker.svelte b/src/lib/components/TableStylePicker.svelte index 93229ca..ad30353 100644 --- a/src/lib/components/TableStylePicker.svelte +++ b/src/lib/components/TableStylePicker.svelte @@ -189,7 +189,7 @@ user-select: none; } - /* Tiles and options side by side, as in Word's Table Design tab. */ + /* Tiles and options side by side. */ .tsp-body { display: flex; align-items: flex-start; diff --git a/src/lib/components/ToolbarExpanded.svelte b/src/lib/components/ToolbarExpanded.svelte index 3c2814a..4c57187 100644 --- a/src/lib/components/ToolbarExpanded.svelte +++ b/src/lib/components/ToolbarExpanded.svelte @@ -274,7 +274,7 @@ return mixed ? '' : (c ?? null); }); - // Sub/superscript are mutually exclusive (Word-style): toggling one clears the + // Sub/superscript are mutually exclusive: toggling one clears the // other. odf-kit maps these marks to text:position (round-trips with LibreOffice // and Word). See export/odt.ts applyRuns for the custom-attr-paragraph path. let isSuperscript = $derived(tick >= 0 && !!editor?.isActive('superscript')); diff --git a/src/lib/editor/contextMenuItems.ts b/src/lib/editor/contextMenuItems.ts index 5b3c6ae..06d7e3e 100644 --- a/src/lib/editor/contextMenuItems.ts +++ b/src/lib/editor/contextMenuItems.ts @@ -4,7 +4,7 @@ import { withShortcut } from '../i18n/shortcut'; import { shortcutHint } from './shortcuts'; import { OPEN_LINK_DIALOG_EVENT } from './extensions/link'; -// The right-click menu's contents, Word's text menu mapped onto this editor. Pure +// The right-click menu's contents, the usual text menu mapped onto this editor. Pure // data + closures — ContextMenu.svelte only renders it. export type MenuEntry = | { kind: 'sep' } diff --git a/src/lib/editor/extensions.ts b/src/lib/editor/extensions.ts index db9a5cf..e571ac5 100644 --- a/src/lib/editor/extensions.ts +++ b/src/lib/editor/extensions.ts @@ -123,11 +123,11 @@ export const extensions = [ TableCell, TableColumnResize, TableRowResize, - // Word-style "Split Cells…" (N×M); merge uses extension-table's built-in mergeCells. + // "Split Cells…" (N×M); merge uses extension-table's built-in mergeCells. TableSplit, // Cell shading: backgroundColor attr on tableCell/tableHeader (→ fo:background-color). TableCellBackground, - // Word-style border presets: per-side border attrs on cells (→ fo:border-*). + // Border presets: per-side border attrs on cells (→ fo:border-*). TableCellBorders, // "Header row" styling preset: bold + light shading on the first row (toggle). TableHeaderRow, diff --git a/src/lib/editor/extensions/columns.ts b/src/lib/editor/extensions/columns.ts index 383c46a..dbea032 100644 --- a/src/lib/editor/extensions/columns.ts +++ b/src/lib/editor/extensions/columns.ts @@ -125,7 +125,7 @@ export function mergeJoinedBlocks(blocks: PMNode[]): PMNode[] { return out; } -// Word's no-selection behavior: apply columns to the whole document. Wrappable runs +// With no selection, columns apply to the whole document. Wrappable runs // (absorbing existing sections) become one section each; tables/boxes/TOCs stay // between them. Replacements run bottom-up so earlier positions stay valid. function applyToWholeDoc(state: EditorState, tr: Transaction, attrs: ColumnsAttrs): boolean { @@ -221,7 +221,7 @@ export const Columns = Node.create({ return { // Inside a section: 1 unwraps the whole chain, 2/3 update its count. Outside: // with a selection, 2/3 wrap the covered top-level blocks (fails on tables - // etc.); with a bare cursor, the whole document gets columns (Word behavior). + // etc.); with a bare cursor, the whole document gets columns. setColumns: (count: number) => ({ state, dispatch }) => { diff --git a/src/lib/editor/extensions/columnsFlow.ts b/src/lib/editor/extensions/columnsFlow.ts index b3e4a1e..1802836 100644 --- a/src/lib/editor/extensions/columnsFlow.ts +++ b/src/lib/editor/extensions/columnsFlow.ts @@ -197,7 +197,7 @@ export const ColumnsFlow = Extension.create({ return open; } - // Split the paragraph at `blockIndex` at the line consuming `budgetPx` (Word's + // Split the paragraph at `blockIndex` at the line consuming `budgetPx` (a // mid-paragraph page break); the second part is marked joinPrev so joins/export // restore the original. `force` takes at least one line for an over-tall paragraph. function splitParagraphInBlock( @@ -289,7 +289,7 @@ export const ColumnsFlow = Extension.create({ if (first + COLUMNS_FIT_MARGIN_PX > available) { // First block alone doesn't fit. Mid-page, pageBreaks pushes the // fragment; at a page top pushing can't help — split the paragraph - // at a line boundary instead (Word's mid-paragraph page break). + // at a line boundary instead (a mid-paragraph page break). if ( available >= vm.contentHeight - 1 && splitParagraphInBlock(frags[i], children, 0, count * (available - SAFETY_PX), scale, true) diff --git a/src/lib/editor/extensions/headerFooter.ts b/src/lib/editor/extensions/headerFooter.ts index 632142c..81db872 100644 --- a/src/lib/editor/extensions/headerFooter.ts +++ b/src/lib/editor/extensions/headerFooter.ts @@ -28,7 +28,7 @@ import { Shortcuts } from './shortcuts'; const HfDocument = Document.extend({ content: 'paragraph' }); // The single-paragraph schema can't split, so plain Enter inserts a line break instead -// (Word: Enter in a footer adds a blank line, growing the zone into the page). Shift-/ +// (Enter in a footer adds a blank line, growing the zone into the page). Shift-/ // Mod-Enter keep their default break binding. const HfHardBreak = HardBreak.extend({ addKeyboardShortcuts() { diff --git a/src/lib/editor/extensions/image.ts b/src/lib/editor/extensions/image.ts index 3c7230d..9cb033d 100644 --- a/src/lib/editor/extensions/image.ts +++ b/src/lib/editor/extensions/image.ts @@ -23,7 +23,7 @@ declare module '@tiptap/core' { export const MIN_SIZE_PX = 24; -// Corners keep aspect (Word-style); edges (n/s/e/w) change one dimension only. +// Corners keep aspect; edges (n/s/e/w) change one dimension only. // Shared with textBox.ts, whose node view uses the same handle set. export const HANDLES: { k: string; x: -1 | 0 | 1; y: -1 | 0 | 1; aspect: boolean }[] = [ { k: 'nw', x: -1, y: -1, aspect: true }, diff --git a/src/lib/editor/extensions/indent.ts b/src/lib/editor/extensions/indent.ts index 6b5c266..f27880b 100644 --- a/src/lib/editor/extensions/indent.ts +++ b/src/lib/editor/extensions/indent.ts @@ -46,7 +46,7 @@ function innermostList($from: ResolvedPos): { node: PMNode; depth: number; depth } // The type-defining attr an explicit same-kind list at `depth0` already uses in this -// tree (DFS-first), so a freshly nested list can reuse it — Word's per-level memory. +// tree (DFS-first), so a freshly nested list can reuse it — the per-level memory. function rememberedListStyle(root: PMNode, kind: string, depth0: number): Record | null { let found: Record | null = null; const walk = (list: PMNode, d: number) => { @@ -104,7 +104,7 @@ function clampIndent(cm: number): number { return Math.round(Math.max(0, Math.min(INDENT_MAX_CM, cm)) * 100) / 100; } -// A first-line indent is signed (negative = Word's hanging indent). +// A first-line indent is signed (negative = a hanging indent). function clampFirst(cm: number): number { return Math.round(Math.max(-INDENT_MAX_CM, Math.min(INDENT_MAX_CM, cm)) * 100) / 100; } @@ -115,7 +115,7 @@ function parseIndent(value: string | null): number | null { return Number.isFinite(n) && n > 0 ? n : null; } -// A first-line indent may be negative (Word's hanging indent), unlike a left margin. +// A first-line indent may be negative (a hanging indent), unlike a left margin. function parseSignedIndent(value: string | null): number | null { if (value == null || value === '') return null; const n = parseFloat(value); @@ -153,7 +153,7 @@ export const Indent = Extension.create({ }; }, }, - // First-line indent in cm; negative is Word's hanging indent. Independent of + // First-line indent in cm; negative is a hanging indent. Independent of // `indent`, which is the whole block's left margin. indentFirst: { default: null, diff --git a/src/lib/editor/extensions/tableSplit.ts b/src/lib/editor/extensions/tableSplit.ts index 9add484..a152686 100644 --- a/src/lib/editor/extensions/tableSplit.ts +++ b/src/lib/editor/extensions/tableSplit.ts @@ -9,7 +9,7 @@ import { columnWeightsFromRow } from './tableView'; // cell selection) into `cols` × `rows` sub-cells. prosemirror-tables only ships // splitCell (un-merge), so this is custom. The table is a strict rectangular grid, // so adding sub-columns/rows means inserting grid lines and letting every other -// cell that crosses a new line grow its span (bridge) — exactly what Word does. +// cell that crosses a new line grow its span (bridge). // Implemented by reading the grid via TableMap, expanding it, partitioning the // cell's region, and rebuilding the table node in one step. diff --git a/src/lib/editor/extensions/textBox.ts b/src/lib/editor/extensions/textBox.ts index de7c707..77926c3 100644 --- a/src/lib/editor/extensions/textBox.ts +++ b/src/lib/editor/extensions/textBox.ts @@ -364,7 +364,7 @@ class TextBoxView { ); } - // Word-like frame hit test: the border plus a few px of the inner padding ring + // Frame hit test: the border plus a few px of the inner padding ring // select the box; anywhere further inside is text area. Rotation-aware (the point // is un-rotated into the rotor's own axes) and zoom-aware. private isFrameHit(e: MouseEvent): boolean { diff --git a/src/lib/utils/dateTime.ts b/src/lib/utils/dateTime.ts index 2bc5b5e..af27eb9 100644 --- a/src/lib/utils/dateTime.ts +++ b/src/lib/utils/dateTime.ts @@ -25,7 +25,7 @@ export interface DtFormat { tokens: Token[]; } -// Word-style catalog. Samples in the picker render the current time, so the exact +// Format catalog. Samples in the picker render the current time, so the exact // ordering/separators are visible when choosing. export const DATE_FORMATS: DtFormat[] = [ { key: 'iso', kind: 'date', tokens: [ diff --git a/src/styles/editor.css b/src/styles/editor.css index 0539128..7da6a77 100644 --- a/src/styles/editor.css +++ b/src/styles/editor.css @@ -280,7 +280,7 @@ .paper .tiptap .image-resize-sw { bottom: -5px; left: -5px; cursor: nesw-resize; } .paper .tiptap .image-resize-w { top: 50%; left: -5px; margin-top: -5px; cursor: ew-resize; } /* Rotate grip floats above top-centre with a short connector line; the icon is a - circular rotation arrow (like Word). */ + circular rotation arrow. */ .paper .tiptap .image-rotate-handle { display: none; position: absolute; @@ -579,7 +579,7 @@ height: 0; } -/* Paragraph mark (¶). Reserved in the layout at all times (Word's model) so the toggle +/* Paragraph mark (¶). Reserved in the layout at all times, so the toggle only recolors it and text never reflows. Zero-size inline-block, glyph overflows visibly: width/height/line-height 0 drop both footprint and line-box strut. */ .paper .tiptap p::after,