document-cli 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import { c as formatDocxExtrasLines, f as loadProvidedFonts, i as formatOdbReportLines, l as readInput, m as inferFormatFromExtension, n as describeOdbReport, o as formatMetadataLines, p as formatToExtension, r as formatOdbFormLines, t as describeOdbForm } from "./odb-structure-Du1w_hOU.js";
2
2
  import { readFile, writeFile } from "node:fs/promises";
3
- import { buildDocxPackage, buildOdtPackage, convertWordprocessingToLayout, createDocx, createFontMeasurer, createFontRegistry, createOdg, createOdp, createOds, createOdt, createPptx, decodeMarkdownText, docxToPdf, encodeMarkdownText, encodePackage, hsqldbCellDisplayText, markdownToPdf, odgToPdf, odpToPdf, odsToPdf, odtToPdf, openDocx, openOdg, openOdp, openOds, openOdt, openPptx, pptxToPdf, readDocxContent, readDocxExtras, readMarkdownContent, readOdbForms, readOdbReportContent, readOdbReports, readOdbTables, readOdgContent, readOdpContent, readOdsContent, readOdtContent, readPdf, readPptxContent, rgbHexToColor, writePdf, xlsxToPdf } from "documents.js";
3
+ import { buildDocxPackage, buildOdtPackage, convertWordprocessingToLayout, createDocx, createFontMeasurer, createFontRegistry, createOdg, createOdp, createOds, createOdt, createPptx, decodeMarkdownText, docxToPdf, encodeMarkdownText, encodePackage, hsqldbCellDisplayText, markdownToPdf, odgToPdf, odpToPdf, odsToPdf, odtToPdf, openDocx, openOdg, openOdp, openOds, openOdt, openPptx, parseXml, pptxToPdf, readDocxContent, readDocxExtras, readMarkdownContent, readOdbForms, readOdbReportContent, readOdbReports, readOdbTables, readOdgContent, readOdpContent, readOdsContent, readOdtContent, readPdf, readPptxContent, rgbHexToColor, writePdf, xlsxToPdf } from "documents.js";
4
4
  import { basename, dirname, extname, join } from "node:path";
5
5
  import { cellReference, columnIndexToLetters, decodePackage as decodePackage$1, encodePackage as encodePackage$1 } from "odf.js";
6
6
  import { readdirSync } from "node:fs";
@@ -230,6 +230,7 @@ function selectionKeyFor(screen) {
230
230
  case "slideDetail":
231
231
  case "notesEditor": return `${screen.kind}:${screen.slideIndex}`;
232
232
  case "shapeEditor": return `shapeEditor:${screen.slideIndex}:${screen.shapeIndex}`;
233
+ case "slideTableDetail": return `slideTableDetail:${screen.slideIndex}:${screen.tableIndex}`;
233
234
  case "spreadsheetGrid":
234
235
  case "printSettingsEditor": return `${screen.kind}:${screen.sheetIndex}`;
235
236
  case "cellDetail": return `cellDetail:${screen.sheetIndex}:${screen.row}:${screen.col}`;
@@ -435,6 +436,38 @@ function mutate(state, doc, apply) {
435
436
  undoStack: pushSnapshot(state.undoStack, snapshot)
436
437
  };
437
438
  }
439
+ function mutateGuarded(state, doc, apply) {
440
+ try {
441
+ return mutate(state, doc, apply);
442
+ } catch (error) {
443
+ return withStatus(state, "warning", error instanceof Error ? error.message : String(error));
444
+ }
445
+ }
446
+ function mergePptxTableCells(table, startRow, startColumn, rowSpan, colSpan) {
447
+ if (!Number.isInteger(rowSpan) || rowSpan < 1 || !Number.isInteger(colSpan) || colSpan < 1) throw new Error(`mergeSlideTableCells: rowSpan and colSpan must be positive integers, got rowSpan=${rowSpan}, colSpan=${colSpan}`);
448
+ const rows = table.rows();
449
+ if (startRow + rowSpan > rows.length) throw new Error(`mergeSlideTableCells: rowSpan ${rowSpan} starting at row ${startRow} exceeds this table's own ${rows.length} rows`);
450
+ const anchorRow = rows[startRow];
451
+ if (anchorRow === void 0) throw new Error(`mergeSlideTableCells: row ${startRow} does not exist in this table`);
452
+ const columnCount = anchorRow.cells().length;
453
+ if (startColumn + colSpan > columnCount) throw new Error(`mergeSlideTableCells: colSpan ${colSpan} starting at column ${startColumn} exceeds this table's own ${columnCount} columns`);
454
+ for (let rowOffset = 0; rowOffset < rowSpan; rowOffset++) {
455
+ const row = rows[startRow + rowOffset];
456
+ if (row === void 0) throw new Error(`mergeSlideTableCells: row ${startRow + rowOffset} does not exist in this table`);
457
+ const cells = row.cells();
458
+ for (let columnOffset = 0; columnOffset < colSpan; columnOffset++) {
459
+ const cell = cells[startColumn + columnOffset];
460
+ if (cell === void 0) throw new Error(`mergeSlideTableCells: column ${startColumn + columnOffset} does not exist in row ${startRow + rowOffset}`);
461
+ if (rowOffset === 0 && columnOffset === 0) {
462
+ cell.colSpan = colSpan;
463
+ cell.rowSpan = rowSpan;
464
+ continue;
465
+ }
466
+ if (columnOffset > 0) cell.horizontalMerge = true;
467
+ if (rowOffset > 0) cell.verticalMerge = true;
468
+ }
469
+ }
470
+ }
438
471
  function mutateMarkdown(state, doc, source) {
439
472
  const snapshot = encodeMarkdownText(doc.source);
440
473
  return {
@@ -536,6 +569,31 @@ function setTextContainerText(container, text) {
536
569
  function setCellText(cell, text) {
537
570
  setTextContainerText(cell, text);
538
571
  }
572
+ function mutableMathMlNode(node) {
573
+ if (node.type === "element") return {
574
+ type: "element",
575
+ tag: node.tag,
576
+ attributes: [...node.attributes],
577
+ children: node.children.map(mutableMathMlNode)
578
+ };
579
+ if (node.type === "text") return {
580
+ type: "text",
581
+ value: node.value
582
+ };
583
+ if (node.type === "cdata" || node.type === "comment") return {
584
+ type: node.type,
585
+ value: ""
586
+ };
587
+ if (node.type === "declaration") return {
588
+ type: "declaration",
589
+ attributes: []
590
+ };
591
+ return {
592
+ type: "pi",
593
+ target: "",
594
+ content: ""
595
+ };
596
+ }
539
597
  function appReducer(state, action) {
540
598
  switch (action.type) {
541
599
  case "PUSH_SCREEN": return {
@@ -685,11 +743,21 @@ function appReducer(state, action) {
685
743
  case "APPEND_TABLE": {
686
744
  const doc = wordprocessingDocument(state);
687
745
  if (doc === void 0) return wrongDocument(state, "a docx or odt document");
688
- return mutate(state, doc, () => {
689
- doc.editor.body.appendTable({
746
+ return mutateGuarded(state, doc, () => {
747
+ const table = doc.editor.body.appendTable({
690
748
  rows: action.rows,
691
749
  columns: action.columns
692
750
  });
751
+ if (action.merge !== void 0) table.mergeCells(action.merge.startRow, action.merge.startColumn, action.merge.rowSpan, action.merge.colSpan);
752
+ });
753
+ }
754
+ case "MERGE_TABLE_CELLS": {
755
+ const doc = wordprocessingDocument(state);
756
+ if (doc === void 0) return wrongDocument(state, "a docx or odt document");
757
+ const table = tableAt(doc, action.tableIndex);
758
+ if (table === void 0) return withStatus(state, "warning", `There is no table at index ${action.tableIndex}`);
759
+ return mutateGuarded(state, doc, () => {
760
+ table.mergeCells(action.startRow, action.startColumn, action.rowSpan, action.colSpan);
693
761
  });
694
762
  }
695
763
  case "SET_TABLE_CELL_TEXT": {
@@ -732,6 +800,39 @@ function appReducer(state, action) {
732
800
  setTextContainerText(item, action.text);
733
801
  });
734
802
  }
803
+ case "INSERT_PARAGRAPH_IMAGE": {
804
+ const doc = wordprocessingDocument(state);
805
+ if (doc === void 0) return wrongDocument(state, "a docx or odt document");
806
+ const paragraph = paragraphAt(doc, action.blockIndex);
807
+ if (paragraph === void 0) return withStatus(state, "warning", `There is no paragraph at index ${action.blockIndex}`);
808
+ return mutate(state, doc, () => {
809
+ paragraph.insertImageAfter({
810
+ format: action.format,
811
+ bytes: action.bytes,
812
+ widthPt: action.widthPt,
813
+ heightPt: action.heightPt,
814
+ altText: action.altText
815
+ });
816
+ });
817
+ }
818
+ case "INSERT_DOCX_FORMULA": {
819
+ const doc = state.openDocument;
820
+ if (doc?.format !== "docx") return wrongDocument(state, "a docx document");
821
+ const paragraph = doc.editor.paragraphs()[action.blockIndex];
822
+ if (paragraph === void 0) return withStatus(state, "warning", `There is no paragraph at index ${action.blockIndex}`);
823
+ let written = true;
824
+ const nextState = mutate(state, doc, () => {
825
+ written = paragraph.appendOfficeMath(action.mathml).written;
826
+ });
827
+ return written ? nextState : withStatus(nextState, "warning", "The formula produced no OMML content and was not written");
828
+ }
829
+ case "INSERT_ODT_FORMULA": {
830
+ const doc = state.openDocument;
831
+ if (doc?.format !== "odt") return wrongDocument(state, "an odt document");
832
+ return mutate(state, doc, () => {
833
+ doc.editor.body.appendFormula({ mathml: action.mathml.map(mutableMathMlNode) }, action.frame);
834
+ });
835
+ }
735
836
  case "ADD_SLIDE": {
736
837
  const doc = presentationDocument(state);
737
838
  if (doc === void 0) return wrongDocument(state, "a pptx or odp document");
@@ -754,6 +855,22 @@ function appReducer(state, action) {
754
855
  });
755
856
  });
756
857
  }
858
+ case "MERGE_SLIDE_TABLE_CELLS": {
859
+ const doc = presentationDocument(state);
860
+ if (doc === void 0) return wrongDocument(state, "a pptx or odp document");
861
+ if (doc.format === "odp") {
862
+ const entry = doc.editor.slides()[action.slideIndex]?.tables()[action.tableIndex];
863
+ if (entry === void 0) return withStatus(state, "warning", `There is no table at index ${action.tableIndex} on slide ${action.slideIndex}`);
864
+ return mutateGuarded(state, doc, () => {
865
+ entry.table.mergeCells(action.startRow, action.startColumn, action.rowSpan, action.colSpan);
866
+ });
867
+ }
868
+ const table = doc.editor.slides()[action.slideIndex]?.tables()[action.tableIndex];
869
+ if (table === void 0) return withStatus(state, "warning", `There is no table at index ${action.tableIndex} on slide ${action.slideIndex}`);
870
+ return mutateGuarded(state, doc, () => {
871
+ mergePptxTableCells(table, action.startRow, action.startColumn, action.rowSpan, action.colSpan);
872
+ });
873
+ }
757
874
  case "ADD_TEXTBOX": {
758
875
  const doc = shapeHostDocument(state);
759
876
  if (doc === void 0) return wrongDocument(state, "a pptx, odp or odg document");
@@ -826,6 +943,15 @@ function appReducer(state, action) {
826
943
  case "SET_CELL_VALUE": return withSheet(state, action.sheetIndex, (sheet) => {
827
944
  sheet.cell(action.row, action.column).value = action.value;
828
945
  });
946
+ case "MERGE_CELLS": {
947
+ const doc = spreadsheetDocument(state);
948
+ if (doc === void 0) return wrongDocument(state, "an ods document");
949
+ const sheet = sheetAt(doc, action.sheetIndex);
950
+ if (sheet === void 0) return withStatus(state, "warning", `There is no sheet at index ${action.sheetIndex}`);
951
+ return mutateGuarded(state, doc, () => {
952
+ sheet.mergeCells(action.startRow, action.startColumn, action.rowSpan, action.colSpan);
953
+ });
954
+ }
829
955
  case "SET_SHEET_PRINT_SETTINGS": return withSheet(state, action.sheetIndex, (sheet) => {
830
956
  sheet.printSettings = action.printSettings;
831
957
  });
@@ -1600,12 +1726,238 @@ function StatusLine() {
1600
1726
  ] });
1601
1727
  }
1602
1728
  //#endregion
1729
+ //#region src/tui/screens/shared/field-wizard.tsx
1730
+ function requireFieldValue(values, key) {
1731
+ const value = values[key];
1732
+ if (value === void 0) throw new Error(`Field wizard field '${key}' was never recorded before building the action.`);
1733
+ return value;
1734
+ }
1735
+ function FieldWizard(props) {
1736
+ const [stepIndex, setStepIndex] = useState(0);
1737
+ const [collected, setCollected] = useState({});
1738
+ const initialField = props.fields[0];
1739
+ const [draft, setDraft] = useState(initialField === void 0 ? "" : initialField.defaultValue);
1740
+ const field = props.fields[stepIndex];
1741
+ if (field === void 0) throw new Error(`FieldWizard stepIndex ${stepIndex} is out of range for ${props.fields.length} fields -- onComplete always fires before stepIndex can advance past the last field, so this indicates a bug in that advance.`);
1742
+ return /* @__PURE__ */ jsxs(Box, {
1743
+ flexDirection: "column",
1744
+ borderStyle: "round",
1745
+ paddingX: 1,
1746
+ children: [
1747
+ /* @__PURE__ */ jsx(Text, {
1748
+ bold: true,
1749
+ children: field.label
1750
+ }),
1751
+ /* @__PURE__ */ jsx(TextField, {
1752
+ value: draft,
1753
+ isFocused: true,
1754
+ onChange: setDraft,
1755
+ onCancel: props.onCancel,
1756
+ onSubmit: (value) => {
1757
+ const recorded = {
1758
+ ...collected,
1759
+ [field.key]: value
1760
+ };
1761
+ const nextIndex = stepIndex + 1;
1762
+ const nextField = props.fields[nextIndex];
1763
+ if (nextField === void 0) {
1764
+ props.onComplete(recorded);
1765
+ return;
1766
+ }
1767
+ setCollected(recorded);
1768
+ setDraft(nextField.defaultValue);
1769
+ setStepIndex(nextIndex);
1770
+ }
1771
+ }),
1772
+ /* @__PURE__ */ jsxs(Text, {
1773
+ dimColor: true,
1774
+ children: [
1775
+ "Step ",
1776
+ stepIndex + 1,
1777
+ " of ",
1778
+ props.fields.length,
1779
+ " -- Enter to continue, Esc to cancel"
1780
+ ]
1781
+ })
1782
+ ]
1783
+ });
1784
+ }
1785
+ //#endregion
1786
+ //#region src/tui/screens/shared/formula-presets.ts
1787
+ function element(tag, children = []) {
1788
+ return {
1789
+ type: "element",
1790
+ tag,
1791
+ attributes: [],
1792
+ children
1793
+ };
1794
+ }
1795
+ function text(value) {
1796
+ return {
1797
+ type: "text",
1798
+ value
1799
+ };
1800
+ }
1801
+ function mi(name) {
1802
+ return element("mi", [text(name)]);
1803
+ }
1804
+ function mn(value) {
1805
+ return element("mn", [text(value)]);
1806
+ }
1807
+ function mo(operator) {
1808
+ return element("mo", [text(operator)]);
1809
+ }
1810
+ const FORMULA_PRESETS = [
1811
+ {
1812
+ label: "Fraction: x / 2",
1813
+ mathml: [element("mfrac", [mi("x"), mn("2")])]
1814
+ },
1815
+ {
1816
+ label: "Power: x^2",
1817
+ mathml: [element("msup", [mi("x"), mn("2")])]
1818
+ },
1819
+ {
1820
+ label: "Subscript: x_i",
1821
+ mathml: [element("msub", [mi("x"), mi("i")])]
1822
+ },
1823
+ {
1824
+ label: "Square root: sqrt(x)",
1825
+ mathml: [element("msqrt", [mi("x")])]
1826
+ },
1827
+ {
1828
+ label: "Summation: sum(i=1..n) i",
1829
+ mathml: [element("munderover", [
1830
+ mo("∑"),
1831
+ element("mrow", [
1832
+ mi("i"),
1833
+ mo("="),
1834
+ mn("1")
1835
+ ]),
1836
+ mi("n")
1837
+ ])]
1838
+ },
1839
+ {
1840
+ label: "Quadratic formula",
1841
+ mathml: [element("mrow", [
1842
+ mi("x"),
1843
+ mo("="),
1844
+ element("mfrac", [element("mrow", [
1845
+ mo("-"),
1846
+ mi("b"),
1847
+ mo("±"),
1848
+ element("msqrt", [element("mrow", [
1849
+ element("msup", [mi("b"), mn("2")]),
1850
+ mo("-"),
1851
+ mn("4"),
1852
+ mi("a"),
1853
+ mi("c")
1854
+ ])])
1855
+ ]), element("mrow", [mn("2"), mi("a")])])
1856
+ ])]
1857
+ }
1858
+ ];
1859
+ //#endregion
1860
+ //#region src/tui/screens/shared/formula-picker.tsx
1861
+ const RAW_ENTRY_LABEL = "Raw MathML...";
1862
+ const PICKER_ROWS = [...FORMULA_PRESETS.map((preset) => ({
1863
+ label: preset.label,
1864
+ mathml: preset.mathml
1865
+ })), {
1866
+ label: RAW_ENTRY_LABEL,
1867
+ mathml: void 0
1868
+ }];
1869
+ function FormulaPicker(props) {
1870
+ const [rawInput, setRawInput] = useState(void 0);
1871
+ const { selectedIndex } = useNavigationInput({
1872
+ itemCount: PICKER_ROWS.length,
1873
+ isActive: props.isActive && rawInput === void 0,
1874
+ onBack: props.onCancel,
1875
+ onSelect: (index) => {
1876
+ const row = PICKER_ROWS[index];
1877
+ if (row === void 0) return;
1878
+ if (row.mathml === void 0) {
1879
+ setRawInput("");
1880
+ return;
1881
+ }
1882
+ props.onMathml(row.mathml);
1883
+ }
1884
+ });
1885
+ if (rawInput !== void 0) return /* @__PURE__ */ jsxs(Box, {
1886
+ flexDirection: "column",
1887
+ borderStyle: "round",
1888
+ paddingX: 1,
1889
+ children: [
1890
+ /* @__PURE__ */ jsx(Text, {
1891
+ bold: true,
1892
+ children: "Raw MathML (the children of the <math> root, e.g. <mfrac>...</mfrac>)"
1893
+ }),
1894
+ /* @__PURE__ */ jsx(TextField, {
1895
+ value: rawInput,
1896
+ isFocused: true,
1897
+ onChange: setRawInput,
1898
+ onCancel: () => {
1899
+ setRawInput(void 0);
1900
+ },
1901
+ onSubmit: (value) => {
1902
+ try {
1903
+ const parsed = parseXml(value);
1904
+ props.onMathml(parsed);
1905
+ } catch (error) {
1906
+ props.onInvalidRawMathml(describeError(error));
1907
+ }
1908
+ setRawInput(void 0);
1909
+ }
1910
+ }),
1911
+ /* @__PURE__ */ jsx(Text, {
1912
+ dimColor: true,
1913
+ children: "Enter to insert, Esc to cancel"
1914
+ })
1915
+ ]
1916
+ });
1917
+ return /* @__PURE__ */ jsxs(Box, {
1918
+ flexDirection: "column",
1919
+ borderStyle: "round",
1920
+ paddingX: 1,
1921
+ children: [
1922
+ /* @__PURE__ */ jsx(Text, {
1923
+ bold: true,
1924
+ children: "Insert formula -- choose a preset or write raw MathML"
1925
+ }),
1926
+ /* @__PURE__ */ jsx(ListView, {
1927
+ items: PICKER_ROWS,
1928
+ selectedIndex,
1929
+ reservedRows: PICKER_ROWS.length + 2,
1930
+ renderItem: (row, isSelected) => /* @__PURE__ */ jsxs(Text, {
1931
+ color: isSelected ? "cyan" : void 0,
1932
+ children: [isSelected ? "> " : " ", row.label]
1933
+ })
1934
+ }),
1935
+ /* @__PURE__ */ jsx(Text, {
1936
+ dimColor: true,
1937
+ children: "Enter to choose, Esc to cancel"
1938
+ })
1939
+ ]
1940
+ });
1941
+ }
1942
+ //#endregion
1603
1943
  //#region src/tui/screens/shared/text.ts
1604
1944
  function truncatePreview(text, maxLength) {
1605
1945
  const singleLine = text.replace(/\s+/gu, " ").trim();
1606
1946
  if (singleLine.length === 0) return "(empty)";
1607
1947
  return singleLine.length > maxLength ? `${singleLine.slice(0, Math.max(0, maxLength - 1))}…` : singleLine;
1608
1948
  }
1949
+ function parsePositiveIntField(raw, fallback) {
1950
+ const parsed = Number.parseInt(raw, 10);
1951
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
1952
+ }
1953
+ function parseNonNegativeIntField(raw, fallback) {
1954
+ const parsed = Number.parseInt(raw, 10);
1955
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
1956
+ }
1957
+ function parseNumberField(raw, fallback) {
1958
+ const parsed = Number.parseFloat(raw);
1959
+ return Number.isFinite(parsed) ? parsed : fallback;
1960
+ }
1609
1961
  //#endregion
1610
1962
  //#region src/tui/screens/shared/paragraph-family.tsx
1611
1963
  function paragraphFamilyDocument(openDocument) {
@@ -1669,6 +2021,38 @@ function tableSummary(table) {
1669
2021
  function listSummary(list, index) {
1670
2022
  return `List ${index} (${list.itemCount} item${list.itemCount === 1 ? "" : "s"})`;
1671
2023
  }
2024
+ const DEFAULT_TABLE_ROWS$1 = 2;
2025
+ const DEFAULT_TABLE_COLUMNS$1 = 2;
2026
+ const FORMULA_FRAME_FIELDS = [
2027
+ {
2028
+ key: "xPt",
2029
+ label: "X (pt)",
2030
+ defaultValue: "40"
2031
+ },
2032
+ {
2033
+ key: "yPt",
2034
+ label: "Y (pt)",
2035
+ defaultValue: "40"
2036
+ },
2037
+ {
2038
+ key: "widthPt",
2039
+ label: "Width (pt)",
2040
+ defaultValue: "120"
2041
+ },
2042
+ {
2043
+ key: "heightPt",
2044
+ label: "Height (pt)",
2045
+ defaultValue: "40"
2046
+ }
2047
+ ];
2048
+ function readFormulaFrame(values) {
2049
+ return {
2050
+ xPt: parseNumberField(requireFieldValue(values, "xPt"), 0),
2051
+ yPt: parseNumberField(requireFieldValue(values, "yPt"), 0),
2052
+ widthPt: parseNumberField(requireFieldValue(values, "widthPt"), 120),
2053
+ heightPt: parseNumberField(requireFieldValue(values, "heightPt"), 40)
2054
+ };
2055
+ }
1672
2056
  function ParagraphFamilyBodyList(props) {
1673
2057
  const { adapter } = props;
1674
2058
  const state = useAppState();
@@ -1711,9 +2095,94 @@ function ParagraphFamilyBodyList(props) {
1711
2095
  if (row.kind !== "header") acc.push(rowIndex);
1712
2096
  return acc;
1713
2097
  }, []);
2098
+ const [tableWizard, setTableWizard] = useState("closed");
2099
+ const [wizardDraft, setWizardDraft] = useState("");
2100
+ const [wizardRows, setWizardRows] = useState(DEFAULT_TABLE_ROWS$1);
2101
+ const [wizardColumns, setWizardColumns] = useState(DEFAULT_TABLE_COLUMNS$1);
2102
+ const [wizardStartRow, setWizardStartRow] = useState(0);
2103
+ const [wizardStartColumn, setWizardStartColumn] = useState(0);
2104
+ const [wizardRowSpan, setWizardRowSpan] = useState(1);
2105
+ const wizardOpen = tableWizard !== "closed";
2106
+ const [formulaFlow, setFormulaFlow] = useState("closed");
2107
+ const [pendingFormulaMathml, setPendingFormulaMathml] = useState(void 0);
2108
+ const formulaFlowOpen = formulaFlow !== "closed";
2109
+ const closeWizard = () => {
2110
+ setTableWizard("closed");
2111
+ };
2112
+ const commitAppendTable = (merge) => {
2113
+ const newIndex = tables.length;
2114
+ dispatch({
2115
+ type: "APPEND_TABLE",
2116
+ rows: wizardRows,
2117
+ columns: wizardColumns,
2118
+ merge
2119
+ });
2120
+ closeWizard();
2121
+ dispatch({
2122
+ type: "PUSH_SCREEN",
2123
+ screen: {
2124
+ kind: "tableView",
2125
+ blockIndex: newIndex
2126
+ }
2127
+ });
2128
+ };
2129
+ useInput((input) => {
2130
+ if (input === "T") {
2131
+ setWizardDraft(String(DEFAULT_TABLE_ROWS$1));
2132
+ setTableWizard("rows");
2133
+ }
2134
+ }, { isActive: !anyOverlayOpen(state) && !wizardOpen && !formulaFlowOpen });
2135
+ useInput((input) => {
2136
+ if (input === "m" && adapter.formatLabel === "odt") setFormulaFlow("picking");
2137
+ }, { isActive: !anyOverlayOpen(state) && !wizardOpen && !formulaFlowOpen });
2138
+ useInput((input, key) => {
2139
+ if (key.escape) {
2140
+ closeWizard();
2141
+ return;
2142
+ }
2143
+ if (input === "y" || input === "Y") {
2144
+ setWizardDraft("0");
2145
+ setTableWizard("mergeStartRow");
2146
+ return;
2147
+ }
2148
+ if (input === "n" || input === "N" || key.return) commitAppendTable(void 0);
2149
+ }, { isActive: !anyOverlayOpen(state) && tableWizard === "mergePrompt" });
2150
+ const submitWizardRows = (raw) => {
2151
+ setWizardRows(parsePositiveIntField(raw, DEFAULT_TABLE_ROWS$1));
2152
+ setWizardDraft(String(DEFAULT_TABLE_COLUMNS$1));
2153
+ setTableWizard("columns");
2154
+ };
2155
+ const submitWizardColumns = (raw) => {
2156
+ setWizardColumns(parsePositiveIntField(raw, DEFAULT_TABLE_COLUMNS$1));
2157
+ setTableWizard("mergePrompt");
2158
+ };
2159
+ const submitWizardMergeStartRow = (raw) => {
2160
+ setWizardStartRow(Math.min(parseNonNegativeIntField(raw, 0), Math.max(0, wizardRows - 1)));
2161
+ setWizardDraft("0");
2162
+ setTableWizard("mergeStartColumn");
2163
+ };
2164
+ const submitWizardMergeStartColumn = (raw) => {
2165
+ setWizardStartColumn(Math.min(parseNonNegativeIntField(raw, 0), Math.max(0, wizardColumns - 1)));
2166
+ setWizardDraft("1");
2167
+ setTableWizard("mergeRowSpan");
2168
+ };
2169
+ const submitWizardMergeRowSpan = (raw) => {
2170
+ setWizardRowSpan(Math.min(parsePositiveIntField(raw, 1), Math.max(1, wizardRows - wizardStartRow)));
2171
+ setWizardDraft("1");
2172
+ setTableWizard("mergeColSpan");
2173
+ };
2174
+ const submitWizardMergeColSpan = (raw) => {
2175
+ const colSpan = Math.min(parsePositiveIntField(raw, 1), Math.max(1, wizardColumns - wizardStartColumn));
2176
+ commitAppendTable({
2177
+ startRow: wizardStartRow,
2178
+ startColumn: wizardStartColumn,
2179
+ rowSpan: wizardRowSpan,
2180
+ colSpan
2181
+ });
2182
+ };
1714
2183
  const { selectedIndex } = usePersistedSelection(selectionKeyFor({ kind: "bodyList" }), {
1715
2184
  itemCount: selectableRowIndices.length,
1716
- isActive: !anyOverlayOpen(state),
2185
+ isActive: !anyOverlayOpen(state) && !wizardOpen && !formulaFlowOpen,
1717
2186
  onBack: () => {
1718
2187
  dispatch({ type: "POP_SCREEN" });
1719
2188
  },
@@ -1800,9 +2269,126 @@ function ParagraphFamilyBodyList(props) {
1800
2269
  });
1801
2270
  }
1802
2271
  }),
1803
- /* @__PURE__ */ jsx(Text, {
2272
+ tableWizard === "rows" ? /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
2273
+ color: "cyan",
2274
+ children: "Rows: "
2275
+ }), /* @__PURE__ */ jsx(TextField, {
2276
+ value: wizardDraft,
2277
+ isFocused: true,
2278
+ onChange: setWizardDraft,
2279
+ onSubmit: submitWizardRows,
2280
+ onCancel: closeWizard
2281
+ })] }) : void 0,
2282
+ tableWizard === "columns" ? /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
2283
+ color: "cyan",
2284
+ children: "Columns: "
2285
+ }), /* @__PURE__ */ jsx(TextField, {
2286
+ value: wizardDraft,
2287
+ isFocused: true,
2288
+ onChange: setWizardDraft,
2289
+ onSubmit: submitWizardColumns,
2290
+ onCancel: closeWizard
2291
+ })] }) : void 0,
2292
+ tableWizard === "mergePrompt" ? /* @__PURE__ */ jsx(Text, {
2293
+ color: "cyan",
2294
+ children: "Merge cells now? y/N"
2295
+ }) : void 0,
2296
+ tableWizard === "mergeStartRow" ? /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsxs(Text, {
2297
+ color: "cyan",
2298
+ children: [
2299
+ "Merge start row (0-",
2300
+ Math.max(0, wizardRows - 1),
2301
+ "): "
2302
+ ]
2303
+ }), /* @__PURE__ */ jsx(TextField, {
2304
+ value: wizardDraft,
2305
+ isFocused: true,
2306
+ onChange: setWizardDraft,
2307
+ onSubmit: submitWizardMergeStartRow,
2308
+ onCancel: closeWizard
2309
+ })] }) : void 0,
2310
+ tableWizard === "mergeStartColumn" ? /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsxs(Text, {
2311
+ color: "cyan",
2312
+ children: [
2313
+ "Merge start column (0-",
2314
+ Math.max(0, wizardColumns - 1),
2315
+ "): "
2316
+ ]
2317
+ }), /* @__PURE__ */ jsx(TextField, {
2318
+ value: wizardDraft,
2319
+ isFocused: true,
2320
+ onChange: setWizardDraft,
2321
+ onSubmit: submitWizardMergeStartColumn,
2322
+ onCancel: closeWizard
2323
+ })] }) : void 0,
2324
+ tableWizard === "mergeRowSpan" ? /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsxs(Text, {
2325
+ color: "cyan",
2326
+ children: [
2327
+ "Merge row span (1-",
2328
+ Math.max(1, wizardRows - wizardStartRow),
2329
+ "): "
2330
+ ]
2331
+ }), /* @__PURE__ */ jsx(TextField, {
2332
+ value: wizardDraft,
2333
+ isFocused: true,
2334
+ onChange: setWizardDraft,
2335
+ onSubmit: submitWizardMergeRowSpan,
2336
+ onCancel: closeWizard
2337
+ })] }) : void 0,
2338
+ tableWizard === "mergeColSpan" ? /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsxs(Text, {
2339
+ color: "cyan",
2340
+ children: [
2341
+ "Merge column span (1-",
2342
+ Math.max(1, wizardColumns - wizardStartColumn),
2343
+ "): "
2344
+ ]
2345
+ }), /* @__PURE__ */ jsx(TextField, {
2346
+ value: wizardDraft,
2347
+ isFocused: true,
2348
+ onChange: setWizardDraft,
2349
+ onSubmit: submitWizardMergeColSpan,
2350
+ onCancel: closeWizard
2351
+ })] }) : void 0,
2352
+ formulaFlow === "picking" ? /* @__PURE__ */ jsx(FormulaPicker, {
2353
+ isActive: !anyOverlayOpen(state),
2354
+ onCancel: () => {
2355
+ setFormulaFlow("closed");
2356
+ },
2357
+ onMathml: (mathml) => {
2358
+ setPendingFormulaMathml(mathml);
2359
+ setFormulaFlow("frame");
2360
+ },
2361
+ onInvalidRawMathml: (message) => {
2362
+ dispatch({
2363
+ type: "SET_STATUS",
2364
+ severity: "warning",
2365
+ text: `Could not parse MathML: ${message}`
2366
+ });
2367
+ }
2368
+ }) : void 0,
2369
+ formulaFlow === "frame" ? /* @__PURE__ */ jsx(FieldWizard, {
2370
+ fields: FORMULA_FRAME_FIELDS,
2371
+ onCancel: () => {
2372
+ setFormulaFlow("closed");
2373
+ setPendingFormulaMathml(void 0);
2374
+ },
2375
+ onComplete: (values) => {
2376
+ if (pendingFormulaMathml !== void 0) dispatch({
2377
+ type: "INSERT_ODT_FORMULA",
2378
+ mathml: pendingFormulaMathml,
2379
+ frame: readFormulaFrame(values)
2380
+ });
2381
+ setFormulaFlow("closed");
2382
+ setPendingFormulaMathml(void 0);
2383
+ }
2384
+ }) : void 0,
2385
+ /* @__PURE__ */ jsxs(Text, {
1804
2386
  dimColor: true,
1805
- children: "Enter to open, a to append a paragraph, Esc back"
2387
+ children: [
2388
+ "Enter to open, a to append a paragraph, T to append a table",
2389
+ adapter.formatLabel === "odt" ? ", m to insert a formula" : "",
2390
+ ", Esc back"
2391
+ ]
1806
2392
  })
1807
2393
  ]
1808
2394
  });
@@ -1822,6 +2408,66 @@ function isValidHexColorInput(input) {
1822
2408
  }
1823
2409
  //#endregion
1824
2410
  //#region src/tui/screens/editors/docx/paragraph-detail.tsx
2411
+ const IMAGE_FIELDS = [
2412
+ {
2413
+ key: "path",
2414
+ label: "Image file path (.png/.jpg/.jpeg)",
2415
+ defaultValue: ""
2416
+ },
2417
+ {
2418
+ key: "widthPt",
2419
+ label: "Width (pt)",
2420
+ defaultValue: "100"
2421
+ },
2422
+ {
2423
+ key: "heightPt",
2424
+ label: "Height (pt)",
2425
+ defaultValue: "60"
2426
+ },
2427
+ {
2428
+ key: "altText",
2429
+ label: "Alt text, blank for none",
2430
+ defaultValue: ""
2431
+ }
2432
+ ];
2433
+ function inferImageFormat$1(path) {
2434
+ const extension = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
2435
+ if (extension === "png") return "png";
2436
+ if (extension === "jpg" || extension === "jpeg") return "jpeg";
2437
+ }
2438
+ async function applyInsertImage(blockIndex, values, dispatch) {
2439
+ const path = requireFieldValue(values, "path");
2440
+ const format = inferImageFormat$1(path);
2441
+ if (format === void 0) {
2442
+ dispatch({
2443
+ type: "SET_STATUS",
2444
+ severity: "warning",
2445
+ text: `${path} is not a .png or .jpg/.jpeg file -- image not inserted`
2446
+ });
2447
+ return;
2448
+ }
2449
+ try {
2450
+ const bytes = new Uint8Array(await readInput(path));
2451
+ const widthPt = parseNumberField(requireFieldValue(values, "widthPt"), 100);
2452
+ const heightPt = parseNumberField(requireFieldValue(values, "heightPt"), 60);
2453
+ const altTextRaw = requireFieldValue(values, "altText").trim();
2454
+ dispatch({
2455
+ type: "INSERT_PARAGRAPH_IMAGE",
2456
+ blockIndex,
2457
+ format,
2458
+ bytes,
2459
+ widthPt,
2460
+ heightPt,
2461
+ altText: altTextRaw.length === 0 ? void 0 : altTextRaw
2462
+ });
2463
+ } catch (error) {
2464
+ dispatch({
2465
+ type: "SET_STATUS",
2466
+ severity: "error",
2467
+ text: `Could not read ${path}: ${describeError(error)}`
2468
+ });
2469
+ }
2470
+ }
1825
2471
  function ParagraphRunsView(props) {
1826
2472
  if (props.runs.length === 0) return /* @__PURE__ */ jsx(Text, {
1827
2473
  dimColor: true,
@@ -1841,6 +2487,8 @@ function ParagraphDetailScreen() {
1841
2487
  const dispatch = useAppDispatch();
1842
2488
  const [runIndex, setRunIndex] = useState(0);
1843
2489
  const [colorInput, setColorInput] = useState(void 0);
2490
+ const [imageWizardOpen, setImageWizardOpen] = useState(false);
2491
+ const [formulaPickerOpen, setFormulaPickerOpen] = useState(false);
1844
2492
  const screen = currentScreen(state);
1845
2493
  const doc = paragraphFamilyDocument(state.openDocument);
1846
2494
  const paragraph = screen.kind === "paragraphDetail" && doc !== void 0 ? liveParagraphAt(doc, screen.blockIndex) : void 0;
@@ -1903,6 +2551,14 @@ function ParagraphDetailScreen() {
1903
2551
  });
1904
2552
  return;
1905
2553
  }
2554
+ if (input === "I") {
2555
+ setImageWizardOpen(true);
2556
+ return;
2557
+ }
2558
+ if (input === "m" && doc?.format === "docx") {
2559
+ setFormulaPickerOpen(true);
2560
+ return;
2561
+ }
1906
2562
  if (selectedRun === void 0) return;
1907
2563
  if (input === "b") {
1908
2564
  dispatch({
@@ -1929,7 +2585,7 @@ function ParagraphDetailScreen() {
1929
2585
  return;
1930
2586
  }
1931
2587
  if (input === "c") setColorInput(selectedRun.color === void 0 ? "" : layoutColorToHex(selectedRun.color).slice(1));
1932
- }, { isActive: !anyOverlayOpen(state) && colorInput === void 0 });
2588
+ }, { isActive: !anyOverlayOpen(state) && colorInput === void 0 && !imageWizardOpen && !formulaPickerOpen });
1933
2589
  if (screen.kind !== "paragraphDetail") return /* @__PURE__ */ jsx(Text, {
1934
2590
  color: "red",
1935
2591
  children: "ParagraphDetailScreen rendered outside a paragraphDetail screen."
@@ -1987,9 +2643,45 @@ function ParagraphDetailScreen() {
1987
2643
  setColorInput(void 0);
1988
2644
  }
1989
2645
  })] }),
1990
- /* @__PURE__ */ jsx(Text, {
2646
+ imageWizardOpen ? /* @__PURE__ */ jsx(FieldWizard, {
2647
+ fields: IMAGE_FIELDS,
2648
+ onCancel: () => {
2649
+ setImageWizardOpen(false);
2650
+ },
2651
+ onComplete: (values) => {
2652
+ applyInsertImage(blockIndex, values, dispatch).then(() => {
2653
+ setImageWizardOpen(false);
2654
+ });
2655
+ }
2656
+ }) : void 0,
2657
+ formulaPickerOpen ? /* @__PURE__ */ jsx(FormulaPicker, {
2658
+ isActive: !anyOverlayOpen(state),
2659
+ onCancel: () => {
2660
+ setFormulaPickerOpen(false);
2661
+ },
2662
+ onMathml: (mathml) => {
2663
+ dispatch({
2664
+ type: "INSERT_DOCX_FORMULA",
2665
+ blockIndex,
2666
+ mathml
2667
+ });
2668
+ setFormulaPickerOpen(false);
2669
+ },
2670
+ onInvalidRawMathml: (message) => {
2671
+ dispatch({
2672
+ type: "SET_STATUS",
2673
+ severity: "warning",
2674
+ text: `Could not parse MathML: ${message}`
2675
+ });
2676
+ }
2677
+ }) : void 0,
2678
+ /* @__PURE__ */ jsxs(Text, {
1991
2679
  dimColor: true,
1992
- children: "<- / -> move, Enter edit text, b/i/u toggle, c colour, a append run, Esc back"
2680
+ children: [
2681
+ "<- / -> move, Enter edit text, b/i/u toggle, c colour, a append run, I image",
2682
+ doc.format === "docx" ? ", m formula" : "",
2683
+ ", Esc back"
2684
+ ]
1993
2685
  })
1994
2686
  ]
1995
2687
  });
@@ -2079,7 +2771,7 @@ function RunEditorScreen() {
2079
2771
  }
2080
2772
  //#endregion
2081
2773
  //#region src/tui/screens/editors/docx/table-view.tsx
2082
- const CELL_WIDTH$1 = 16;
2774
+ const CELL_WIDTH$2 = 16;
2083
2775
  function TableViewScreen() {
2084
2776
  const state = useAppState();
2085
2777
  const dispatch = useAppDispatch();
@@ -2087,6 +2779,7 @@ function TableViewScreen() {
2087
2779
  row: 0,
2088
2780
  column: 0
2089
2781
  });
2782
+ const [mergeAnchor, setMergeAnchor] = useState(void 0);
2090
2783
  const screen = currentScreen(state);
2091
2784
  const doc = paragraphFamilyDocument(state.openDocument);
2092
2785
  const table = screen.kind === "tableView" && doc !== void 0 ? liveTableAt(doc, screen.blockIndex) : void 0;
@@ -2095,6 +2788,22 @@ function TableViewScreen() {
2095
2788
  const columnCount = rows[0]?.cells().length ?? 0;
2096
2789
  const clampedRow = rowCount === 0 ? 0 : Math.min(cursor.row, rowCount - 1);
2097
2790
  const clampedColumn = columnCount === 0 ? 0 : Math.min(cursor.column, columnCount - 1);
2791
+ const commitMerge = (anchor) => {
2792
+ if (screen.kind !== "tableView") return;
2793
+ const startRow = Math.min(anchor.row, clampedRow);
2794
+ const startColumn = Math.min(anchor.column, clampedColumn);
2795
+ const rowSpan = Math.abs(clampedRow - anchor.row) + 1;
2796
+ const colSpan = Math.abs(clampedColumn - anchor.column) + 1;
2797
+ dispatch({
2798
+ type: "MERGE_TABLE_CELLS",
2799
+ tableIndex: screen.blockIndex,
2800
+ startRow,
2801
+ startColumn,
2802
+ rowSpan,
2803
+ colSpan
2804
+ });
2805
+ setMergeAnchor(void 0);
2806
+ };
2098
2807
  useInput((input, key) => {
2099
2808
  if (table === void 0 || screen.kind !== "tableView") return;
2100
2809
  if (key.upArrow || input === "k") {
@@ -2126,18 +2835,36 @@ function TableViewScreen() {
2126
2835
  return;
2127
2836
  }
2128
2837
  if (key.escape) {
2838
+ if (mergeAnchor !== void 0) {
2839
+ setMergeAnchor(void 0);
2840
+ return;
2841
+ }
2129
2842
  dispatch({ type: "POP_SCREEN" });
2130
2843
  return;
2131
2844
  }
2132
- if (key.return && rowCount > 0 && columnCount > 0) dispatch({
2133
- type: "PUSH_SCREEN",
2134
- screen: {
2135
- kind: "tableCellDetail",
2136
- blockIndex: screen.blockIndex,
2137
- row: clampedRow,
2138
- col: clampedColumn
2845
+ if (input === "m" && rowCount > 0 && columnCount > 0) {
2846
+ if (mergeAnchor === void 0) setMergeAnchor({
2847
+ row: clampedRow,
2848
+ column: clampedColumn
2849
+ });
2850
+ else commitMerge(mergeAnchor);
2851
+ return;
2852
+ }
2853
+ if (key.return && rowCount > 0 && columnCount > 0) {
2854
+ if (mergeAnchor !== void 0) {
2855
+ commitMerge(mergeAnchor);
2856
+ return;
2139
2857
  }
2140
- });
2858
+ dispatch({
2859
+ type: "PUSH_SCREEN",
2860
+ screen: {
2861
+ kind: "tableCellDetail",
2862
+ blockIndex: screen.blockIndex,
2863
+ row: clampedRow,
2864
+ col: clampedColumn
2865
+ }
2866
+ });
2867
+ }
2141
2868
  }, { isActive: !anyOverlayOpen(state) });
2142
2869
  if (screen.kind !== "tableView") return /* @__PURE__ */ jsx(Text, {
2143
2870
  color: "red",
@@ -2175,10 +2902,11 @@ function TableViewScreen() {
2175
2902
  children: "This table has no rows."
2176
2903
  }) : rows.map((row, rowIndex) => /* @__PURE__ */ jsx(Box, { children: row.cells().map((cell, columnIndex) => {
2177
2904
  const isSelected = rowIndex === clampedRow && columnIndex === clampedColumn;
2905
+ const isAnchor = rowIndex === mergeAnchor?.row && columnIndex === mergeAnchor?.column;
2178
2906
  return /* @__PURE__ */ jsx(Box, {
2179
- width: CELL_WIDTH$1,
2907
+ width: CELL_WIDTH$2,
2180
2908
  borderStyle: "single",
2181
- borderColor: isSelected ? "cyan" : "gray",
2909
+ borderColor: isSelected ? "cyan" : isAnchor ? "yellow" : "gray",
2182
2910
  children: /* @__PURE__ */ jsx(Text, {
2183
2911
  color: isSelected ? "cyan" : void 0,
2184
2912
  inverse: isSelected,
@@ -2188,7 +2916,7 @@ function TableViewScreen() {
2188
2916
  }) }, rowIndex)),
2189
2917
  /* @__PURE__ */ jsx(Text, {
2190
2918
  dimColor: true,
2191
- children: "Arrows/hjkl move, Enter to edit a cell, Esc back"
2919
+ children: mergeAnchor === void 0 ? "Arrows/hjkl move, Enter to edit a cell, m to anchor a merge, Esc back" : "Arrows/hjkl to the opposite corner, m/Enter to merge, Esc to cancel"
2192
2920
  })
2193
2921
  ]
2194
2922
  });
@@ -3092,10 +3820,6 @@ function describeFillStroke(vector) {
3092
3820
  if (vector.stroke !== void 0) parts.push(`stroke ${formatColor$1(vector.stroke.color)} ${formatPt(vector.stroke.widthPt)}pt`);
3093
3821
  return parts.length === 0 ? "no fill or stroke" : parts.join(", ");
3094
3822
  }
3095
- function parseNumberField(raw, fallback) {
3096
- const parsed = Number.parseFloat(raw);
3097
- return Number.isFinite(parsed) ? parsed : fallback;
3098
- }
3099
3823
  function parseColorField(raw) {
3100
3824
  const trimmed = raw.trim();
3101
3825
  if (trimmed.length === 0) return;
@@ -3152,11 +3876,6 @@ function defaultTriangleSubpaths(widthPt, heightPt) {
3152
3876
  closed: true
3153
3877
  }];
3154
3878
  }
3155
- function requireFieldValue(values, key) {
3156
- const value = values[key];
3157
- if (value === void 0) throw new Error(`Add-item field '${key}' was never recorded before building the action.`);
3158
- return value;
3159
- }
3160
3879
  //#endregion
3161
3880
  //#region src/tui/screens/editors/odg/page-list.tsx
3162
3881
  function OdgPageListScreen() {
@@ -3463,56 +4182,6 @@ async function applyAddKind(kind, pageIndex, doc, values, dispatch) {
3463
4182
  }
3464
4183
  }
3465
4184
  }
3466
- function FieldWizard(props) {
3467
- const [stepIndex, setStepIndex] = useState(0);
3468
- const [collected, setCollected] = useState({});
3469
- const initialField = props.fields[0];
3470
- const [draft, setDraft] = useState(initialField === void 0 ? "" : initialField.defaultValue);
3471
- const field = props.fields[stepIndex];
3472
- if (field === void 0) throw new Error(`FieldWizard stepIndex ${stepIndex} is out of range for ${props.fields.length} fields -- onComplete always fires before stepIndex can advance past the last field, so this indicates a bug in that advance.`);
3473
- return /* @__PURE__ */ jsxs(Box, {
3474
- flexDirection: "column",
3475
- borderStyle: "round",
3476
- paddingX: 1,
3477
- children: [
3478
- /* @__PURE__ */ jsx(Text, {
3479
- bold: true,
3480
- children: field.label
3481
- }),
3482
- /* @__PURE__ */ jsx(TextField, {
3483
- value: draft,
3484
- isFocused: true,
3485
- onChange: setDraft,
3486
- onCancel: props.onCancel,
3487
- onSubmit: (value) => {
3488
- const recorded = {
3489
- ...collected,
3490
- [field.key]: value
3491
- };
3492
- const nextIndex = stepIndex + 1;
3493
- const nextField = props.fields[nextIndex];
3494
- if (nextField === void 0) {
3495
- props.onComplete(recorded);
3496
- return;
3497
- }
3498
- setCollected(recorded);
3499
- setDraft(nextField.defaultValue);
3500
- setStepIndex(nextIndex);
3501
- }
3502
- }),
3503
- /* @__PURE__ */ jsxs(Text, {
3504
- dimColor: true,
3505
- children: [
3506
- "Step ",
3507
- stepIndex + 1,
3508
- " of ",
3509
- props.fields.length,
3510
- " -- Enter to continue, Esc to cancel"
3511
- ]
3512
- })
3513
- ]
3514
- });
3515
- }
3516
4185
  function AddItemFlow(props) {
3517
4186
  const dispatch = useAppDispatch();
3518
4187
  const [kind, setKind] = useState(void 0);
@@ -4367,6 +5036,30 @@ function ShapeEditorScreen(props) {
4367
5036
  });
4368
5037
  }
4369
5038
  //#endregion
5039
+ //#region src/tui/screens/shared/slide-table.ts
5040
+ function resolveSlideTable(doc, slideIndex, tableIndex) {
5041
+ const content = doc.format === "odp" ? readOdpContent(doc.editor.toPackage()) : readPptxContent(doc.editor.toPackage());
5042
+ if (content.kind !== "presentation") throw new Error("readPptxContent/readOdpContent always resolve a presentation package to the presentation ContentDocument variant.");
5043
+ const slide = content.slides[slideIndex];
5044
+ if (slide === void 0) return;
5045
+ return slide.shapes.flatMap((shape) => shape.blocks.filter((block) => block.kind === "table"))[tableIndex];
5046
+ }
5047
+ function slideTableCellText(cell) {
5048
+ return cell.blocks.filter((block) => block.kind === "paragraph").map((paragraph) => paragraph.runs.map((run) => run.text).join("")).join("\n");
5049
+ }
5050
+ function summarizeGridTable(table, index) {
5051
+ const rows = table.rows();
5052
+ return {
5053
+ index,
5054
+ rowCount: rows.length,
5055
+ columnCount: rows[0]?.cells().length ?? 0
5056
+ };
5057
+ }
5058
+ function summarizeSlideTables(doc, slideIndex) {
5059
+ if (doc.format === "odp") return (doc.editor.slides()[slideIndex]?.tables() ?? []).map((entry, index) => summarizeGridTable(entry.table, index));
5060
+ return (doc.editor.slides()[slideIndex]?.tables() ?? []).map((table, index) => summarizeGridTable(table, index));
5061
+ }
5062
+ //#endregion
4370
5063
  //#region src/tui/screens/editors/pptx/slide-detail.tsx
4371
5064
  const IMAGE_EXTENSION_TO_FORMAT = {
4372
5065
  png: "png",
@@ -4375,10 +5068,6 @@ const IMAGE_EXTENSION_TO_FORMAT = {
4375
5068
  };
4376
5069
  const DEFAULT_TABLE_ROWS = 2;
4377
5070
  const DEFAULT_TABLE_COLUMNS = 2;
4378
- function parsePositiveIntField(raw, fallback) {
4379
- const parsed = Number.parseInt(raw, 10);
4380
- return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
4381
- }
4382
5071
  function imageFormatFromPath(path) {
4383
5072
  const dotIndex = path.lastIndexOf(".");
4384
5073
  if (dotIndex < 0) return;
@@ -4399,34 +5088,63 @@ function SlideDetailScreen(props) {
4399
5088
  const doc = assertPresentationDocument(state.openDocument);
4400
5089
  const { slideIndex } = props.screen;
4401
5090
  const slide = doc.editor.slides()[slideIndex];
4402
- const rows = (slide === void 0 ? [] : slide.shapes()).map((shape, index) => ({
5091
+ const shapes = slide === void 0 ? [] : slide.shapes();
5092
+ const tableSummaries = slide === void 0 ? [] : summarizeSlideTables(doc, slideIndex);
5093
+ const rows = [...shapes.map((shape, index) => ({
5094
+ kind: "shape",
4403
5095
  index,
4404
5096
  text: shape.text,
4405
5097
  frame: shape.frame
4406
- }));
5098
+ })), ...tableSummaries.length > 0 ? [{
5099
+ kind: "tablesHeader",
5100
+ count: tableSummaries.length
5101
+ }, ...tableSummaries.map((summary) => ({
5102
+ kind: "table",
5103
+ index: summary.index,
5104
+ rowCount: summary.rowCount,
5105
+ columnCount: summary.columnCount
5106
+ }))] : []];
5107
+ const selectableRowIndices = rows.reduce((acc, row, index) => {
5108
+ if (row.kind !== "tablesHeader") acc.push(index);
5109
+ return acc;
5110
+ }, []);
4407
5111
  const [addMode, setAddMode] = useState("closed");
4408
5112
  const [draft, setDraft] = useState("");
4409
5113
  const [imageError, setImageError] = useState(void 0);
4410
5114
  const [tableRows, setTableRows] = useState(DEFAULT_TABLE_ROWS);
4411
5115
  const formIsOpen = addMode !== "closed";
4412
5116
  const { selectedIndex } = useNavigationInput({
4413
- itemCount: rows.length,
5117
+ itemCount: selectableRowIndices.length,
4414
5118
  isActive: !overlayOpen && !formIsOpen,
4415
5119
  onBack: () => {
4416
5120
  dispatch({ type: "POP_SCREEN" });
4417
5121
  },
4418
5122
  onSelect: (index) => {
4419
- dispatch({
4420
- type: "SET_SELECTION",
4421
- key: selectionKeyFor(props.screen),
4422
- index
4423
- });
4424
- dispatch({
5123
+ const rowIndex = selectableRowIndices[index];
5124
+ const row = rowIndex === void 0 ? void 0 : rows[rowIndex];
5125
+ if (row === void 0) return;
5126
+ if (row.kind === "shape") {
5127
+ dispatch({
5128
+ type: "SET_SELECTION",
5129
+ key: selectionKeyFor(props.screen),
5130
+ index
5131
+ });
5132
+ dispatch({
5133
+ type: "PUSH_SCREEN",
5134
+ screen: {
5135
+ kind: "shapeEditor",
5136
+ slideIndex,
5137
+ shapeIndex: row.index
5138
+ }
5139
+ });
5140
+ return;
5141
+ }
5142
+ if (row.kind === "table") dispatch({
4425
5143
  type: "PUSH_SCREEN",
4426
5144
  screen: {
4427
- kind: "shapeEditor",
5145
+ kind: "slideTableDetail",
4428
5146
  slideIndex,
4429
- shapeIndex: index
5147
+ tableIndex: row.index
4430
5148
  }
4431
5149
  });
4432
5150
  },
@@ -4434,6 +5152,7 @@ function SlideDetailScreen(props) {
4434
5152
  setAddMode("chooseKind");
4435
5153
  }
4436
5154
  });
5155
+ const listSelectedIndex = selectableRowIndices[selectedIndex] ?? -1;
4437
5156
  useInput((input, key) => {
4438
5157
  if (key.escape) {
4439
5158
  setAddMode("closed");
@@ -4516,9 +5235,9 @@ function SlideDetailScreen(props) {
4516
5235
  "Slide ",
4517
5236
  slideIndex + 1,
4518
5237
  " -- ",
4519
- rows.length,
5238
+ shapes.length,
4520
5239
  " shape",
4521
- rows.length === 1 ? "" : "s"
5240
+ shapes.length === 1 ? "" : "s"
4522
5241
  ]
4523
5242
  }),
4524
5243
  slide === void 0 ? /* @__PURE__ */ jsx(Text, {
@@ -4526,20 +5245,45 @@ function SlideDetailScreen(props) {
4526
5245
  children: "This slide no longer exists -- press Esc to go back"
4527
5246
  }) : /* @__PURE__ */ jsx(ListView, {
4528
5247
  items: rows,
4529
- selectedIndex,
5248
+ selectedIndex: listSelectedIndex,
4530
5249
  emptyMessage: "No shapes yet -- press 'a' to add one",
4531
- renderItem: (row, isSelected) => /* @__PURE__ */ jsxs(Text, {
4532
- color: isSelected ? "cyan" : void 0,
4533
- inverse: isSelected,
4534
- children: [
4535
- row.index + 1,
4536
- ". ",
4537
- describeSlideFamilyShape({
4538
- text: row.text,
4539
- frame: row.frame
4540
- })
4541
- ]
4542
- })
5250
+ renderItem: (row, isSelected) => {
5251
+ if (row.kind === "tablesHeader") return /* @__PURE__ */ jsxs(Text, {
5252
+ bold: true,
5253
+ dimColor: true,
5254
+ children: [
5255
+ "Tables (",
5256
+ row.count,
5257
+ ")"
5258
+ ]
5259
+ });
5260
+ if (row.kind === "table") return /* @__PURE__ */ jsxs(Text, {
5261
+ color: isSelected ? "cyan" : void 0,
5262
+ inverse: isSelected,
5263
+ children: [
5264
+ " ",
5265
+ "Table ",
5266
+ row.index + 1,
5267
+ " (",
5268
+ row.rowCount,
5269
+ "x",
5270
+ row.columnCount,
5271
+ ")"
5272
+ ]
5273
+ });
5274
+ return /* @__PURE__ */ jsxs(Text, {
5275
+ color: isSelected ? "cyan" : void 0,
5276
+ inverse: isSelected,
5277
+ children: [
5278
+ row.index + 1,
5279
+ ". ",
5280
+ describeSlideFamilyShape({
5281
+ text: row.text,
5282
+ frame: row.frame
5283
+ })
5284
+ ]
5285
+ });
5286
+ }
4543
5287
  }),
4544
5288
  addMode === "chooseKind" ? /* @__PURE__ */ jsx(Text, {
4545
5289
  color: "cyan",
@@ -4607,6 +5351,146 @@ function SlideDetailScreen(props) {
4607
5351
  });
4608
5352
  }
4609
5353
  //#endregion
5354
+ //#region src/tui/screens/editors/pptx/slide-table-detail.tsx
5355
+ const CELL_WIDTH$1 = 16;
5356
+ function SlideTableDetailScreen(props) {
5357
+ const state = useAppState();
5358
+ const dispatch = useAppDispatch();
5359
+ const doc = assertPresentationDocument(state.openDocument);
5360
+ const { slideIndex, tableIndex } = props.screen;
5361
+ const [cursor, setCursor] = useState({
5362
+ row: 0,
5363
+ column: 0
5364
+ });
5365
+ const [mergeAnchor, setMergeAnchor] = useState(void 0);
5366
+ const table = resolveSlideTable(doc, slideIndex, tableIndex);
5367
+ const rows = table?.rows ?? [];
5368
+ const rowCount = rows.length;
5369
+ const columnCount = rows[0]?.cells.length ?? 0;
5370
+ const clampedRow = rowCount === 0 ? 0 : Math.min(cursor.row, rowCount - 1);
5371
+ const clampedColumn = columnCount === 0 ? 0 : Math.min(cursor.column, columnCount - 1);
5372
+ const commitMerge = (anchor) => {
5373
+ const startRow = Math.min(anchor.row, clampedRow);
5374
+ const startColumn = Math.min(anchor.column, clampedColumn);
5375
+ const rowSpan = Math.abs(clampedRow - anchor.row) + 1;
5376
+ const colSpan = Math.abs(clampedColumn - anchor.column) + 1;
5377
+ dispatch({
5378
+ type: "MERGE_SLIDE_TABLE_CELLS",
5379
+ slideIndex,
5380
+ tableIndex,
5381
+ startRow,
5382
+ startColumn,
5383
+ rowSpan,
5384
+ colSpan
5385
+ });
5386
+ setMergeAnchor(void 0);
5387
+ };
5388
+ useInput((input, key) => {
5389
+ if (table === void 0) return;
5390
+ if (key.upArrow || input === "k") {
5391
+ setCursor({
5392
+ row: Math.max(0, clampedRow - 1),
5393
+ column: clampedColumn
5394
+ });
5395
+ return;
5396
+ }
5397
+ if (key.downArrow || input === "j") {
5398
+ setCursor({
5399
+ row: rowCount === 0 ? 0 : Math.min(rowCount - 1, clampedRow + 1),
5400
+ column: clampedColumn
5401
+ });
5402
+ return;
5403
+ }
5404
+ if (key.leftArrow || input === "h") {
5405
+ setCursor({
5406
+ row: clampedRow,
5407
+ column: Math.max(0, clampedColumn - 1)
5408
+ });
5409
+ return;
5410
+ }
5411
+ if (key.rightArrow || input === "l") {
5412
+ setCursor({
5413
+ row: clampedRow,
5414
+ column: columnCount === 0 ? 0 : Math.min(columnCount - 1, clampedColumn + 1)
5415
+ });
5416
+ return;
5417
+ }
5418
+ if (key.escape) {
5419
+ if (mergeAnchor !== void 0) {
5420
+ setMergeAnchor(void 0);
5421
+ return;
5422
+ }
5423
+ dispatch({ type: "POP_SCREEN" });
5424
+ return;
5425
+ }
5426
+ if (input === "m" || key.return) {
5427
+ if (mergeAnchor === void 0) {
5428
+ setMergeAnchor({
5429
+ row: clampedRow,
5430
+ column: clampedColumn
5431
+ });
5432
+ return;
5433
+ }
5434
+ commitMerge(mergeAnchor);
5435
+ }
5436
+ }, { isActive: !anyOverlayOpen(state) });
5437
+ if (table === void 0) return /* @__PURE__ */ jsxs(Box, {
5438
+ flexDirection: "column",
5439
+ children: [/* @__PURE__ */ jsxs(Text, {
5440
+ bold: true,
5441
+ children: [
5442
+ "Slide ",
5443
+ slideIndex + 1,
5444
+ ", table ",
5445
+ tableIndex + 1
5446
+ ]
5447
+ }), /* @__PURE__ */ jsx(Text, {
5448
+ color: "yellow",
5449
+ children: "This table no longer exists -- press Esc to go back"
5450
+ })]
5451
+ });
5452
+ return /* @__PURE__ */ jsxs(Box, {
5453
+ flexDirection: "column",
5454
+ children: [
5455
+ /* @__PURE__ */ jsxs(Text, {
5456
+ bold: true,
5457
+ children: [
5458
+ "Slide ",
5459
+ slideIndex + 1,
5460
+ ", table ",
5461
+ tableIndex + 1,
5462
+ " (",
5463
+ rowCount,
5464
+ "x",
5465
+ columnCount,
5466
+ ")"
5467
+ ]
5468
+ }),
5469
+ rows.length === 0 ? /* @__PURE__ */ jsx(Text, {
5470
+ dimColor: true,
5471
+ children: "This table has no rows."
5472
+ }) : rows.map((row, rowIndex) => /* @__PURE__ */ jsx(Box, { children: row.cells.map((cell, columnIndex) => {
5473
+ const isCursor = rowIndex === clampedRow && columnIndex === clampedColumn;
5474
+ const isAnchor = rowIndex === mergeAnchor?.row && columnIndex === mergeAnchor?.column;
5475
+ return /* @__PURE__ */ jsx(Box, {
5476
+ width: CELL_WIDTH$1,
5477
+ borderStyle: "single",
5478
+ borderColor: isCursor ? "cyan" : isAnchor ? "yellow" : "gray",
5479
+ children: /* @__PURE__ */ jsx(Text, {
5480
+ color: isCursor ? "cyan" : void 0,
5481
+ inverse: isCursor,
5482
+ children: truncatePreview(slideTableCellText(cell), 14)
5483
+ })
5484
+ }, columnIndex);
5485
+ }) }, rowIndex)),
5486
+ /* @__PURE__ */ jsx(Text, {
5487
+ dimColor: true,
5488
+ children: mergeAnchor === void 0 ? "Arrows/hjkl move, m to anchor a merge, Esc back" : "Arrows/hjkl to the opposite corner, m/Enter to merge, Esc to cancel"
5489
+ })
5490
+ ]
5491
+ });
5492
+ }
5493
+ //#endregion
4610
5494
  //#region src/tui/screens/editors/pptx/index.tsx
4611
5495
  function PptxSlideListScreen() {
4612
5496
  const doc = useAppState().openDocument;
@@ -5225,7 +6109,8 @@ const RESERVED_LETTERS = /* @__PURE__ */ new Set([
5225
6109
  "k",
5226
6110
  "l",
5227
6111
  "p",
5228
- "t"
6112
+ "t",
6113
+ "m"
5229
6114
  ]);
5230
6115
  function windowStart(cursor, total, viewport) {
5231
6116
  const maxStart = Math.max(0, total - viewport);
@@ -5248,6 +6133,7 @@ function OdsSpreadsheetGridScreen() {
5248
6133
  const [cursorColumn, setCursorColumn] = useState(0);
5249
6134
  const [viewMode, setViewMode] = useState("grid");
5250
6135
  const [editSession, setEditSession] = useState(void 0);
6136
+ const [mergeAnchor, setMergeAnchor] = useState(void 0);
5251
6137
  const { columns: terminalColumns, rows: terminalRows } = useWindowSize();
5252
6138
  const sheet = resolveSheet(doc.editor, sheetIndex);
5253
6139
  const { rowCount, columnCount } = sheetExtent(sheet);
@@ -5266,6 +6152,21 @@ function OdsSpreadsheetGridScreen() {
5266
6152
  seedKind
5267
6153
  });
5268
6154
  }
6155
+ function commitMerge(anchor) {
6156
+ const startRow = Math.min(anchor.row, clampedRow);
6157
+ const startColumn = Math.min(anchor.column, clampedColumn);
6158
+ const rowSpan = Math.abs(clampedRow - anchor.row) + 1;
6159
+ const colSpan = Math.abs(clampedColumn - anchor.column) + 1;
6160
+ dispatch({
6161
+ type: "MERGE_CELLS",
6162
+ sheetIndex,
6163
+ startRow,
6164
+ startColumn,
6165
+ rowSpan,
6166
+ colSpan
6167
+ });
6168
+ setMergeAnchor(void 0);
6169
+ }
5269
6170
  useInput((input) => {
5270
6171
  if (input === "t") {
5271
6172
  setViewMode((mode) => mode === "grid" ? "compact" : "grid");
@@ -5313,10 +6214,26 @@ function OdsSpreadsheetGridScreen() {
5313
6214
  return;
5314
6215
  }
5315
6216
  if (key.escape) {
6217
+ if (mergeAnchor !== void 0) {
6218
+ setMergeAnchor(void 0);
6219
+ return;
6220
+ }
5316
6221
  dispatch({ type: "POP_SCREEN" });
5317
6222
  return;
5318
6223
  }
6224
+ if (input === "m") {
6225
+ if (mergeAnchor === void 0) setMergeAnchor({
6226
+ row: clampedRow,
6227
+ column: clampedColumn
6228
+ });
6229
+ else commitMerge(mergeAnchor);
6230
+ return;
6231
+ }
5319
6232
  if (key.return) {
6233
+ if (mergeAnchor !== void 0) {
6234
+ commitMerge(mergeAnchor);
6235
+ return;
6236
+ }
5320
6237
  const cell = cells.get(cellKey(clampedRow, clampedColumn));
5321
6238
  const seedKind = cell === void 0 || cell.value.kind === "empty" ? "string" : cell.value.kind;
5322
6239
  beginEdit(cell === void 0 ? "" : rawEditableText(cell.value), seedKind);
@@ -5399,10 +6316,11 @@ function OdsSpreadsheetGridScreen() {
5399
6316
  children: [`${row + 1}`.padStart(4), " "]
5400
6317
  }), visibleColumns.map((column) => {
5401
6318
  const isCursor = row === clampedRow && column === clampedColumn;
6319
+ const isAnchor = row === mergeAnchor?.row && column === mergeAnchor?.column;
5402
6320
  const cell = cells.get(cellKey(row, column));
5403
6321
  return /* @__PURE__ */ jsx(Text, {
5404
6322
  inverse: isCursor,
5405
- color: isCursor ? "cyan" : void 0,
6323
+ color: isCursor ? "cyan" : isAnchor ? "yellow" : void 0,
5406
6324
  children: padCell(cell === void 0 ? "" : cell.displayText, CELL_WIDTH)
5407
6325
  }, column);
5408
6326
  })] }, row))]
@@ -5440,13 +6358,9 @@ function OdsSpreadsheetGridScreen() {
5440
6358
  setEditSession(void 0);
5441
6359
  }
5442
6360
  }),
5443
- /* @__PURE__ */ jsxs(Text, {
6361
+ /* @__PURE__ */ jsx(Text, {
5444
6362
  dimColor: true,
5445
- children: [
5446
- "hjkl/arrows move, Enter/type to edit, p print settings, t ",
5447
- viewMode === "grid" ? "compact list" : "grid",
5448
- " view, Esc back"
5449
- ]
6363
+ children: mergeAnchor === void 0 ? `hjkl/arrows move, Enter/type to edit, m to anchor a merge, p print settings, t ${viewMode === "grid" ? "compact list" : "grid"} view, Esc back` : "hjkl/arrows to the opposite corner, m/Enter to merge, Esc to cancel"
5450
6364
  })
5451
6365
  ]
5452
6366
  });
@@ -6498,6 +7412,7 @@ function ScreenBody({ screen }) {
6498
7412
  case "slideList": return format === "odp" ? /* @__PURE__ */ jsx(OdpSlideListScreen, {}) : /* @__PURE__ */ jsx(PptxSlideListScreen, {});
6499
7413
  case "slideDetail": return /* @__PURE__ */ jsx(SlideDetailScreen, { screen });
6500
7414
  case "shapeEditor": return /* @__PURE__ */ jsx(ShapeEditorScreen, { screen });
7415
+ case "slideTableDetail": return /* @__PURE__ */ jsx(SlideTableDetailScreen, { screen });
6501
7416
  case "notesEditor": return /* @__PURE__ */ jsx(NotesEditorScreen, { screen });
6502
7417
  case "sheetList": return /* @__PURE__ */ jsx(OdsSheetListScreen, {});
6503
7418
  case "spreadsheetGrid": return /* @__PURE__ */ jsx(OdsSpreadsheetGridScreen, {});