document-cli 1.5.0 → 1.7.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.
@@ -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 {
@@ -490,10 +523,6 @@ function shapeAt(doc, containerIndex, shapeIndex) {
490
523
  if (doc.format === "odg") return doc.editor.pages()[containerIndex]?.shapes()[shapeIndex];
491
524
  return doc.editor.slides()[containerIndex]?.shapes()[shapeIndex];
492
525
  }
493
- function rotatableShapeAt(doc, containerIndex, shapeIndex) {
494
- if (doc.format === "odg") return doc.editor.pages()[containerIndex]?.shapes()[shapeIndex];
495
- return doc.editor.slides()[containerIndex]?.shapes()[shapeIndex];
496
- }
497
526
  function sheetAt(doc, sheetIndex) {
498
527
  return doc.editor.sheets()[sheetIndex];
499
528
  }
@@ -526,8 +555,8 @@ function withSheet(state, sheetIndex, apply) {
526
555
  apply(sheet);
527
556
  });
528
557
  }
529
- function setCellText(cell, text) {
530
- const paragraph = cell.paragraphs()[0] ?? cell.appendParagraph();
558
+ function setTextContainerText(container, text) {
559
+ const paragraph = container.paragraphs()[0] ?? container.appendParagraph();
531
560
  const runs = paragraph.runs();
532
561
  const firstRun = runs[0];
533
562
  if (firstRun === void 0) {
@@ -537,6 +566,9 @@ function setCellText(cell, text) {
537
566
  firstRun.text = text;
538
567
  for (const extra of runs.slice(1)) extra.remove();
539
568
  }
569
+ function setCellText(cell, text) {
570
+ setTextContainerText(cell, text);
571
+ }
540
572
  function appReducer(state, action) {
541
573
  switch (action.type) {
542
574
  case "PUSH_SCREEN": return {
@@ -686,11 +718,21 @@ function appReducer(state, action) {
686
718
  case "APPEND_TABLE": {
687
719
  const doc = wordprocessingDocument(state);
688
720
  if (doc === void 0) return wrongDocument(state, "a docx or odt document");
689
- return mutate(state, doc, () => {
690
- doc.editor.body.appendTable({
721
+ return mutateGuarded(state, doc, () => {
722
+ const table = doc.editor.body.appendTable({
691
723
  rows: action.rows,
692
724
  columns: action.columns
693
725
  });
726
+ if (action.merge !== void 0) table.mergeCells(action.merge.startRow, action.merge.startColumn, action.merge.rowSpan, action.merge.colSpan);
727
+ });
728
+ }
729
+ case "MERGE_TABLE_CELLS": {
730
+ const doc = wordprocessingDocument(state);
731
+ if (doc === void 0) return wrongDocument(state, "a docx or odt document");
732
+ const table = tableAt(doc, action.tableIndex);
733
+ if (table === void 0) return withStatus(state, "warning", `There is no table at index ${action.tableIndex}`);
734
+ return mutateGuarded(state, doc, () => {
735
+ table.mergeCells(action.startRow, action.startColumn, action.rowSpan, action.colSpan);
694
736
  });
695
737
  }
696
738
  case "SET_TABLE_CELL_TEXT": {
@@ -721,6 +763,18 @@ function appReducer(state, action) {
721
763
  appended.list = membership;
722
764
  });
723
765
  }
766
+ case "SET_LIST_ITEM_TEXT": {
767
+ const doc = wordprocessingDocument(state);
768
+ if (doc === void 0) return wrongDocument(state, "a docx or odt document");
769
+ if (doc.format !== "odt") return wrongDocument(state, "an odt document (lists are an odt-only concept)");
770
+ const list = doc.editor.lists()[action.blockIndex];
771
+ if (list === void 0) return withStatus(state, "warning", `There is no list at index ${action.blockIndex}`);
772
+ const item = list.items()[action.itemIndex];
773
+ if (item === void 0) return withStatus(state, "warning", `List ${action.blockIndex} has no item at index ${action.itemIndex}`);
774
+ return mutate(state, doc, () => {
775
+ setTextContainerText(item, action.text);
776
+ });
777
+ }
724
778
  case "ADD_SLIDE": {
725
779
  const doc = presentationDocument(state);
726
780
  if (doc === void 0) return wrongDocument(state, "a pptx or odp document");
@@ -743,6 +797,22 @@ function appReducer(state, action) {
743
797
  });
744
798
  });
745
799
  }
800
+ case "MERGE_SLIDE_TABLE_CELLS": {
801
+ const doc = presentationDocument(state);
802
+ if (doc === void 0) return wrongDocument(state, "a pptx or odp document");
803
+ if (doc.format === "odp") {
804
+ const entry = doc.editor.slides()[action.slideIndex]?.tables()[action.tableIndex];
805
+ if (entry === void 0) return withStatus(state, "warning", `There is no table at index ${action.tableIndex} on slide ${action.slideIndex}`);
806
+ return mutateGuarded(state, doc, () => {
807
+ entry.table.mergeCells(action.startRow, action.startColumn, action.rowSpan, action.colSpan);
808
+ });
809
+ }
810
+ const table = doc.editor.slides()[action.slideIndex]?.tables()[action.tableIndex];
811
+ if (table === void 0) return withStatus(state, "warning", `There is no table at index ${action.tableIndex} on slide ${action.slideIndex}`);
812
+ return mutateGuarded(state, doc, () => {
813
+ mergePptxTableCells(table, action.startRow, action.startColumn, action.rowSpan, action.colSpan);
814
+ });
815
+ }
746
816
  case "ADD_TEXTBOX": {
747
817
  const doc = shapeHostDocument(state);
748
818
  if (doc === void 0) return wrongDocument(state, "a pptx, odp or odg document");
@@ -793,16 +863,9 @@ function appReducer(state, action) {
793
863
  case "SET_SHAPE_FRAME": return withShape(state, action.containerIndex, action.shapeIndex, (shape) => {
794
864
  shape.frame = action.frame;
795
865
  });
796
- case "SET_SHAPE_ROTATION": {
797
- const doc = shapeHostDocument(state);
798
- if (doc === void 0) return wrongDocument(state, "a pptx, odp or odg document");
799
- if (doc.format === "pptx") return withStatus(state, "warning", "documents.js has no rotation setter for a pptx shape; rotate the shape in odp instead");
800
- const shape = rotatableShapeAt(doc, action.containerIndex, action.shapeIndex);
801
- if (shape === void 0) return withStatus(state, "warning", `There is no shape ${action.shapeIndex} at index ${action.containerIndex}`);
802
- return mutate(state, doc, () => {
803
- shape.rotationDeg = action.rotationDeg;
804
- });
805
- }
866
+ case "SET_SHAPE_ROTATION": return withShape(state, action.containerIndex, action.shapeIndex, (shape) => {
867
+ shape.rotationDeg = action.rotationDeg;
868
+ });
806
869
  case "SET_SLIDE_NOTES": {
807
870
  const doc = presentationDocument(state);
808
871
  if (doc === void 0) return wrongDocument(state, "a pptx or odp document");
@@ -822,6 +885,15 @@ function appReducer(state, action) {
822
885
  case "SET_CELL_VALUE": return withSheet(state, action.sheetIndex, (sheet) => {
823
886
  sheet.cell(action.row, action.column).value = action.value;
824
887
  });
888
+ case "MERGE_CELLS": {
889
+ const doc = spreadsheetDocument(state);
890
+ if (doc === void 0) return wrongDocument(state, "an ods document");
891
+ const sheet = sheetAt(doc, action.sheetIndex);
892
+ if (sheet === void 0) return withStatus(state, "warning", `There is no sheet at index ${action.sheetIndex}`);
893
+ return mutateGuarded(state, doc, () => {
894
+ sheet.mergeCells(action.startRow, action.startColumn, action.rowSpan, action.colSpan);
895
+ });
896
+ }
825
897
  case "SET_SHEET_PRINT_SETTINGS": return withSheet(state, action.sheetIndex, (sheet) => {
826
898
  sheet.printSettings = action.printSettings;
827
899
  });
@@ -1020,6 +1092,11 @@ const COMMANDS = [
1020
1092
  usage: ":close",
1021
1093
  description: "Close the open document"
1022
1094
  },
1095
+ {
1096
+ name: "undo",
1097
+ usage: ":undo",
1098
+ description: "Undo the last change"
1099
+ },
1023
1100
  {
1024
1101
  name: "help",
1025
1102
  usage: ":help",
@@ -1152,6 +1229,9 @@ async function runCommand(line, state, dispatch) {
1152
1229
  case "close":
1153
1230
  dispatch({ type: "REQUEST_CLOSE" });
1154
1231
  return;
1232
+ case "undo":
1233
+ dispatch({ type: "UNDO" });
1234
+ return;
1155
1235
  case "help":
1156
1236
  dispatch({
1157
1237
  type: "OPEN_OVERLAY",
@@ -1430,6 +1510,10 @@ const GLOBAL_KEYS = [
1430
1510
  keys: "Ctrl+W",
1431
1511
  description: "Close the open document"
1432
1512
  },
1513
+ {
1514
+ keys: "Ctrl+Z",
1515
+ description: "Undo the last change"
1516
+ },
1433
1517
  {
1434
1518
  keys: "q / Ctrl+C",
1435
1519
  description: "Quit"
@@ -1590,6 +1674,14 @@ function truncatePreview(text, maxLength) {
1590
1674
  if (singleLine.length === 0) return "(empty)";
1591
1675
  return singleLine.length > maxLength ? `${singleLine.slice(0, Math.max(0, maxLength - 1))}…` : singleLine;
1592
1676
  }
1677
+ function parsePositiveIntField(raw, fallback) {
1678
+ const parsed = Number.parseInt(raw, 10);
1679
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
1680
+ }
1681
+ function parseNonNegativeIntField(raw, fallback) {
1682
+ const parsed = Number.parseInt(raw, 10);
1683
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
1684
+ }
1593
1685
  //#endregion
1594
1686
  //#region src/tui/screens/shared/paragraph-family.tsx
1595
1687
  function paragraphFamilyDocument(openDocument) {
@@ -1653,6 +1745,8 @@ function tableSummary(table) {
1653
1745
  function listSummary(list, index) {
1654
1746
  return `List ${index} (${list.itemCount} item${list.itemCount === 1 ? "" : "s"})`;
1655
1747
  }
1748
+ const DEFAULT_TABLE_ROWS$1 = 2;
1749
+ const DEFAULT_TABLE_COLUMNS$1 = 2;
1656
1750
  function ParagraphFamilyBodyList(props) {
1657
1751
  const { adapter } = props;
1658
1752
  const state = useAppState();
@@ -1695,9 +1789,88 @@ function ParagraphFamilyBodyList(props) {
1695
1789
  if (row.kind !== "header") acc.push(rowIndex);
1696
1790
  return acc;
1697
1791
  }, []);
1792
+ const [tableWizard, setTableWizard] = useState("closed");
1793
+ const [wizardDraft, setWizardDraft] = useState("");
1794
+ const [wizardRows, setWizardRows] = useState(DEFAULT_TABLE_ROWS$1);
1795
+ const [wizardColumns, setWizardColumns] = useState(DEFAULT_TABLE_COLUMNS$1);
1796
+ const [wizardStartRow, setWizardStartRow] = useState(0);
1797
+ const [wizardStartColumn, setWizardStartColumn] = useState(0);
1798
+ const [wizardRowSpan, setWizardRowSpan] = useState(1);
1799
+ const wizardOpen = tableWizard !== "closed";
1800
+ const closeWizard = () => {
1801
+ setTableWizard("closed");
1802
+ };
1803
+ const commitAppendTable = (merge) => {
1804
+ const newIndex = tables.length;
1805
+ dispatch({
1806
+ type: "APPEND_TABLE",
1807
+ rows: wizardRows,
1808
+ columns: wizardColumns,
1809
+ merge
1810
+ });
1811
+ closeWizard();
1812
+ dispatch({
1813
+ type: "PUSH_SCREEN",
1814
+ screen: {
1815
+ kind: "tableView",
1816
+ blockIndex: newIndex
1817
+ }
1818
+ });
1819
+ };
1820
+ useInput((input) => {
1821
+ if (input === "T") {
1822
+ setWizardDraft(String(DEFAULT_TABLE_ROWS$1));
1823
+ setTableWizard("rows");
1824
+ }
1825
+ }, { isActive: !anyOverlayOpen(state) && !wizardOpen });
1826
+ useInput((input, key) => {
1827
+ if (key.escape) {
1828
+ closeWizard();
1829
+ return;
1830
+ }
1831
+ if (input === "y" || input === "Y") {
1832
+ setWizardDraft("0");
1833
+ setTableWizard("mergeStartRow");
1834
+ return;
1835
+ }
1836
+ if (input === "n" || input === "N" || key.return) commitAppendTable(void 0);
1837
+ }, { isActive: !anyOverlayOpen(state) && tableWizard === "mergePrompt" });
1838
+ const submitWizardRows = (raw) => {
1839
+ setWizardRows(parsePositiveIntField(raw, DEFAULT_TABLE_ROWS$1));
1840
+ setWizardDraft(String(DEFAULT_TABLE_COLUMNS$1));
1841
+ setTableWizard("columns");
1842
+ };
1843
+ const submitWizardColumns = (raw) => {
1844
+ setWizardColumns(parsePositiveIntField(raw, DEFAULT_TABLE_COLUMNS$1));
1845
+ setTableWizard("mergePrompt");
1846
+ };
1847
+ const submitWizardMergeStartRow = (raw) => {
1848
+ setWizardStartRow(Math.min(parseNonNegativeIntField(raw, 0), Math.max(0, wizardRows - 1)));
1849
+ setWizardDraft("0");
1850
+ setTableWizard("mergeStartColumn");
1851
+ };
1852
+ const submitWizardMergeStartColumn = (raw) => {
1853
+ setWizardStartColumn(Math.min(parseNonNegativeIntField(raw, 0), Math.max(0, wizardColumns - 1)));
1854
+ setWizardDraft("1");
1855
+ setTableWizard("mergeRowSpan");
1856
+ };
1857
+ const submitWizardMergeRowSpan = (raw) => {
1858
+ setWizardRowSpan(Math.min(parsePositiveIntField(raw, 1), Math.max(1, wizardRows - wizardStartRow)));
1859
+ setWizardDraft("1");
1860
+ setTableWizard("mergeColSpan");
1861
+ };
1862
+ const submitWizardMergeColSpan = (raw) => {
1863
+ const colSpan = Math.min(parsePositiveIntField(raw, 1), Math.max(1, wizardColumns - wizardStartColumn));
1864
+ commitAppendTable({
1865
+ startRow: wizardStartRow,
1866
+ startColumn: wizardStartColumn,
1867
+ rowSpan: wizardRowSpan,
1868
+ colSpan
1869
+ });
1870
+ };
1698
1871
  const { selectedIndex } = usePersistedSelection(selectionKeyFor({ kind: "bodyList" }), {
1699
1872
  itemCount: selectableRowIndices.length,
1700
- isActive: !anyOverlayOpen(state),
1873
+ isActive: !anyOverlayOpen(state) && !wizardOpen,
1701
1874
  onBack: () => {
1702
1875
  dispatch({ type: "POP_SCREEN" });
1703
1876
  },
@@ -1784,9 +1957,89 @@ function ParagraphFamilyBodyList(props) {
1784
1957
  });
1785
1958
  }
1786
1959
  }),
1960
+ tableWizard === "rows" ? /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
1961
+ color: "cyan",
1962
+ children: "Rows: "
1963
+ }), /* @__PURE__ */ jsx(TextField, {
1964
+ value: wizardDraft,
1965
+ isFocused: true,
1966
+ onChange: setWizardDraft,
1967
+ onSubmit: submitWizardRows,
1968
+ onCancel: closeWizard
1969
+ })] }) : void 0,
1970
+ tableWizard === "columns" ? /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
1971
+ color: "cyan",
1972
+ children: "Columns: "
1973
+ }), /* @__PURE__ */ jsx(TextField, {
1974
+ value: wizardDraft,
1975
+ isFocused: true,
1976
+ onChange: setWizardDraft,
1977
+ onSubmit: submitWizardColumns,
1978
+ onCancel: closeWizard
1979
+ })] }) : void 0,
1980
+ tableWizard === "mergePrompt" ? /* @__PURE__ */ jsx(Text, {
1981
+ color: "cyan",
1982
+ children: "Merge cells now? y/N"
1983
+ }) : void 0,
1984
+ tableWizard === "mergeStartRow" ? /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsxs(Text, {
1985
+ color: "cyan",
1986
+ children: [
1987
+ "Merge start row (0-",
1988
+ Math.max(0, wizardRows - 1),
1989
+ "): "
1990
+ ]
1991
+ }), /* @__PURE__ */ jsx(TextField, {
1992
+ value: wizardDraft,
1993
+ isFocused: true,
1994
+ onChange: setWizardDraft,
1995
+ onSubmit: submitWizardMergeStartRow,
1996
+ onCancel: closeWizard
1997
+ })] }) : void 0,
1998
+ tableWizard === "mergeStartColumn" ? /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsxs(Text, {
1999
+ color: "cyan",
2000
+ children: [
2001
+ "Merge start column (0-",
2002
+ Math.max(0, wizardColumns - 1),
2003
+ "): "
2004
+ ]
2005
+ }), /* @__PURE__ */ jsx(TextField, {
2006
+ value: wizardDraft,
2007
+ isFocused: true,
2008
+ onChange: setWizardDraft,
2009
+ onSubmit: submitWizardMergeStartColumn,
2010
+ onCancel: closeWizard
2011
+ })] }) : void 0,
2012
+ tableWizard === "mergeRowSpan" ? /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsxs(Text, {
2013
+ color: "cyan",
2014
+ children: [
2015
+ "Merge row span (1-",
2016
+ Math.max(1, wizardRows - wizardStartRow),
2017
+ "): "
2018
+ ]
2019
+ }), /* @__PURE__ */ jsx(TextField, {
2020
+ value: wizardDraft,
2021
+ isFocused: true,
2022
+ onChange: setWizardDraft,
2023
+ onSubmit: submitWizardMergeRowSpan,
2024
+ onCancel: closeWizard
2025
+ })] }) : void 0,
2026
+ tableWizard === "mergeColSpan" ? /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsxs(Text, {
2027
+ color: "cyan",
2028
+ children: [
2029
+ "Merge column span (1-",
2030
+ Math.max(1, wizardColumns - wizardStartColumn),
2031
+ "): "
2032
+ ]
2033
+ }), /* @__PURE__ */ jsx(TextField, {
2034
+ value: wizardDraft,
2035
+ isFocused: true,
2036
+ onChange: setWizardDraft,
2037
+ onSubmit: submitWizardMergeColSpan,
2038
+ onCancel: closeWizard
2039
+ })] }) : void 0,
1787
2040
  /* @__PURE__ */ jsx(Text, {
1788
2041
  dimColor: true,
1789
- children: "Enter to open, a to append a paragraph, Esc back"
2042
+ children: "Enter to open, a to append a paragraph, T to append a table, Esc back"
1790
2043
  })
1791
2044
  ]
1792
2045
  });
@@ -2063,7 +2316,7 @@ function RunEditorScreen() {
2063
2316
  }
2064
2317
  //#endregion
2065
2318
  //#region src/tui/screens/editors/docx/table-view.tsx
2066
- const CELL_WIDTH$1 = 16;
2319
+ const CELL_WIDTH$2 = 16;
2067
2320
  function TableViewScreen() {
2068
2321
  const state = useAppState();
2069
2322
  const dispatch = useAppDispatch();
@@ -2071,6 +2324,7 @@ function TableViewScreen() {
2071
2324
  row: 0,
2072
2325
  column: 0
2073
2326
  });
2327
+ const [mergeAnchor, setMergeAnchor] = useState(void 0);
2074
2328
  const screen = currentScreen(state);
2075
2329
  const doc = paragraphFamilyDocument(state.openDocument);
2076
2330
  const table = screen.kind === "tableView" && doc !== void 0 ? liveTableAt(doc, screen.blockIndex) : void 0;
@@ -2079,6 +2333,22 @@ function TableViewScreen() {
2079
2333
  const columnCount = rows[0]?.cells().length ?? 0;
2080
2334
  const clampedRow = rowCount === 0 ? 0 : Math.min(cursor.row, rowCount - 1);
2081
2335
  const clampedColumn = columnCount === 0 ? 0 : Math.min(cursor.column, columnCount - 1);
2336
+ const commitMerge = (anchor) => {
2337
+ if (screen.kind !== "tableView") return;
2338
+ const startRow = Math.min(anchor.row, clampedRow);
2339
+ const startColumn = Math.min(anchor.column, clampedColumn);
2340
+ const rowSpan = Math.abs(clampedRow - anchor.row) + 1;
2341
+ const colSpan = Math.abs(clampedColumn - anchor.column) + 1;
2342
+ dispatch({
2343
+ type: "MERGE_TABLE_CELLS",
2344
+ tableIndex: screen.blockIndex,
2345
+ startRow,
2346
+ startColumn,
2347
+ rowSpan,
2348
+ colSpan
2349
+ });
2350
+ setMergeAnchor(void 0);
2351
+ };
2082
2352
  useInput((input, key) => {
2083
2353
  if (table === void 0 || screen.kind !== "tableView") return;
2084
2354
  if (key.upArrow || input === "k") {
@@ -2110,18 +2380,36 @@ function TableViewScreen() {
2110
2380
  return;
2111
2381
  }
2112
2382
  if (key.escape) {
2383
+ if (mergeAnchor !== void 0) {
2384
+ setMergeAnchor(void 0);
2385
+ return;
2386
+ }
2113
2387
  dispatch({ type: "POP_SCREEN" });
2114
2388
  return;
2115
2389
  }
2116
- if (key.return && rowCount > 0 && columnCount > 0) dispatch({
2117
- type: "PUSH_SCREEN",
2118
- screen: {
2119
- kind: "tableCellDetail",
2120
- blockIndex: screen.blockIndex,
2390
+ if (input === "m" && rowCount > 0 && columnCount > 0) {
2391
+ if (mergeAnchor === void 0) setMergeAnchor({
2121
2392
  row: clampedRow,
2122
- col: clampedColumn
2393
+ column: clampedColumn
2394
+ });
2395
+ else commitMerge(mergeAnchor);
2396
+ return;
2397
+ }
2398
+ if (key.return && rowCount > 0 && columnCount > 0) {
2399
+ if (mergeAnchor !== void 0) {
2400
+ commitMerge(mergeAnchor);
2401
+ return;
2123
2402
  }
2124
- });
2403
+ dispatch({
2404
+ type: "PUSH_SCREEN",
2405
+ screen: {
2406
+ kind: "tableCellDetail",
2407
+ blockIndex: screen.blockIndex,
2408
+ row: clampedRow,
2409
+ col: clampedColumn
2410
+ }
2411
+ });
2412
+ }
2125
2413
  }, { isActive: !anyOverlayOpen(state) });
2126
2414
  if (screen.kind !== "tableView") return /* @__PURE__ */ jsx(Text, {
2127
2415
  color: "red",
@@ -2159,10 +2447,11 @@ function TableViewScreen() {
2159
2447
  children: "This table has no rows."
2160
2448
  }) : rows.map((row, rowIndex) => /* @__PURE__ */ jsx(Box, { children: row.cells().map((cell, columnIndex) => {
2161
2449
  const isSelected = rowIndex === clampedRow && columnIndex === clampedColumn;
2450
+ const isAnchor = rowIndex === mergeAnchor?.row && columnIndex === mergeAnchor?.column;
2162
2451
  return /* @__PURE__ */ jsx(Box, {
2163
- width: CELL_WIDTH$1,
2452
+ width: CELL_WIDTH$2,
2164
2453
  borderStyle: "single",
2165
- borderColor: isSelected ? "cyan" : "gray",
2454
+ borderColor: isSelected ? "cyan" : isAnchor ? "yellow" : "gray",
2166
2455
  children: /* @__PURE__ */ jsx(Text, {
2167
2456
  color: isSelected ? "cyan" : void 0,
2168
2457
  inverse: isSelected,
@@ -2172,7 +2461,7 @@ function TableViewScreen() {
2172
2461
  }) }, rowIndex)),
2173
2462
  /* @__PURE__ */ jsx(Text, {
2174
2463
  dimColor: true,
2175
- children: "Arrows/hjkl move, Enter to edit a cell, Esc back"
2464
+ 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"
2176
2465
  })
2177
2466
  ]
2178
2467
  });
@@ -3019,6 +3308,10 @@ function requireShapeOrVectorDetailScreen(state) {
3019
3308
  if (screen.kind !== "shapeOrVectorDetail") throw new Error(`OdgShapeOrVectorDetailScreen rendered while the top of the stack is '${screen.kind}', not 'shapeOrVectorDetail' -- app.tsx's ScreenBody switch only mounts this component for that screen kind, so this cannot happen without a routing bug.`);
3020
3309
  return screen;
3021
3310
  }
3311
+ function vectorsParityMatch(liveVectors, contentVectors) {
3312
+ if (liveVectors.length !== contentVectors.length) return false;
3313
+ return liveVectors.every((live, index) => live.kind === contentVectors[index]?.kind);
3314
+ }
3022
3315
  function buildPageItems(doc, pageIndex) {
3023
3316
  const page = doc.editor.pages()[pageIndex];
3024
3317
  if (page === void 0) return [];
@@ -3026,9 +3319,12 @@ function buildPageItems(doc, pageIndex) {
3026
3319
  if (content.kind !== "drawing") throw new Error(`readOdgContent(doc.editor.toPackage()) returned a '${content.kind}' ContentDocument for an odg-format open document -- an odg package should always read back as the 'drawing' variant, so this indicates a real inconsistency in documents.js's own reader, not a state this screen should paper over.`);
3027
3320
  const contentPage = content.pages[pageIndex];
3028
3321
  if (contentPage === void 0) throw new Error(`readOdgContent found no page at index ${pageIndex}, but doc.editor.pages() has a page there -- the two read the same live package, so they should always agree on page count.`);
3029
- const vectorItems = contentPage.vectors.map((vector) => ({
3322
+ const liveVectors = page.vectors();
3323
+ const parityOk = vectorsParityMatch(liveVectors, contentPage.vectors);
3324
+ const vectorItems = contentPage.vectors.map((vector, index) => ({
3030
3325
  kind: "vector",
3031
- vector
3326
+ vector,
3327
+ liveVector: parityOk ? liveVectors[index] : void 0
3032
3328
  }));
3033
3329
  const shapeItems = page.shapes().map((shape, index) => {
3034
3330
  return {
@@ -3320,15 +3616,19 @@ function warnVectorIsViewOnly(dispatch, label) {
3320
3616
  dispatch({
3321
3617
  type: "SET_STATUS",
3322
3618
  severity: "info",
3323
- text: `${label} added -- documents.js exposes no way to enumerate an existing odg vector, so it will show in this list read-only and cannot be edited or removed from the TUI.`
3619
+ text: `${label} added -- but this page also has a vector element documents.js's OdgPage.vectors() cannot wrap, so every vector on it (including this one) shows read-only in this list rather than risk pairing the wrong live handle to the wrong row.`
3324
3620
  });
3325
3621
  }
3622
+ function warnIfVectorAddedReadOnly(doc, pageIndex, dispatch, label) {
3623
+ const added = buildPageItems(doc, pageIndex).filter((item) => item.kind === "vector").at(-1);
3624
+ if (added !== void 0 && added.liveVector === void 0) warnVectorIsViewOnly(dispatch, label);
3625
+ }
3326
3626
  function inferImageFormat(path) {
3327
3627
  const extension = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
3328
3628
  if (extension === "png") return "png";
3329
3629
  if (extension === "jpg" || extension === "jpeg") return "jpeg";
3330
3630
  }
3331
- async function applyAddKind(kind, pageIndex, values, dispatch) {
3631
+ async function applyAddKind(kind, pageIndex, doc, values, dispatch) {
3332
3632
  switch (kind) {
3333
3633
  case "rect":
3334
3634
  dispatch({
@@ -3340,7 +3640,7 @@ async function applyAddKind(kind, pageIndex, values, dispatch) {
3340
3640
  stroke: parseStrokeField(requireFieldValue(values, "stroke"))
3341
3641
  }
3342
3642
  });
3343
- warnVectorIsViewOnly(dispatch, "Rectangle");
3643
+ warnIfVectorAddedReadOnly(doc, pageIndex, dispatch, "Rectangle");
3344
3644
  return;
3345
3645
  case "ellipse":
3346
3646
  dispatch({
@@ -3352,7 +3652,7 @@ async function applyAddKind(kind, pageIndex, values, dispatch) {
3352
3652
  stroke: parseStrokeField(requireFieldValue(values, "stroke"))
3353
3653
  }
3354
3654
  });
3355
- warnVectorIsViewOnly(dispatch, "Ellipse");
3655
+ warnIfVectorAddedReadOnly(doc, pageIndex, dispatch, "Ellipse");
3356
3656
  return;
3357
3657
  case "line":
3358
3658
  dispatch({
@@ -3377,7 +3677,7 @@ async function applyAddKind(kind, pageIndex, values, dispatch) {
3377
3677
  }
3378
3678
  }
3379
3679
  });
3380
- warnVectorIsViewOnly(dispatch, "Line");
3680
+ warnIfVectorAddedReadOnly(doc, pageIndex, dispatch, "Line");
3381
3681
  return;
3382
3682
  case "path": {
3383
3683
  const frame = readFrame(values);
@@ -3391,7 +3691,7 @@ async function applyAddKind(kind, pageIndex, values, dispatch) {
3391
3691
  stroke: parseStrokeField(requireFieldValue(values, "stroke"))
3392
3692
  }
3393
3693
  });
3394
- warnVectorIsViewOnly(dispatch, "Path");
3694
+ warnIfVectorAddedReadOnly(doc, pageIndex, dispatch, "Path");
3395
3695
  return;
3396
3696
  }
3397
3697
  case "textbox":
@@ -3520,7 +3820,7 @@ function AddItemFlow(props) {
3520
3820
  fields: fieldsForAddKind(kind),
3521
3821
  onCancel: props.onCancel,
3522
3822
  onComplete: (values) => {
3523
- applyAddKind(kind, props.pageIndex, values, dispatch).then(props.onCreated);
3823
+ applyAddKind(kind, props.pageIndex, props.doc, values, dispatch).then(props.onCreated);
3524
3824
  }
3525
3825
  });
3526
3826
  }
@@ -3574,6 +3874,7 @@ function OdgPageDetailScreen() {
3574
3874
  });
3575
3875
  if (isAdding) return /* @__PURE__ */ jsx(AddItemFlow, {
3576
3876
  pageIndex,
3877
+ doc,
3577
3878
  isActive: !overlayOpen,
3578
3879
  onCancel: () => {
3579
3880
  setIsAdding(false);
@@ -3607,19 +3908,75 @@ function OdgPageDetailScreen() {
3607
3908
  }
3608
3909
  //#endregion
3609
3910
  //#region src/tui/screens/editors/odg/shape-or-vector-detail.tsx
3911
+ function buildVectorRows(vector, liveVector, dispatch) {
3912
+ const rows = [];
3913
+ if (vector.kind !== "line" && liveVector.kind !== "line") {
3914
+ const fillTarget = liveVector;
3915
+ rows.push({
3916
+ label: `Fill: ${vector.fill === void 0 ? "none" : formatColor$1(vector.fill)}`,
3917
+ currentValue: vector.fill === void 0 ? "" : `${vector.fill.r} ${vector.fill.g} ${vector.fill.b}`,
3918
+ commit: (raw) => {
3919
+ dispatch({
3920
+ type: "SET_VECTOR_FILL",
3921
+ vector: fillTarget,
3922
+ fill: parseColorField(raw)
3923
+ });
3924
+ }
3925
+ });
3926
+ }
3927
+ rows.push({
3928
+ label: `Stroke: ${vector.stroke === void 0 ? "none" : `${formatColor$1(vector.stroke.color)} ${formatPt(vector.stroke.widthPt)}pt`}`,
3929
+ currentValue: vector.stroke === void 0 ? "" : `${vector.stroke.color.r} ${vector.stroke.color.g} ${vector.stroke.color.b} ${vector.stroke.widthPt}`,
3930
+ commit: (raw) => {
3931
+ const stroke = parseStrokeField(raw);
3932
+ if (stroke === void 0) {
3933
+ dispatch({
3934
+ type: "SET_STATUS",
3935
+ severity: "warning",
3936
+ text: "A vector stroke cannot be cleared to none through this editor -- enter \"r g b widthPt\" (0-1 colour, pt width) instead"
3937
+ });
3938
+ return;
3939
+ }
3940
+ dispatch({
3941
+ type: "SET_VECTOR_STROKE",
3942
+ vector: liveVector,
3943
+ stroke
3944
+ });
3945
+ }
3946
+ });
3947
+ return rows;
3948
+ }
3610
3949
  function VectorDetail(props) {
3611
- return /* @__PURE__ */ jsxs(Box, {
3950
+ const dispatch = useAppDispatch();
3951
+ const [editingField, setEditingField] = useState(void 0);
3952
+ const [draft, setDraft] = useState("");
3953
+ const { vector, liveVector, isActive } = props;
3954
+ const rows = liveVector === void 0 ? [] : buildVectorRows(vector, liveVector, dispatch);
3955
+ const { selectedIndex } = useNavigationInput({
3956
+ itemCount: rows.length,
3957
+ isActive: isActive && editingField === void 0,
3958
+ onBack: () => {
3959
+ dispatch({ type: "POP_SCREEN" });
3960
+ },
3961
+ onSelect: (index) => {
3962
+ const row = rows[index];
3963
+ if (row === void 0) return;
3964
+ setDraft(row.currentValue);
3965
+ setEditingField(index);
3966
+ }
3967
+ });
3968
+ if (liveVector === void 0) return /* @__PURE__ */ jsxs(Box, {
3612
3969
  flexDirection: "column",
3613
3970
  children: [
3614
3971
  /* @__PURE__ */ jsxs(Text, {
3615
3972
  bold: true,
3616
- children: [vectorKindLabel(props.vector.kind), " (view-only)"]
3973
+ children: [vectorKindLabel(vector.kind), " (view-only)"]
3617
3974
  }),
3618
- /* @__PURE__ */ jsxs(Text, { children: ["Geometry: ", describeVectorGeometry(props.vector)] }),
3619
- /* @__PURE__ */ jsx(Text, { children: describeFillStroke(props.vector) }),
3975
+ /* @__PURE__ */ jsxs(Text, { children: ["Geometry: ", describeVectorGeometry(vector)] }),
3976
+ /* @__PURE__ */ jsx(Text, { children: describeFillStroke(vector) }),
3620
3977
  /* @__PURE__ */ jsx(Text, {
3621
3978
  color: "yellow",
3622
- children: "documents.js's OdgPage has no accessor for an existing rect/ellipse/line/path vector, so there is no live handle to edit or remove this one from the TUI -- open the file in a real ODF editor to change it."
3979
+ children: "This page's live vectors (documents.js's `OdgPage.vectors()`) don't line up one-to-one with what odf.js's own reader found here -- likely a `draw:circle`/`polygon`/`polyline`/`custom-shape` element the live accessor has no wrapper for, sitting alongside a plain rect/ellipse/line/path it does. Rather than risk pairing the wrong live handle to this row, every vector on this page is shown read-only until that mismatch is resolved outside this TUI. Open the file in a real ODF editor to change it."
3623
3980
  }),
3624
3981
  /* @__PURE__ */ jsx(Text, {
3625
3982
  dimColor: true,
@@ -3627,6 +3984,53 @@ function VectorDetail(props) {
3627
3984
  })
3628
3985
  ]
3629
3986
  });
3987
+ if (editingField !== void 0) {
3988
+ const row = rows[editingField];
3989
+ if (row === void 0) throw new Error(`VectorDetail is editing field index ${editingField}, but there are only ${rows.length} rows -- selecting a row always sets editingField to a valid index from that same rows array, so this indicates a bug in that selection.`);
3990
+ return /* @__PURE__ */ jsxs(Box, {
3991
+ flexDirection: "column",
3992
+ borderStyle: "round",
3993
+ paddingX: 1,
3994
+ children: [/* @__PURE__ */ jsx(Text, {
3995
+ bold: true,
3996
+ children: row.label
3997
+ }), /* @__PURE__ */ jsx(TextField, {
3998
+ value: draft,
3999
+ isFocused: true,
4000
+ onChange: setDraft,
4001
+ onCancel: () => {
4002
+ setEditingField(void 0);
4003
+ },
4004
+ onSubmit: (value) => {
4005
+ row.commit(value);
4006
+ setEditingField(void 0);
4007
+ }
4008
+ })]
4009
+ });
4010
+ }
4011
+ return /* @__PURE__ */ jsxs(Box, {
4012
+ flexDirection: "column",
4013
+ children: [
4014
+ /* @__PURE__ */ jsx(Text, {
4015
+ bold: true,
4016
+ children: vectorKindLabel(vector.kind)
4017
+ }),
4018
+ /* @__PURE__ */ jsxs(Text, { children: ["Geometry: ", describeVectorGeometry(vector)] }),
4019
+ /* @__PURE__ */ jsx(ListView, {
4020
+ items: rows,
4021
+ selectedIndex,
4022
+ reservedRows: 5,
4023
+ renderItem: (row, isSelected) => /* @__PURE__ */ jsxs(Text, {
4024
+ color: isSelected ? "cyan" : void 0,
4025
+ children: [isSelected ? "> " : " ", row.label]
4026
+ })
4027
+ }),
4028
+ /* @__PURE__ */ jsx(Text, {
4029
+ dimColor: true,
4030
+ children: "Enter to edit a field, Esc to go back"
4031
+ })
4032
+ ]
4033
+ });
3630
4034
  }
3631
4035
  function ShapeDetail(props) {
3632
4036
  const dispatch = useAppDispatch();
@@ -3796,7 +4200,7 @@ function OdgShapeOrVectorDetailScreen() {
3796
4200
  const item = items[itemIndex];
3797
4201
  useInput((input, key) => {
3798
4202
  if (key.escape || key.leftArrow || input === "h") dispatch({ type: "POP_SCREEN" });
3799
- }, { isActive: !overlayOpen && item?.kind !== "shape" });
4203
+ }, { isActive: !overlayOpen && item === void 0 });
3800
4204
  if (item === void 0) return /* @__PURE__ */ jsxs(Box, {
3801
4205
  flexDirection: "column",
3802
4206
  children: [/* @__PURE__ */ jsxs(Text, {
@@ -3813,7 +4217,11 @@ function OdgShapeOrVectorDetailScreen() {
3813
4217
  children: "Esc to go back"
3814
4218
  })]
3815
4219
  });
3816
- if (item.kind === "vector") return /* @__PURE__ */ jsx(VectorDetail, { vector: item.vector });
4220
+ if (item.kind === "vector") return /* @__PURE__ */ jsx(VectorDetail, {
4221
+ vector: item.vector,
4222
+ liveVector: item.liveVector,
4223
+ isActive: !overlayOpen
4224
+ });
3817
4225
  const vectorCount = items.filter((entry) => entry.kind === "vector").length;
3818
4226
  return /* @__PURE__ */ jsx(ShapeDetail, {
3819
4227
  pageIndex,
@@ -3955,10 +4363,6 @@ function formatRotationDeg(value) {
3955
4363
  return value === void 0 ? "(unset)" : `${Math.round(value * ROTATION_DISPLAY_PRECISION) / ROTATION_DISPLAY_PRECISION}°`;
3956
4364
  }
3957
4365
  function RotationField(props) {
3958
- if (!props.isEditable) return /* @__PURE__ */ jsx(Text, {
3959
- dimColor: true,
3960
- children: "[R] Rotation: not available for pptx shapes"
3961
- });
3962
4366
  if (props.isEditing) return /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
3963
4367
  color: "cyan",
3964
4368
  children: "[R] Rotation (deg, blank to unset): "
@@ -3992,7 +4396,6 @@ const FIELD_LABELS = {
3992
4396
  width: "Width (pt)",
3993
4397
  height: "Height (pt)"
3994
4398
  };
3995
- const ROTATION_UNAVAILABLE_MESSAGE = "documents.js has no rotation setter for a pptx shape; rotate the shape in odp instead";
3996
4399
  const POINT_DISPLAY_PRECISION = 100;
3997
4400
  function formatPoints(value) {
3998
4401
  return value === void 0 ? "(unset)" : `${Math.round(value * POINT_DISPLAY_PRECISION) / POINT_DISPLAY_PRECISION}pt`;
@@ -4012,9 +4415,9 @@ function describeFieldValue(key, shape) {
4012
4415
  function roundForDisplay(value) {
4013
4416
  return Math.round(value * POINT_DISPLAY_PRECISION) / POINT_DISPLAY_PRECISION;
4014
4417
  }
4015
- function initialDraftFor(key, shape, rotatableShape) {
4418
+ function initialDraftFor(key, shape) {
4016
4419
  if (key === "rotation") {
4017
- const value = rotatableShape?.rotationDeg;
4420
+ const value = shape.rotationDeg;
4018
4421
  return value === void 0 ? "" : String(roundForDisplay(value));
4019
4422
  }
4020
4423
  if (key === "text") return shape.text;
@@ -4027,10 +4430,9 @@ function initialDraftFor(key, shape, rotatableShape) {
4027
4430
  }
4028
4431
  }
4029
4432
  function FieldRow(props) {
4030
- const { fieldKey, shape, rotatableShape, isOdp, isSelected, isEditing, draft, onDraftChange, onSubmit, onCancel } = props;
4433
+ const { fieldKey, shape, isSelected, isEditing, draft, onDraftChange, onSubmit, onCancel } = props;
4031
4434
  if (fieldKey === "rotation") return /* @__PURE__ */ jsx(RotationField, {
4032
- rotationDeg: rotatableShape?.rotationDeg,
4033
- isEditable: isOdp,
4435
+ rotationDeg: shape.rotationDeg,
4034
4436
  isSelected,
4035
4437
  isEditing,
4036
4438
  draftValue: draft,
@@ -4079,9 +4481,7 @@ function ShapeEditorScreen(props) {
4079
4481
  const overlayOpen = anyOverlayOpen(state);
4080
4482
  const doc = assertPresentationDocument(state.openDocument);
4081
4483
  const { slideIndex, shapeIndex } = props.screen;
4082
- const isOdp = doc.format === "odp";
4083
4484
  const shape = doc.editor.slides()[slideIndex]?.shapes()[shapeIndex];
4084
- const rotatableShape = doc.format === "odp" ? doc.editor.slides()[slideIndex]?.shapes()[shapeIndex] : void 0;
4085
4485
  const [editingField, setEditingField] = useState(void 0);
4086
4486
  const [draft, setDraft] = useState("");
4087
4487
  const { selectedIndex } = useNavigationInput({
@@ -4094,20 +4494,12 @@ function ShapeEditorScreen(props) {
4094
4494
  if (shape === void 0) return;
4095
4495
  const key = FIELD_KEYS[index];
4096
4496
  if (key === void 0) return;
4097
- if (key === "rotation" && !isOdp) {
4098
- dispatch({
4099
- type: "SET_STATUS",
4100
- severity: "warning",
4101
- text: ROTATION_UNAVAILABLE_MESSAGE
4102
- });
4103
- return;
4104
- }
4105
4497
  dispatch({
4106
4498
  type: "SET_SELECTION",
4107
4499
  key: selectionKeyFor(props.screen),
4108
4500
  index
4109
4501
  });
4110
- setDraft(initialDraftFor(key, shape, rotatableShape));
4502
+ setDraft(initialDraftFor(key, shape));
4111
4503
  setEditingField(key);
4112
4504
  }
4113
4505
  });
@@ -4232,8 +4624,6 @@ function ShapeEditorScreen(props) {
4232
4624
  renderItem: (key, isSelected) => /* @__PURE__ */ jsx(FieldRow, {
4233
4625
  fieldKey: key,
4234
4626
  shape,
4235
- rotatableShape,
4236
- isOdp,
4237
4627
  isSelected,
4238
4628
  isEditing: editingField === key,
4239
4629
  draft,
@@ -4250,6 +4640,30 @@ function ShapeEditorScreen(props) {
4250
4640
  });
4251
4641
  }
4252
4642
  //#endregion
4643
+ //#region src/tui/screens/shared/slide-table.ts
4644
+ function resolveSlideTable(doc, slideIndex, tableIndex) {
4645
+ const content = doc.format === "odp" ? readOdpContent(doc.editor.toPackage()) : readPptxContent(doc.editor.toPackage());
4646
+ if (content.kind !== "presentation") throw new Error("readPptxContent/readOdpContent always resolve a presentation package to the presentation ContentDocument variant.");
4647
+ const slide = content.slides[slideIndex];
4648
+ if (slide === void 0) return;
4649
+ return slide.shapes.flatMap((shape) => shape.blocks.filter((block) => block.kind === "table"))[tableIndex];
4650
+ }
4651
+ function slideTableCellText(cell) {
4652
+ return cell.blocks.filter((block) => block.kind === "paragraph").map((paragraph) => paragraph.runs.map((run) => run.text).join("")).join("\n");
4653
+ }
4654
+ function summarizeGridTable(table, index) {
4655
+ const rows = table.rows();
4656
+ return {
4657
+ index,
4658
+ rowCount: rows.length,
4659
+ columnCount: rows[0]?.cells().length ?? 0
4660
+ };
4661
+ }
4662
+ function summarizeSlideTables(doc, slideIndex) {
4663
+ if (doc.format === "odp") return (doc.editor.slides()[slideIndex]?.tables() ?? []).map((entry, index) => summarizeGridTable(entry.table, index));
4664
+ return (doc.editor.slides()[slideIndex]?.tables() ?? []).map((table, index) => summarizeGridTable(table, index));
4665
+ }
4666
+ //#endregion
4253
4667
  //#region src/tui/screens/editors/pptx/slide-detail.tsx
4254
4668
  const IMAGE_EXTENSION_TO_FORMAT = {
4255
4669
  png: "png",
@@ -4258,10 +4672,6 @@ const IMAGE_EXTENSION_TO_FORMAT = {
4258
4672
  };
4259
4673
  const DEFAULT_TABLE_ROWS = 2;
4260
4674
  const DEFAULT_TABLE_COLUMNS = 2;
4261
- function parsePositiveIntField(raw, fallback) {
4262
- const parsed = Number.parseInt(raw, 10);
4263
- return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
4264
- }
4265
4675
  function imageFormatFromPath(path) {
4266
4676
  const dotIndex = path.lastIndexOf(".");
4267
4677
  if (dotIndex < 0) return;
@@ -4282,34 +4692,63 @@ function SlideDetailScreen(props) {
4282
4692
  const doc = assertPresentationDocument(state.openDocument);
4283
4693
  const { slideIndex } = props.screen;
4284
4694
  const slide = doc.editor.slides()[slideIndex];
4285
- const rows = (slide === void 0 ? [] : slide.shapes()).map((shape, index) => ({
4695
+ const shapes = slide === void 0 ? [] : slide.shapes();
4696
+ const tableSummaries = slide === void 0 ? [] : summarizeSlideTables(doc, slideIndex);
4697
+ const rows = [...shapes.map((shape, index) => ({
4698
+ kind: "shape",
4286
4699
  index,
4287
4700
  text: shape.text,
4288
4701
  frame: shape.frame
4289
- }));
4702
+ })), ...tableSummaries.length > 0 ? [{
4703
+ kind: "tablesHeader",
4704
+ count: tableSummaries.length
4705
+ }, ...tableSummaries.map((summary) => ({
4706
+ kind: "table",
4707
+ index: summary.index,
4708
+ rowCount: summary.rowCount,
4709
+ columnCount: summary.columnCount
4710
+ }))] : []];
4711
+ const selectableRowIndices = rows.reduce((acc, row, index) => {
4712
+ if (row.kind !== "tablesHeader") acc.push(index);
4713
+ return acc;
4714
+ }, []);
4290
4715
  const [addMode, setAddMode] = useState("closed");
4291
4716
  const [draft, setDraft] = useState("");
4292
4717
  const [imageError, setImageError] = useState(void 0);
4293
4718
  const [tableRows, setTableRows] = useState(DEFAULT_TABLE_ROWS);
4294
4719
  const formIsOpen = addMode !== "closed";
4295
4720
  const { selectedIndex } = useNavigationInput({
4296
- itemCount: rows.length,
4721
+ itemCount: selectableRowIndices.length,
4297
4722
  isActive: !overlayOpen && !formIsOpen,
4298
4723
  onBack: () => {
4299
4724
  dispatch({ type: "POP_SCREEN" });
4300
4725
  },
4301
4726
  onSelect: (index) => {
4302
- dispatch({
4303
- type: "SET_SELECTION",
4304
- key: selectionKeyFor(props.screen),
4305
- index
4306
- });
4307
- dispatch({
4727
+ const rowIndex = selectableRowIndices[index];
4728
+ const row = rowIndex === void 0 ? void 0 : rows[rowIndex];
4729
+ if (row === void 0) return;
4730
+ if (row.kind === "shape") {
4731
+ dispatch({
4732
+ type: "SET_SELECTION",
4733
+ key: selectionKeyFor(props.screen),
4734
+ index
4735
+ });
4736
+ dispatch({
4737
+ type: "PUSH_SCREEN",
4738
+ screen: {
4739
+ kind: "shapeEditor",
4740
+ slideIndex,
4741
+ shapeIndex: row.index
4742
+ }
4743
+ });
4744
+ return;
4745
+ }
4746
+ if (row.kind === "table") dispatch({
4308
4747
  type: "PUSH_SCREEN",
4309
4748
  screen: {
4310
- kind: "shapeEditor",
4749
+ kind: "slideTableDetail",
4311
4750
  slideIndex,
4312
- shapeIndex: index
4751
+ tableIndex: row.index
4313
4752
  }
4314
4753
  });
4315
4754
  },
@@ -4317,6 +4756,7 @@ function SlideDetailScreen(props) {
4317
4756
  setAddMode("chooseKind");
4318
4757
  }
4319
4758
  });
4759
+ const listSelectedIndex = selectableRowIndices[selectedIndex] ?? -1;
4320
4760
  useInput((input, key) => {
4321
4761
  if (key.escape) {
4322
4762
  setAddMode("closed");
@@ -4399,9 +4839,9 @@ function SlideDetailScreen(props) {
4399
4839
  "Slide ",
4400
4840
  slideIndex + 1,
4401
4841
  " -- ",
4402
- rows.length,
4842
+ shapes.length,
4403
4843
  " shape",
4404
- rows.length === 1 ? "" : "s"
4844
+ shapes.length === 1 ? "" : "s"
4405
4845
  ]
4406
4846
  }),
4407
4847
  slide === void 0 ? /* @__PURE__ */ jsx(Text, {
@@ -4409,20 +4849,45 @@ function SlideDetailScreen(props) {
4409
4849
  children: "This slide no longer exists -- press Esc to go back"
4410
4850
  }) : /* @__PURE__ */ jsx(ListView, {
4411
4851
  items: rows,
4412
- selectedIndex,
4852
+ selectedIndex: listSelectedIndex,
4413
4853
  emptyMessage: "No shapes yet -- press 'a' to add one",
4414
- renderItem: (row, isSelected) => /* @__PURE__ */ jsxs(Text, {
4415
- color: isSelected ? "cyan" : void 0,
4416
- inverse: isSelected,
4417
- children: [
4418
- row.index + 1,
4419
- ". ",
4420
- describeSlideFamilyShape({
4421
- text: row.text,
4422
- frame: row.frame
4423
- })
4424
- ]
4425
- })
4854
+ renderItem: (row, isSelected) => {
4855
+ if (row.kind === "tablesHeader") return /* @__PURE__ */ jsxs(Text, {
4856
+ bold: true,
4857
+ dimColor: true,
4858
+ children: [
4859
+ "Tables (",
4860
+ row.count,
4861
+ ")"
4862
+ ]
4863
+ });
4864
+ if (row.kind === "table") return /* @__PURE__ */ jsxs(Text, {
4865
+ color: isSelected ? "cyan" : void 0,
4866
+ inverse: isSelected,
4867
+ children: [
4868
+ " ",
4869
+ "Table ",
4870
+ row.index + 1,
4871
+ " (",
4872
+ row.rowCount,
4873
+ "x",
4874
+ row.columnCount,
4875
+ ")"
4876
+ ]
4877
+ });
4878
+ return /* @__PURE__ */ jsxs(Text, {
4879
+ color: isSelected ? "cyan" : void 0,
4880
+ inverse: isSelected,
4881
+ children: [
4882
+ row.index + 1,
4883
+ ". ",
4884
+ describeSlideFamilyShape({
4885
+ text: row.text,
4886
+ frame: row.frame
4887
+ })
4888
+ ]
4889
+ });
4890
+ }
4426
4891
  }),
4427
4892
  addMode === "chooseKind" ? /* @__PURE__ */ jsx(Text, {
4428
4893
  color: "cyan",
@@ -4490,6 +4955,146 @@ function SlideDetailScreen(props) {
4490
4955
  });
4491
4956
  }
4492
4957
  //#endregion
4958
+ //#region src/tui/screens/editors/pptx/slide-table-detail.tsx
4959
+ const CELL_WIDTH$1 = 16;
4960
+ function SlideTableDetailScreen(props) {
4961
+ const state = useAppState();
4962
+ const dispatch = useAppDispatch();
4963
+ const doc = assertPresentationDocument(state.openDocument);
4964
+ const { slideIndex, tableIndex } = props.screen;
4965
+ const [cursor, setCursor] = useState({
4966
+ row: 0,
4967
+ column: 0
4968
+ });
4969
+ const [mergeAnchor, setMergeAnchor] = useState(void 0);
4970
+ const table = resolveSlideTable(doc, slideIndex, tableIndex);
4971
+ const rows = table?.rows ?? [];
4972
+ const rowCount = rows.length;
4973
+ const columnCount = rows[0]?.cells.length ?? 0;
4974
+ const clampedRow = rowCount === 0 ? 0 : Math.min(cursor.row, rowCount - 1);
4975
+ const clampedColumn = columnCount === 0 ? 0 : Math.min(cursor.column, columnCount - 1);
4976
+ const commitMerge = (anchor) => {
4977
+ const startRow = Math.min(anchor.row, clampedRow);
4978
+ const startColumn = Math.min(anchor.column, clampedColumn);
4979
+ const rowSpan = Math.abs(clampedRow - anchor.row) + 1;
4980
+ const colSpan = Math.abs(clampedColumn - anchor.column) + 1;
4981
+ dispatch({
4982
+ type: "MERGE_SLIDE_TABLE_CELLS",
4983
+ slideIndex,
4984
+ tableIndex,
4985
+ startRow,
4986
+ startColumn,
4987
+ rowSpan,
4988
+ colSpan
4989
+ });
4990
+ setMergeAnchor(void 0);
4991
+ };
4992
+ useInput((input, key) => {
4993
+ if (table === void 0) return;
4994
+ if (key.upArrow || input === "k") {
4995
+ setCursor({
4996
+ row: Math.max(0, clampedRow - 1),
4997
+ column: clampedColumn
4998
+ });
4999
+ return;
5000
+ }
5001
+ if (key.downArrow || input === "j") {
5002
+ setCursor({
5003
+ row: rowCount === 0 ? 0 : Math.min(rowCount - 1, clampedRow + 1),
5004
+ column: clampedColumn
5005
+ });
5006
+ return;
5007
+ }
5008
+ if (key.leftArrow || input === "h") {
5009
+ setCursor({
5010
+ row: clampedRow,
5011
+ column: Math.max(0, clampedColumn - 1)
5012
+ });
5013
+ return;
5014
+ }
5015
+ if (key.rightArrow || input === "l") {
5016
+ setCursor({
5017
+ row: clampedRow,
5018
+ column: columnCount === 0 ? 0 : Math.min(columnCount - 1, clampedColumn + 1)
5019
+ });
5020
+ return;
5021
+ }
5022
+ if (key.escape) {
5023
+ if (mergeAnchor !== void 0) {
5024
+ setMergeAnchor(void 0);
5025
+ return;
5026
+ }
5027
+ dispatch({ type: "POP_SCREEN" });
5028
+ return;
5029
+ }
5030
+ if (input === "m" || key.return) {
5031
+ if (mergeAnchor === void 0) {
5032
+ setMergeAnchor({
5033
+ row: clampedRow,
5034
+ column: clampedColumn
5035
+ });
5036
+ return;
5037
+ }
5038
+ commitMerge(mergeAnchor);
5039
+ }
5040
+ }, { isActive: !anyOverlayOpen(state) });
5041
+ if (table === void 0) return /* @__PURE__ */ jsxs(Box, {
5042
+ flexDirection: "column",
5043
+ children: [/* @__PURE__ */ jsxs(Text, {
5044
+ bold: true,
5045
+ children: [
5046
+ "Slide ",
5047
+ slideIndex + 1,
5048
+ ", table ",
5049
+ tableIndex + 1
5050
+ ]
5051
+ }), /* @__PURE__ */ jsx(Text, {
5052
+ color: "yellow",
5053
+ children: "This table no longer exists -- press Esc to go back"
5054
+ })]
5055
+ });
5056
+ return /* @__PURE__ */ jsxs(Box, {
5057
+ flexDirection: "column",
5058
+ children: [
5059
+ /* @__PURE__ */ jsxs(Text, {
5060
+ bold: true,
5061
+ children: [
5062
+ "Slide ",
5063
+ slideIndex + 1,
5064
+ ", table ",
5065
+ tableIndex + 1,
5066
+ " (",
5067
+ rowCount,
5068
+ "x",
5069
+ columnCount,
5070
+ ")"
5071
+ ]
5072
+ }),
5073
+ rows.length === 0 ? /* @__PURE__ */ jsx(Text, {
5074
+ dimColor: true,
5075
+ children: "This table has no rows."
5076
+ }) : rows.map((row, rowIndex) => /* @__PURE__ */ jsx(Box, { children: row.cells.map((cell, columnIndex) => {
5077
+ const isCursor = rowIndex === clampedRow && columnIndex === clampedColumn;
5078
+ const isAnchor = rowIndex === mergeAnchor?.row && columnIndex === mergeAnchor?.column;
5079
+ return /* @__PURE__ */ jsx(Box, {
5080
+ width: CELL_WIDTH$1,
5081
+ borderStyle: "single",
5082
+ borderColor: isCursor ? "cyan" : isAnchor ? "yellow" : "gray",
5083
+ children: /* @__PURE__ */ jsx(Text, {
5084
+ color: isCursor ? "cyan" : void 0,
5085
+ inverse: isCursor,
5086
+ children: truncatePreview(slideTableCellText(cell), 14)
5087
+ })
5088
+ }, columnIndex);
5089
+ }) }, rowIndex)),
5090
+ /* @__PURE__ */ jsx(Text, {
5091
+ dimColor: true,
5092
+ 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"
5093
+ })
5094
+ ]
5095
+ });
5096
+ }
5097
+ //#endregion
4493
5098
  //#region src/tui/screens/editors/pptx/index.tsx
4494
5099
  function PptxSlideListScreen() {
4495
5100
  const doc = useAppState().openDocument;
@@ -5108,7 +5713,8 @@ const RESERVED_LETTERS = /* @__PURE__ */ new Set([
5108
5713
  "k",
5109
5714
  "l",
5110
5715
  "p",
5111
- "t"
5716
+ "t",
5717
+ "m"
5112
5718
  ]);
5113
5719
  function windowStart(cursor, total, viewport) {
5114
5720
  const maxStart = Math.max(0, total - viewport);
@@ -5131,6 +5737,7 @@ function OdsSpreadsheetGridScreen() {
5131
5737
  const [cursorColumn, setCursorColumn] = useState(0);
5132
5738
  const [viewMode, setViewMode] = useState("grid");
5133
5739
  const [editSession, setEditSession] = useState(void 0);
5740
+ const [mergeAnchor, setMergeAnchor] = useState(void 0);
5134
5741
  const { columns: terminalColumns, rows: terminalRows } = useWindowSize();
5135
5742
  const sheet = resolveSheet(doc.editor, sheetIndex);
5136
5743
  const { rowCount, columnCount } = sheetExtent(sheet);
@@ -5149,6 +5756,21 @@ function OdsSpreadsheetGridScreen() {
5149
5756
  seedKind
5150
5757
  });
5151
5758
  }
5759
+ function commitMerge(anchor) {
5760
+ const startRow = Math.min(anchor.row, clampedRow);
5761
+ const startColumn = Math.min(anchor.column, clampedColumn);
5762
+ const rowSpan = Math.abs(clampedRow - anchor.row) + 1;
5763
+ const colSpan = Math.abs(clampedColumn - anchor.column) + 1;
5764
+ dispatch({
5765
+ type: "MERGE_CELLS",
5766
+ sheetIndex,
5767
+ startRow,
5768
+ startColumn,
5769
+ rowSpan,
5770
+ colSpan
5771
+ });
5772
+ setMergeAnchor(void 0);
5773
+ }
5152
5774
  useInput((input) => {
5153
5775
  if (input === "t") {
5154
5776
  setViewMode((mode) => mode === "grid" ? "compact" : "grid");
@@ -5196,10 +5818,26 @@ function OdsSpreadsheetGridScreen() {
5196
5818
  return;
5197
5819
  }
5198
5820
  if (key.escape) {
5821
+ if (mergeAnchor !== void 0) {
5822
+ setMergeAnchor(void 0);
5823
+ return;
5824
+ }
5199
5825
  dispatch({ type: "POP_SCREEN" });
5200
5826
  return;
5201
5827
  }
5828
+ if (input === "m") {
5829
+ if (mergeAnchor === void 0) setMergeAnchor({
5830
+ row: clampedRow,
5831
+ column: clampedColumn
5832
+ });
5833
+ else commitMerge(mergeAnchor);
5834
+ return;
5835
+ }
5202
5836
  if (key.return) {
5837
+ if (mergeAnchor !== void 0) {
5838
+ commitMerge(mergeAnchor);
5839
+ return;
5840
+ }
5203
5841
  const cell = cells.get(cellKey(clampedRow, clampedColumn));
5204
5842
  const seedKind = cell === void 0 || cell.value.kind === "empty" ? "string" : cell.value.kind;
5205
5843
  beginEdit(cell === void 0 ? "" : rawEditableText(cell.value), seedKind);
@@ -5282,10 +5920,11 @@ function OdsSpreadsheetGridScreen() {
5282
5920
  children: [`${row + 1}`.padStart(4), " "]
5283
5921
  }), visibleColumns.map((column) => {
5284
5922
  const isCursor = row === clampedRow && column === clampedColumn;
5923
+ const isAnchor = row === mergeAnchor?.row && column === mergeAnchor?.column;
5285
5924
  const cell = cells.get(cellKey(row, column));
5286
5925
  return /* @__PURE__ */ jsx(Text, {
5287
5926
  inverse: isCursor,
5288
- color: isCursor ? "cyan" : void 0,
5927
+ color: isCursor ? "cyan" : isAnchor ? "yellow" : void 0,
5289
5928
  children: padCell(cell === void 0 ? "" : cell.displayText, CELL_WIDTH)
5290
5929
  }, column);
5291
5930
  })] }, row))]
@@ -5323,13 +5962,9 @@ function OdsSpreadsheetGridScreen() {
5323
5962
  setEditSession(void 0);
5324
5963
  }
5325
5964
  }),
5326
- /* @__PURE__ */ jsxs(Text, {
5965
+ /* @__PURE__ */ jsx(Text, {
5327
5966
  dimColor: true,
5328
- children: [
5329
- "hjkl/arrows move, Enter/type to edit, p print settings, t ",
5330
- viewMode === "grid" ? "compact list" : "grid",
5331
- " view, Esc back"
5332
- ]
5967
+ 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"
5333
5968
  })
5334
5969
  ]
5335
5970
  });
@@ -5339,28 +5974,40 @@ function OdsSpreadsheetGridScreen() {
5339
5974
  function ListEditorScreen() {
5340
5975
  const state = useAppState();
5341
5976
  const dispatch = useAppDispatch();
5977
+ const overlayOpen = anyOverlayOpen(state);
5342
5978
  const [isAdding, setIsAdding] = useState(false);
5343
5979
  const [newItemText, setNewItemText] = useState("");
5980
+ const [editingIndex, setEditingIndex] = useState(void 0);
5344
5981
  const screen = currentScreen(state);
5345
5982
  const doc = paragraphFamilyDocument(state.openDocument);
5346
5983
  const list = screen.kind === "listEditor" && doc?.format === "odt" ? doc.editor.lists()[screen.blockIndex] : void 0;
5347
- const itemCount = list === void 0 ? 0 : list.items().length;
5348
- useInput((input, key) => {
5349
- if (list === void 0 || screen.kind !== "listEditor") return;
5350
- if (key.escape) {
5984
+ const items = list === void 0 ? [] : list.items();
5985
+ const rows = items.map((item, index) => ({
5986
+ item,
5987
+ index
5988
+ }));
5989
+ const itemCount = items.length;
5990
+ const isNavigationActive = !overlayOpen && !isAdding && editingIndex === void 0;
5991
+ const { selectedIndex } = useNavigationInput({
5992
+ itemCount,
5993
+ isActive: isNavigationActive,
5994
+ onBack: () => {
5351
5995
  dispatch({ type: "POP_SCREEN" });
5352
- return;
5353
- }
5354
- if (input === "a") {
5996
+ },
5997
+ onSelect: (index) => {
5998
+ setEditingIndex(index);
5999
+ },
6000
+ onAppend: () => {
5355
6001
  setIsAdding(true);
5356
- return;
5357
6002
  }
6003
+ });
6004
+ useInput((input, key) => {
5358
6005
  if (key.tab || input === ">" || input === "<") dispatch({
5359
6006
  type: "SET_STATUS",
5360
6007
  severity: "warning",
5361
6008
  text: "Indenting a list item needs a new reducer action this pass didn't add -- OdtListItem.addNestedList() has no wiring yet"
5362
6009
  });
5363
- }, { isActive: !anyOverlayOpen(state) && !isAdding });
6010
+ }, { isActive: isNavigationActive });
5364
6011
  if (screen.kind !== "listEditor") return /* @__PURE__ */ jsx(Text, {
5365
6012
  color: "red",
5366
6013
  children: "ListEditorScreen rendered outside a listEditor screen."
@@ -5377,6 +6024,36 @@ function ListEditorScreen() {
5377
6024
  "."
5378
6025
  ]
5379
6026
  });
6027
+ if (editingIndex !== void 0) {
6028
+ const item = items[editingIndex];
6029
+ if (item === void 0) throw new Error(`ListEditorScreen is editing item index ${editingIndex}, but list ${screen.blockIndex} only has ${items.length} items -- selecting a row always sets editingIndex to a valid index from that same items array, so this indicates a bug in that selection.`);
6030
+ return /* @__PURE__ */ jsxs(Box, {
6031
+ flexDirection: "column",
6032
+ children: [/* @__PURE__ */ jsxs(Text, {
6033
+ bold: true,
6034
+ children: [
6035
+ "List ",
6036
+ screen.blockIndex,
6037
+ ", item ",
6038
+ editingIndex + 1
6039
+ ]
6040
+ }), /* @__PURE__ */ jsx(RunTextEditor, {
6041
+ initialText: item.text,
6042
+ onCommit: (text) => {
6043
+ dispatch({
6044
+ type: "SET_LIST_ITEM_TEXT",
6045
+ blockIndex: screen.blockIndex,
6046
+ itemIndex: editingIndex,
6047
+ text
6048
+ });
6049
+ setEditingIndex(void 0);
6050
+ },
6051
+ onCancel: () => {
6052
+ setEditingIndex(void 0);
6053
+ }
6054
+ })]
6055
+ });
6056
+ }
5380
6057
  return /* @__PURE__ */ jsxs(Box, {
5381
6058
  flexDirection: "column",
5382
6059
  children: [
@@ -5392,13 +6069,23 @@ function ListEditorScreen() {
5392
6069
  ")"
5393
6070
  ]
5394
6071
  }),
5395
- itemCount === 0 ? /* @__PURE__ */ jsx(Text, {
5396
- dimColor: true,
5397
- children: "This list has no items yet."
5398
- }) : Array.from({ length: itemCount }, (_, index) => /* @__PURE__ */ jsxs(Text, {
5399
- dimColor: true,
5400
- children: [index + 1, ". (item content is not readable through documents.js's OdtListItem API)"]
5401
- }, index)),
6072
+ /* @__PURE__ */ jsx(ListView, {
6073
+ items: rows,
6074
+ selectedIndex,
6075
+ emptyMessage: "This list has no items yet -- press 'a' to add one.",
6076
+ renderItem: (row, isSelected) => {
6077
+ const trimmed = row.item.text.trim();
6078
+ return /* @__PURE__ */ jsxs(Text, {
6079
+ color: isSelected ? "cyan" : void 0,
6080
+ inverse: isSelected,
6081
+ children: [
6082
+ row.index + 1,
6083
+ ". ",
6084
+ trimmed.length === 0 ? "(empty)" : row.item.text
6085
+ ]
6086
+ });
6087
+ }
6088
+ }),
5402
6089
  isAdding ? /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
5403
6090
  color: "cyan",
5404
6091
  children: "+ "
@@ -5422,7 +6109,7 @@ function ListEditorScreen() {
5422
6109
  }
5423
6110
  })] }) : /* @__PURE__ */ jsx(Text, {
5424
6111
  dimColor: true,
5425
- children: "a to add an item, Esc back"
6112
+ children: "Enter to edit an item, a to add, Esc back"
5426
6113
  })
5427
6114
  ]
5428
6115
  });
@@ -6329,6 +7016,7 @@ function ScreenBody({ screen }) {
6329
7016
  case "slideList": return format === "odp" ? /* @__PURE__ */ jsx(OdpSlideListScreen, {}) : /* @__PURE__ */ jsx(PptxSlideListScreen, {});
6330
7017
  case "slideDetail": return /* @__PURE__ */ jsx(SlideDetailScreen, { screen });
6331
7018
  case "shapeEditor": return /* @__PURE__ */ jsx(ShapeEditorScreen, { screen });
7019
+ case "slideTableDetail": return /* @__PURE__ */ jsx(SlideTableDetailScreen, { screen });
6332
7020
  case "notesEditor": return /* @__PURE__ */ jsx(NotesEditorScreen, { screen });
6333
7021
  case "sheetList": return /* @__PURE__ */ jsx(OdsSheetListScreen, {});
6334
7022
  case "spreadsheetGrid": return /* @__PURE__ */ jsx(OdsSpreadsheetGridScreen, {});
@@ -6441,6 +7129,10 @@ function AppShell({ startPath }) {
6441
7129
  dispatch({ type: "REQUEST_CLOSE" });
6442
7130
  return;
6443
7131
  }
7132
+ if (key.ctrl && input === "z") {
7133
+ dispatch({ type: "UNDO" });
7134
+ return;
7135
+ }
6444
7136
  if (input === ":") {
6445
7137
  dispatch({
6446
7138
  type: "OPEN_OVERLAY",