vomebook commited on
Commit
f412e6e
·
verified ·
1 Parent(s): e695b45

Upload 2 files

Browse files
Files changed (2) hide show
  1. static/app.js +174 -5
  2. static/style.css +140 -0
static/app.js CHANGED
@@ -45,6 +45,7 @@ const PERIOD_PRESETS = [
45
  { label: "南巡-2012", from: "19920118", to: "20121114" },
46
  { label: "2012-", from: "20121115", to: "" },
47
  ];
 
48
 
49
  const DOM = {};
50
 
@@ -582,6 +583,7 @@ function clearPreviewDom() {
582
  DOM.previewPanel.hidden = true;
583
  DOM.previewPanel.style.display = "none";
584
  DOM.previewPanel.innerHTML = "";
 
585
  }
586
 
587
  function closePreview(useHistory = false) {
@@ -1274,6 +1276,7 @@ async function restorePreview(scroll = true) {
1274
  setCachedPreview(docId, item);
1275
  }
1276
  if (requestSeq !== previewRequestSeq || STATE.preview !== docId) return;
 
1277
  DOM.previewPanel.innerHTML = previewHTML(item);
1278
  DOM.previewPanel.hidden = false;
1279
  DOM.previewPanel.style.display = "flex";
@@ -1307,7 +1310,7 @@ function previewHTML(item, compareItem = null) {
1307
  const diff = diffHTML(item.content || "", compareItem.content || "");
1308
  return `<div class="preview-header"><div class="preview-heading">${previewTitleHTML(item)}<div class="preview-compare-note">正在对比:${escapeHTML(item.publication_name)} / ${escapeHTML(compareItem.publication_name)}</div></div><div class="preview-actions">${actions}</div></div><div class="preview-compare-grid"><div>${previewMetaHTML(item)}<pre class="preview-body compare-body">${diff.left}</pre></div><div>${previewMetaHTML(compareItem)}<pre class="preview-body compare-body">${diff.right}</pre></div></div>`;
1309
  }
1310
- return `<div class="preview-header"><div class="preview-heading">${previewTitleHTML(item)}${previewMetaHTML(item)}</div><div class="preview-actions">${actions}</div></div><pre class="preview-body">${highlightPreviewText(item.content || "")}</pre>`;
1311
  }
1312
 
1313
  function previewDownloadHTML(item) {
@@ -1412,9 +1415,130 @@ function changePreviewTagPage(button) {
1412
  }
1413
 
1414
  function compareSelectHTML(item) {
1415
- if (!item.variants || !item.variants.length) return "";
 
 
 
1416
  const options = item.variants.map((variant) => `<option value="${escapeHTML(variant.doc_id)}">${escapeHTML(variant.publication_name)}</option>`).join("");
1417
- return `<label class="preview-compare-control" title="选择对比版本"><span>对比版本</span><select data-compare-select aria-label="选择对比版本"><option value="">对比版本</option>${options}</select></label>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1418
  }
1419
 
1420
  async function comparePreview(docId) {
@@ -1437,6 +1561,7 @@ async function comparePreview(docId) {
1437
  if (!currentCached) setCachedPreview(baseDocId, current);
1438
  if (!targetCached) setCachedPreview(docId, target);
1439
  compareAbortController = null;
 
1440
  DOM.previewPanel.innerHTML = previewHTML(current, target);
1441
  } catch (error) {
1442
  if (error.name === "AbortError" || requestSeq !== compareRequestSeq) return;
@@ -1708,6 +1833,27 @@ function attachEvents() {
1708
  }
1709
  });
1710
  DOM.previewPanel.addEventListener("click", (event) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1711
  const tagPageButton = event.target.closest("[data-preview-tag-page-change]");
1712
  if (tagPageButton) {
1713
  changePreviewTagPage(tagPageButton);
@@ -1721,8 +1867,31 @@ function attachEvents() {
1721
  });
1722
  DOM.previewPanel.addEventListener("change", (event) => {
1723
  const select = event.target.closest("[data-compare-select]");
1724
- if (!select || !select.value) return;
1725
- comparePreview(select.value);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1726
  });
1727
  DOM.prevPage.addEventListener("click", () => {
1728
  DOM.pageInput.value = String(STATE.page);
 
45
  { label: "南巡-2012", from: "19920118", to: "20121114" },
46
  { label: "2012-", from: "20121115", to: "" },
47
  ];
48
+ const TAG_TYPES = ["文稿大类", "文稿类型", "地点", "人物", "出版方/发行方", "主题/事件", "记录", "审核", "翻译", "翻印/传抄"];
49
 
50
  const DOM = {};
51
 
 
583
  DOM.previewPanel.hidden = true;
584
  DOM.previewPanel.style.display = "none";
585
  DOM.previewPanel.innerHTML = "";
586
+ DOM.previewPanel.classList.remove("proofread-mode");
587
  }
588
 
589
  function closePreview(useHistory = false) {
 
1276
  setCachedPreview(docId, item);
1277
  }
1278
  if (requestSeq !== previewRequestSeq || STATE.preview !== docId) return;
1279
+ DOM.previewPanel.classList.remove("proofread-mode");
1280
  DOM.previewPanel.innerHTML = previewHTML(item);
1281
  DOM.previewPanel.hidden = false;
1282
  DOM.previewPanel.style.display = "flex";
 
1310
  const diff = diffHTML(item.content || "", compareItem.content || "");
1311
  return `<div class="preview-header"><div class="preview-heading">${previewTitleHTML(item)}<div class="preview-compare-note">正在对比:${escapeHTML(item.publication_name)} / ${escapeHTML(compareItem.publication_name)}</div></div><div class="preview-actions">${actions}</div></div><div class="preview-compare-grid"><div>${previewMetaHTML(item)}<pre class="preview-body compare-body">${diff.left}</pre></div><div>${previewMetaHTML(compareItem)}<pre class="preview-body compare-body">${diff.right}</pre></div></div>`;
1312
  }
1313
+ return `<div class="preview-header"><div class="preview-heading">${previewTitleHTML(item)}${previewMetaHTML(item)}</div><div class="preview-actions">${actions}</div></div><pre class="preview-body" data-proofread-select>${highlightPreviewText(item.content || "")}</pre>`;
1314
  }
1315
 
1316
  function previewDownloadHTML(item) {
 
1415
  }
1416
 
1417
  function compareSelectHTML(item) {
1418
+ const proofread = item.article && Array.isArray(item.article.parts) && item.source_files && item.source_files.length
1419
+ ? '<option value="__proofread__">对比原文件并校对</option>'
1420
+ : "";
1421
+ if ((!item.variants || !item.variants.length) && !proofread) return "";
1422
  const options = item.variants.map((variant) => `<option value="${escapeHTML(variant.doc_id)}">${escapeHTML(variant.publication_name)}</option>`).join("");
1423
+ return `<label class="preview-compare-control" title="选择对比版本"><span>对比版本</span><select data-compare-select aria-label="选择对比版本"><option value="">对比版本</option>${proofread}${options}</select></label>`;
1424
+ }
1425
+
1426
+ function textDelta(original, edited) {
1427
+ if (original === edited) return "";
1428
+ const removed = original ? `-${original.length}` : "";
1429
+ const added = edited ? `+${encodeURI(edited).replace(/%20/g, " ")}` : "";
1430
+ return [removed, added].filter(Boolean).join("\t");
1431
+ }
1432
+
1433
+ function partTextWithPivots(article, partIndex) {
1434
+ const chars = Array.from(String(article.parts[partIndex].text || ""));
1435
+ (article.comment_pivots || [])
1436
+ .filter((pivot) => Number(pivot.part_idx) === partIndex)
1437
+ .sort((a, b) => Number(b.offset) - Number(a.offset))
1438
+ .forEach((pivot) => chars.splice(Number(pivot.offset), 0, `〔${pivot.index}〕`));
1439
+ return chars.join("");
1440
+ }
1441
+
1442
+ function listEditorHTML(kind, values, fields) {
1443
+ const rows = values.map((value) => {
1444
+ const inputs = fields.map((field) => {
1445
+ const current = typeof value === "object" ? (value[field.key] == null ? "" : value[field.key]) : value;
1446
+ if (field.options) return `<select data-list-field="${field.key}" aria-label="${field.label}"><option value="">${field.label}</option>${field.options.map((option) => `<option value="${escapeHTML(option)}" ${current === option ? "selected" : ""}>${escapeHTML(option)}</option>`).join("")}</select>`;
1447
+ return `<input data-list-field="${field.key}" type="${field.type || "text"}" placeholder="${field.label}" value="${escapeHTML(current)}">`;
1448
+ }).join("");
1449
+ return `<div class="proofread-list-row" data-list-row>${inputs}<button type="button" class="icon-btn-sm" data-remove-list aria-label="删除条目">×</button></div>`;
1450
+ }).join("");
1451
+ return `<div class="proofread-list" data-list="${kind}">${rows}<button type="button" class="text-btn-sm" data-add-list="${kind}">添加</button></div>`;
1452
+ }
1453
+
1454
+ function proofreadHTML(item) {
1455
+ const article = item.article || {};
1456
+ const sourceFiles = item.source_files || [];
1457
+ const requestedPage = Math.max(1, Number(article.page_start) || 1);
1458
+ const defaultFile = sourceFiles.length > 1 ? Math.min(sourceFiles.length - 1, requestedPage - 1) : 0;
1459
+ const sourceOptions = sourceFiles.map((file) => `<option value="${file.index}" ${Number(file.index) === defaultFile ? "selected" : ""}>${escapeHTML(file.name)}</option>`).join("");
1460
+ const sourcePicker = sourceFiles.length > 1 ? `<select data-proofread-source-file aria-label="选择原文件">${sourceOptions}</select>` : "";
1461
+ const pdfPage = sourceFiles.length === 1 ? `#page=${requestedPage}` : "";
1462
+ const parts = (article.parts || []).map((part, index) => `<label class="proofread-field proofread-part" data-proofread-part="${index}"><span>段落 ${index + 1} / ${escapeHTML(part.type || "正文")}</span><textarea data-proofread-part-input="${index}">${escapeHTML(partTextWithPivots(article, index))}</textarea></label>`).join("");
1463
+ const comments = (article.comments || []).map((comment, index) => `<label class="proofread-field"><span>注释 ${index + 1}</span><textarea data-proofread-comment="${index}">${escapeHTML(comment)}</textarea></label>`).join("");
1464
+ const tags = Array.isArray(article.tags) ? article.tags : [];
1465
+ const actions = `<div class="preview-tools"><button type="button" class="icon-btn-sm" data-close-preview aria-label="关闭预览">×</button></div>`;
1466
+ return `<div class="preview-header"><div class="preview-heading">${previewTitleHTML(item)}<div class="preview-compare-note">对比原文件并校对</div></div><div class="preview-actions">${actions}</div></div><div class="proofread-grid"><section class="proofread-source"><div class="proofread-pane-header"><span>原文件</span>${sourcePicker}</div><iframe data-proofread-source title="原文件" src="/api/download/source/${encodeURIComponent(item.doc_id)}?file=${defaultFile}&inline=true${pdfPage}"></iframe></section><form class="proofread-editor" data-proofread-form><div class="proofread-pane-header"><span>校订内容</span><button type="submit" class="result-action-btn primary">提交校订</button></div><fieldset><legend>文章元数据</legend><label class="proofread-field"><span>标题</span><input data-meta-title value="${escapeHTML(article.title || item.title || "")}"></label><label class="proofread-field"><span>作者</span>${listEditorHTML("authors", article.authors || item.authors || [], [{ key: "value", label: "作者" }])}</label><label class="proofread-field"><span>日期</span>${listEditorHTML("dates", article.dates || [], [{ key: "year", label: "年", type: "number" }, { key: "month", label: "月", type: "number" }, { key: "day", label: "日", type: "number" }])}</label><label class="proofread-field"><span>标签</span>${listEditorHTML("tags", tags, [{ key: "name", label: "名称" }, { key: "type", label: "类型", options: TAG_TYPES }])}</label></fieldset><fieldset><legend>来源元数据</legend><label class="proofread-field"><span>来源名称</span><input data-source-name value="${escapeHTML(item.publication_name || "")}"></label><label class="proofread-field"><span>来源作者</span><input data-source-author value="${escapeHTML(item.publication_author || "")}"></label><label class="proofread-field"><span>来源类型</span><input data-source-type value="${escapeHTML(item.publication_type || "")}"></label><label class="proofread-field"><span>来源文件</span>${listEditorHTML("files", sourceFiles.map((file) => file.url), [{ key: "value", label: "URL" }])}</label></fieldset><fieldset><legend>正文</legend><label class="proofread-field"><span>描述</span><textarea data-proofread-description>${escapeHTML(article.description || "")}</textarea></label>${parts}</fieldset><fieldset><legend>注释</legend>${comments || '<div class="proofread-empty">无注释</div>'}</fieldset><label class="proofread-field"><span>说明</span><textarea data-proofread-note maxlength="2000"></textarea></label></form></div>`;
1467
+ }
1468
+
1469
+ function openProofread(item, focusPart = null) {
1470
+ if (!item) return;
1471
+ DOM.previewPanel.classList.add("proofread-mode");
1472
+ DOM.previewPanel.innerHTML = proofreadHTML(item);
1473
+ if (focusPart != null) requestAnimationFrame(() => {
1474
+ const input = DOM.previewPanel.querySelector(`[data-proofread-part-input="${focusPart}"]`);
1475
+ if (input) input.focus();
1476
+ });
1477
+ }
1478
+
1479
+ function readList(form, kind) {
1480
+ return [...form.querySelectorAll(`[data-list="${kind}"] [data-list-row]`)].map((row) => {
1481
+ const values = {};
1482
+ row.querySelectorAll("[data-list-field]").forEach((input) => {
1483
+ if (input.value !== "") values[input.dataset.listField] = input.type === "number" ? Number(input.value) : input.value.trim();
1484
+ });
1485
+ return kind === "authors" || kind === "files" ? (values.value || "") : values;
1486
+ }).filter((value) => typeof value === "string" ? Boolean(value) : Object.keys(value).length);
1487
+ }
1488
+
1489
+ function proofreadPayload(item, form) {
1490
+ const article = item.article || {};
1491
+ const patch = { version: 2, parts: {}, comments: {}, description: "" };
1492
+ form.querySelectorAll("[data-proofread-part-input]").forEach((input) => {
1493
+ const index = Number(input.dataset.proofreadPartInput);
1494
+ const delta = textDelta(partTextWithPivots(article, index), input.value);
1495
+ if (delta) patch.parts[index] = { diff: delta };
1496
+ });
1497
+ form.querySelectorAll("[data-proofread-comment]").forEach((input) => {
1498
+ const index = Number(input.dataset.proofreadComment);
1499
+ const delta = textDelta(String((article.comments || [])[index] || ""), input.value);
1500
+ if (delta) patch.comments[index + 1] = { diff: delta };
1501
+ });
1502
+ patch.description = textDelta(String(article.description || ""), form.querySelector("[data-proofread-description]").value);
1503
+ const articleMeta = {
1504
+ title: form.querySelector("[data-meta-title]").value.trim(),
1505
+ authors: readList(form, "authors"), dates: readList(form, "dates"), tags: readList(form, "tags"),
1506
+ };
1507
+ const sourceMeta = {
1508
+ name: form.querySelector("[data-source-name]").value.trim(),
1509
+ author: form.querySelector("[data-source-author]").value.trim(),
1510
+ type: form.querySelector("[data-source-type]").value.trim(), files: readList(form, "files"),
1511
+ };
1512
+ const changedPatch = Object.keys(patch.parts).length || Object.keys(patch.comments).length || patch.description;
1513
+ const same = (left, right) => JSON.stringify(left) === JSON.stringify(right);
1514
+ const changedArticle = !same(articleMeta, { title: article.title || item.title || "", authors: article.authors || item.authors || [], dates: article.dates || [], tags: article.tags || [] });
1515
+ const changedSource = !same(sourceMeta, { name: item.publication_name || "", author: item.publication_author || "", type: item.publication_type || "", files: (item.source_files || []).map((file) => file.url) });
1516
+ return {
1517
+ doc_id: item.doc_id,
1518
+ patch: changedPatch ? patch : null,
1519
+ metadata: changedArticle || changedSource ? { ...(changedArticle ? { article: articleMeta } : {}), ...(changedSource ? { source: sourceMeta } : {}) } : null,
1520
+ description: form.querySelector("[data-proofread-note]").value.trim(),
1521
+ };
1522
+ }
1523
+
1524
+ async function submitProofreadForm(form) {
1525
+ const item = getCachedPreview(STATE.preview);
1526
+ if (!item) return;
1527
+ const payload = proofreadPayload(item, form);
1528
+ if (!payload.patch && !payload.metadata) {
1529
+ showToast("没有需要提交的修改");
1530
+ return;
1531
+ }
1532
+ const submit = form.querySelector('button[type="submit"]');
1533
+ submit.disabled = true;
1534
+ try {
1535
+ await apiWithTimeout("/api/proofread", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }, 30000);
1536
+ showToast("校订已提交,等待生成 PR");
1537
+ } catch (error) {
1538
+ showToast("校订提交失败");
1539
+ } finally {
1540
+ submit.disabled = false;
1541
+ }
1542
  }
1543
 
1544
  async function comparePreview(docId) {
 
1561
  if (!currentCached) setCachedPreview(baseDocId, current);
1562
  if (!targetCached) setCachedPreview(docId, target);
1563
  compareAbortController = null;
1564
+ DOM.previewPanel.classList.remove("proofread-mode");
1565
  DOM.previewPanel.innerHTML = previewHTML(current, target);
1566
  } catch (error) {
1567
  if (error.name === "AbortError" || requestSeq !== compareRequestSeq) return;
 
1833
  }
1834
  });
1835
  DOM.previewPanel.addEventListener("click", (event) => {
1836
+ const addList = event.target.closest("[data-add-list]");
1837
+ if (addList) {
1838
+ const kind = addList.dataset.addList;
1839
+ const fields = kind === "dates"
1840
+ ? [{ key: "year", label: "年", type: "number" }, { key: "month", label: "月", type: "number" }, { key: "day", label: "日", type: "number" }]
1841
+ : kind === "tags" ? [{ key: "name", label: "名称" }, { key: "type", label: "类型", options: TAG_TYPES }]
1842
+ : [{ key: "value", label: kind === "authors" ? "作者" : "URL" }];
1843
+ const holder = document.createElement("div");
1844
+ holder.innerHTML = listEditorHTML(kind, [kind === "dates" || kind === "tags" ? {} : ""], fields);
1845
+ const row = holder.querySelector("[data-list-row]");
1846
+ addList.before(row);
1847
+ const firstInput = row.querySelector("input");
1848
+ if (firstInput) firstInput.focus();
1849
+ return;
1850
+ }
1851
+ const removeList = event.target.closest("[data-remove-list]");
1852
+ if (removeList) {
1853
+ const row = removeList.closest("[data-list-row]");
1854
+ if (row) row.remove();
1855
+ return;
1856
+ }
1857
  const tagPageButton = event.target.closest("[data-preview-tag-page-change]");
1858
  if (tagPageButton) {
1859
  changePreviewTagPage(tagPageButton);
 
1867
  });
1868
  DOM.previewPanel.addEventListener("change", (event) => {
1869
  const select = event.target.closest("[data-compare-select]");
1870
+ if (select && select.value) {
1871
+ if (select.value === "__proofread__") openProofread(getCachedPreview(STATE.preview));
1872
+ else comparePreview(select.value);
1873
+ return;
1874
+ }
1875
+ const sourceFile = event.target.closest("[data-proofread-source-file]");
1876
+ if (sourceFile) {
1877
+ const frame = DOM.previewPanel.querySelector("[data-proofread-source]");
1878
+ if (frame) frame.src = `/api/download/source/${encodeURIComponent(STATE.preview)}?file=${encodeURIComponent(sourceFile.value)}&inline=true`;
1879
+ }
1880
+ });
1881
+ DOM.previewPanel.addEventListener("submit", (event) => {
1882
+ const form = event.target.closest("[data-proofread-form]");
1883
+ if (!form) return;
1884
+ event.preventDefault();
1885
+ submitProofreadForm(form);
1886
+ });
1887
+ DOM.previewPanel.addEventListener("mouseup", (event) => {
1888
+ if (!event.target.closest("[data-proofread-select]")) return;
1889
+ const selection = window.getSelection();
1890
+ const selected = String(selection ? selection.toString() : "").trim();
1891
+ const item = getCachedPreview(STATE.preview);
1892
+ if (!selected || !item || !item.article || !item.article.parts) return;
1893
+ const partIndex = item.article.parts.findIndex((_part, index) => partTextWithPivots(item.article, index).includes(selected));
1894
+ openProofread(item, partIndex >= 0 ? partIndex : null);
1895
  });
1896
  DOM.prevPage.addEventListener("click", () => {
1897
  DOM.pageInput.value = String(STATE.page);
static/style.css CHANGED
@@ -559,6 +559,126 @@ ul, li {
559
  border-right: 1px solid var(--outline-variant);
560
  }
561
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
562
  .preview-compare-grid .preview-path {
563
  padding: 10px 14px 0;
564
  }
@@ -650,6 +770,26 @@ body.mobile .preview-compare-grid {
650
  overflow-y: auto;
651
  }
652
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
653
  body.mobile .preview-compare-grid > div:first-child {
654
  border-right: none;
655
  border-bottom: 1px solid var(--outline-variant);
 
559
  border-right: 1px solid var(--outline-variant);
560
  }
561
 
562
+ .preview-panel.proofread-mode {
563
+ max-height: calc(100vh - 110px);
564
+ }
565
+
566
+ .proofread-grid {
567
+ display: grid;
568
+ grid-template-columns: minmax(360px, 1fr) minmax(420px, 1fr);
569
+ min-height: 0;
570
+ flex: 1;
571
+ }
572
+
573
+ .proofread-source,
574
+ .proofread-editor {
575
+ min-width: 0;
576
+ min-height: 0;
577
+ }
578
+
579
+ .proofread-source {
580
+ display: flex;
581
+ flex-direction: column;
582
+ border-right: 1px solid var(--outline-variant);
583
+ }
584
+
585
+ .proofread-source iframe {
586
+ width: 100%;
587
+ min-height: 520px;
588
+ flex: 1;
589
+ border: 0;
590
+ background: #fff;
591
+ }
592
+
593
+ .proofread-pane-header {
594
+ display: flex;
595
+ min-height: 42px;
596
+ align-items: center;
597
+ justify-content: space-between;
598
+ gap: 8px;
599
+ padding: 7px 12px;
600
+ border-bottom: 1px solid var(--outline-variant);
601
+ font-size: 13px;
602
+ font-weight: 600;
603
+ }
604
+
605
+ .proofread-pane-header select {
606
+ max-width: 70%;
607
+ }
608
+
609
+ .proofread-editor {
610
+ overflow: auto;
611
+ padding: 12px;
612
+ }
613
+
614
+ .proofread-editor fieldset {
615
+ margin: 0 0 14px;
616
+ padding: 10px;
617
+ border: 1px solid var(--outline-variant);
618
+ border-radius: 6px;
619
+ }
620
+
621
+ .proofread-editor legend {
622
+ padding: 0 5px;
623
+ color: var(--on-surface-variant);
624
+ font-size: 12px;
625
+ }
626
+
627
+ .proofread-field {
628
+ display: grid;
629
+ gap: 5px;
630
+ margin-bottom: 10px;
631
+ font-size: 12px;
632
+ color: var(--on-surface-variant);
633
+ }
634
+
635
+ .proofread-field input,
636
+ .proofread-field select,
637
+ .proofread-field textarea,
638
+ .proofread-pane-header select {
639
+ box-sizing: border-box;
640
+ width: 100%;
641
+ border: 1px solid var(--outline-variant);
642
+ border-radius: 4px;
643
+ background: var(--surface);
644
+ color: var(--on-surface);
645
+ padding: 7px 8px;
646
+ font: inherit;
647
+ }
648
+
649
+ .proofread-field textarea {
650
+ min-height: 76px;
651
+ resize: vertical;
652
+ line-height: 1.6;
653
+ }
654
+
655
+ .proofread-part textarea {
656
+ min-height: 120px;
657
+ font-family: "Noto Serif SC", "Songti SC", "STSong", "SimSun", var(--font-sans);
658
+ font-size: 15px;
659
+ }
660
+
661
+ .proofread-list {
662
+ display: grid;
663
+ gap: 6px;
664
+ }
665
+
666
+ .proofread-list-row {
667
+ display: grid;
668
+ grid-template-columns: repeat(auto-fit, minmax(72px, 1fr)) 28px;
669
+ gap: 5px;
670
+ align-items: center;
671
+ }
672
+
673
+ .proofread-list > .text-btn-sm {
674
+ justify-self: start;
675
+ }
676
+
677
+ .proofread-empty {
678
+ color: var(--on-surface-variant);
679
+ font-size: 12px;
680
+ }
681
+
682
  .preview-compare-grid .preview-path {
683
  padding: 10px 14px 0;
684
  }
 
770
  overflow-y: auto;
771
  }
772
 
773
+ body.mobile .proofread-grid {
774
+ display: block;
775
+ overflow-y: auto;
776
+ }
777
+
778
+ body.mobile .proofread-source {
779
+ height: 52vh;
780
+ border-right: 0;
781
+ border-bottom: 1px solid var(--reader-divider, var(--outline-variant));
782
+ }
783
+
784
+ body.mobile .proofread-source iframe {
785
+ min-height: 0;
786
+ }
787
+
788
+ body.mobile .proofread-editor {
789
+ overflow: visible;
790
+ padding-bottom: max(28px, env(safe-area-inset-bottom));
791
+ }
792
+
793
  body.mobile .preview-compare-grid > div:first-child {
794
  border-right: none;
795
  border-bottom: 1px solid var(--outline-variant);