From 4b66631b56fe5f69985e921f916dbae3875af3a2 Mon Sep 17 00:00:00 2001 From: Steffen Becker Date: Wed, 2 Sep 2026 12:38:47 +0000 Subject: [PATCH] feat: the Review tab picks how much markup is shown and where MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/App.svelte | 21 +- src/lib/components/ribbon/Ribbon.svelte | 8 +- src/lib/components/ribbon/icons.ts | 1 + .../components/ribbon/tabs/ReviewTab.svelte | 179 +++++++++++++++--- src/lib/export/CLAUDE.md | 5 + src/lib/export/pdf.ts | 5 +- src/lib/i18n/locales/de.ts | 17 +- src/lib/i18n/locales/en.ts | 17 +- src/styles/global.css | 7 + 9 files changed, 209 insertions(+), 51 deletions(-) diff --git a/src/App.svelte b/src/App.svelte index fcc96b0..fcd6b5d 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -46,6 +46,7 @@ import { loadLineNumbering, saveLineNumbering, DEFAULT_LINE_NUMBERING, type LineNumbering } from './lib/storage/lineNumbering'; import { loadFoldMarks, saveFoldMarks } from './lib/storage/foldMarks'; import { printMarkup } from './lib/storage/printMarkup.svelte'; + import { commentsInPane, changesInPane, markupAttrs, setShowChanges, setShowComments } from './lib/storage/markup.svelte'; import { loadDocumentLanguage, saveDocumentLanguage, odfFromLanguage, type DocumentLanguage } from './lib/storage/documentLanguage'; import { setTableLanguage } from './lib/storage/tableOptions.svelte'; import { spellController } from './lib/spell/controller'; @@ -192,8 +193,6 @@ let autoCorrectOpen = $state(false); let autoTextOpen = $state(false); let thesaurusOpen = $state(false); - let commentsOpen = $state(false); - let revisionsOpen = $state(false); let navigatorOpen = $state(false); // Width of the hidden mirror span (below), so the title input grows/shrinks @@ -749,7 +748,8 @@ const text = prompt(t().comments.prompt, ''); if (text === null) return; editor.chain().focus().addComment({ author: docProps.author.trim(), text: text.trim() }).run(); - commentsOpen = true; + // A comment nobody can see is worse than none: the new one brings its kind back. + setShowComments(true); } $effect(() => { @@ -1003,6 +1003,7 @@ differentOddEven, commentLabels: { heading: t().comments.title, onPage: t().comments.onPage }, printMarkup: printMarkup(), + markupAttrs: markupAttrs(), }); } catch (err) { console.error('[pdf] Print failed:', err); @@ -1184,10 +1185,6 @@ onAutoCorrect={() => (autoCorrectOpen = true)} onAutoText={() => (autoTextOpen = true)} onNewComment={addComment} - {commentsOpen} - onToggleComments={() => (commentsOpen = !commentsOpen)} - {revisionsOpen} - onToggleRevisions={() => (revisionsOpen = !revisionsOpen)} {navigatorOpen} onToggleNavigator={() => (navigatorOpen = !navigatorOpen)} /> @@ -1513,13 +1510,13 @@ {#if navigatorOpen} (navigatorOpen = false)} /> {/if} - {#if commentsOpen} - (commentsOpen = false)} /> + {#if commentsInPane()} + setShowComments(false)} /> {/if} - {#if revisionsOpen} - (revisionsOpen = false)} /> + {#if changesInPane()} + setShowChanges(false)} /> {/if} - {#if commentsOpen || revisionsOpen} + {#if commentsInPane() || changesInPane()} {/if} diff --git a/src/lib/components/ribbon/Ribbon.svelte b/src/lib/components/ribbon/Ribbon.svelte index 8e911b2..9ad40aa 100644 --- a/src/lib/components/ribbon/Ribbon.svelte +++ b/src/lib/components/ribbon/Ribbon.svelte @@ -68,7 +68,7 @@ onSelectTheme, docxBusy = false, pdfBusy = false, - onNew, onNewFromTemplate, onOpen, onSave, onSaveAs, onSaveDocx, onSaveTemplate, onExportPdf, onPrintPdf, onPrint, onAbout, onDocProperties, onAutoCorrect, onAutoText, onNewComment, commentsOpen = false, onToggleComments, revisionsOpen = false, onToggleRevisions, + onNew, onNewFromTemplate, onOpen, onSave, onSaveAs, onSaveDocx, onSaveTemplate, onExportPdf, onPrintPdf, onPrint, onAbout, onDocProperties, onAutoCorrect, onAutoText, onNewComment, navigatorOpen = false, onToggleNavigator, recentFiles = [], onOpenRecent, onForgetRecent, }: { @@ -125,10 +125,6 @@ onAutoCorrect?: () => void; onAutoText?: () => void; onNewComment?: () => void; - commentsOpen?: boolean; - onToggleComments?: () => void; - revisionsOpen?: boolean; - onToggleRevisions?: () => void; navigatorOpen?: boolean; onToggleNavigator?: () => void; } = $props(); @@ -360,7 +356,7 @@ {:else if tab === 'references'} {:else if tab === 'review'} - + {:else if tab === 'view'} {:else if tab === 'tableDesign' || tab === 'tableLayout'} diff --git a/src/lib/components/ribbon/icons.ts b/src/lib/components/ribbon/icons.ts index 54220a0..82d3598 100644 --- a/src/lib/components/ribbon/icons.ts +++ b/src/lib/components/ribbon/icons.ts @@ -11,6 +11,7 @@ const DOC_OUTLINE = [ export const STROKED = { chevronDown: ['M3.5 6 8 10.5 12.5 6'], chevronRight: ['M6 3.5 10.5 8 6 12.5'], + chevronLeft: ['M10 3.5 5.5 8 10 12.5'], check: ['M3.5 8.5 6.5 11.5 12.5 5'], doc: DOC_OUTLINE, newDoc: [...DOC_OUTLINE, 'M8 8v3.5M6.25 9.75h3.5'], diff --git a/src/lib/components/ribbon/tabs/ReviewTab.svelte b/src/lib/components/ribbon/tabs/ReviewTab.svelte index ac799b7..972556c 100644 --- a/src/lib/components/ribbon/tabs/ReviewTab.svelte +++ b/src/lib/components/ribbon/tabs/ReviewTab.svelte @@ -2,32 +2,61 @@ import type { Editor } from '@tiptap/core'; import RibbonGroup from '../RibbonGroup.svelte'; import RibbonButton from '../RibbonButton.svelte'; + import Icon from '../Icon.svelte'; import LanguagePicker from '../../LanguagePicker.svelte'; - import { captionClicks, anchored, clickOutside, isMenuOpen, toggleMenu } from '../menu.svelte'; + import { captionClicks, anchored, clickOutside, closeMenu, isMenuOpen, toggleMenu } from '../menu.svelte'; import { countText, type TextStats } from '../../../utils/wordCount'; import type { DocumentLanguage } from '../../../storage/documentLanguage'; import { recordChanges, setRecordChanges } from '../../../storage/trackChanges.svelte'; import { printMarkup, setPrintMarkup } from '../../../storage/printMarkup.svelte'; + import { + MARKUP_MODES, MARKUP_PLACES, markupMode, setMarkupMode, + showChanges, setShowChanges, showComments, setShowComments, + commentPlace, setCommentPlace, changePlace, setChangePlace, + type MarkupMode, type MarkupPlace, + } from '../../../storage/markup.svelte'; import { revisions } from '../../../editor/extensions/trackChanges'; + import { comments } from '../../../editor/extensions/comment'; + import { selectRange, step, visibleComments, visibleRevisions } from '../../reviewItems'; import { OPEN_THESAURUS_EVENT } from '../../../spell/thesaurus'; import { shortcutHint } from '../../../editor/shortcuts'; import { t } from '../../../i18n/i18n.svelte'; - let { editor, tick, documentLanguage, onLanguage, onAutoCorrect, onNewComment, commentsOpen = false, onToggleComments, revisionsOpen = false, onToggleRevisions }: { + let { editor, tick, documentLanguage, onLanguage, onAutoCorrect, onNewComment }: { editor: Editor | null; tick: number; documentLanguage: DocumentLanguage; onLanguage: (code: DocumentLanguage) => void; onAutoCorrect?: () => void; onNewComment?: () => void; - commentsOpen?: boolean; - onToggleComments?: () => void; - revisionsOpen?: boolean; - onToggleRevisions?: () => void; } = $props(); let hasSelection = $derived(tick >= 0 && !!editor && !editor.state.selection.empty); + // Accepting a change stays possible even where the view hides it, as it is in Word. let hasRevisions = $derived(tick >= 0 && !!editor && revisions(editor.state.doc).length > 0); + let hasComments = $derived(tick >= 0 && !!editor && comments(editor.state.doc).length > 0); + + const PLACE_LABEL: Record string> = { + balloons: () => t().revisions.placeBalloons, + pane: () => t().revisions.placePane, + }; + + const MODE_LABEL: Record { label: string; hint: string }> = { + all: () => ({ label: t().revisions.displayAll, hint: t().revisions.displayAllHint }), + simple: () => ({ label: t().revisions.displaySimple, hint: t().revisions.displaySimpleHint }), + none: () => ({ label: t().revisions.displayNone, hint: t().revisions.displayNoneHint }), + original: () => ({ label: t().revisions.displayOriginal, hint: t().revisions.displayOriginalHint }), + }; + + // Only what the view shows is walked: a hidden change is not a place to jump to. + function go(kind: 'comment' | 'revision', dir: 1 | -1): void { + if (!editor) return; + const doc = editor.state.doc; + const list: { from: number; to: number }[] = + kind === 'comment' ? visibleComments(doc) : visibleRevisions(doc); + const hit = step(list, editor.state.selection.from, dir); + if (hit) selectRange(editor, hit.from, hit.to); + } let stats = $derived.by(() => { if (tick < 0 || !editor) return { words: 0, charsWithSpaces: 0, charsNoSpaces: 0, paragraphs: 0 }; @@ -93,6 +122,34 @@
+ +{#snippet showKind(id: string, label: string, on: boolean, toggle: () => void, + at: MarkupPlace, setAt: (p: MarkupPlace) => void)} +
+ toggleMenu(id)} + /> + {#if isMenuOpen(id)} + + {/if} +
+{/snippet} + onNewComment?.()} /> - onToggleComments?.()} - /> + {@render showKind('commentPlace', t().comments.showPane, showComments(), + () => setShowComments(!showComments()), commentPlace(), setCommentPlace)} +
+ go('comment', -1)} + /> + go('comment', 1)} + /> +
@@ -123,14 +190,28 @@ active={recordChanges()} onclick={() => setRecordChanges(!recordChanges())} /> - onToggleRevisions?.()} - /> +
+ toggleMenu('markupDisplay')} + /> + {#if isMenuOpen('markupDisplay')} + + {/if} +
+ {@render showKind('changePlace', t().revisions.showPane, showChanges(), + () => setShowChanges(!showChanges()), changePlace(), setChangePlace)}
editor?.chain().focus().rejectRevisions(true).run()} />
- setPrintMarkup(!printMarkup())} - /> +
+ go('revision', -1)} + /> + go('revision', 1)} + /> +
+ + +
+ + + + +
@@ -190,6 +296,19 @@ .stats-menu { min-width: 240px; } .rb-col { display: flex; flex-direction: column; gap: 2px; } + + .rb-check { + display: flex; + align-items: center; + gap: 6px; + padding: 2px 6px; + font-family: var(--w-font); + font-size: 12px; + color: var(--w-text); + white-space: nowrap; + cursor: pointer; + } + .rb-check input { accent-color: var(--w-accent); cursor: pointer; } .rb-col :global(.rb-small) { width: 100%; } .stat { diff --git a/src/lib/export/CLAUDE.md b/src/lib/export/CLAUDE.md index 9014f76..87108c2 100644 --- a/src/lib/export/CLAUDE.md +++ b/src/lib/export/CLAUDE.md @@ -70,6 +70,11 @@ before (no list, no CSS, no margin shift). The Review tab's **Print markup** tog (`storage/printMarkup.svelte.ts`) turns the whole thing off, as both products offer; `printPdf` rebuilds its options field by field, so a new one has to be copied there too. +**The display mode prints too** (`storage/markup`, Word's "Display for review"): a page in +*no markup* prints as it reads. The two paths that rasterise the live `.paper` inherit its +attributes with the clone; `printPdf` builds its page from the document instead, so it +takes them as `markupAttrs`. `printMarkup` still governs the bar and the list beside it. + - **The margin strip is paid for.** The print engine clips to the page area, so a bar at a negative offset is silently dropped — measured, not assumed. `printPdf` takes `BAR_STRIP_CM` out of the left `@page` margin and gives it back as `.paper` padding, diff --git a/src/lib/export/pdf.ts b/src/lib/export/pdf.ts index fe47675..6fbdc1b 100644 --- a/src/lib/export/pdf.ts +++ b/src/lib/export/pdf.ts @@ -467,6 +467,8 @@ export interface PrintPdfOptions { commentLabels?: CommentLabels; /** Whether the review markup prints at all (`storage/printMarkup`); default on. */ printMarkup?: boolean; + /** The display mode as `.paper` attributes (`storage/markup`): printed as seen. */ + markupAttrs?: string; } // First-row column weights from table JSON (honours colspan); mirrors tableView. @@ -641,6 +643,7 @@ export function printPdf(opts: PrintPdfOptions): void { differentOddEven: opts.differentOddEven ?? false, commentLabels: opts.commentLabels, printMarkup: opts.printMarkup, + markupAttrs: opts.markupAttrs, }; const title = (o.fileName ?? deriveFilename(o.json)).replace(/\.(odt|pdf)$/i, ''); // The layers are Svelte DOM and generateHTML knows nothing of them, so this path @@ -662,7 +665,7 @@ export function printPdf(opts: PrintPdfOptions): void { idoc.write( `${title}` + `` + - `
${host.innerHTML}
${list}
`, + `
${host.innerHTML}
${list}
`, ); idoc.close(); diff --git a/src/lib/i18n/locales/de.ts b/src/lib/i18n/locales/de.ts index 148ae3c..27356c8 100644 --- a/src/lib/i18n/locales/de.ts +++ b/src/lib/i18n/locales/de.ts @@ -830,6 +830,19 @@ const de: Messages = { }, revisions: { record: 'Änderungen verfolgen', + display: 'Anzeige für Überprüfung', + displayAll: 'Alle Markups', + displayAllHint: 'Jede Änderung und jeder Kommentar', + displaySimple: 'Einfaches Markup', + displaySimpleHint: 'Endtext mit Änderungsbalken', + displayNone: 'Kein Markup', + displayNoneHint: 'Der Text, wie er sich liest', + displayOriginal: 'Original', + displayOriginalHint: 'Der Text vor den Änderungen', + placeBalloons: 'In Sprechblasen', + placePane: 'In der Leiste', + prev: 'Vorherige', + next: 'Nächste', printMarkup: 'Markup drucken', printMarkupHint: 'Randbalken und Kommentarliste mitdrucken', recordHint: 'Jede Bearbeitung als Änderung aufzeichnen', @@ -840,7 +853,8 @@ const de: Messages = { acceptAll: 'Alle annehmen', rejectAll: 'Alle verwerfen', pane: 'Änderungen', - showPane: 'Liste der aufgezeichneten Änderungen anzeigen', + showPane: 'Änderungen anzeigen', + showPaneHint: 'Liste der aufgezeichneten Änderungen anzeigen', empty: 'Keine aufgezeichneten Änderungen.', unknownAuthor: 'Unbekannter Autor', inserted: 'Eingefügt', @@ -913,6 +927,7 @@ const de: Messages = { citations: 'Zitate und Literaturverzeichnis', index: 'Index', revisions: 'Nachverfolgung', + markup: 'Markup', proofing: 'Rechtschreibung', language: 'Sprache', show: 'Anzeigen', diff --git a/src/lib/i18n/locales/en.ts b/src/lib/i18n/locales/en.ts index c1191a2..e286f3c 100644 --- a/src/lib/i18n/locales/en.ts +++ b/src/lib/i18n/locales/en.ts @@ -836,6 +836,19 @@ const en = { }, revisions: { record: 'Track changes', + display: 'Display for review', + displayAll: 'All markup', + displayAllHint: 'Every change and comment', + displaySimple: 'Simple markup', + displaySimpleHint: 'Final text with change bars', + displayNone: 'No markup', + displayNoneHint: 'The text as it would read', + displayOriginal: 'Original', + displayOriginalHint: 'The text before the changes', + placeBalloons: 'In balloons', + placePane: 'In the pane', + prev: 'Previous', + next: 'Next', printMarkup: 'Print markup', printMarkupHint: 'Print the margin bar and the comment list', recordHint: 'Record every edit as a revision', @@ -846,7 +859,8 @@ const en = { acceptAll: 'Accept all', rejectAll: 'Reject all', pane: 'Revisions', - showPane: 'Show the list of recorded changes', + showPane: 'Show revisions', + showPaneHint: 'Show the list of recorded changes', empty: 'No recorded changes.', unknownAuthor: 'Unknown author', inserted: 'Inserted', @@ -919,6 +933,7 @@ const en = { citations: 'Citations & Bibliography', index: 'Index', revisions: 'Tracking', + markup: 'Markup', proofing: 'Proofing', language: 'Language', show: 'Show', diff --git a/src/styles/global.css b/src/styles/global.css index 44b4bbe..110f2ad 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -393,6 +393,13 @@ .ribbon-menu .menu-key, .ribbon-menu .menu-sub { margin-left: auto; padding-left: 16px; color: var(--w-text-dim); } .ribbon-menu .menu-key { font-size: 12px; } + +/* A checkable row keeps the slot whether it is checked or not, so the labels line up. */ +.ribbon-menu .menu-check { + flex: none; + width: 14px; + color: var(--w-accent); +} .ribbon-menu .menu-sub { font-size: 11px; } /* Section label above a run of rows, and the sticky action row below the list. */