diff --git a/DocDiff.html b/DocDiff.html index 655eced..49349c1 100644 --- a/DocDiff.html +++ b/DocDiff.html @@ -99,7 +99,7 @@ "sameAs": [ "https://github.com/tschnorpfeil/docdiff" ], - "softwareVersion": "1.6.0" + "softwareVersion": "1.6.1" }, { "@type": "SoftwareSourceCode", @@ -219,7 +219,12 @@ --text-ui: 0.875rem; /* 14px – controls, list text */ --text-doc: 0.96875rem; /* 15.5px – document text, side by side */ --text-doc-lg: 1rem; /* 16px – document text, one column */ - --text-title: 1.0625rem; /* 17px – headings in the document, panel titles */ + --text-xs: 0.75rem; /* 12px – badges and key caps only */ + --text-title: 1.0625rem; /* 17px – headings in the document, panel titles, lead text */ + --text-doc-h1: 1.1875rem; /* 19px – document title */ + --text-section: 1.375rem; /* 22px – start page section headings */ + --text-lead: 1.0625rem; /* 17px – start page lead text */ + --text-display: clamp(1.875rem, 1.2rem + 2vw, 2.75rem); /* 30–44px – start page headline */ --radius-sm: 4px; /* marks, small chips */ --radius-md: 8px; /* buttons, inputs, cards, menu items */ @@ -413,7 +418,7 @@ box-shadow: inset 0 0 0 1px var(--kante-300), 0 1px 0 var(--kante-300); color: hsl(var(--foreground)); font-family: var(--font-mono); - font-size: 12.5px; + font-size: var(--text-xs); font-weight: 600; line-height: 1.55; text-align: center; @@ -539,7 +544,7 @@ display: block; } .app-title { - font-size: 15px; + font-size: var(--text-doc); font-weight: 600; letter-spacing: -0.02em; white-space: nowrap; @@ -627,7 +632,7 @@ background: hsl(var(--ton-2)); font-family: var(--font-mono); font-variant-numeric: tabular-nums; - font-size: 13px; + font-size: var(--text-ui-sm); font-weight: 600; } @media (hover: hover) { @@ -741,7 +746,7 @@ } .menu-label { padding: 6px 10px 4px 10px; - font-size: 13px; + font-size: var(--text-ui-sm); font-weight: 600; color: hsl(var(--muted-foreground)); } @@ -807,7 +812,7 @@ margin-left: auto; padding-left: 16px; color: hsl(var(--muted-foreground)); - font-size: 13px; + font-size: var(--text-ui-sm); font-weight: 400; } .mi-kbd { @@ -952,7 +957,7 @@ background: transparent; color: hsl(var(--foreground)); font: inherit; - font-size: 15px; + font-size: var(--text-doc); } .search-panel input::-webkit-search-cancel-button { display: none; @@ -970,7 +975,7 @@ .search-count { flex-shrink: 0; padding: 0 4px; - font-size: 13px; + font-size: var(--text-ui-sm); color: hsl(var(--muted-foreground)); font-variant-numeric: tabular-nums; white-space: nowrap; @@ -1014,7 +1019,7 @@ .landing-title { max-width: 30ch; - font-size: clamp(21.9px, 14.2px + 1.7vw, 30.4px); + font-size: var(--text-display); font-weight: 600; line-height: 1.18; letter-spacing: -0.028em; @@ -1050,7 +1055,7 @@ } .lang-hint { margin-bottom: 20px; - font-size: 13px; + font-size: var(--text-ui-sm); } .lang-hint a { display: inline-flex; @@ -1068,41 +1073,14 @@ outline-offset: 4px; } .landing-sub { - margin-top: 12px; - max-width: 60ch; - font-size: 14.5px; + margin-top: 16px; + max-width: 70ch; + font-size: var(--text-lead); line-height: 1.55; color: hsl(var(--muted-foreground)); - text-wrap: pretty; + text-wrap: balance; } -.trust-list { - list-style: none; - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-top: 18px; -} -.trust-list li { - display: inline-flex; - align-items: center; - gap: 6px; - min-height: 28px; - padding: 3px 10px 3px 8px; - border-radius: 999px; - background: hsl(var(--card)); - box-shadow: inset 0 0 0 1px var(--kante-200); - font-size: 13px; - font-weight: 500; - line-height: 1.35; - color: hsl(var(--foreground)); -} -.trust-list .icon { - width: 14px; - height: 14px; - stroke-width: 2.4; - color: hsl(var(--success-ink)); -} /* Two large drop slots: the whole slot is one button and a drop target */ .drop-slots { @@ -1118,7 +1096,7 @@ .drop-slot-btn { width: 100%; height: 100%; - min-height: 156px; + min-height: 128px; padding: 20px 22px; display: flex; flex-direction: column; @@ -1164,19 +1142,22 @@ border-radius: 999px; background: hsl(var(--ton-2)); font-family: var(--font-mono); - font-size: 13px; + font-size: var(--text-ui-sm); font-weight: 700; color: hsl(var(--foreground)); } .drop-slot-title { min-width: 0; - font-size: 14.5px; + font-size: var(--text-doc-lg); font-weight: 600; letter-spacing: -0.01em; overflow-wrap: anywhere; } +.drop-slot-hint:empty { + display: none; +} .drop-slot-hint { - font-size: 13.5px; + font-size: var(--text-ui); line-height: 1.45; color: hsl(var(--muted-foreground)); } @@ -1199,7 +1180,7 @@ background: hsl(var(--card)); box-shadow: inset 0 0 0 1px var(--kante-300), var(--elev-1); color: hsl(var(--foreground)); - font-size: 14px; + font-size: var(--text-ui); font-weight: 600; } .drop-slot.is-next-step .drop-slot-cta-btn { @@ -1207,8 +1188,9 @@ box-shadow: none; color: hsl(var(--primary-foreground)); } -.drop-slot-drag { - font-size: 13.5px; +.landing-formats { + margin-top: 12px; + font-size: var(--text-ui); color: hsl(var(--muted-foreground)); } .drop-slot.is-loaded .drop-slot-btn { @@ -1246,22 +1228,41 @@ color: hsl(var(--foreground)); } -.drop-both-hint { - margin-top: 10px; - font-size: 13px; - color: hsl(var(--muted-foreground)); -} .landing-secondary { display: flex; flex-wrap: wrap; - gap: 10px; - margin-top: 18px; + align-items: center; + gap: 4px 10px; + margin-top: 20px; +} +/* Less frequent ways in: plain text buttons next to the example */ +.btn-quiet { + min-height: 38px; + padding: 0 8px; + border: 0; + border-radius: var(--radius-md); + background: transparent; + color: hsl(var(--muted-foreground)); + font: inherit; + font-size: var(--text-ui); + font-weight: 500; + cursor: pointer; +} +.btn-quiet:hover { + color: hsl(var(--foreground)); + text-decoration: underline; + text-underline-offset: 3px; +} +.landing-trust { + margin-top: 28px; + font-size: var(--text-ui); + color: hsl(var(--muted-foreground)); } .landing-secondary .btn { height: 38px; padding: 0 16px; - font-size: 14px; + font-size: var(--text-ui); } .landing-secondary .btn .icon { width: 15px; @@ -1275,7 +1276,7 @@ border-top: 1px solid hsl(var(--border)); } .landing-section h2 { - font-size: 17px; + font-size: var(--text-section); font-weight: 600; letter-spacing: -0.015em; color: hsl(var(--foreground)); @@ -1290,7 +1291,7 @@ .how-steps li { display: flex; gap: 12px; - font-size: 14.5px; + font-size: var(--text-doc-lg); line-height: 1.55; color: hsl(var(--muted-foreground)); } @@ -1311,7 +1312,7 @@ background: hsl(var(--ton-2)); color: hsl(var(--foreground)); font-family: var(--font-mono); - font-size: 13px; + font-size: var(--text-ui-sm); font-weight: 700; } .scope-grid { @@ -1327,7 +1328,7 @@ box-shadow: inset 0 0 0 1px var(--kante-200); } .scope-col h3 { - font-size: 15px; + font-size: var(--text-title); font-weight: 600; color: hsl(var(--foreground)); } @@ -1341,7 +1342,7 @@ .scope-col li { position: relative; padding-left: 24px; - font-size: 14.5px; + font-size: var(--text-doc-lg); line-height: 1.5; color: hsl(var(--muted-foreground)); } @@ -1377,7 +1378,7 @@ padding: 10px 2px; list-style: none; cursor: pointer; - font-size: 15px; + font-size: var(--text-doc-lg); font-weight: 600; color: hsl(var(--foreground)); } @@ -1403,7 +1404,7 @@ display: flex; flex-direction: column; gap: 10px; - font-size: 14.5px; + font-size: var(--text-doc-lg); line-height: 1.6; color: hsl(var(--muted-foreground)); } @@ -1419,7 +1420,7 @@ flex-wrap: wrap; align-items: center; gap: 8px; - font-size: 13px; + font-size: var(--text-ui-sm); color: hsl(var(--muted-foreground)); } .lp-link { @@ -1585,7 +1586,7 @@ background: hsl(var(--card)); border-radius: var(--radius-md); box-shadow: 0 0 0 1px hsl(var(--border)), var(--elev-3); - font-size: 15px; + font-size: var(--text-doc); font-weight: 600; } @@ -1688,7 +1689,7 @@ } .clause.kind-h1 { font-weight: 600; - font-size: 1.1875rem; + font-size: var(--text-doc-h1); line-height: 1.35; padding-top: var(--sp-3); padding-bottom: var(--sp-2); @@ -1817,7 +1818,7 @@ display: block; grid-column: 1 / -1; margin-bottom: 2px; - font-size: 13px; + font-size: var(--text-ui-sm); font-weight: 600; color: hsl(var(--muted-foreground)); } @@ -2028,6 +2029,50 @@ width: auto; } +/* Phones: the margin only holds the ✓ / ✗ mark of a reviewed paragraph; the buttons (with undo) + sit in a row just above the bottom bar, so the narrow text column keeps its width. */ +body.tier-phone { + --rv-gutter: 30px; +} +body.tier-phone .review-status { + right: 0; + width: 28px; +} +body.tier-phone .rs-mark { + width: 28px; + height: 28px; +} +body.tier-phone .ra-undo { + display: none; +} +.ra-label { + display: none; +} +body.tier-phone .bottom-bar .ra-btn:not(.ra-edit) { + width: auto; + gap: 6px; + padding: 0 14px 0 12px; + color: hsl(var(--foreground)); + font-size: var(--text-ui); + font-weight: 600; +} +body.tier-phone .bottom-bar .ra-btn:not(.ra-edit) .ra-label { + display: inline; +} +body.tier-phone .bottom-bar .ra-accept .icon { color: hsl(var(--success-ink)); } +body.tier-phone .bottom-bar .ra-reject .icon { color: hsl(var(--destructive-ink)); } +body.tier-phone .bottom-bar .review-bar { + top: auto; + right: max(8px, env(safe-area-inset-right)); + bottom: calc(100% + 8px); + flex-direction: row; + gap: 4px; + padding: 4px; + border-radius: var(--radius-lg); + background: hsl(var(--card)); + box-shadow: 0 0 0 1px hsl(var(--border)), 0 8px 24px -10px hsl(var(--foreground) / 0.3); +} + /* Inline editor */ .clause.is-editing { display: block !important; @@ -2059,7 +2104,7 @@ align-items: center; justify-content: space-between; gap: 8px; - font-size: 13px; + font-size: var(--text-ui-sm); font-weight: 400; color: hsl(var(--muted-foreground)); } @@ -2280,7 +2325,7 @@ padding: 0 6px; border-radius: 999px; background: hsl(var(--ton-2)); - font-size: 12px; + font-size: var(--text-xs); font-weight: 500; line-height: 18px; color: hsl(var(--muted-foreground)); @@ -2330,12 +2375,12 @@ text-align: center; } .empty-title { - font-size: 15px; + font-size: var(--text-doc); font-weight: 600; } .empty-desc { margin-top: 6px; - font-size: 14px; + font-size: var(--text-ui); color: hsl(var(--muted-foreground)); } @@ -2430,7 +2475,7 @@ background: hsl(var(--foreground)); color: hsl(var(--background)); box-shadow: var(--elev-3); - font-size: 13px; + font-size: var(--text-ui-sm); font-weight: 500; line-height: 1.4; pointer-events: none; @@ -2476,7 +2521,7 @@ justify-content: space-between; } .dialog-title { - font-size: 17px; + font-size: var(--text-title); font-weight: 600; letter-spacing: -0.01em; } @@ -2496,7 +2541,7 @@ flex-direction: column; gap: 16px; overflow-y: auto; - font-size: 15px; + font-size: var(--text-doc); line-height: 1.6; } .dialog-col { @@ -2516,7 +2561,7 @@ border-radius: var(--radius-md); box-shadow: inset 0 0 0 1px var(--control-edge); font-family: var(--font-sans); - font-size: 15px; + font-size: var(--text-doc); line-height: 1.55; resize: vertical; } @@ -2559,18 +2604,18 @@ margin-top: 18px; } .help-sec h3 { - font-size: 15px; + font-size: var(--text-doc); font-weight: 600; margin-bottom: 4px; } .help-sec p { - font-size: 15px; + font-size: var(--text-doc); line-height: 1.6; } .help-keys { width: 100%; border-collapse: collapse; - font-size: 15px; + font-size: var(--text-doc); } .help-keys th, .help-keys td { @@ -2602,7 +2647,7 @@ align-items: center; gap: 10px; min-height: 40px; - font-size: 15px; + font-size: var(--text-doc); cursor: pointer; } .help-setting input { @@ -2677,7 +2722,7 @@ background: hsl(var(--destructive-ton-1)); color: hsl(var(--destructive-ink)); box-shadow: inset 0 0 0 1px hsl(var(--destructive) / 0.35), var(--elev-3); - font-size: 14px; + font-size: var(--text-ui); line-height: 1.5; } .import-notice[hidden] { display: none; } @@ -2806,12 +2851,12 @@ } body:not(.is-comparing) .app-title { display: inline; - font-size: 16px; + font-size: var(--text-doc-lg); } .tb-files { padding: 0 6px 0 8px; box-shadow: none; - font-size: 13.5px; + font-size: var(--text-ui); } .tb-icon-btn { width: 44px; @@ -2828,7 +2873,8 @@ display: none; } .doc-pane-unified { - padding: 10px 10px calc(84px + env(safe-area-inset-bottom)); + /* bottom bar + the review buttons above it */ + padding: 10px 10px calc(144px + env(safe-area-inset-bottom)); } .doc-page-header { padding: var(--sp-5) 0 var(--sp-3) 14px; @@ -2868,7 +2914,7 @@ background: transparent; color: hsl(var(--foreground)); font: inherit; - font-size: 15px; + font-size: var(--text-doc); font-weight: 600; display: inline-flex; align-items: center; @@ -2962,7 +3008,7 @@ height: 52px; } .search-panel input { - font-size: 16px; /* no iOS zoom on focus */ + font-size: var(--text-doc-lg); /* no iOS zoom on focus */ } .search-panel .tb-icon-btn.sm { width: 40px; @@ -2986,7 +3032,7 @@ } .dialog-textarea { height: 150px; - font-size: 16px; + font-size: var(--text-doc-lg); } .dialog-footer { padding: 10px 16px calc(10px + env(safe-area-inset-bottom)); @@ -3432,12 +3478,6 @@ .landing-inner { padding-top: 24px; } - .landing-sub { - font-size: 15.5px; - } - .trust-list li { - font-size: 13px; - } .drop-slots { grid-template-columns: 1fr; gap: 12px; @@ -3454,9 +3494,12 @@ height: 44px; } .landing-secondary .btn { - flex: 1 1 140px; + flex: 1 1 100%; height: 44px; } + .btn-quiet { + min-height: 44px; + } .landing-section { margin-top: 40px; padding-top: 24px; @@ -3730,12 +3773,7 @@

Doc Diff

Dokumente einfach vergleichen.

-

Zwei Fassungen als PDF, Word oder Text: Jede Änderung wird Wort für Wort markiert. Ihre Dateien verlassen dabei nie Ihr Gerät.

- +

Jede Änderung Wort für Wort markiert. Ihre Dateien verlassen nie Ihr Gerät.

@@ -3744,10 +3782,9 @@

1 Alte Fassung - PDF (mit Text), Word (.docx), Text + Datei wählen - oder hierher ziehen @@ -3758,31 +3795,25 @@

2 Neue Fassung - PDF (mit Text), Word (.docx), Text + Datei wählen - oder hierher ziehen

-

Sie können auch beide Dateien auf einmal hierher ziehen – die erste gilt als alte Fassung.

+

PDF, Word (.docx) oder Text · Dateien auch hierher ziehen

- - + +
+

Kostenlos und Open Source (MIT) · Keine Werbung, kein Tracking, keine Cookies

@@ -4415,18 +4446,15 @@

Ihre Daten

pasteText: 'Text einfügen', loadSample: 'Beispiel ansehen', heroTitle: 'Dokumente einfach vergleichen.', - heroSub: 'Zwei Fassungen als PDF, Word oder Text: Jede Änderung wird Wort für Wort markiert. Ihre Dateien verlassen dabei nie Ihr Gerät.', - trustFree: 'Kostenlos & Open Source (MIT)', - trustLocal: 'Keine Uploads – alles läuft lokal im Browser', - trustNoTracking: 'Keine Werbung, kein Tracking, keine Cookies', + heroSub: 'Jede Änderung Wort für Wort markiert. Ihre Dateien verlassen nie Ihr Gerät.', + trustLine: 'Kostenlos und Open Source (MIT) · Keine Werbung, kein Tracking, keine Cookies', slotLeftTitle: 'Alte Fassung', slotRightTitle: 'Neue Fassung', - slotHint: 'PDF (mit Text), Word (.docx), Text', + slotHint: 'PDF, Word (.docx) oder Text', slotChoose: 'Datei wählen', - slotDrag: 'oder hierher ziehen', + slotDrag: ' · Dateien auch hierher ziehen', slotClearLeft: 'Alte Fassung entfernen', slotClearRight: 'Neue Fassung entfernen', - dropBothHint: 'Sie können auch beide Dateien auf einmal hierher ziehen – die erste gilt als alte Fassung.', howTitle: 'So funktioniert’s', how1Title: 'Zwei Fassungen wählen', how1Text: 'Als PDF, Word-Datei oder eingefügten Text. Die Dateien werden nur in diesem Browser-Tab gelesen.', @@ -4815,18 +4843,15 @@

Your data

pasteText: 'Paste text', loadSample: 'View example', heroTitle: 'Compare documents easily.', - heroSub: 'Two versions as PDF, Word or text: every change is marked word by word. Your files never leave your device.', - trustFree: 'Free & open source (MIT)', - trustLocal: 'No uploads – everything runs locally in your browser', - trustNoTracking: 'No ads, no tracking, no cookies', + heroSub: 'Every change marked word by word. Your files never leave your device.', + trustLine: 'Free and open source (MIT) · No ads, no tracking, no cookies', slotLeftTitle: 'Old version', slotRightTitle: 'New version', - slotHint: 'PDF (with text), Word (.docx), text', + slotHint: 'PDF, Word (.docx) or text', slotChoose: 'Choose file', - slotDrag: 'or drop it here', + slotDrag: ' · or drop the files here', slotClearLeft: 'Remove old version', slotClearRight: 'Remove new version', - dropBothHint: 'You can also drop both files here at once – the first one becomes the old version.', howTitle: 'How it works', how1Title: 'Choose two versions', how1Text: 'As PDF, Word file or pasted text. The files are only read inside this browser tab.', @@ -7423,8 +7448,8 @@

Your data

} else { titleEl.setAttribute('data-i18n', titleKey); titleEl.textContent = t(titleKey); - hintEl.setAttribute('data-i18n', 'slotHint'); - hintEl.textContent = t('slotHint'); + // The formats are named once, under both slots. + hintEl.textContent = ''; ctaEl.setAttribute('data-i18n', 'slotChoose'); ctaEl.textContent = t('slotChoose'); } @@ -9473,6 +9498,8 @@

Your data

* One place only: a compact column of three icon buttons (accept, reject, edit) in the right * margin of the current paragraph of the new version (or of the stacked view). Every * paragraph there keeps that margin free, so selecting a paragraph never moves any text. + * On phones the margin would cost too much of the narrow text column: the buttons sit in a + * row just above the bottom bar instead, within reach of the thumb. */ // Created on first use (the module is also loaded without a DOM by the export tests). @@ -9487,6 +9514,13 @@

Your data

reviewBar.addEventListener('dblclick', e => e.stopPropagation()); // Roving focus inside the group: one tab stop, arrow keys move between the buttons. reviewBar.addEventListener('keydown', e => handleRovingKeys(e, reviewBar)); + // In the panes, clicks are handled by the pane; above the bottom bar, here. + reviewBar.addEventListener('click', e => { + const btn = e.target.closest('[data-review-action]'); + if (!btn || !reviewBar.closest('.bottom-bar')) return; + e.stopPropagation(); + runReviewAction(btn.getAttribute('data-review-action'), false); + }); return reviewBar; } @@ -9498,11 +9532,15 @@

Your data

const ICON_X = ''; const ICON_EDIT = ''; -// Icon buttons: accept / reject for an open change, edit for any paragraph of the new version. -function reviewActionsHtml(item) { +// Icon buttons: accept / reject for an open change, edit for any paragraph of the new version; +// with `withUndo` (phones, where the margin has no undo button) also undo for a reviewed one. +function reviewActionsHtml(item, withUndo = false) { const btn = (action, icon, label, title, key, cls) => - ``; + ``; const edit = btn('edit', ICON_EDIT, t('editBtn'), t('editTitle'), 'E', 'ra-edit'); + if (withUndo && item && item.status !== 'open') { + return rovingFirst(btn('reopen', ICON_UNDO, t('reopenBtn'), t('reopenTitle'), '', 'ra-reopen') + edit); + } if (item && item.status === 'open') { return rovingFirst( btn('accept', ICON_CHECK, t('acceptBtn'), t('acceptTitle'), 'A', 'ra-accept') + @@ -9550,15 +9588,18 @@

Your data

} const bar = getReviewBar(); const item = getActiveItem(); + const phone = document.body.classList.contains('tier-phone'); + const barHost = phone ? document.getElementById('mobile-bottom-bar') : host; const hadFocus = bar.contains(document.activeElement); const focusedAction = hadFocus ? document.activeElement.getAttribute('data-review-action') : null; - bar.innerHTML = reviewActionsHtml(item); + bar.innerHTML = reviewActionsHtml(item, phone); + bar.setAttribute('aria-orientation', phone ? 'horizontal' : 'vertical'); bar.setAttribute( 'aria-label', item ? formatMsg('reviewBarLabel', { i: item.index, n: state.listItems.length }) : t('paraActionsLabel') ); const buttons = bar.querySelectorAll('button'); - if (bar.parentElement !== host) host.appendChild(bar); + if (bar.parentElement !== barHost) barHost.appendChild(bar); const request = reviewFocusRequest; reviewFocusRequest = null; let target = null; @@ -13942,7 +13983,8 @@

${escapeHtml(t('exportRedlineHeading'))}

// Phones show one column only. if (tier === 'phone' && state.viewMode !== 'inline') setViewMode('inline'); else if (!initial && prev === 'phone' && !state.viewModeChosen) setViewMode(initialInline && tier !== 'desktop' ? 'inline' : 'split'); - applySidebarState(); + applySidebarState(); // The review buttons sit in the paragraph margin, or above the bottom bar on phones. + if (!initial && typeof placeReviewBar === 'function') placeReviewBar(); } function applySidebarState() { @@ -14492,7 +14534,7 @@

${escapeHtml(t('exportRedlineHeading'))}

* entry; version 1 files (no review status) still open. */ -const DOCDIFF_APP_VERSION = '1.6.0'; +const DOCDIFF_APP_VERSION = '1.6.1'; const SESSION_FORMAT_ID = 'docdiff-session'; const SESSION_FORMAT_VERSION = 2; const SESSION_FILE_EXT = '.docdiff'; diff --git a/docs/releases/v1.6.1.md b/docs/releases/v1.6.1.md new file mode 100644 index 0000000..895233a --- /dev/null +++ b/docs/releases/v1.6.1.md @@ -0,0 +1,19 @@ +# v1.6.1 — Identical columns, calmer start page, more room on phones + +## Comparison + +- **Unchanged paragraphs wrap identically on both sides.** The new version's text column was up to 42px narrower than the old one (review margin, scrollbar and divider only on the right), so the same text broke into different lines. Both columns now have exactly the same text width at every window size. +- **More room for text on phones.** The accept, reject and edit buttons sit in a labelled row just above the bottom bar, within reach of the thumb, instead of a button column next to every paragraph. The text column is about 8% wider, with fewer hyphenations; selecting a paragraph still never moves any text. + +## Start page + +- **Clearer hierarchy:** a larger headline and lead text, body text at reading size. +- **Less on the screen:** one short line under the headline, the supported formats named once under the two file slots, "View example" as the only button next to them, "Paste text" and "Open saved work" as quiet text buttons, and the trust points in one line instead of three badges. + +## Type + +- One small type scale (13, 14, 15.5, 16, 17, 19px, plus the start page headings) instead of thirteen sizes. Header and footer lines in the document are no longer set smaller than the rest of the text. + +## Offline version + +Download `DocDiff.html` below and open it from disk. It works without an internet connection. diff --git a/site/en/index.html b/site/en/index.html index 64dec7e..ac9c721 100644 --- a/site/en/index.html +++ b/site/en/index.html @@ -102,7 +102,7 @@ "sameAs": [ "https://github.com/tschnorpfeil/docdiff" ], - "softwareVersion": "1.6.0" + "softwareVersion": "1.6.1" }, { "@type": "SoftwareSourceCode", @@ -222,7 +222,12 @@ --text-ui: 0.875rem; /* 14px – controls, list text */ --text-doc: 0.96875rem; /* 15.5px – document text, side by side */ --text-doc-lg: 1rem; /* 16px – document text, one column */ - --text-title: 1.0625rem; /* 17px – headings in the document, panel titles */ + --text-xs: 0.75rem; /* 12px – badges and key caps only */ + --text-title: 1.0625rem; /* 17px – headings in the document, panel titles, lead text */ + --text-doc-h1: 1.1875rem; /* 19px – document title */ + --text-section: 1.375rem; /* 22px – start page section headings */ + --text-lead: 1.0625rem; /* 17px – start page lead text */ + --text-display: clamp(1.875rem, 1.2rem + 2vw, 2.75rem); /* 30–44px – start page headline */ --radius-sm: 4px; /* marks, small chips */ --radius-md: 8px; /* buttons, inputs, cards, menu items */ @@ -416,7 +421,7 @@ box-shadow: inset 0 0 0 1px var(--kante-300), 0 1px 0 var(--kante-300); color: hsl(var(--foreground)); font-family: var(--font-mono); - font-size: 12.5px; + font-size: var(--text-xs); font-weight: 600; line-height: 1.55; text-align: center; @@ -542,7 +547,7 @@ display: block; } .app-title { - font-size: 15px; + font-size: var(--text-doc); font-weight: 600; letter-spacing: -0.02em; white-space: nowrap; @@ -630,7 +635,7 @@ background: hsl(var(--ton-2)); font-family: var(--font-mono); font-variant-numeric: tabular-nums; - font-size: 13px; + font-size: var(--text-ui-sm); font-weight: 600; } @media (hover: hover) { @@ -744,7 +749,7 @@ } .menu-label { padding: 6px 10px 4px 10px; - font-size: 13px; + font-size: var(--text-ui-sm); font-weight: 600; color: hsl(var(--muted-foreground)); } @@ -810,7 +815,7 @@ margin-left: auto; padding-left: 16px; color: hsl(var(--muted-foreground)); - font-size: 13px; + font-size: var(--text-ui-sm); font-weight: 400; } .mi-kbd { @@ -955,7 +960,7 @@ background: transparent; color: hsl(var(--foreground)); font: inherit; - font-size: 15px; + font-size: var(--text-doc); } .search-panel input::-webkit-search-cancel-button { display: none; @@ -973,7 +978,7 @@ .search-count { flex-shrink: 0; padding: 0 4px; - font-size: 13px; + font-size: var(--text-ui-sm); color: hsl(var(--muted-foreground)); font-variant-numeric: tabular-nums; white-space: nowrap; @@ -1017,7 +1022,7 @@ .landing-title { max-width: 30ch; - font-size: clamp(21.9px, 14.2px + 1.7vw, 30.4px); + font-size: var(--text-display); font-weight: 600; line-height: 1.18; letter-spacing: -0.028em; @@ -1053,7 +1058,7 @@ } .lang-hint { margin-bottom: 20px; - font-size: 13px; + font-size: var(--text-ui-sm); } .lang-hint a { display: inline-flex; @@ -1071,41 +1076,14 @@ outline-offset: 4px; } .landing-sub { - margin-top: 12px; - max-width: 60ch; - font-size: 14.5px; + margin-top: 16px; + max-width: 70ch; + font-size: var(--text-lead); line-height: 1.55; color: hsl(var(--muted-foreground)); - text-wrap: pretty; + text-wrap: balance; } -.trust-list { - list-style: none; - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-top: 18px; -} -.trust-list li { - display: inline-flex; - align-items: center; - gap: 6px; - min-height: 28px; - padding: 3px 10px 3px 8px; - border-radius: 999px; - background: hsl(var(--card)); - box-shadow: inset 0 0 0 1px var(--kante-200); - font-size: 13px; - font-weight: 500; - line-height: 1.35; - color: hsl(var(--foreground)); -} -.trust-list .icon { - width: 14px; - height: 14px; - stroke-width: 2.4; - color: hsl(var(--success-ink)); -} /* Two large drop slots: the whole slot is one button and a drop target */ .drop-slots { @@ -1121,7 +1099,7 @@ .drop-slot-btn { width: 100%; height: 100%; - min-height: 156px; + min-height: 128px; padding: 20px 22px; display: flex; flex-direction: column; @@ -1167,19 +1145,22 @@ border-radius: 999px; background: hsl(var(--ton-2)); font-family: var(--font-mono); - font-size: 13px; + font-size: var(--text-ui-sm); font-weight: 700; color: hsl(var(--foreground)); } .drop-slot-title { min-width: 0; - font-size: 14.5px; + font-size: var(--text-doc-lg); font-weight: 600; letter-spacing: -0.01em; overflow-wrap: anywhere; } +.drop-slot-hint:empty { + display: none; +} .drop-slot-hint { - font-size: 13.5px; + font-size: var(--text-ui); line-height: 1.45; color: hsl(var(--muted-foreground)); } @@ -1202,7 +1183,7 @@ background: hsl(var(--card)); box-shadow: inset 0 0 0 1px var(--kante-300), var(--elev-1); color: hsl(var(--foreground)); - font-size: 14px; + font-size: var(--text-ui); font-weight: 600; } .drop-slot.is-next-step .drop-slot-cta-btn { @@ -1210,8 +1191,9 @@ box-shadow: none; color: hsl(var(--primary-foreground)); } -.drop-slot-drag { - font-size: 13.5px; +.landing-formats { + margin-top: 12px; + font-size: var(--text-ui); color: hsl(var(--muted-foreground)); } .drop-slot.is-loaded .drop-slot-btn { @@ -1249,22 +1231,41 @@ color: hsl(var(--foreground)); } -.drop-both-hint { - margin-top: 10px; - font-size: 13px; - color: hsl(var(--muted-foreground)); -} .landing-secondary { display: flex; flex-wrap: wrap; - gap: 10px; - margin-top: 18px; + align-items: center; + gap: 4px 10px; + margin-top: 20px; +} +/* Less frequent ways in: plain text buttons next to the example */ +.btn-quiet { + min-height: 38px; + padding: 0 8px; + border: 0; + border-radius: var(--radius-md); + background: transparent; + color: hsl(var(--muted-foreground)); + font: inherit; + font-size: var(--text-ui); + font-weight: 500; + cursor: pointer; +} +.btn-quiet:hover { + color: hsl(var(--foreground)); + text-decoration: underline; + text-underline-offset: 3px; +} +.landing-trust { + margin-top: 28px; + font-size: var(--text-ui); + color: hsl(var(--muted-foreground)); } .landing-secondary .btn { height: 38px; padding: 0 16px; - font-size: 14px; + font-size: var(--text-ui); } .landing-secondary .btn .icon { width: 15px; @@ -1278,7 +1279,7 @@ border-top: 1px solid hsl(var(--border)); } .landing-section h2 { - font-size: 17px; + font-size: var(--text-section); font-weight: 600; letter-spacing: -0.015em; color: hsl(var(--foreground)); @@ -1293,7 +1294,7 @@ .how-steps li { display: flex; gap: 12px; - font-size: 14.5px; + font-size: var(--text-doc-lg); line-height: 1.55; color: hsl(var(--muted-foreground)); } @@ -1314,7 +1315,7 @@ background: hsl(var(--ton-2)); color: hsl(var(--foreground)); font-family: var(--font-mono); - font-size: 13px; + font-size: var(--text-ui-sm); font-weight: 700; } .scope-grid { @@ -1330,7 +1331,7 @@ box-shadow: inset 0 0 0 1px var(--kante-200); } .scope-col h3 { - font-size: 15px; + font-size: var(--text-title); font-weight: 600; color: hsl(var(--foreground)); } @@ -1344,7 +1345,7 @@ .scope-col li { position: relative; padding-left: 24px; - font-size: 14.5px; + font-size: var(--text-doc-lg); line-height: 1.5; color: hsl(var(--muted-foreground)); } @@ -1380,7 +1381,7 @@ padding: 10px 2px; list-style: none; cursor: pointer; - font-size: 15px; + font-size: var(--text-doc-lg); font-weight: 600; color: hsl(var(--foreground)); } @@ -1406,7 +1407,7 @@ display: flex; flex-direction: column; gap: 10px; - font-size: 14.5px; + font-size: var(--text-doc-lg); line-height: 1.6; color: hsl(var(--muted-foreground)); } @@ -1422,7 +1423,7 @@ flex-wrap: wrap; align-items: center; gap: 8px; - font-size: 13px; + font-size: var(--text-ui-sm); color: hsl(var(--muted-foreground)); } .lp-link { @@ -1588,7 +1589,7 @@ background: hsl(var(--card)); border-radius: var(--radius-md); box-shadow: 0 0 0 1px hsl(var(--border)), var(--elev-3); - font-size: 15px; + font-size: var(--text-doc); font-weight: 600; } @@ -1691,7 +1692,7 @@ } .clause.kind-h1 { font-weight: 600; - font-size: 1.1875rem; + font-size: var(--text-doc-h1); line-height: 1.35; padding-top: var(--sp-3); padding-bottom: var(--sp-2); @@ -1820,7 +1821,7 @@ display: block; grid-column: 1 / -1; margin-bottom: 2px; - font-size: 13px; + font-size: var(--text-ui-sm); font-weight: 600; color: hsl(var(--muted-foreground)); } @@ -2031,6 +2032,50 @@ width: auto; } +/* Phones: the margin only holds the ✓ / ✗ mark of a reviewed paragraph; the buttons (with undo) + sit in a row just above the bottom bar, so the narrow text column keeps its width. */ +body.tier-phone { + --rv-gutter: 30px; +} +body.tier-phone .review-status { + right: 0; + width: 28px; +} +body.tier-phone .rs-mark { + width: 28px; + height: 28px; +} +body.tier-phone .ra-undo { + display: none; +} +.ra-label { + display: none; +} +body.tier-phone .bottom-bar .ra-btn:not(.ra-edit) { + width: auto; + gap: 6px; + padding: 0 14px 0 12px; + color: hsl(var(--foreground)); + font-size: var(--text-ui); + font-weight: 600; +} +body.tier-phone .bottom-bar .ra-btn:not(.ra-edit) .ra-label { + display: inline; +} +body.tier-phone .bottom-bar .ra-accept .icon { color: hsl(var(--success-ink)); } +body.tier-phone .bottom-bar .ra-reject .icon { color: hsl(var(--destructive-ink)); } +body.tier-phone .bottom-bar .review-bar { + top: auto; + right: max(8px, env(safe-area-inset-right)); + bottom: calc(100% + 8px); + flex-direction: row; + gap: 4px; + padding: 4px; + border-radius: var(--radius-lg); + background: hsl(var(--card)); + box-shadow: 0 0 0 1px hsl(var(--border)), 0 8px 24px -10px hsl(var(--foreground) / 0.3); +} + /* Inline editor */ .clause.is-editing { display: block !important; @@ -2062,7 +2107,7 @@ align-items: center; justify-content: space-between; gap: 8px; - font-size: 13px; + font-size: var(--text-ui-sm); font-weight: 400; color: hsl(var(--muted-foreground)); } @@ -2283,7 +2328,7 @@ padding: 0 6px; border-radius: 999px; background: hsl(var(--ton-2)); - font-size: 12px; + font-size: var(--text-xs); font-weight: 500; line-height: 18px; color: hsl(var(--muted-foreground)); @@ -2333,12 +2378,12 @@ text-align: center; } .empty-title { - font-size: 15px; + font-size: var(--text-doc); font-weight: 600; } .empty-desc { margin-top: 6px; - font-size: 14px; + font-size: var(--text-ui); color: hsl(var(--muted-foreground)); } @@ -2433,7 +2478,7 @@ background: hsl(var(--foreground)); color: hsl(var(--background)); box-shadow: var(--elev-3); - font-size: 13px; + font-size: var(--text-ui-sm); font-weight: 500; line-height: 1.4; pointer-events: none; @@ -2479,7 +2524,7 @@ justify-content: space-between; } .dialog-title { - font-size: 17px; + font-size: var(--text-title); font-weight: 600; letter-spacing: -0.01em; } @@ -2499,7 +2544,7 @@ flex-direction: column; gap: 16px; overflow-y: auto; - font-size: 15px; + font-size: var(--text-doc); line-height: 1.6; } .dialog-col { @@ -2519,7 +2564,7 @@ border-radius: var(--radius-md); box-shadow: inset 0 0 0 1px var(--control-edge); font-family: var(--font-sans); - font-size: 15px; + font-size: var(--text-doc); line-height: 1.55; resize: vertical; } @@ -2562,18 +2607,18 @@ margin-top: 18px; } .help-sec h3 { - font-size: 15px; + font-size: var(--text-doc); font-weight: 600; margin-bottom: 4px; } .help-sec p { - font-size: 15px; + font-size: var(--text-doc); line-height: 1.6; } .help-keys { width: 100%; border-collapse: collapse; - font-size: 15px; + font-size: var(--text-doc); } .help-keys th, .help-keys td { @@ -2605,7 +2650,7 @@ align-items: center; gap: 10px; min-height: 40px; - font-size: 15px; + font-size: var(--text-doc); cursor: pointer; } .help-setting input { @@ -2680,7 +2725,7 @@ background: hsl(var(--destructive-ton-1)); color: hsl(var(--destructive-ink)); box-shadow: inset 0 0 0 1px hsl(var(--destructive) / 0.35), var(--elev-3); - font-size: 14px; + font-size: var(--text-ui); line-height: 1.5; } .import-notice[hidden] { display: none; } @@ -2809,12 +2854,12 @@ } body:not(.is-comparing) .app-title { display: inline; - font-size: 16px; + font-size: var(--text-doc-lg); } .tb-files { padding: 0 6px 0 8px; box-shadow: none; - font-size: 13.5px; + font-size: var(--text-ui); } .tb-icon-btn { width: 44px; @@ -2831,7 +2876,8 @@ display: none; } .doc-pane-unified { - padding: 10px 10px calc(84px + env(safe-area-inset-bottom)); + /* bottom bar + the review buttons above it */ + padding: 10px 10px calc(144px + env(safe-area-inset-bottom)); } .doc-page-header { padding: var(--sp-5) 0 var(--sp-3) 14px; @@ -2871,7 +2917,7 @@ background: transparent; color: hsl(var(--foreground)); font: inherit; - font-size: 15px; + font-size: var(--text-doc); font-weight: 600; display: inline-flex; align-items: center; @@ -2965,7 +3011,7 @@ height: 52px; } .search-panel input { - font-size: 16px; /* no iOS zoom on focus */ + font-size: var(--text-doc-lg); /* no iOS zoom on focus */ } .search-panel .tb-icon-btn.sm { width: 40px; @@ -2989,7 +3035,7 @@ } .dialog-textarea { height: 150px; - font-size: 16px; + font-size: var(--text-doc-lg); } .dialog-footer { padding: 10px 16px calc(10px + env(safe-area-inset-bottom)); @@ -3435,12 +3481,6 @@ .landing-inner { padding-top: 24px; } - .landing-sub { - font-size: 15.5px; - } - .trust-list li { - font-size: 13px; - } .drop-slots { grid-template-columns: 1fr; gap: 12px; @@ -3457,9 +3497,12 @@ height: 44px; } .landing-secondary .btn { - flex: 1 1 140px; + flex: 1 1 100%; height: 44px; } + .btn-quiet { + min-height: 44px; + } .landing-section { margin-top: 40px; padding-top: 24px; @@ -3733,12 +3776,7 @@

Doc Diff

Compare documents easily.

-

Two versions as PDF, Word or text: every change is marked word by word. Your files never leave your device.

- +

Every change marked word by word. Your files never leave your device.

@@ -3747,10 +3785,9 @@

1 Old version - PDF (with text), Word (.docx), text + Choose file - or drop it here @@ -3761,31 +3798,25 @@

2 New version - PDF (with text), Word (.docx), text + Choose file - or drop it here

-

You can also drop both files here at once – the first one becomes the old version.

+

PDF, Word (.docx) or text · or drop the files here

- - + +
+

Free and open source (MIT) · No ads, no tracking, no cookies

@@ -4418,18 +4449,15 @@

Ihre Daten

pasteText: 'Text einfügen', loadSample: 'Beispiel ansehen', heroTitle: 'Dokumente einfach vergleichen.', - heroSub: 'Zwei Fassungen als PDF, Word oder Text: Jede Änderung wird Wort für Wort markiert. Ihre Dateien verlassen dabei nie Ihr Gerät.', - trustFree: 'Kostenlos & Open Source (MIT)', - trustLocal: 'Keine Uploads – alles läuft lokal im Browser', - trustNoTracking: 'Keine Werbung, kein Tracking, keine Cookies', + heroSub: 'Jede Änderung Wort für Wort markiert. Ihre Dateien verlassen nie Ihr Gerät.', + trustLine: 'Kostenlos und Open Source (MIT) · Keine Werbung, kein Tracking, keine Cookies', slotLeftTitle: 'Alte Fassung', slotRightTitle: 'Neue Fassung', - slotHint: 'PDF (mit Text), Word (.docx), Text', + slotHint: 'PDF, Word (.docx) oder Text', slotChoose: 'Datei wählen', - slotDrag: 'oder hierher ziehen', + slotDrag: ' · Dateien auch hierher ziehen', slotClearLeft: 'Alte Fassung entfernen', slotClearRight: 'Neue Fassung entfernen', - dropBothHint: 'Sie können auch beide Dateien auf einmal hierher ziehen – die erste gilt als alte Fassung.', howTitle: 'So funktioniert’s', how1Title: 'Zwei Fassungen wählen', how1Text: 'Als PDF, Word-Datei oder eingefügten Text. Die Dateien werden nur in diesem Browser-Tab gelesen.', @@ -4818,18 +4846,15 @@

Your data

pasteText: 'Paste text', loadSample: 'View example', heroTitle: 'Compare documents easily.', - heroSub: 'Two versions as PDF, Word or text: every change is marked word by word. Your files never leave your device.', - trustFree: 'Free & open source (MIT)', - trustLocal: 'No uploads – everything runs locally in your browser', - trustNoTracking: 'No ads, no tracking, no cookies', + heroSub: 'Every change marked word by word. Your files never leave your device.', + trustLine: 'Free and open source (MIT) · No ads, no tracking, no cookies', slotLeftTitle: 'Old version', slotRightTitle: 'New version', - slotHint: 'PDF (with text), Word (.docx), text', + slotHint: 'PDF, Word (.docx) or text', slotChoose: 'Choose file', - slotDrag: 'or drop it here', + slotDrag: ' · or drop the files here', slotClearLeft: 'Remove old version', slotClearRight: 'Remove new version', - dropBothHint: 'You can also drop both files here at once – the first one becomes the old version.', howTitle: 'How it works', how1Title: 'Choose two versions', how1Text: 'As PDF, Word file or pasted text. The files are only read inside this browser tab.', @@ -7426,8 +7451,8 @@

Your data

} else { titleEl.setAttribute('data-i18n', titleKey); titleEl.textContent = t(titleKey); - hintEl.setAttribute('data-i18n', 'slotHint'); - hintEl.textContent = t('slotHint'); + // The formats are named once, under both slots. + hintEl.textContent = ''; ctaEl.setAttribute('data-i18n', 'slotChoose'); ctaEl.textContent = t('slotChoose'); } @@ -9476,6 +9501,8 @@

Your data

* One place only: a compact column of three icon buttons (accept, reject, edit) in the right * margin of the current paragraph of the new version (or of the stacked view). Every * paragraph there keeps that margin free, so selecting a paragraph never moves any text. + * On phones the margin would cost too much of the narrow text column: the buttons sit in a + * row just above the bottom bar instead, within reach of the thumb. */ // Created on first use (the module is also loaded without a DOM by the export tests). @@ -9490,6 +9517,13 @@

Your data

reviewBar.addEventListener('dblclick', e => e.stopPropagation()); // Roving focus inside the group: one tab stop, arrow keys move between the buttons. reviewBar.addEventListener('keydown', e => handleRovingKeys(e, reviewBar)); + // In the panes, clicks are handled by the pane; above the bottom bar, here. + reviewBar.addEventListener('click', e => { + const btn = e.target.closest('[data-review-action]'); + if (!btn || !reviewBar.closest('.bottom-bar')) return; + e.stopPropagation(); + runReviewAction(btn.getAttribute('data-review-action'), false); + }); return reviewBar; } @@ -9501,11 +9535,15 @@

Your data

const ICON_X = ''; const ICON_EDIT = ''; -// Icon buttons: accept / reject for an open change, edit for any paragraph of the new version. -function reviewActionsHtml(item) { +// Icon buttons: accept / reject for an open change, edit for any paragraph of the new version; +// with `withUndo` (phones, where the margin has no undo button) also undo for a reviewed one. +function reviewActionsHtml(item, withUndo = false) { const btn = (action, icon, label, title, key, cls) => - ``; + ``; const edit = btn('edit', ICON_EDIT, t('editBtn'), t('editTitle'), 'E', 'ra-edit'); + if (withUndo && item && item.status !== 'open') { + return rovingFirst(btn('reopen', ICON_UNDO, t('reopenBtn'), t('reopenTitle'), '', 'ra-reopen') + edit); + } if (item && item.status === 'open') { return rovingFirst( btn('accept', ICON_CHECK, t('acceptBtn'), t('acceptTitle'), 'A', 'ra-accept') + @@ -9553,15 +9591,18 @@

Your data

} const bar = getReviewBar(); const item = getActiveItem(); + const phone = document.body.classList.contains('tier-phone'); + const barHost = phone ? document.getElementById('mobile-bottom-bar') : host; const hadFocus = bar.contains(document.activeElement); const focusedAction = hadFocus ? document.activeElement.getAttribute('data-review-action') : null; - bar.innerHTML = reviewActionsHtml(item); + bar.innerHTML = reviewActionsHtml(item, phone); + bar.setAttribute('aria-orientation', phone ? 'horizontal' : 'vertical'); bar.setAttribute( 'aria-label', item ? formatMsg('reviewBarLabel', { i: item.index, n: state.listItems.length }) : t('paraActionsLabel') ); const buttons = bar.querySelectorAll('button'); - if (bar.parentElement !== host) host.appendChild(bar); + if (bar.parentElement !== barHost) barHost.appendChild(bar); const request = reviewFocusRequest; reviewFocusRequest = null; let target = null; @@ -13945,7 +13986,8 @@

${escapeHtml(t('exportRedlineHeading'))}

// Phones show one column only. if (tier === 'phone' && state.viewMode !== 'inline') setViewMode('inline'); else if (!initial && prev === 'phone' && !state.viewModeChosen) setViewMode(initialInline && tier !== 'desktop' ? 'inline' : 'split'); - applySidebarState(); + applySidebarState(); // The review buttons sit in the paragraph margin, or above the bottom bar on phones. + if (!initial && typeof placeReviewBar === 'function') placeReviewBar(); } function applySidebarState() { @@ -14495,7 +14537,7 @@

${escapeHtml(t('exportRedlineHeading'))}

* entry; version 1 files (no review status) still open. */ -const DOCDIFF_APP_VERSION = '1.6.0'; +const DOCDIFF_APP_VERSION = '1.6.1'; const SESSION_FORMAT_ID = 'docdiff-session'; const SESSION_FORMAT_VERSION = 2; const SESSION_FILE_EXT = '.docdiff'; diff --git a/site/index.html b/site/index.html index 240930c..559da10 100644 --- a/site/index.html +++ b/site/index.html @@ -102,7 +102,7 @@ "sameAs": [ "https://github.com/tschnorpfeil/docdiff" ], - "softwareVersion": "1.6.0" + "softwareVersion": "1.6.1" }, { "@type": "SoftwareSourceCode", @@ -222,7 +222,12 @@ --text-ui: 0.875rem; /* 14px – controls, list text */ --text-doc: 0.96875rem; /* 15.5px – document text, side by side */ --text-doc-lg: 1rem; /* 16px – document text, one column */ - --text-title: 1.0625rem; /* 17px – headings in the document, panel titles */ + --text-xs: 0.75rem; /* 12px – badges and key caps only */ + --text-title: 1.0625rem; /* 17px – headings in the document, panel titles, lead text */ + --text-doc-h1: 1.1875rem; /* 19px – document title */ + --text-section: 1.375rem; /* 22px – start page section headings */ + --text-lead: 1.0625rem; /* 17px – start page lead text */ + --text-display: clamp(1.875rem, 1.2rem + 2vw, 2.75rem); /* 30–44px – start page headline */ --radius-sm: 4px; /* marks, small chips */ --radius-md: 8px; /* buttons, inputs, cards, menu items */ @@ -416,7 +421,7 @@ box-shadow: inset 0 0 0 1px var(--kante-300), 0 1px 0 var(--kante-300); color: hsl(var(--foreground)); font-family: var(--font-mono); - font-size: 12.5px; + font-size: var(--text-xs); font-weight: 600; line-height: 1.55; text-align: center; @@ -542,7 +547,7 @@ display: block; } .app-title { - font-size: 15px; + font-size: var(--text-doc); font-weight: 600; letter-spacing: -0.02em; white-space: nowrap; @@ -630,7 +635,7 @@ background: hsl(var(--ton-2)); font-family: var(--font-mono); font-variant-numeric: tabular-nums; - font-size: 13px; + font-size: var(--text-ui-sm); font-weight: 600; } @media (hover: hover) { @@ -744,7 +749,7 @@ } .menu-label { padding: 6px 10px 4px 10px; - font-size: 13px; + font-size: var(--text-ui-sm); font-weight: 600; color: hsl(var(--muted-foreground)); } @@ -810,7 +815,7 @@ margin-left: auto; padding-left: 16px; color: hsl(var(--muted-foreground)); - font-size: 13px; + font-size: var(--text-ui-sm); font-weight: 400; } .mi-kbd { @@ -955,7 +960,7 @@ background: transparent; color: hsl(var(--foreground)); font: inherit; - font-size: 15px; + font-size: var(--text-doc); } .search-panel input::-webkit-search-cancel-button { display: none; @@ -973,7 +978,7 @@ .search-count { flex-shrink: 0; padding: 0 4px; - font-size: 13px; + font-size: var(--text-ui-sm); color: hsl(var(--muted-foreground)); font-variant-numeric: tabular-nums; white-space: nowrap; @@ -1017,7 +1022,7 @@ .landing-title { max-width: 30ch; - font-size: clamp(21.9px, 14.2px + 1.7vw, 30.4px); + font-size: var(--text-display); font-weight: 600; line-height: 1.18; letter-spacing: -0.028em; @@ -1053,7 +1058,7 @@ } .lang-hint { margin-bottom: 20px; - font-size: 13px; + font-size: var(--text-ui-sm); } .lang-hint a { display: inline-flex; @@ -1071,41 +1076,14 @@ outline-offset: 4px; } .landing-sub { - margin-top: 12px; - max-width: 60ch; - font-size: 14.5px; + margin-top: 16px; + max-width: 70ch; + font-size: var(--text-lead); line-height: 1.55; color: hsl(var(--muted-foreground)); - text-wrap: pretty; + text-wrap: balance; } -.trust-list { - list-style: none; - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-top: 18px; -} -.trust-list li { - display: inline-flex; - align-items: center; - gap: 6px; - min-height: 28px; - padding: 3px 10px 3px 8px; - border-radius: 999px; - background: hsl(var(--card)); - box-shadow: inset 0 0 0 1px var(--kante-200); - font-size: 13px; - font-weight: 500; - line-height: 1.35; - color: hsl(var(--foreground)); -} -.trust-list .icon { - width: 14px; - height: 14px; - stroke-width: 2.4; - color: hsl(var(--success-ink)); -} /* Two large drop slots: the whole slot is one button and a drop target */ .drop-slots { @@ -1121,7 +1099,7 @@ .drop-slot-btn { width: 100%; height: 100%; - min-height: 156px; + min-height: 128px; padding: 20px 22px; display: flex; flex-direction: column; @@ -1167,19 +1145,22 @@ border-radius: 999px; background: hsl(var(--ton-2)); font-family: var(--font-mono); - font-size: 13px; + font-size: var(--text-ui-sm); font-weight: 700; color: hsl(var(--foreground)); } .drop-slot-title { min-width: 0; - font-size: 14.5px; + font-size: var(--text-doc-lg); font-weight: 600; letter-spacing: -0.01em; overflow-wrap: anywhere; } +.drop-slot-hint:empty { + display: none; +} .drop-slot-hint { - font-size: 13.5px; + font-size: var(--text-ui); line-height: 1.45; color: hsl(var(--muted-foreground)); } @@ -1202,7 +1183,7 @@ background: hsl(var(--card)); box-shadow: inset 0 0 0 1px var(--kante-300), var(--elev-1); color: hsl(var(--foreground)); - font-size: 14px; + font-size: var(--text-ui); font-weight: 600; } .drop-slot.is-next-step .drop-slot-cta-btn { @@ -1210,8 +1191,9 @@ box-shadow: none; color: hsl(var(--primary-foreground)); } -.drop-slot-drag { - font-size: 13.5px; +.landing-formats { + margin-top: 12px; + font-size: var(--text-ui); color: hsl(var(--muted-foreground)); } .drop-slot.is-loaded .drop-slot-btn { @@ -1249,22 +1231,41 @@ color: hsl(var(--foreground)); } -.drop-both-hint { - margin-top: 10px; - font-size: 13px; - color: hsl(var(--muted-foreground)); -} .landing-secondary { display: flex; flex-wrap: wrap; - gap: 10px; - margin-top: 18px; + align-items: center; + gap: 4px 10px; + margin-top: 20px; +} +/* Less frequent ways in: plain text buttons next to the example */ +.btn-quiet { + min-height: 38px; + padding: 0 8px; + border: 0; + border-radius: var(--radius-md); + background: transparent; + color: hsl(var(--muted-foreground)); + font: inherit; + font-size: var(--text-ui); + font-weight: 500; + cursor: pointer; +} +.btn-quiet:hover { + color: hsl(var(--foreground)); + text-decoration: underline; + text-underline-offset: 3px; +} +.landing-trust { + margin-top: 28px; + font-size: var(--text-ui); + color: hsl(var(--muted-foreground)); } .landing-secondary .btn { height: 38px; padding: 0 16px; - font-size: 14px; + font-size: var(--text-ui); } .landing-secondary .btn .icon { width: 15px; @@ -1278,7 +1279,7 @@ border-top: 1px solid hsl(var(--border)); } .landing-section h2 { - font-size: 17px; + font-size: var(--text-section); font-weight: 600; letter-spacing: -0.015em; color: hsl(var(--foreground)); @@ -1293,7 +1294,7 @@ .how-steps li { display: flex; gap: 12px; - font-size: 14.5px; + font-size: var(--text-doc-lg); line-height: 1.55; color: hsl(var(--muted-foreground)); } @@ -1314,7 +1315,7 @@ background: hsl(var(--ton-2)); color: hsl(var(--foreground)); font-family: var(--font-mono); - font-size: 13px; + font-size: var(--text-ui-sm); font-weight: 700; } .scope-grid { @@ -1330,7 +1331,7 @@ box-shadow: inset 0 0 0 1px var(--kante-200); } .scope-col h3 { - font-size: 15px; + font-size: var(--text-title); font-weight: 600; color: hsl(var(--foreground)); } @@ -1344,7 +1345,7 @@ .scope-col li { position: relative; padding-left: 24px; - font-size: 14.5px; + font-size: var(--text-doc-lg); line-height: 1.5; color: hsl(var(--muted-foreground)); } @@ -1380,7 +1381,7 @@ padding: 10px 2px; list-style: none; cursor: pointer; - font-size: 15px; + font-size: var(--text-doc-lg); font-weight: 600; color: hsl(var(--foreground)); } @@ -1406,7 +1407,7 @@ display: flex; flex-direction: column; gap: 10px; - font-size: 14.5px; + font-size: var(--text-doc-lg); line-height: 1.6; color: hsl(var(--muted-foreground)); } @@ -1422,7 +1423,7 @@ flex-wrap: wrap; align-items: center; gap: 8px; - font-size: 13px; + font-size: var(--text-ui-sm); color: hsl(var(--muted-foreground)); } .lp-link { @@ -1588,7 +1589,7 @@ background: hsl(var(--card)); border-radius: var(--radius-md); box-shadow: 0 0 0 1px hsl(var(--border)), var(--elev-3); - font-size: 15px; + font-size: var(--text-doc); font-weight: 600; } @@ -1691,7 +1692,7 @@ } .clause.kind-h1 { font-weight: 600; - font-size: 1.1875rem; + font-size: var(--text-doc-h1); line-height: 1.35; padding-top: var(--sp-3); padding-bottom: var(--sp-2); @@ -1820,7 +1821,7 @@ display: block; grid-column: 1 / -1; margin-bottom: 2px; - font-size: 13px; + font-size: var(--text-ui-sm); font-weight: 600; color: hsl(var(--muted-foreground)); } @@ -2031,6 +2032,50 @@ width: auto; } +/* Phones: the margin only holds the ✓ / ✗ mark of a reviewed paragraph; the buttons (with undo) + sit in a row just above the bottom bar, so the narrow text column keeps its width. */ +body.tier-phone { + --rv-gutter: 30px; +} +body.tier-phone .review-status { + right: 0; + width: 28px; +} +body.tier-phone .rs-mark { + width: 28px; + height: 28px; +} +body.tier-phone .ra-undo { + display: none; +} +.ra-label { + display: none; +} +body.tier-phone .bottom-bar .ra-btn:not(.ra-edit) { + width: auto; + gap: 6px; + padding: 0 14px 0 12px; + color: hsl(var(--foreground)); + font-size: var(--text-ui); + font-weight: 600; +} +body.tier-phone .bottom-bar .ra-btn:not(.ra-edit) .ra-label { + display: inline; +} +body.tier-phone .bottom-bar .ra-accept .icon { color: hsl(var(--success-ink)); } +body.tier-phone .bottom-bar .ra-reject .icon { color: hsl(var(--destructive-ink)); } +body.tier-phone .bottom-bar .review-bar { + top: auto; + right: max(8px, env(safe-area-inset-right)); + bottom: calc(100% + 8px); + flex-direction: row; + gap: 4px; + padding: 4px; + border-radius: var(--radius-lg); + background: hsl(var(--card)); + box-shadow: 0 0 0 1px hsl(var(--border)), 0 8px 24px -10px hsl(var(--foreground) / 0.3); +} + /* Inline editor */ .clause.is-editing { display: block !important; @@ -2062,7 +2107,7 @@ align-items: center; justify-content: space-between; gap: 8px; - font-size: 13px; + font-size: var(--text-ui-sm); font-weight: 400; color: hsl(var(--muted-foreground)); } @@ -2283,7 +2328,7 @@ padding: 0 6px; border-radius: 999px; background: hsl(var(--ton-2)); - font-size: 12px; + font-size: var(--text-xs); font-weight: 500; line-height: 18px; color: hsl(var(--muted-foreground)); @@ -2333,12 +2378,12 @@ text-align: center; } .empty-title { - font-size: 15px; + font-size: var(--text-doc); font-weight: 600; } .empty-desc { margin-top: 6px; - font-size: 14px; + font-size: var(--text-ui); color: hsl(var(--muted-foreground)); } @@ -2433,7 +2478,7 @@ background: hsl(var(--foreground)); color: hsl(var(--background)); box-shadow: var(--elev-3); - font-size: 13px; + font-size: var(--text-ui-sm); font-weight: 500; line-height: 1.4; pointer-events: none; @@ -2479,7 +2524,7 @@ justify-content: space-between; } .dialog-title { - font-size: 17px; + font-size: var(--text-title); font-weight: 600; letter-spacing: -0.01em; } @@ -2499,7 +2544,7 @@ flex-direction: column; gap: 16px; overflow-y: auto; - font-size: 15px; + font-size: var(--text-doc); line-height: 1.6; } .dialog-col { @@ -2519,7 +2564,7 @@ border-radius: var(--radius-md); box-shadow: inset 0 0 0 1px var(--control-edge); font-family: var(--font-sans); - font-size: 15px; + font-size: var(--text-doc); line-height: 1.55; resize: vertical; } @@ -2562,18 +2607,18 @@ margin-top: 18px; } .help-sec h3 { - font-size: 15px; + font-size: var(--text-doc); font-weight: 600; margin-bottom: 4px; } .help-sec p { - font-size: 15px; + font-size: var(--text-doc); line-height: 1.6; } .help-keys { width: 100%; border-collapse: collapse; - font-size: 15px; + font-size: var(--text-doc); } .help-keys th, .help-keys td { @@ -2605,7 +2650,7 @@ align-items: center; gap: 10px; min-height: 40px; - font-size: 15px; + font-size: var(--text-doc); cursor: pointer; } .help-setting input { @@ -2680,7 +2725,7 @@ background: hsl(var(--destructive-ton-1)); color: hsl(var(--destructive-ink)); box-shadow: inset 0 0 0 1px hsl(var(--destructive) / 0.35), var(--elev-3); - font-size: 14px; + font-size: var(--text-ui); line-height: 1.5; } .import-notice[hidden] { display: none; } @@ -2809,12 +2854,12 @@ } body:not(.is-comparing) .app-title { display: inline; - font-size: 16px; + font-size: var(--text-doc-lg); } .tb-files { padding: 0 6px 0 8px; box-shadow: none; - font-size: 13.5px; + font-size: var(--text-ui); } .tb-icon-btn { width: 44px; @@ -2831,7 +2876,8 @@ display: none; } .doc-pane-unified { - padding: 10px 10px calc(84px + env(safe-area-inset-bottom)); + /* bottom bar + the review buttons above it */ + padding: 10px 10px calc(144px + env(safe-area-inset-bottom)); } .doc-page-header { padding: var(--sp-5) 0 var(--sp-3) 14px; @@ -2871,7 +2917,7 @@ background: transparent; color: hsl(var(--foreground)); font: inherit; - font-size: 15px; + font-size: var(--text-doc); font-weight: 600; display: inline-flex; align-items: center; @@ -2965,7 +3011,7 @@ height: 52px; } .search-panel input { - font-size: 16px; /* no iOS zoom on focus */ + font-size: var(--text-doc-lg); /* no iOS zoom on focus */ } .search-panel .tb-icon-btn.sm { width: 40px; @@ -2989,7 +3035,7 @@ } .dialog-textarea { height: 150px; - font-size: 16px; + font-size: var(--text-doc-lg); } .dialog-footer { padding: 10px 16px calc(10px + env(safe-area-inset-bottom)); @@ -3435,12 +3481,6 @@ .landing-inner { padding-top: 24px; } - .landing-sub { - font-size: 15.5px; - } - .trust-list li { - font-size: 13px; - } .drop-slots { grid-template-columns: 1fr; gap: 12px; @@ -3457,9 +3497,12 @@ height: 44px; } .landing-secondary .btn { - flex: 1 1 140px; + flex: 1 1 100%; height: 44px; } + .btn-quiet { + min-height: 44px; + } .landing-section { margin-top: 40px; padding-top: 24px; @@ -3733,12 +3776,7 @@

Doc Diff

Dokumente einfach vergleichen.

-

Zwei Fassungen als PDF, Word oder Text: Jede Änderung wird Wort für Wort markiert. Ihre Dateien verlassen dabei nie Ihr Gerät.

-
    -
  • Kostenlos & Open Source (MIT)
  • -
  • Keine Uploads – alles läuft lokal im Browser
  • -
  • Keine Werbung, kein Tracking, keine Cookies
  • -
+

Jede Änderung Wort für Wort markiert. Ihre Dateien verlassen nie Ihr Gerät.

@@ -3747,10 +3785,9 @@

1 Alte Fassung - PDF (mit Text), Word (.docx), Text + Datei wählen - oder hierher ziehen @@ -3761,31 +3798,25 @@

2 Neue Fassung - PDF (mit Text), Word (.docx), Text + Datei wählen - oder hierher ziehen

-

Sie können auch beide Dateien auf einmal hierher ziehen – die erste gilt als alte Fassung.

+

PDF, Word (.docx) oder Text · Dateien auch hierher ziehen

- - + +
+

Kostenlos und Open Source (MIT) · Keine Werbung, kein Tracking, keine Cookies

@@ -4418,18 +4449,15 @@

Ihre Daten

pasteText: 'Text einfügen', loadSample: 'Beispiel ansehen', heroTitle: 'Dokumente einfach vergleichen.', - heroSub: 'Zwei Fassungen als PDF, Word oder Text: Jede Änderung wird Wort für Wort markiert. Ihre Dateien verlassen dabei nie Ihr Gerät.', - trustFree: 'Kostenlos & Open Source (MIT)', - trustLocal: 'Keine Uploads – alles läuft lokal im Browser', - trustNoTracking: 'Keine Werbung, kein Tracking, keine Cookies', + heroSub: 'Jede Änderung Wort für Wort markiert. Ihre Dateien verlassen nie Ihr Gerät.', + trustLine: 'Kostenlos und Open Source (MIT) · Keine Werbung, kein Tracking, keine Cookies', slotLeftTitle: 'Alte Fassung', slotRightTitle: 'Neue Fassung', - slotHint: 'PDF (mit Text), Word (.docx), Text', + slotHint: 'PDF, Word (.docx) oder Text', slotChoose: 'Datei wählen', - slotDrag: 'oder hierher ziehen', + slotDrag: ' · Dateien auch hierher ziehen', slotClearLeft: 'Alte Fassung entfernen', slotClearRight: 'Neue Fassung entfernen', - dropBothHint: 'Sie können auch beide Dateien auf einmal hierher ziehen – die erste gilt als alte Fassung.', howTitle: 'So funktioniert’s', how1Title: 'Zwei Fassungen wählen', how1Text: 'Als PDF, Word-Datei oder eingefügten Text. Die Dateien werden nur in diesem Browser-Tab gelesen.', @@ -4818,18 +4846,15 @@

Your data

pasteText: 'Paste text', loadSample: 'View example', heroTitle: 'Compare documents easily.', - heroSub: 'Two versions as PDF, Word or text: every change is marked word by word. Your files never leave your device.', - trustFree: 'Free & open source (MIT)', - trustLocal: 'No uploads – everything runs locally in your browser', - trustNoTracking: 'No ads, no tracking, no cookies', + heroSub: 'Every change marked word by word. Your files never leave your device.', + trustLine: 'Free and open source (MIT) · No ads, no tracking, no cookies', slotLeftTitle: 'Old version', slotRightTitle: 'New version', - slotHint: 'PDF (with text), Word (.docx), text', + slotHint: 'PDF, Word (.docx) or text', slotChoose: 'Choose file', - slotDrag: 'or drop it here', + slotDrag: ' · or drop the files here', slotClearLeft: 'Remove old version', slotClearRight: 'Remove new version', - dropBothHint: 'You can also drop both files here at once – the first one becomes the old version.', howTitle: 'How it works', how1Title: 'Choose two versions', how1Text: 'As PDF, Word file or pasted text. The files are only read inside this browser tab.', @@ -7426,8 +7451,8 @@

Your data

} else { titleEl.setAttribute('data-i18n', titleKey); titleEl.textContent = t(titleKey); - hintEl.setAttribute('data-i18n', 'slotHint'); - hintEl.textContent = t('slotHint'); + // The formats are named once, under both slots. + hintEl.textContent = ''; ctaEl.setAttribute('data-i18n', 'slotChoose'); ctaEl.textContent = t('slotChoose'); } @@ -9476,6 +9501,8 @@

Your data

* One place only: a compact column of three icon buttons (accept, reject, edit) in the right * margin of the current paragraph of the new version (or of the stacked view). Every * paragraph there keeps that margin free, so selecting a paragraph never moves any text. + * On phones the margin would cost too much of the narrow text column: the buttons sit in a + * row just above the bottom bar instead, within reach of the thumb. */ // Created on first use (the module is also loaded without a DOM by the export tests). @@ -9490,6 +9517,13 @@

Your data

reviewBar.addEventListener('dblclick', e => e.stopPropagation()); // Roving focus inside the group: one tab stop, arrow keys move between the buttons. reviewBar.addEventListener('keydown', e => handleRovingKeys(e, reviewBar)); + // In the panes, clicks are handled by the pane; above the bottom bar, here. + reviewBar.addEventListener('click', e => { + const btn = e.target.closest('[data-review-action]'); + if (!btn || !reviewBar.closest('.bottom-bar')) return; + e.stopPropagation(); + runReviewAction(btn.getAttribute('data-review-action'), false); + }); return reviewBar; } @@ -9501,11 +9535,15 @@

Your data

const ICON_X = ''; const ICON_EDIT = ''; -// Icon buttons: accept / reject for an open change, edit for any paragraph of the new version. -function reviewActionsHtml(item) { +// Icon buttons: accept / reject for an open change, edit for any paragraph of the new version; +// with `withUndo` (phones, where the margin has no undo button) also undo for a reviewed one. +function reviewActionsHtml(item, withUndo = false) { const btn = (action, icon, label, title, key, cls) => - ``; + ``; const edit = btn('edit', ICON_EDIT, t('editBtn'), t('editTitle'), 'E', 'ra-edit'); + if (withUndo && item && item.status !== 'open') { + return rovingFirst(btn('reopen', ICON_UNDO, t('reopenBtn'), t('reopenTitle'), '', 'ra-reopen') + edit); + } if (item && item.status === 'open') { return rovingFirst( btn('accept', ICON_CHECK, t('acceptBtn'), t('acceptTitle'), 'A', 'ra-accept') + @@ -9553,15 +9591,18 @@

Your data

} const bar = getReviewBar(); const item = getActiveItem(); + const phone = document.body.classList.contains('tier-phone'); + const barHost = phone ? document.getElementById('mobile-bottom-bar') : host; const hadFocus = bar.contains(document.activeElement); const focusedAction = hadFocus ? document.activeElement.getAttribute('data-review-action') : null; - bar.innerHTML = reviewActionsHtml(item); + bar.innerHTML = reviewActionsHtml(item, phone); + bar.setAttribute('aria-orientation', phone ? 'horizontal' : 'vertical'); bar.setAttribute( 'aria-label', item ? formatMsg('reviewBarLabel', { i: item.index, n: state.listItems.length }) : t('paraActionsLabel') ); const buttons = bar.querySelectorAll('button'); - if (bar.parentElement !== host) host.appendChild(bar); + if (bar.parentElement !== barHost) barHost.appendChild(bar); const request = reviewFocusRequest; reviewFocusRequest = null; let target = null; @@ -13945,7 +13986,8 @@

${escapeHtml(t('exportRedlineHeading'))}

// Phones show one column only. if (tier === 'phone' && state.viewMode !== 'inline') setViewMode('inline'); else if (!initial && prev === 'phone' && !state.viewModeChosen) setViewMode(initialInline && tier !== 'desktop' ? 'inline' : 'split'); - applySidebarState(); + applySidebarState(); // The review buttons sit in the paragraph margin, or above the bottom bar on phones. + if (!initial && typeof placeReviewBar === 'function') placeReviewBar(); } function applySidebarState() { @@ -14495,7 +14537,7 @@

${escapeHtml(t('exportRedlineHeading'))}

* entry; version 1 files (no review status) still open. */ -const DOCDIFF_APP_VERSION = '1.6.0'; +const DOCDIFF_APP_VERSION = '1.6.1'; const SESSION_FORMAT_ID = 'docdiff-session'; const SESSION_FORMAT_VERSION = 2; const SESSION_FILE_EXT = '.docdiff'; diff --git a/src/js/01-i18n.js b/src/js/01-i18n.js index 053b4c2..9e92e5a 100644 --- a/src/js/01-i18n.js +++ b/src/js/01-i18n.js @@ -342,18 +342,15 @@ const I18N = { pasteText: 'Text einfügen', loadSample: 'Beispiel ansehen', heroTitle: 'Dokumente einfach vergleichen.', - heroSub: 'Zwei Fassungen als PDF, Word oder Text: Jede Änderung wird Wort für Wort markiert. Ihre Dateien verlassen dabei nie Ihr Gerät.', - trustFree: 'Kostenlos & Open Source (MIT)', - trustLocal: 'Keine Uploads – alles läuft lokal im Browser', - trustNoTracking: 'Keine Werbung, kein Tracking, keine Cookies', + heroSub: 'Jede Änderung Wort für Wort markiert. Ihre Dateien verlassen nie Ihr Gerät.', + trustLine: 'Kostenlos und Open Source (MIT) · Keine Werbung, kein Tracking, keine Cookies', slotLeftTitle: 'Alte Fassung', slotRightTitle: 'Neue Fassung', - slotHint: 'PDF (mit Text), Word (.docx), Text', + slotHint: 'PDF, Word (.docx) oder Text', slotChoose: 'Datei wählen', - slotDrag: 'oder hierher ziehen', + slotDrag: ' · Dateien auch hierher ziehen', slotClearLeft: 'Alte Fassung entfernen', slotClearRight: 'Neue Fassung entfernen', - dropBothHint: 'Sie können auch beide Dateien auf einmal hierher ziehen – die erste gilt als alte Fassung.', howTitle: 'So funktioniert’s', how1Title: 'Zwei Fassungen wählen', how1Text: 'Als PDF, Word-Datei oder eingefügten Text. Die Dateien werden nur in diesem Browser-Tab gelesen.', @@ -742,18 +739,15 @@ const I18N = { pasteText: 'Paste text', loadSample: 'View example', heroTitle: 'Compare documents easily.', - heroSub: 'Two versions as PDF, Word or text: every change is marked word by word. Your files never leave your device.', - trustFree: 'Free & open source (MIT)', - trustLocal: 'No uploads – everything runs locally in your browser', - trustNoTracking: 'No ads, no tracking, no cookies', + heroSub: 'Every change marked word by word. Your files never leave your device.', + trustLine: 'Free and open source (MIT) · No ads, no tracking, no cookies', slotLeftTitle: 'Old version', slotRightTitle: 'New version', - slotHint: 'PDF (with text), Word (.docx), text', + slotHint: 'PDF, Word (.docx) or text', slotChoose: 'Choose file', - slotDrag: 'or drop it here', + slotDrag: ' · or drop the files here', slotClearLeft: 'Remove old version', slotClearRight: 'Remove new version', - dropBothHint: 'You can also drop both files here at once – the first one becomes the old version.', howTitle: 'How it works', how1Title: 'Choose two versions', how1Text: 'As PDF, Word file or pasted text. The files are only read inside this browser tab.', diff --git a/src/js/05-render.js b/src/js/05-render.js index bcd15e1..0692175 100644 --- a/src/js/05-render.js +++ b/src/js/05-render.js @@ -312,8 +312,8 @@ function updateLandingSlots(left, right) { } else { titleEl.setAttribute('data-i18n', titleKey); titleEl.textContent = t(titleKey); - hintEl.setAttribute('data-i18n', 'slotHint'); - hintEl.textContent = t('slotHint'); + // The formats are named once, under both slots. + hintEl.textContent = ''; ctaEl.setAttribute('data-i18n', 'slotChoose'); ctaEl.textContent = t('slotChoose'); } diff --git a/src/js/06d-review.js b/src/js/06d-review.js index 9a8c8e4..7d9290e 100644 --- a/src/js/06d-review.js +++ b/src/js/06d-review.js @@ -436,6 +436,8 @@ function syncStatusTabStops() { * One place only: a compact column of three icon buttons (accept, reject, edit) in the right * margin of the current paragraph of the new version (or of the stacked view). Every * paragraph there keeps that margin free, so selecting a paragraph never moves any text. + * On phones the margin would cost too much of the narrow text column: the buttons sit in a + * row just above the bottom bar instead, within reach of the thumb. */ // Created on first use (the module is also loaded without a DOM by the export tests). @@ -450,6 +452,13 @@ function getReviewBar() { reviewBar.addEventListener('dblclick', e => e.stopPropagation()); // Roving focus inside the group: one tab stop, arrow keys move between the buttons. reviewBar.addEventListener('keydown', e => handleRovingKeys(e, reviewBar)); + // In the panes, clicks are handled by the pane; above the bottom bar, here. + reviewBar.addEventListener('click', e => { + const btn = e.target.closest('[data-review-action]'); + if (!btn || !reviewBar.closest('.bottom-bar')) return; + e.stopPropagation(); + runReviewAction(btn.getAttribute('data-review-action'), false); + }); return reviewBar; } @@ -461,11 +470,15 @@ const ICON_CHECK = '`; const edit = btn('edit', ICON_EDIT, t('editBtn'), t('editTitle'), 'E', 'ra-edit'); + if (withUndo && item && item.status !== 'open') { + return rovingFirst(btn('reopen', ICON_UNDO, t('reopenBtn'), t('reopenTitle'), '', 'ra-reopen') + edit); + } if (item && item.status === 'open') { return rovingFirst( btn('accept', ICON_CHECK, t('acceptBtn'), t('acceptTitle'), 'A', 'ra-accept') + @@ -513,15 +526,18 @@ function placeReviewBar() { } const bar = getReviewBar(); const item = getActiveItem(); + const phone = document.body.classList.contains('tier-phone'); + const barHost = phone ? document.getElementById('mobile-bottom-bar') : host; const hadFocus = bar.contains(document.activeElement); const focusedAction = hadFocus ? document.activeElement.getAttribute('data-review-action') : null; - bar.innerHTML = reviewActionsHtml(item); + bar.innerHTML = reviewActionsHtml(item, phone); + bar.setAttribute('aria-orientation', phone ? 'horizontal' : 'vertical'); bar.setAttribute( 'aria-label', item ? formatMsg('reviewBarLabel', { i: item.index, n: state.listItems.length }) : t('paraActionsLabel') ); const buttons = bar.querySelectorAll('button'); - if (bar.parentElement !== host) host.appendChild(bar); + if (bar.parentElement !== barHost) barHost.appendChild(bar); const request = reviewFocusRequest; reviewFocusRequest = null; let target = null; diff --git a/src/js/10-controls.js b/src/js/10-controls.js index a98f135..8057df0 100644 --- a/src/js/10-controls.js +++ b/src/js/10-controls.js @@ -367,7 +367,8 @@ function applyLayoutTier(initial) { // Phones show one column only. if (tier === 'phone' && state.viewMode !== 'inline') setViewMode('inline'); else if (!initial && prev === 'phone' && !state.viewModeChosen) setViewMode(initialInline && tier !== 'desktop' ? 'inline' : 'split'); - applySidebarState(); + applySidebarState(); // The review buttons sit in the paragraph margin, or above the bottom bar on phones. + if (!initial && typeof placeReviewBar === 'function') placeReviewBar(); } function applySidebarState() { diff --git a/src/js/11-session.js b/src/js/11-session.js index f620b40..f3f3f9d 100644 --- a/src/js/11-session.js +++ b/src/js/11-session.js @@ -17,7 +17,7 @@ * entry; version 1 files (no review status) still open. */ -const DOCDIFF_APP_VERSION = '1.6.0'; +const DOCDIFF_APP_VERSION = '1.6.1'; const SESSION_FORMAT_ID = 'docdiff-session'; const SESSION_FORMAT_VERSION = 2; const SESSION_FILE_EXT = '.docdiff'; diff --git a/src/styles.css b/src/styles.css index f9bdffe..4e74049 100644 --- a/src/styles.css +++ b/src/styles.css @@ -44,7 +44,12 @@ --text-ui: 0.875rem; /* 14px – controls, list text */ --text-doc: 0.96875rem; /* 15.5px – document text, side by side */ --text-doc-lg: 1rem; /* 16px – document text, one column */ - --text-title: 1.0625rem; /* 17px – headings in the document, panel titles */ + --text-xs: 0.75rem; /* 12px – badges and key caps only */ + --text-title: 1.0625rem; /* 17px – headings in the document, panel titles, lead text */ + --text-doc-h1: 1.1875rem; /* 19px – document title */ + --text-section: 1.375rem; /* 22px – start page section headings */ + --text-lead: 1.0625rem; /* 17px – start page lead text */ + --text-display: clamp(1.875rem, 1.2rem + 2vw, 2.75rem); /* 30–44px – start page headline */ --radius-sm: 4px; /* marks, small chips */ --radius-md: 8px; /* buttons, inputs, cards, menu items */ @@ -238,7 +243,7 @@ kbd { box-shadow: inset 0 0 0 1px var(--kante-300), 0 1px 0 var(--kante-300); color: hsl(var(--foreground)); font-family: var(--font-mono); - font-size: 12.5px; + font-size: var(--text-xs); font-weight: 600; line-height: 1.55; text-align: center; @@ -364,7 +369,7 @@ body:not(.is-comparing) .topbar { display: block; } .app-title { - font-size: 15px; + font-size: var(--text-doc); font-weight: 600; letter-spacing: -0.02em; white-space: nowrap; @@ -452,7 +457,7 @@ body:not(.is-comparing) .topbar { background: hsl(var(--ton-2)); font-family: var(--font-mono); font-variant-numeric: tabular-nums; - font-size: 13px; + font-size: var(--text-ui-sm); font-weight: 600; } @media (hover: hover) { @@ -566,7 +571,7 @@ body:not(.is-comparing) .topbar { } .menu-label { padding: 6px 10px 4px 10px; - font-size: 13px; + font-size: var(--text-ui-sm); font-weight: 600; color: hsl(var(--muted-foreground)); } @@ -632,7 +637,7 @@ body:not(.is-comparing) .topbar { margin-left: auto; padding-left: 16px; color: hsl(var(--muted-foreground)); - font-size: 13px; + font-size: var(--text-ui-sm); font-weight: 400; } .mi-kbd { @@ -777,7 +782,7 @@ body:not(.is-comparing) .topbar { background: transparent; color: hsl(var(--foreground)); font: inherit; - font-size: 15px; + font-size: var(--text-doc); } .search-panel input::-webkit-search-cancel-button { display: none; @@ -795,7 +800,7 @@ body:not(.is-comparing) .topbar { .search-count { flex-shrink: 0; padding: 0 4px; - font-size: 13px; + font-size: var(--text-ui-sm); color: hsl(var(--muted-foreground)); font-variant-numeric: tabular-nums; white-space: nowrap; @@ -839,7 +844,7 @@ body.is-comparing .empty-launchpad { .landing-title { max-width: 30ch; - font-size: clamp(21.9px, 14.2px + 1.7vw, 30.4px); + font-size: var(--text-display); font-weight: 600; line-height: 1.18; letter-spacing: -0.028em; @@ -875,7 +880,7 @@ body.is-comparing .empty-launchpad { } .lang-hint { margin-bottom: 20px; - font-size: 13px; + font-size: var(--text-ui-sm); } .lang-hint a { display: inline-flex; @@ -893,41 +898,14 @@ body.is-comparing .empty-launchpad { outline-offset: 4px; } .landing-sub { - margin-top: 12px; - max-width: 60ch; - font-size: 14.5px; + margin-top: 16px; + max-width: 70ch; + font-size: var(--text-lead); line-height: 1.55; color: hsl(var(--muted-foreground)); - text-wrap: pretty; + text-wrap: balance; } -.trust-list { - list-style: none; - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-top: 18px; -} -.trust-list li { - display: inline-flex; - align-items: center; - gap: 6px; - min-height: 28px; - padding: 3px 10px 3px 8px; - border-radius: 999px; - background: hsl(var(--card)); - box-shadow: inset 0 0 0 1px var(--kante-200); - font-size: 13px; - font-weight: 500; - line-height: 1.35; - color: hsl(var(--foreground)); -} -.trust-list .icon { - width: 14px; - height: 14px; - stroke-width: 2.4; - color: hsl(var(--success-ink)); -} /* Two large drop slots: the whole slot is one button and a drop target */ .drop-slots { @@ -943,7 +921,7 @@ body.is-comparing .empty-launchpad { .drop-slot-btn { width: 100%; height: 100%; - min-height: 156px; + min-height: 128px; padding: 20px 22px; display: flex; flex-direction: column; @@ -989,19 +967,22 @@ body.is-dragging-file .drop-slot-btn { border-radius: 999px; background: hsl(var(--ton-2)); font-family: var(--font-mono); - font-size: 13px; + font-size: var(--text-ui-sm); font-weight: 700; color: hsl(var(--foreground)); } .drop-slot-title { min-width: 0; - font-size: 14.5px; + font-size: var(--text-doc-lg); font-weight: 600; letter-spacing: -0.01em; overflow-wrap: anywhere; } +.drop-slot-hint:empty { + display: none; +} .drop-slot-hint { - font-size: 13.5px; + font-size: var(--text-ui); line-height: 1.45; color: hsl(var(--muted-foreground)); } @@ -1024,7 +1005,7 @@ body.is-dragging-file .drop-slot-btn { background: hsl(var(--card)); box-shadow: inset 0 0 0 1px var(--kante-300), var(--elev-1); color: hsl(var(--foreground)); - font-size: 14px; + font-size: var(--text-ui); font-weight: 600; } .drop-slot.is-next-step .drop-slot-cta-btn { @@ -1032,8 +1013,9 @@ body.is-dragging-file .drop-slot-btn { box-shadow: none; color: hsl(var(--primary-foreground)); } -.drop-slot-drag { - font-size: 13.5px; +.landing-formats { + margin-top: 12px; + font-size: var(--text-ui); color: hsl(var(--muted-foreground)); } .drop-slot.is-loaded .drop-slot-btn { @@ -1071,22 +1053,41 @@ body.is-dragging-file .drop-slot-btn { color: hsl(var(--foreground)); } -.drop-both-hint { - margin-top: 10px; - font-size: 13px; - color: hsl(var(--muted-foreground)); -} .landing-secondary { display: flex; flex-wrap: wrap; - gap: 10px; - margin-top: 18px; + align-items: center; + gap: 4px 10px; + margin-top: 20px; +} +/* Less frequent ways in: plain text buttons next to the example */ +.btn-quiet { + min-height: 38px; + padding: 0 8px; + border: 0; + border-radius: var(--radius-md); + background: transparent; + color: hsl(var(--muted-foreground)); + font: inherit; + font-size: var(--text-ui); + font-weight: 500; + cursor: pointer; +} +.btn-quiet:hover { + color: hsl(var(--foreground)); + text-decoration: underline; + text-underline-offset: 3px; +} +.landing-trust { + margin-top: 28px; + font-size: var(--text-ui); + color: hsl(var(--muted-foreground)); } .landing-secondary .btn { height: 38px; padding: 0 16px; - font-size: 14px; + font-size: var(--text-ui); } .landing-secondary .btn .icon { width: 15px; @@ -1100,7 +1101,7 @@ body.is-dragging-file .drop-slot-btn { border-top: 1px solid hsl(var(--border)); } .landing-section h2 { - font-size: 17px; + font-size: var(--text-section); font-weight: 600; letter-spacing: -0.015em; color: hsl(var(--foreground)); @@ -1115,7 +1116,7 @@ body.is-dragging-file .drop-slot-btn { .how-steps li { display: flex; gap: 12px; - font-size: 14.5px; + font-size: var(--text-doc-lg); line-height: 1.55; color: hsl(var(--muted-foreground)); } @@ -1136,7 +1137,7 @@ body.is-dragging-file .drop-slot-btn { background: hsl(var(--ton-2)); color: hsl(var(--foreground)); font-family: var(--font-mono); - font-size: 13px; + font-size: var(--text-ui-sm); font-weight: 700; } .scope-grid { @@ -1152,7 +1153,7 @@ body.is-dragging-file .drop-slot-btn { box-shadow: inset 0 0 0 1px var(--kante-200); } .scope-col h3 { - font-size: 15px; + font-size: var(--text-title); font-weight: 600; color: hsl(var(--foreground)); } @@ -1166,7 +1167,7 @@ body.is-dragging-file .drop-slot-btn { .scope-col li { position: relative; padding-left: 24px; - font-size: 14.5px; + font-size: var(--text-doc-lg); line-height: 1.5; color: hsl(var(--muted-foreground)); } @@ -1202,7 +1203,7 @@ body.is-dragging-file .drop-slot-btn { padding: 10px 2px; list-style: none; cursor: pointer; - font-size: 15px; + font-size: var(--text-doc-lg); font-weight: 600; color: hsl(var(--foreground)); } @@ -1228,7 +1229,7 @@ body.is-dragging-file .drop-slot-btn { display: flex; flex-direction: column; gap: 10px; - font-size: 14.5px; + font-size: var(--text-doc-lg); line-height: 1.6; color: hsl(var(--muted-foreground)); } @@ -1244,7 +1245,7 @@ body.is-dragging-file .drop-slot-btn { flex-wrap: wrap; align-items: center; gap: 8px; - font-size: 13px; + font-size: var(--text-ui-sm); color: hsl(var(--muted-foreground)); } .lp-link { @@ -1410,7 +1411,7 @@ body.is-dragging-file .drop-zone-overlay, background: hsl(var(--card)); border-radius: var(--radius-md); box-shadow: 0 0 0 1px hsl(var(--border)), var(--elev-3); - font-size: 15px; + font-size: var(--text-doc); font-weight: 600; } @@ -1513,7 +1514,7 @@ body.only-changes .doc-page-card:not(:has(.diff-row-anchor:not([data-type="equal } .clause.kind-h1 { font-weight: 600; - font-size: 1.1875rem; + font-size: var(--text-doc-h1); line-height: 1.35; padding-top: var(--sp-3); padding-bottom: var(--sp-2); @@ -1642,7 +1643,7 @@ ins.tok-ins.tok-marker { display: block; grid-column: 1 / -1; margin-bottom: 2px; - font-size: 13px; + font-size: var(--text-ui-sm); font-weight: 600; color: hsl(var(--muted-foreground)); } @@ -1853,6 +1854,50 @@ ins.tok-ins.tok-marker { width: auto; } +/* Phones: the margin only holds the ✓ / ✗ mark of a reviewed paragraph; the buttons (with undo) + sit in a row just above the bottom bar, so the narrow text column keeps its width. */ +body.tier-phone { + --rv-gutter: 30px; +} +body.tier-phone .review-status { + right: 0; + width: 28px; +} +body.tier-phone .rs-mark { + width: 28px; + height: 28px; +} +body.tier-phone .ra-undo { + display: none; +} +.ra-label { + display: none; +} +body.tier-phone .bottom-bar .ra-btn:not(.ra-edit) { + width: auto; + gap: 6px; + padding: 0 14px 0 12px; + color: hsl(var(--foreground)); + font-size: var(--text-ui); + font-weight: 600; +} +body.tier-phone .bottom-bar .ra-btn:not(.ra-edit) .ra-label { + display: inline; +} +body.tier-phone .bottom-bar .ra-accept .icon { color: hsl(var(--success-ink)); } +body.tier-phone .bottom-bar .ra-reject .icon { color: hsl(var(--destructive-ink)); } +body.tier-phone .bottom-bar .review-bar { + top: auto; + right: max(8px, env(safe-area-inset-right)); + bottom: calc(100% + 8px); + flex-direction: row; + gap: 4px; + padding: 4px; + border-radius: var(--radius-lg); + background: hsl(var(--card)); + box-shadow: 0 0 0 1px hsl(var(--border)), 0 8px 24px -10px hsl(var(--foreground) / 0.3); +} + /* Inline editor */ .clause.is-editing { display: block !important; @@ -1884,7 +1929,7 @@ ins.tok-ins.tok-marker { align-items: center; justify-content: space-between; gap: 8px; - font-size: 13px; + font-size: var(--text-ui-sm); font-weight: 400; color: hsl(var(--muted-foreground)); } @@ -2105,7 +2150,7 @@ ins.tok-ins.tok-marker { padding: 0 6px; border-radius: 999px; background: hsl(var(--ton-2)); - font-size: 12px; + font-size: var(--text-xs); font-weight: 500; line-height: 18px; color: hsl(var(--muted-foreground)); @@ -2155,12 +2200,12 @@ ins.tok-ins.tok-marker { text-align: center; } .empty-title { - font-size: 15px; + font-size: var(--text-doc); font-weight: 600; } .empty-desc { margin-top: 6px; - font-size: 14px; + font-size: var(--text-ui); color: hsl(var(--muted-foreground)); } @@ -2255,7 +2300,7 @@ body:not(.is-comparing) .toast-region { background: hsl(var(--foreground)); color: hsl(var(--background)); box-shadow: var(--elev-3); - font-size: 13px; + font-size: var(--text-ui-sm); font-weight: 500; line-height: 1.4; pointer-events: none; @@ -2301,7 +2346,7 @@ dialog.prime-dialog::backdrop { justify-content: space-between; } .dialog-title { - font-size: 17px; + font-size: var(--text-title); font-weight: 600; letter-spacing: -0.01em; } @@ -2321,7 +2366,7 @@ dialog.prime-dialog::backdrop { flex-direction: column; gap: 16px; overflow-y: auto; - font-size: 15px; + font-size: var(--text-doc); line-height: 1.6; } .dialog-col { @@ -2341,7 +2386,7 @@ dialog.prime-dialog::backdrop { border-radius: var(--radius-md); box-shadow: inset 0 0 0 1px var(--control-edge); font-family: var(--font-sans); - font-size: 15px; + font-size: var(--text-doc); line-height: 1.55; resize: vertical; } @@ -2384,18 +2429,18 @@ dialog.prime-dialog::backdrop { margin-top: 18px; } .help-sec h3 { - font-size: 15px; + font-size: var(--text-doc); font-weight: 600; margin-bottom: 4px; } .help-sec p { - font-size: 15px; + font-size: var(--text-doc); line-height: 1.6; } .help-keys { width: 100%; border-collapse: collapse; - font-size: 15px; + font-size: var(--text-doc); } .help-keys th, .help-keys td { @@ -2427,7 +2472,7 @@ dialog.prime-dialog::backdrop { align-items: center; gap: 10px; min-height: 40px; - font-size: 15px; + font-size: var(--text-doc); cursor: pointer; } .help-setting input { @@ -2502,7 +2547,7 @@ body.is-diff-busy .doc-sheet { background: hsl(var(--destructive-ton-1)); color: hsl(var(--destructive-ink)); box-shadow: inset 0 0 0 1px hsl(var(--destructive) / 0.35), var(--elev-3); - font-size: 14px; + font-size: var(--text-ui); line-height: 1.5; } .import-notice[hidden] { display: none; } @@ -2631,12 +2676,12 @@ body.is-diff-busy .doc-sheet { } body:not(.is-comparing) .app-title { display: inline; - font-size: 16px; + font-size: var(--text-doc-lg); } .tb-files { padding: 0 6px 0 8px; box-shadow: none; - font-size: 13.5px; + font-size: var(--text-ui); } .tb-icon-btn { width: 44px; @@ -2653,7 +2698,8 @@ body.is-diff-busy .doc-sheet { display: none; } .doc-pane-unified { - padding: 10px 10px calc(84px + env(safe-area-inset-bottom)); + /* bottom bar + the review buttons above it */ + padding: 10px 10px calc(144px + env(safe-area-inset-bottom)); } .doc-page-header { padding: var(--sp-5) 0 var(--sp-3) 14px; @@ -2693,7 +2739,7 @@ body.is-diff-busy .doc-sheet { background: transparent; color: hsl(var(--foreground)); font: inherit; - font-size: 15px; + font-size: var(--text-doc); font-weight: 600; display: inline-flex; align-items: center; @@ -2787,7 +2833,7 @@ body.is-diff-busy .doc-sheet { height: 52px; } .search-panel input { - font-size: 16px; /* no iOS zoom on focus */ + font-size: var(--text-doc-lg); /* no iOS zoom on focus */ } .search-panel .tb-icon-btn.sm { width: 40px; @@ -2811,7 +2857,7 @@ body.is-diff-busy .doc-sheet { } .dialog-textarea { height: 150px; - font-size: 16px; + font-size: var(--text-doc-lg); } .dialog-footer { padding: 10px 16px calc(10px + env(safe-area-inset-bottom)); @@ -3257,12 +3303,6 @@ body.list-open .toast-region { .landing-inner { padding-top: 24px; } - .landing-sub { - font-size: 15.5px; - } - .trust-list li { - font-size: 13px; - } .drop-slots { grid-template-columns: 1fr; gap: 12px; @@ -3279,9 +3319,12 @@ body.list-open .toast-region { height: 44px; } .landing-secondary .btn { - flex: 1 1 140px; + flex: 1 1 100%; height: 44px; } + .btn-quiet { + min-height: 44px; + } .landing-section { margin-top: 40px; padding-top: 24px; diff --git a/src/template.html b/src/template.html index 9b21084..205289d 100644 --- a/src/template.html +++ b/src/template.html @@ -325,12 +325,7 @@

Doc Diff

Dokumente einfach vergleichen.

-

PDF, Word oder Text: Jede Änderung wird Wort für Wort markiert. Ihre Dateien verlassen dabei nie Ihren Computer.

-
    -
  • Kostenlos & Open Source (MIT)
  • -
  • Keine Uploads – alles läuft lokal im Browser
  • -
  • Keine Werbung, kein Tracking, keine Cookies
  • -
+

Jede Änderung Wort für Wort markiert. Ihre Dateien verlassen nie Ihr Gerät.

@@ -339,10 +334,9 @@

1 Alte Fassung - PDF (mit Text), Word (.docx), Text + Datei wählen - oder hierher ziehen @@ -353,31 +347,25 @@

2 Neue Fassung - PDF (mit Text), Word (.docx), Text + Datei wählen - oder hierher ziehen

-

Sie können auch beide Dateien auf einmal hierher ziehen – die erste gilt als alte Fassung.

+

PDF, Word (.docx) oder Text · Dateien auch hierher ziehen

- - + +
+

Kostenlos und Open Source (MIT) · Keine Werbung, kein Tracking, keine Cookies