diff --git a/src/__tests__/pdf-shared.test.ts b/src/__tests__/pdf-shared.test.ts
index a283bd7..679c8f5 100644
--- a/src/__tests__/pdf-shared.test.ts
+++ b/src/__tests__/pdf-shared.test.ts
@@ -32,6 +32,17 @@ describe("cleanQuillHtml — inline-style whitelist", () => {
);
});
+ it("restores Quill 2's empty
blank lines to
", () => {
+ // Quill 2's semantic HTML drops the
placeholder from blank lines.
+ expect(cleanQuillHtml("A
B
")).toBe(
+ "A
B
",
+ );
+ // Attribute-carrying and whitespace-only empties too.
+ expect(cleanQuillHtml('
')).toBe(
+ '
',
+ );
+ });
+
it("keeps stripping scripts, event handlers and js: URLs", () => {
const out = cleanQuillHtml(
'' +
diff --git a/src/admin/pages/InvoiceDetail.tsx b/src/admin/pages/InvoiceDetail.tsx
index 7ef2c2a..a80b655 100644
--- a/src/admin/pages/InvoiceDetail.tsx
+++ b/src/admin/pages/InvoiceDetail.tsx
@@ -58,6 +58,7 @@ import {
formatCurrency,
formatDate,
numberOr,
+ restoreQuillBlankLines,
todayLocalStr,
} from "../utils/formatters";
import { normalizeDateStr } from "../utils/attendanceHelpers";
@@ -1670,7 +1671,9 @@ export default function InvoiceDetail() {
{(s.content || "").replace(/<[^>]*>/g, "").trim() && (
)}
@@ -1690,7 +1693,9 @@ export default function InvoiceDetail() {
invoice.internal_notes !== "
" ? (
) : (
diff --git a/src/admin/pages/OrderDetail.tsx b/src/admin/pages/OrderDetail.tsx
index e8c3f51..00f9911 100644
--- a/src/admin/pages/OrderDetail.tsx
+++ b/src/admin/pages/OrderDetail.tsx
@@ -18,7 +18,11 @@ import Typography from "@mui/material/Typography";
import OrderConfirmationModal from "../components/OrderConfirmationModal";
import Forbidden from "../components/Forbidden";
import apiFetch from "../utils/api";
-import { formatCurrency, formatDate } from "../utils/formatters";
+import {
+ formatCurrency,
+ formatDate,
+ restoreQuillBlankLines,
+} from "../utils/formatters";
import { ORDER_STATUS, statusLabel, statusColor } from "../lib/documentStatus";
import {
Button,
@@ -713,7 +717,9 @@ export default function OrderDetail() {
)}
diff --git a/src/admin/utils/formatters.ts b/src/admin/utils/formatters.ts
index 55b2723..b59e755 100644
--- a/src/admin/utils/formatters.ts
+++ b/src/admin/utils/formatters.ts
@@ -76,3 +76,15 @@ export function czechPlural(
if (n >= 2 && n <= 4) return few;
return many;
}
+
+/**
+ * Quill 2's semantic HTML saves blank lines as truly empty , which
+ * collapse to zero height outside the editor — restore the
placeholder
+ * before rendering stored rich text read-only. (The PDF path does the same
+ * in cleanQuillHtml.)
+ */
+export function restoreQuillBlankLines(
+ html: string | null | undefined,
+): string {
+ return (html || "").replace(/]*)>\s*<\/p>/gi, "
");
+}
diff --git a/src/utils/pdf-shared.ts b/src/utils/pdf-shared.ts
index d325f76..6275ebc 100644
--- a/src/utils/pdf-shared.ts
+++ b/src/utils/pdf-shared.ts
@@ -141,6 +141,10 @@ export function cleanQuillHtml(html: string | null | undefined): string {
);
// Replace with regular space (outside of tags)
s = s.replace(/( )/g, " ");
+ // Quill 2's semantic HTML saves blank lines as truly empty , which
+ // collapse to zero height in the PDF — restore the
placeholder so an
+ // editor blank line stays a blank line.
+ s = s.replace(/]*)>\s*<\/p>/gi, "
");
// Inline styles: Quill writes text/background colors as style attributes.
// Keep ONLY color/background-color with literal color values — everything
// else (url(), expression(), positioning…) must never reach Puppeteer.