document-cli 1.7.0 → 1.9.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, bytesToBase64, 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";
@@ -508,6 +508,11 @@ function drawingDocument(state) {
508
508
  if (doc === void 0) return;
509
509
  return doc.format === "odg" ? doc : void 0;
510
510
  }
511
+ function vectorHostDocument(state) {
512
+ const doc = state.openDocument;
513
+ if (doc === void 0) return;
514
+ return doc.format === "odg" || doc.format === "odp" ? doc : void 0;
515
+ }
511
516
  function markdownDocument(state) {
512
517
  const doc = state.openDocument;
513
518
  if (doc === void 0) return;
@@ -569,6 +574,31 @@ function setTextContainerText(container, text) {
569
574
  function setCellText(cell, text) {
570
575
  setTextContainerText(cell, text);
571
576
  }
577
+ function mutableMathMlNode(node) {
578
+ if (node.type === "element") return {
579
+ type: "element",
580
+ tag: node.tag,
581
+ attributes: [...node.attributes],
582
+ children: node.children.map(mutableMathMlNode)
583
+ };
584
+ if (node.type === "text") return {
585
+ type: "text",
586
+ value: node.value
587
+ };
588
+ if (node.type === "cdata" || node.type === "comment") return {
589
+ type: node.type,
590
+ value: ""
591
+ };
592
+ if (node.type === "declaration") return {
593
+ type: "declaration",
594
+ attributes: []
595
+ };
596
+ return {
597
+ type: "pi",
598
+ target: "",
599
+ content: ""
600
+ };
601
+ }
572
602
  function appReducer(state, action) {
573
603
  switch (action.type) {
574
604
  case "PUSH_SCREEN": return {
@@ -715,6 +745,12 @@ function appReducer(state, action) {
715
745
  case "SET_RUN_COLOR": return withRun(state, action.blockIndex, action.runIndex, (run) => {
716
746
  run.color = action.color;
717
747
  });
748
+ case "SET_RUN_FONT_FAMILY": return withRun(state, action.blockIndex, action.runIndex, (run) => {
749
+ run.fontFamily = action.fontFamily;
750
+ });
751
+ case "SET_RUN_FONT_SIZE": return withRun(state, action.blockIndex, action.runIndex, (run) => {
752
+ run.sizePt = action.sizePt;
753
+ });
718
754
  case "APPEND_TABLE": {
719
755
  const doc = wordprocessingDocument(state);
720
756
  if (doc === void 0) return wrongDocument(state, "a docx or odt document");
@@ -775,6 +811,47 @@ function appReducer(state, action) {
775
811
  setTextContainerText(item, action.text);
776
812
  });
777
813
  }
814
+ case "ADD_LIST": {
815
+ const doc = wordprocessingDocument(state);
816
+ if (doc === void 0) return wrongDocument(state, "a docx or odt document");
817
+ if (doc.format !== "odt") return wrongDocument(state, "an odt document (lists are an odt-only concept)");
818
+ return mutate(state, doc, () => {
819
+ doc.editor.body.appendList();
820
+ });
821
+ }
822
+ case "INSERT_PARAGRAPH_IMAGE": {
823
+ const doc = wordprocessingDocument(state);
824
+ if (doc === void 0) return wrongDocument(state, "a docx or odt document");
825
+ const paragraph = paragraphAt(doc, action.blockIndex);
826
+ if (paragraph === void 0) return withStatus(state, "warning", `There is no paragraph at index ${action.blockIndex}`);
827
+ return mutate(state, doc, () => {
828
+ paragraph.insertImageAfter({
829
+ format: action.format,
830
+ bytes: action.bytes,
831
+ widthPt: action.widthPt,
832
+ heightPt: action.heightPt,
833
+ altText: action.altText
834
+ });
835
+ });
836
+ }
837
+ case "INSERT_DOCX_FORMULA": {
838
+ const doc = state.openDocument;
839
+ if (doc?.format !== "docx") return wrongDocument(state, "a docx document");
840
+ const paragraph = doc.editor.paragraphs()[action.blockIndex];
841
+ if (paragraph === void 0) return withStatus(state, "warning", `There is no paragraph at index ${action.blockIndex}`);
842
+ let written = true;
843
+ const nextState = mutate(state, doc, () => {
844
+ written = paragraph.appendOfficeMath(action.mathml).written;
845
+ });
846
+ return written ? nextState : withStatus(nextState, "warning", "The formula produced no OMML content and was not written");
847
+ }
848
+ case "INSERT_ODT_FORMULA": {
849
+ const doc = state.openDocument;
850
+ if (doc?.format !== "odt") return wrongDocument(state, "an odt document");
851
+ return mutate(state, doc, () => {
852
+ doc.editor.body.appendFormula({ mathml: action.mathml.map(mutableMathMlNode) }, action.frame);
853
+ });
854
+ }
778
855
  case "ADD_SLIDE": {
779
856
  const doc = presentationDocument(state);
780
857
  if (doc === void 0) return wrongDocument(state, "a pptx or odp document");
@@ -885,6 +962,23 @@ function appReducer(state, action) {
885
962
  case "SET_CELL_VALUE": return withSheet(state, action.sheetIndex, (sheet) => {
886
963
  sheet.cell(action.row, action.column).value = action.value;
887
964
  });
965
+ case "SET_CELL_FORMULA": return withSheet(state, action.sheetIndex, (sheet) => {
966
+ sheet.cell(action.row, action.column).formula = action.formula;
967
+ });
968
+ case "ADD_SHEET_IMAGE": return withSheet(state, action.sheetIndex, (sheet) => {
969
+ sheet.addImage({
970
+ kind: "image",
971
+ format: action.format,
972
+ base64: bytesToBase64(action.bytes),
973
+ widthPt: action.widthPt,
974
+ heightPt: action.heightPt,
975
+ altText: action.altText,
976
+ anchorRow: action.anchorRow,
977
+ anchorColumn: action.anchorColumn,
978
+ offsetXPt: action.offsetXPt,
979
+ offsetYPt: action.offsetYPt
980
+ });
981
+ });
888
982
  case "MERGE_CELLS": {
889
983
  const doc = spreadsheetDocument(state);
890
984
  if (doc === void 0) return wrongDocument(state, "an ods document");
@@ -908,23 +1002,64 @@ function appReducer(state, action) {
908
1002
  case "ADD_ELLIPSE":
909
1003
  case "ADD_LINE":
910
1004
  case "ADD_PATH": {
911
- const doc = drawingDocument(state);
912
- if (doc === void 0) return wrongDocument(state, "an odg document");
913
- const page = doc.editor.pages()[action.pageIndex];
914
- if (page === void 0) return withStatus(state, "warning", `There is no page at index ${action.pageIndex}`);
1005
+ const doc = vectorHostDocument(state);
1006
+ if (doc === void 0) return wrongDocument(state, "an odg or odp document");
1007
+ if (doc.format === "odg") {
1008
+ const page = doc.editor.pages()[action.containerIndex];
1009
+ if (page === void 0) return withStatus(state, "warning", `There is no page at index ${action.containerIndex}`);
1010
+ return mutate(state, doc, () => {
1011
+ switch (action.type) {
1012
+ case "ADD_RECT":
1013
+ page.addRect(action.init);
1014
+ return;
1015
+ case "ADD_ELLIPSE":
1016
+ page.addEllipse(action.init);
1017
+ return;
1018
+ case "ADD_LINE":
1019
+ page.addLine(action.init);
1020
+ return;
1021
+ case "ADD_PATH":
1022
+ page.addPath(action.init);
1023
+ return;
1024
+ }
1025
+ });
1026
+ }
1027
+ const slide = doc.editor.slides()[action.containerIndex];
1028
+ if (slide === void 0) return withStatus(state, "warning", `There is no slide at index ${action.containerIndex}`);
915
1029
  return mutate(state, doc, () => {
916
1030
  switch (action.type) {
917
1031
  case "ADD_RECT":
918
- page.addRect(action.init);
1032
+ slide.addVector({
1033
+ kind: "rect",
1034
+ frame: action.init.frame,
1035
+ fill: action.init.fill,
1036
+ stroke: action.init.stroke
1037
+ });
919
1038
  return;
920
1039
  case "ADD_ELLIPSE":
921
- page.addEllipse(action.init);
1040
+ slide.addVector({
1041
+ kind: "ellipse",
1042
+ frame: action.init.frame,
1043
+ fill: action.init.fill,
1044
+ stroke: action.init.stroke
1045
+ });
922
1046
  return;
923
1047
  case "ADD_LINE":
924
- page.addLine(action.init);
1048
+ slide.addVector({
1049
+ kind: "line",
1050
+ from: action.init.from,
1051
+ to: action.init.to,
1052
+ stroke: action.init.stroke
1053
+ });
925
1054
  return;
926
1055
  case "ADD_PATH":
927
- page.addPath(action.init);
1056
+ slide.addVector({
1057
+ kind: "path",
1058
+ frame: action.init.frame,
1059
+ subpaths: [...action.init.subpaths],
1060
+ fill: action.init.fill,
1061
+ stroke: action.init.stroke
1062
+ });
928
1063
  return;
929
1064
  }
930
1065
  });
@@ -1668,6 +1803,220 @@ function StatusLine() {
1668
1803
  ] });
1669
1804
  }
1670
1805
  //#endregion
1806
+ //#region src/tui/screens/shared/field-wizard.tsx
1807
+ function requireFieldValue(values, key) {
1808
+ const value = values[key];
1809
+ if (value === void 0) throw new Error(`Field wizard field '${key}' was never recorded before building the action.`);
1810
+ return value;
1811
+ }
1812
+ function FieldWizard(props) {
1813
+ const [stepIndex, setStepIndex] = useState(0);
1814
+ const [collected, setCollected] = useState({});
1815
+ const initialField = props.fields[0];
1816
+ const [draft, setDraft] = useState(initialField === void 0 ? "" : initialField.defaultValue);
1817
+ const field = props.fields[stepIndex];
1818
+ 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.`);
1819
+ return /* @__PURE__ */ jsxs(Box, {
1820
+ flexDirection: "column",
1821
+ borderStyle: "round",
1822
+ paddingX: 1,
1823
+ children: [
1824
+ /* @__PURE__ */ jsx(Text, {
1825
+ bold: true,
1826
+ children: field.label
1827
+ }),
1828
+ /* @__PURE__ */ jsx(TextField, {
1829
+ value: draft,
1830
+ isFocused: true,
1831
+ onChange: setDraft,
1832
+ onCancel: props.onCancel,
1833
+ onSubmit: (value) => {
1834
+ const recorded = {
1835
+ ...collected,
1836
+ [field.key]: value
1837
+ };
1838
+ const nextIndex = stepIndex + 1;
1839
+ const nextField = props.fields[nextIndex];
1840
+ if (nextField === void 0) {
1841
+ props.onComplete(recorded);
1842
+ return;
1843
+ }
1844
+ setCollected(recorded);
1845
+ setDraft(nextField.defaultValue);
1846
+ setStepIndex(nextIndex);
1847
+ }
1848
+ }),
1849
+ /* @__PURE__ */ jsxs(Text, {
1850
+ dimColor: true,
1851
+ children: [
1852
+ "Step ",
1853
+ stepIndex + 1,
1854
+ " of ",
1855
+ props.fields.length,
1856
+ " -- Enter to continue, Esc to cancel"
1857
+ ]
1858
+ })
1859
+ ]
1860
+ });
1861
+ }
1862
+ //#endregion
1863
+ //#region src/tui/screens/shared/formula-presets.ts
1864
+ function element(tag, children = []) {
1865
+ return {
1866
+ type: "element",
1867
+ tag,
1868
+ attributes: [],
1869
+ children
1870
+ };
1871
+ }
1872
+ function text(value) {
1873
+ return {
1874
+ type: "text",
1875
+ value
1876
+ };
1877
+ }
1878
+ function mi(name) {
1879
+ return element("mi", [text(name)]);
1880
+ }
1881
+ function mn(value) {
1882
+ return element("mn", [text(value)]);
1883
+ }
1884
+ function mo(operator) {
1885
+ return element("mo", [text(operator)]);
1886
+ }
1887
+ const FORMULA_PRESETS = [
1888
+ {
1889
+ label: "Fraction: x / 2",
1890
+ mathml: [element("mfrac", [mi("x"), mn("2")])]
1891
+ },
1892
+ {
1893
+ label: "Power: x^2",
1894
+ mathml: [element("msup", [mi("x"), mn("2")])]
1895
+ },
1896
+ {
1897
+ label: "Subscript: x_i",
1898
+ mathml: [element("msub", [mi("x"), mi("i")])]
1899
+ },
1900
+ {
1901
+ label: "Square root: sqrt(x)",
1902
+ mathml: [element("msqrt", [mi("x")])]
1903
+ },
1904
+ {
1905
+ label: "Summation: sum(i=1..n) i",
1906
+ mathml: [element("munderover", [
1907
+ mo("∑"),
1908
+ element("mrow", [
1909
+ mi("i"),
1910
+ mo("="),
1911
+ mn("1")
1912
+ ]),
1913
+ mi("n")
1914
+ ])]
1915
+ },
1916
+ {
1917
+ label: "Quadratic formula",
1918
+ mathml: [element("mrow", [
1919
+ mi("x"),
1920
+ mo("="),
1921
+ element("mfrac", [element("mrow", [
1922
+ mo("-"),
1923
+ mi("b"),
1924
+ mo("±"),
1925
+ element("msqrt", [element("mrow", [
1926
+ element("msup", [mi("b"), mn("2")]),
1927
+ mo("-"),
1928
+ mn("4"),
1929
+ mi("a"),
1930
+ mi("c")
1931
+ ])])
1932
+ ]), element("mrow", [mn("2"), mi("a")])])
1933
+ ])]
1934
+ }
1935
+ ];
1936
+ //#endregion
1937
+ //#region src/tui/screens/shared/formula-picker.tsx
1938
+ const RAW_ENTRY_LABEL = "Raw MathML...";
1939
+ const PICKER_ROWS = [...FORMULA_PRESETS.map((preset) => ({
1940
+ label: preset.label,
1941
+ mathml: preset.mathml
1942
+ })), {
1943
+ label: RAW_ENTRY_LABEL,
1944
+ mathml: void 0
1945
+ }];
1946
+ function FormulaPicker(props) {
1947
+ const [rawInput, setRawInput] = useState(void 0);
1948
+ const { selectedIndex } = useNavigationInput({
1949
+ itemCount: PICKER_ROWS.length,
1950
+ isActive: props.isActive && rawInput === void 0,
1951
+ onBack: props.onCancel,
1952
+ onSelect: (index) => {
1953
+ const row = PICKER_ROWS[index];
1954
+ if (row === void 0) return;
1955
+ if (row.mathml === void 0) {
1956
+ setRawInput("");
1957
+ return;
1958
+ }
1959
+ props.onMathml(row.mathml);
1960
+ }
1961
+ });
1962
+ if (rawInput !== void 0) return /* @__PURE__ */ jsxs(Box, {
1963
+ flexDirection: "column",
1964
+ borderStyle: "round",
1965
+ paddingX: 1,
1966
+ children: [
1967
+ /* @__PURE__ */ jsx(Text, {
1968
+ bold: true,
1969
+ children: "Raw MathML (the children of the <math> root, e.g. <mfrac>...</mfrac>)"
1970
+ }),
1971
+ /* @__PURE__ */ jsx(TextField, {
1972
+ value: rawInput,
1973
+ isFocused: true,
1974
+ onChange: setRawInput,
1975
+ onCancel: () => {
1976
+ setRawInput(void 0);
1977
+ },
1978
+ onSubmit: (value) => {
1979
+ try {
1980
+ const parsed = parseXml(value);
1981
+ props.onMathml(parsed);
1982
+ } catch (error) {
1983
+ props.onInvalidRawMathml(describeError(error));
1984
+ }
1985
+ setRawInput(void 0);
1986
+ }
1987
+ }),
1988
+ /* @__PURE__ */ jsx(Text, {
1989
+ dimColor: true,
1990
+ children: "Enter to insert, Esc to cancel"
1991
+ })
1992
+ ]
1993
+ });
1994
+ return /* @__PURE__ */ jsxs(Box, {
1995
+ flexDirection: "column",
1996
+ borderStyle: "round",
1997
+ paddingX: 1,
1998
+ children: [
1999
+ /* @__PURE__ */ jsx(Text, {
2000
+ bold: true,
2001
+ children: "Insert formula -- choose a preset or write raw MathML"
2002
+ }),
2003
+ /* @__PURE__ */ jsx(ListView, {
2004
+ items: PICKER_ROWS,
2005
+ selectedIndex,
2006
+ reservedRows: PICKER_ROWS.length + 2,
2007
+ renderItem: (row, isSelected) => /* @__PURE__ */ jsxs(Text, {
2008
+ color: isSelected ? "cyan" : void 0,
2009
+ children: [isSelected ? "> " : " ", row.label]
2010
+ })
2011
+ }),
2012
+ /* @__PURE__ */ jsx(Text, {
2013
+ dimColor: true,
2014
+ children: "Enter to choose, Esc to cancel"
2015
+ })
2016
+ ]
2017
+ });
2018
+ }
2019
+ //#endregion
1671
2020
  //#region src/tui/screens/shared/text.ts
1672
2021
  function truncatePreview(text, maxLength) {
1673
2022
  const singleLine = text.replace(/\s+/gu, " ").trim();
@@ -1682,6 +2031,10 @@ function parseNonNegativeIntField(raw, fallback) {
1682
2031
  const parsed = Number.parseInt(raw, 10);
1683
2032
  return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
1684
2033
  }
2034
+ function parseNumberField(raw, fallback) {
2035
+ const parsed = Number.parseFloat(raw);
2036
+ return Number.isFinite(parsed) ? parsed : fallback;
2037
+ }
1685
2038
  //#endregion
1686
2039
  //#region src/tui/screens/shared/paragraph-family.tsx
1687
2040
  function paragraphFamilyDocument(openDocument) {
@@ -1747,6 +2100,36 @@ function listSummary(list, index) {
1747
2100
  }
1748
2101
  const DEFAULT_TABLE_ROWS$1 = 2;
1749
2102
  const DEFAULT_TABLE_COLUMNS$1 = 2;
2103
+ const FORMULA_FRAME_FIELDS = [
2104
+ {
2105
+ key: "xPt",
2106
+ label: "X (pt)",
2107
+ defaultValue: "40"
2108
+ },
2109
+ {
2110
+ key: "yPt",
2111
+ label: "Y (pt)",
2112
+ defaultValue: "40"
2113
+ },
2114
+ {
2115
+ key: "widthPt",
2116
+ label: "Width (pt)",
2117
+ defaultValue: "120"
2118
+ },
2119
+ {
2120
+ key: "heightPt",
2121
+ label: "Height (pt)",
2122
+ defaultValue: "40"
2123
+ }
2124
+ ];
2125
+ function readFormulaFrame(values) {
2126
+ return {
2127
+ xPt: parseNumberField(requireFieldValue(values, "xPt"), 0),
2128
+ yPt: parseNumberField(requireFieldValue(values, "yPt"), 0),
2129
+ widthPt: parseNumberField(requireFieldValue(values, "widthPt"), 120),
2130
+ heightPt: parseNumberField(requireFieldValue(values, "heightPt"), 40)
2131
+ };
2132
+ }
1750
2133
  function ParagraphFamilyBodyList(props) {
1751
2134
  const { adapter } = props;
1752
2135
  const state = useAppState();
@@ -1797,6 +2180,9 @@ function ParagraphFamilyBodyList(props) {
1797
2180
  const [wizardStartColumn, setWizardStartColumn] = useState(0);
1798
2181
  const [wizardRowSpan, setWizardRowSpan] = useState(1);
1799
2182
  const wizardOpen = tableWizard !== "closed";
2183
+ const [formulaFlow, setFormulaFlow] = useState("closed");
2184
+ const [pendingFormulaMathml, setPendingFormulaMathml] = useState(void 0);
2185
+ const formulaFlowOpen = formulaFlow !== "closed";
1800
2186
  const closeWizard = () => {
1801
2187
  setTableWizard("closed");
1802
2188
  };
@@ -1822,7 +2208,23 @@ function ParagraphFamilyBodyList(props) {
1822
2208
  setWizardDraft(String(DEFAULT_TABLE_ROWS$1));
1823
2209
  setTableWizard("rows");
1824
2210
  }
1825
- }, { isActive: !anyOverlayOpen(state) && !wizardOpen });
2211
+ }, { isActive: !anyOverlayOpen(state) && !wizardOpen && !formulaFlowOpen });
2212
+ useInput((input) => {
2213
+ if (input === "L" && adapter.lists !== void 0) {
2214
+ const newIndex = adapter.lists().length;
2215
+ dispatch({ type: "ADD_LIST" });
2216
+ dispatch({
2217
+ type: "PUSH_SCREEN",
2218
+ screen: {
2219
+ kind: "listEditor",
2220
+ blockIndex: newIndex
2221
+ }
2222
+ });
2223
+ }
2224
+ }, { isActive: !anyOverlayOpen(state) && !wizardOpen && !formulaFlowOpen });
2225
+ useInput((input) => {
2226
+ if (input === "m" && adapter.formatLabel === "odt") setFormulaFlow("picking");
2227
+ }, { isActive: !anyOverlayOpen(state) && !wizardOpen && !formulaFlowOpen });
1826
2228
  useInput((input, key) => {
1827
2229
  if (key.escape) {
1828
2230
  closeWizard();
@@ -1870,7 +2272,7 @@ function ParagraphFamilyBodyList(props) {
1870
2272
  };
1871
2273
  const { selectedIndex } = usePersistedSelection(selectionKeyFor({ kind: "bodyList" }), {
1872
2274
  itemCount: selectableRowIndices.length,
1873
- isActive: !anyOverlayOpen(state) && !wizardOpen,
2275
+ isActive: !anyOverlayOpen(state) && !wizardOpen && !formulaFlowOpen,
1874
2276
  onBack: () => {
1875
2277
  dispatch({ type: "POP_SCREEN" });
1876
2278
  },
@@ -2037,9 +2439,47 @@ function ParagraphFamilyBodyList(props) {
2037
2439
  onSubmit: submitWizardMergeColSpan,
2038
2440
  onCancel: closeWizard
2039
2441
  })] }) : void 0,
2040
- /* @__PURE__ */ jsx(Text, {
2442
+ formulaFlow === "picking" ? /* @__PURE__ */ jsx(FormulaPicker, {
2443
+ isActive: !anyOverlayOpen(state),
2444
+ onCancel: () => {
2445
+ setFormulaFlow("closed");
2446
+ },
2447
+ onMathml: (mathml) => {
2448
+ setPendingFormulaMathml(mathml);
2449
+ setFormulaFlow("frame");
2450
+ },
2451
+ onInvalidRawMathml: (message) => {
2452
+ dispatch({
2453
+ type: "SET_STATUS",
2454
+ severity: "warning",
2455
+ text: `Could not parse MathML: ${message}`
2456
+ });
2457
+ }
2458
+ }) : void 0,
2459
+ formulaFlow === "frame" ? /* @__PURE__ */ jsx(FieldWizard, {
2460
+ fields: FORMULA_FRAME_FIELDS,
2461
+ onCancel: () => {
2462
+ setFormulaFlow("closed");
2463
+ setPendingFormulaMathml(void 0);
2464
+ },
2465
+ onComplete: (values) => {
2466
+ if (pendingFormulaMathml !== void 0) dispatch({
2467
+ type: "INSERT_ODT_FORMULA",
2468
+ mathml: pendingFormulaMathml,
2469
+ frame: readFormulaFrame(values)
2470
+ });
2471
+ setFormulaFlow("closed");
2472
+ setPendingFormulaMathml(void 0);
2473
+ }
2474
+ }) : void 0,
2475
+ /* @__PURE__ */ jsxs(Text, {
2041
2476
  dimColor: true,
2042
- children: "Enter to open, a to append a paragraph, T to append a table, Esc back"
2477
+ children: [
2478
+ "Enter to open, a to append a paragraph, T to append a table",
2479
+ adapter.lists !== void 0 ? ", L to add a list" : "",
2480
+ adapter.formatLabel === "odt" ? ", m to insert a formula" : "",
2481
+ ", Esc back"
2482
+ ]
2043
2483
  })
2044
2484
  ]
2045
2485
  });
@@ -2059,6 +2499,67 @@ function isValidHexColorInput(input) {
2059
2499
  }
2060
2500
  //#endregion
2061
2501
  //#region src/tui/screens/editors/docx/paragraph-detail.tsx
2502
+ const DEFAULT_RUN_SIZE_PT = 12;
2503
+ const IMAGE_FIELDS = [
2504
+ {
2505
+ key: "path",
2506
+ label: "Image file path (.png/.jpg/.jpeg)",
2507
+ defaultValue: ""
2508
+ },
2509
+ {
2510
+ key: "widthPt",
2511
+ label: "Width (pt)",
2512
+ defaultValue: "100"
2513
+ },
2514
+ {
2515
+ key: "heightPt",
2516
+ label: "Height (pt)",
2517
+ defaultValue: "60"
2518
+ },
2519
+ {
2520
+ key: "altText",
2521
+ label: "Alt text, blank for none",
2522
+ defaultValue: ""
2523
+ }
2524
+ ];
2525
+ function inferImageFormat$1(path) {
2526
+ const extension = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
2527
+ if (extension === "png") return "png";
2528
+ if (extension === "jpg" || extension === "jpeg") return "jpeg";
2529
+ }
2530
+ async function applyInsertImage(blockIndex, values, dispatch) {
2531
+ const path = requireFieldValue(values, "path");
2532
+ const format = inferImageFormat$1(path);
2533
+ if (format === void 0) {
2534
+ dispatch({
2535
+ type: "SET_STATUS",
2536
+ severity: "warning",
2537
+ text: `${path} is not a .png or .jpg/.jpeg file -- image not inserted`
2538
+ });
2539
+ return;
2540
+ }
2541
+ try {
2542
+ const bytes = new Uint8Array(await readInput(path));
2543
+ const widthPt = parseNumberField(requireFieldValue(values, "widthPt"), 100);
2544
+ const heightPt = parseNumberField(requireFieldValue(values, "heightPt"), 60);
2545
+ const altTextRaw = requireFieldValue(values, "altText").trim();
2546
+ dispatch({
2547
+ type: "INSERT_PARAGRAPH_IMAGE",
2548
+ blockIndex,
2549
+ format,
2550
+ bytes,
2551
+ widthPt,
2552
+ heightPt,
2553
+ altText: altTextRaw.length === 0 ? void 0 : altTextRaw
2554
+ });
2555
+ } catch (error) {
2556
+ dispatch({
2557
+ type: "SET_STATUS",
2558
+ severity: "error",
2559
+ text: `Could not read ${path}: ${describeError(error)}`
2560
+ });
2561
+ }
2562
+ }
2062
2563
  function ParagraphRunsView(props) {
2063
2564
  if (props.runs.length === 0) return /* @__PURE__ */ jsx(Text, {
2064
2565
  dimColor: true,
@@ -2078,6 +2579,10 @@ function ParagraphDetailScreen() {
2078
2579
  const dispatch = useAppDispatch();
2079
2580
  const [runIndex, setRunIndex] = useState(0);
2080
2581
  const [colorInput, setColorInput] = useState(void 0);
2582
+ const [fontFamilyInput, setFontFamilyInput] = useState(void 0);
2583
+ const [fontSizeInput, setFontSizeInput] = useState(void 0);
2584
+ const [imageWizardOpen, setImageWizardOpen] = useState(false);
2585
+ const [formulaPickerOpen, setFormulaPickerOpen] = useState(false);
2081
2586
  const screen = currentScreen(state);
2082
2587
  const doc = paragraphFamilyDocument(state.openDocument);
2083
2588
  const paragraph = screen.kind === "paragraphDetail" && doc !== void 0 ? liveParagraphAt(doc, screen.blockIndex) : void 0;
@@ -2140,6 +2645,14 @@ function ParagraphDetailScreen() {
2140
2645
  });
2141
2646
  return;
2142
2647
  }
2648
+ if (input === "I") {
2649
+ setImageWizardOpen(true);
2650
+ return;
2651
+ }
2652
+ if (input === "m" && doc?.format === "docx") {
2653
+ setFormulaPickerOpen(true);
2654
+ return;
2655
+ }
2143
2656
  if (selectedRun === void 0) return;
2144
2657
  if (input === "b") {
2145
2658
  dispatch({
@@ -2165,8 +2678,16 @@ function ParagraphDetailScreen() {
2165
2678
  });
2166
2679
  return;
2167
2680
  }
2168
- if (input === "c") setColorInput(selectedRun.color === void 0 ? "" : layoutColorToHex(selectedRun.color).slice(1));
2169
- }, { isActive: !anyOverlayOpen(state) && colorInput === void 0 });
2681
+ if (input === "c") {
2682
+ setColorInput(selectedRun.color === void 0 ? "" : layoutColorToHex(selectedRun.color).slice(1));
2683
+ return;
2684
+ }
2685
+ if (input === "f") {
2686
+ setFontFamilyInput(selectedRun.fontFamily ?? "");
2687
+ return;
2688
+ }
2689
+ if (input === "s") setFontSizeInput(selectedRun.sizePt === void 0 ? "" : String(selectedRun.sizePt));
2690
+ }, { isActive: !anyOverlayOpen(state) && colorInput === void 0 && fontFamilyInput === void 0 && fontSizeInput === void 0 && !imageWizardOpen && !formulaPickerOpen });
2170
2691
  if (screen.kind !== "paragraphDetail") return /* @__PURE__ */ jsx(Text, {
2171
2692
  color: "red",
2172
2693
  children: "ParagraphDetailScreen rendered outside a paragraphDetail screen."
@@ -2224,9 +2745,99 @@ function ParagraphDetailScreen() {
2224
2745
  setColorInput(void 0);
2225
2746
  }
2226
2747
  })] }),
2227
- /* @__PURE__ */ jsx(Text, {
2748
+ fontFamilyInput === void 0 ? void 0 : /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
2749
+ color: "cyan",
2750
+ children: "Font family: "
2751
+ }), /* @__PURE__ */ jsx(TextField, {
2752
+ value: fontFamilyInput,
2753
+ isFocused: true,
2754
+ placeholder: "e.g. Calibri",
2755
+ onChange: setFontFamilyInput,
2756
+ onSubmit: (value) => {
2757
+ const trimmed = value.trim();
2758
+ if (trimmed.length === 0) dispatch({
2759
+ type: "SET_STATUS",
2760
+ severity: "warning",
2761
+ text: "A font family name cannot be blank"
2762
+ });
2763
+ else dispatch({
2764
+ type: "SET_RUN_FONT_FAMILY",
2765
+ blockIndex,
2766
+ runIndex: clampedRunIndex,
2767
+ fontFamily: trimmed
2768
+ });
2769
+ setFontFamilyInput(void 0);
2770
+ },
2771
+ onCancel: () => {
2772
+ setFontFamilyInput(void 0);
2773
+ }
2774
+ })] }),
2775
+ fontSizeInput === void 0 ? void 0 : /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
2776
+ color: "cyan",
2777
+ children: "Font size (pt): "
2778
+ }), /* @__PURE__ */ jsx(TextField, {
2779
+ value: fontSizeInput,
2780
+ isFocused: true,
2781
+ placeholder: "e.g. 12",
2782
+ onChange: setFontSizeInput,
2783
+ onSubmit: (value) => {
2784
+ const sizePt = parseNumberField(value, selectedRun?.sizePt ?? DEFAULT_RUN_SIZE_PT);
2785
+ if (sizePt <= 0) dispatch({
2786
+ type: "SET_STATUS",
2787
+ severity: "warning",
2788
+ text: `"${value}" is not a positive font size`
2789
+ });
2790
+ else dispatch({
2791
+ type: "SET_RUN_FONT_SIZE",
2792
+ blockIndex,
2793
+ runIndex: clampedRunIndex,
2794
+ sizePt
2795
+ });
2796
+ setFontSizeInput(void 0);
2797
+ },
2798
+ onCancel: () => {
2799
+ setFontSizeInput(void 0);
2800
+ }
2801
+ })] }),
2802
+ imageWizardOpen ? /* @__PURE__ */ jsx(FieldWizard, {
2803
+ fields: IMAGE_FIELDS,
2804
+ onCancel: () => {
2805
+ setImageWizardOpen(false);
2806
+ },
2807
+ onComplete: (values) => {
2808
+ applyInsertImage(blockIndex, values, dispatch).then(() => {
2809
+ setImageWizardOpen(false);
2810
+ });
2811
+ }
2812
+ }) : void 0,
2813
+ formulaPickerOpen ? /* @__PURE__ */ jsx(FormulaPicker, {
2814
+ isActive: !anyOverlayOpen(state),
2815
+ onCancel: () => {
2816
+ setFormulaPickerOpen(false);
2817
+ },
2818
+ onMathml: (mathml) => {
2819
+ dispatch({
2820
+ type: "INSERT_DOCX_FORMULA",
2821
+ blockIndex,
2822
+ mathml
2823
+ });
2824
+ setFormulaPickerOpen(false);
2825
+ },
2826
+ onInvalidRawMathml: (message) => {
2827
+ dispatch({
2828
+ type: "SET_STATUS",
2829
+ severity: "warning",
2830
+ text: `Could not parse MathML: ${message}`
2831
+ });
2832
+ }
2833
+ }) : void 0,
2834
+ /* @__PURE__ */ jsxs(Text, {
2228
2835
  dimColor: true,
2229
- children: "<- / -> move, Enter edit text, b/i/u toggle, c colour, a append run, Esc back"
2836
+ children: [
2837
+ "<- / -> move, Enter edit text, b/i/u toggle, c colour, f font, s size, a append run, I image",
2838
+ doc.format === "docx" ? ", m formula" : "",
2839
+ ", Esc back"
2840
+ ]
2230
2841
  })
2231
2842
  ]
2232
2843
  });
@@ -3292,6 +3903,64 @@ function OdbTableRowsScreen() {
3292
3903
  });
3293
3904
  }
3294
3905
  //#endregion
3906
+ //#region src/tui/screens/shared/vector-fields.ts
3907
+ function parseColorField(raw) {
3908
+ const trimmed = raw.trim();
3909
+ if (trimmed.length === 0) return;
3910
+ const [r, g, b] = trimmed.split(/\s+/).map((part) => Number.parseFloat(part));
3911
+ if (r === void 0 || g === void 0 || b === void 0 || ![
3912
+ r,
3913
+ g,
3914
+ b
3915
+ ].every((value) => Number.isFinite(value))) return;
3916
+ return {
3917
+ r,
3918
+ g,
3919
+ b
3920
+ };
3921
+ }
3922
+ function parseStrokeField(raw) {
3923
+ const trimmed = raw.trim();
3924
+ if (trimmed.length === 0) return;
3925
+ const [r, g, b, widthPt] = trimmed.split(/\s+/).map((part) => Number.parseFloat(part));
3926
+ if (r === void 0 || g === void 0 || b === void 0 || widthPt === void 0 || ![
3927
+ r,
3928
+ g,
3929
+ b,
3930
+ widthPt
3931
+ ].every((value) => Number.isFinite(value))) return;
3932
+ return {
3933
+ color: {
3934
+ r,
3935
+ g,
3936
+ b
3937
+ },
3938
+ widthPt
3939
+ };
3940
+ }
3941
+ function defaultTriangleSubpaths(widthPt, heightPt) {
3942
+ return [{
3943
+ start: {
3944
+ xPt: 0,
3945
+ yPt: heightPt
3946
+ },
3947
+ segments: [{
3948
+ kind: "line",
3949
+ to: {
3950
+ xPt: widthPt / 2,
3951
+ yPt: 0
3952
+ }
3953
+ }, {
3954
+ kind: "line",
3955
+ to: {
3956
+ xPt: widthPt,
3957
+ yPt: heightPt
3958
+ }
3959
+ }],
3960
+ closed: true
3961
+ }];
3962
+ }
3963
+ //#endregion
3295
3964
  //#region src/tui/screens/editors/odg/shared.ts
3296
3965
  function requireOdgDocument(state) {
3297
3966
  const doc = state.openDocument;
@@ -3350,85 +4019,20 @@ function formatFrame(box) {
3350
4019
  return `${formatPt(box.xPt)},${formatPt(box.yPt)} ${formatPt(box.widthPt)}x${formatPt(box.heightPt)}pt`;
3351
4020
  }
3352
4021
  function formatPoint$1(point) {
3353
- return `${formatPt(point.xPt)},${formatPt(point.yPt)}`;
3354
- }
3355
- function formatColor$1(color) {
3356
- return `rgb(${color.r.toFixed(2)}, ${color.g.toFixed(2)}, ${color.b.toFixed(2)})`;
3357
- }
3358
- function describeVectorGeometry(vector) {
3359
- if (vector.kind === "line") return `${formatPoint$1(vector.from)} -> ${formatPoint$1(vector.to)}`;
3360
- return formatFrame(vector.frame);
3361
- }
3362
- function describeFillStroke(vector) {
3363
- const parts = [];
3364
- if (vector.kind !== "line" && vector.fill !== void 0) parts.push(`fill ${formatColor$1(vector.fill)}`);
3365
- if (vector.stroke !== void 0) parts.push(`stroke ${formatColor$1(vector.stroke.color)} ${formatPt(vector.stroke.widthPt)}pt`);
3366
- return parts.length === 0 ? "no fill or stroke" : parts.join(", ");
3367
- }
3368
- function parseNumberField(raw, fallback) {
3369
- const parsed = Number.parseFloat(raw);
3370
- return Number.isFinite(parsed) ? parsed : fallback;
3371
- }
3372
- function parseColorField(raw) {
3373
- const trimmed = raw.trim();
3374
- if (trimmed.length === 0) return;
3375
- const [r, g, b] = trimmed.split(/\s+/).map((part) => Number.parseFloat(part));
3376
- if (r === void 0 || g === void 0 || b === void 0 || ![
3377
- r,
3378
- g,
3379
- b
3380
- ].every((value) => Number.isFinite(value))) return;
3381
- return {
3382
- r,
3383
- g,
3384
- b
3385
- };
3386
- }
3387
- function parseStrokeField(raw) {
3388
- const trimmed = raw.trim();
3389
- if (trimmed.length === 0) return;
3390
- const [r, g, b, widthPt] = trimmed.split(/\s+/).map((part) => Number.parseFloat(part));
3391
- if (r === void 0 || g === void 0 || b === void 0 || widthPt === void 0 || ![
3392
- r,
3393
- g,
3394
- b,
3395
- widthPt
3396
- ].every((value) => Number.isFinite(value))) return;
3397
- return {
3398
- color: {
3399
- r,
3400
- g,
3401
- b
3402
- },
3403
- widthPt
3404
- };
4022
+ return `${formatPt(point.xPt)},${formatPt(point.yPt)}`;
3405
4023
  }
3406
- function defaultTriangleSubpaths(widthPt, heightPt) {
3407
- return [{
3408
- start: {
3409
- xPt: 0,
3410
- yPt: heightPt
3411
- },
3412
- segments: [{
3413
- kind: "line",
3414
- to: {
3415
- xPt: widthPt / 2,
3416
- yPt: 0
3417
- }
3418
- }, {
3419
- kind: "line",
3420
- to: {
3421
- xPt: widthPt,
3422
- yPt: heightPt
3423
- }
3424
- }],
3425
- closed: true
3426
- }];
4024
+ function formatColor$1(color) {
4025
+ return `rgb(${color.r.toFixed(2)}, ${color.g.toFixed(2)}, ${color.b.toFixed(2)})`;
3427
4026
  }
3428
- function requireFieldValue(values, key) {
3429
- const value = values[key];
3430
- if (value === void 0) throw new Error(`Add-item field '${key}' was never recorded before building the action.`);
3431
- return value;
4027
+ function describeVectorGeometry(vector) {
4028
+ if (vector.kind === "line") return `${formatPoint$1(vector.from)} -> ${formatPoint$1(vector.to)}`;
4029
+ return formatFrame(vector.frame);
4030
+ }
4031
+ function describeFillStroke(vector) {
4032
+ const parts = [];
4033
+ if (vector.kind !== "line" && vector.fill !== void 0) parts.push(`fill ${formatColor$1(vector.fill)}`);
4034
+ if (vector.stroke !== void 0) parts.push(`stroke ${formatColor$1(vector.stroke.color)} ${formatPt(vector.stroke.widthPt)}pt`);
4035
+ return parts.length === 0 ? "no fill or stroke" : parts.join(", ");
3432
4036
  }
3433
4037
  //#endregion
3434
4038
  //#region src/tui/screens/editors/odg/page-list.tsx
@@ -3633,7 +4237,7 @@ async function applyAddKind(kind, pageIndex, doc, values, dispatch) {
3633
4237
  case "rect":
3634
4238
  dispatch({
3635
4239
  type: "ADD_RECT",
3636
- pageIndex,
4240
+ containerIndex: pageIndex,
3637
4241
  init: {
3638
4242
  frame: readFrame(values),
3639
4243
  fill: parseColorField(requireFieldValue(values, "fill")),
@@ -3645,7 +4249,7 @@ async function applyAddKind(kind, pageIndex, doc, values, dispatch) {
3645
4249
  case "ellipse":
3646
4250
  dispatch({
3647
4251
  type: "ADD_ELLIPSE",
3648
- pageIndex,
4252
+ containerIndex: pageIndex,
3649
4253
  init: {
3650
4254
  frame: readFrame(values),
3651
4255
  fill: parseColorField(requireFieldValue(values, "fill")),
@@ -3657,7 +4261,7 @@ async function applyAddKind(kind, pageIndex, doc, values, dispatch) {
3657
4261
  case "line":
3658
4262
  dispatch({
3659
4263
  type: "ADD_LINE",
3660
- pageIndex,
4264
+ containerIndex: pageIndex,
3661
4265
  init: {
3662
4266
  from: {
3663
4267
  xPt: parseNumberField(requireFieldValue(values, "fromXPt"), 0),
@@ -3683,7 +4287,7 @@ async function applyAddKind(kind, pageIndex, doc, values, dispatch) {
3683
4287
  const frame = readFrame(values);
3684
4288
  dispatch({
3685
4289
  type: "ADD_PATH",
3686
- pageIndex,
4290
+ containerIndex: pageIndex,
3687
4291
  init: {
3688
4292
  frame,
3689
4293
  subpaths: defaultTriangleSubpaths(frame.widthPt, frame.heightPt),
@@ -3736,56 +4340,6 @@ async function applyAddKind(kind, pageIndex, doc, values, dispatch) {
3736
4340
  }
3737
4341
  }
3738
4342
  }
3739
- function FieldWizard(props) {
3740
- const [stepIndex, setStepIndex] = useState(0);
3741
- const [collected, setCollected] = useState({});
3742
- const initialField = props.fields[0];
3743
- const [draft, setDraft] = useState(initialField === void 0 ? "" : initialField.defaultValue);
3744
- const field = props.fields[stepIndex];
3745
- 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.`);
3746
- return /* @__PURE__ */ jsxs(Box, {
3747
- flexDirection: "column",
3748
- borderStyle: "round",
3749
- paddingX: 1,
3750
- children: [
3751
- /* @__PURE__ */ jsx(Text, {
3752
- bold: true,
3753
- children: field.label
3754
- }),
3755
- /* @__PURE__ */ jsx(TextField, {
3756
- value: draft,
3757
- isFocused: true,
3758
- onChange: setDraft,
3759
- onCancel: props.onCancel,
3760
- onSubmit: (value) => {
3761
- const recorded = {
3762
- ...collected,
3763
- [field.key]: value
3764
- };
3765
- const nextIndex = stepIndex + 1;
3766
- const nextField = props.fields[nextIndex];
3767
- if (nextField === void 0) {
3768
- props.onComplete(recorded);
3769
- return;
3770
- }
3771
- setCollected(recorded);
3772
- setDraft(nextField.defaultValue);
3773
- setStepIndex(nextIndex);
3774
- }
3775
- }),
3776
- /* @__PURE__ */ jsxs(Text, {
3777
- dimColor: true,
3778
- children: [
3779
- "Step ",
3780
- stepIndex + 1,
3781
- " of ",
3782
- props.fields.length,
3783
- " -- Enter to continue, Esc to cancel"
3784
- ]
3785
- })
3786
- ]
3787
- });
3788
- }
3789
4343
  function AddItemFlow(props) {
3790
4344
  const dispatch = useAppDispatch();
3791
4345
  const [kind, setKind] = useState(void 0);
@@ -4665,17 +5219,149 @@ function summarizeSlideTables(doc, slideIndex) {
4665
5219
  }
4666
5220
  //#endregion
4667
5221
  //#region src/tui/screens/editors/pptx/slide-detail.tsx
4668
- const IMAGE_EXTENSION_TO_FORMAT = {
5222
+ const IMAGE_EXTENSION_TO_FORMAT$1 = {
4669
5223
  png: "png",
4670
5224
  jpg: "jpeg",
4671
5225
  jpeg: "jpeg"
4672
5226
  };
4673
5227
  const DEFAULT_TABLE_ROWS = 2;
4674
5228
  const DEFAULT_TABLE_COLUMNS = 2;
5229
+ const VECTOR_GEOMETRY_FIELDS = [
5230
+ {
5231
+ key: "xPt",
5232
+ label: "X (pt)",
5233
+ defaultValue: "40"
5234
+ },
5235
+ {
5236
+ key: "yPt",
5237
+ label: "Y (pt)",
5238
+ defaultValue: "40"
5239
+ },
5240
+ {
5241
+ key: "widthPt",
5242
+ label: "Width (pt)",
5243
+ defaultValue: "160"
5244
+ },
5245
+ {
5246
+ key: "heightPt",
5247
+ label: "Height (pt)",
5248
+ defaultValue: "100"
5249
+ }
5250
+ ];
5251
+ const VECTOR_FILL_FIELD = {
5252
+ key: "fill",
5253
+ label: "Fill \"r g b\" (0-1 each), blank for none",
5254
+ defaultValue: "0.8 0.8 0.8"
5255
+ };
5256
+ const VECTOR_STROKE_FIELD = {
5257
+ key: "stroke",
5258
+ label: "Stroke \"r g b widthPt\" (0-1 colour, pt width), blank for none",
5259
+ defaultValue: "0 0 0 1"
5260
+ };
5261
+ const VECTOR_LINE_FIELDS = [
5262
+ {
5263
+ key: "fromXPt",
5264
+ label: "From X (pt)",
5265
+ defaultValue: "40"
5266
+ },
5267
+ {
5268
+ key: "fromYPt",
5269
+ label: "From Y (pt)",
5270
+ defaultValue: "40"
5271
+ },
5272
+ {
5273
+ key: "toXPt",
5274
+ label: "To X (pt)",
5275
+ defaultValue: "200"
5276
+ },
5277
+ {
5278
+ key: "toYPt",
5279
+ label: "To Y (pt)",
5280
+ defaultValue: "40"
5281
+ },
5282
+ VECTOR_STROKE_FIELD
5283
+ ];
5284
+ function fieldsForVectorKind(kind) {
5285
+ switch (kind) {
5286
+ case "rect":
5287
+ case "ellipse":
5288
+ case "path": return [
5289
+ ...VECTOR_GEOMETRY_FIELDS,
5290
+ VECTOR_FILL_FIELD,
5291
+ VECTOR_STROKE_FIELD
5292
+ ];
5293
+ case "line": return VECTOR_LINE_FIELDS;
5294
+ }
5295
+ }
5296
+ function readVectorFrame(values) {
5297
+ return {
5298
+ xPt: parseNumberField(requireFieldValue(values, "xPt"), 0),
5299
+ yPt: parseNumberField(requireFieldValue(values, "yPt"), 0),
5300
+ widthPt: parseNumberField(requireFieldValue(values, "widthPt"), 160),
5301
+ heightPt: parseNumberField(requireFieldValue(values, "heightPt"), 100)
5302
+ };
5303
+ }
5304
+ function buildVectorAction(kind, slideIndex, values) {
5305
+ switch (kind) {
5306
+ case "rect": return {
5307
+ type: "ADD_RECT",
5308
+ containerIndex: slideIndex,
5309
+ init: {
5310
+ frame: readVectorFrame(values),
5311
+ fill: parseColorField(requireFieldValue(values, "fill")),
5312
+ stroke: parseStrokeField(requireFieldValue(values, "stroke"))
5313
+ }
5314
+ };
5315
+ case "ellipse": return {
5316
+ type: "ADD_ELLIPSE",
5317
+ containerIndex: slideIndex,
5318
+ init: {
5319
+ frame: readVectorFrame(values),
5320
+ fill: parseColorField(requireFieldValue(values, "fill")),
5321
+ stroke: parseStrokeField(requireFieldValue(values, "stroke"))
5322
+ }
5323
+ };
5324
+ case "line": return {
5325
+ type: "ADD_LINE",
5326
+ containerIndex: slideIndex,
5327
+ init: {
5328
+ from: {
5329
+ xPt: parseNumberField(requireFieldValue(values, "fromXPt"), 0),
5330
+ yPt: parseNumberField(requireFieldValue(values, "fromYPt"), 0)
5331
+ },
5332
+ to: {
5333
+ xPt: parseNumberField(requireFieldValue(values, "toXPt"), 100),
5334
+ yPt: parseNumberField(requireFieldValue(values, "toYPt"), 0)
5335
+ },
5336
+ stroke: parseStrokeField(requireFieldValue(values, "stroke")) ?? {
5337
+ color: {
5338
+ r: 0,
5339
+ g: 0,
5340
+ b: 0
5341
+ },
5342
+ widthPt: 1
5343
+ }
5344
+ }
5345
+ };
5346
+ case "path": {
5347
+ const frame = readVectorFrame(values);
5348
+ return {
5349
+ type: "ADD_PATH",
5350
+ containerIndex: slideIndex,
5351
+ init: {
5352
+ frame,
5353
+ subpaths: defaultTriangleSubpaths(frame.widthPt, frame.heightPt),
5354
+ fill: parseColorField(requireFieldValue(values, "fill")),
5355
+ stroke: parseStrokeField(requireFieldValue(values, "stroke"))
5356
+ }
5357
+ };
5358
+ }
5359
+ }
5360
+ }
4675
5361
  function imageFormatFromPath(path) {
4676
5362
  const dotIndex = path.lastIndexOf(".");
4677
5363
  if (dotIndex < 0) return;
4678
- return IMAGE_EXTENSION_TO_FORMAT[path.slice(dotIndex + 1).toLowerCase()];
5364
+ return IMAGE_EXTENSION_TO_FORMAT$1[path.slice(dotIndex + 1).toLowerCase()];
4679
5365
  }
4680
5366
  async function readImageForShape(path) {
4681
5367
  const format = imageFormatFromPath(path);
@@ -4716,6 +5402,7 @@ function SlideDetailScreen(props) {
4716
5402
  const [draft, setDraft] = useState("");
4717
5403
  const [imageError, setImageError] = useState(void 0);
4718
5404
  const [tableRows, setTableRows] = useState(DEFAULT_TABLE_ROWS);
5405
+ const [vectorKind, setVectorKind] = useState(void 0);
4719
5406
  const formIsOpen = addMode !== "closed";
4720
5407
  const { selectedIndex } = useNavigationInput({
4721
5408
  itemCount: selectableRowIndices.length,
@@ -4776,6 +5463,27 @@ function SlideDetailScreen(props) {
4776
5463
  if (input === "b") {
4777
5464
  setDraft(String(DEFAULT_TABLE_ROWS));
4778
5465
  setAddMode("tableRows");
5466
+ return;
5467
+ }
5468
+ if (doc.format !== "odp") return;
5469
+ if (input === "r") {
5470
+ setVectorKind("rect");
5471
+ setAddMode("vector");
5472
+ return;
5473
+ }
5474
+ if (input === "e") {
5475
+ setVectorKind("ellipse");
5476
+ setAddMode("vector");
5477
+ return;
5478
+ }
5479
+ if (input === "n") {
5480
+ setVectorKind("line");
5481
+ setAddMode("vector");
5482
+ return;
5483
+ }
5484
+ if (input === "p") {
5485
+ setVectorKind("path");
5486
+ setAddMode("vector");
4779
5487
  }
4780
5488
  }, { isActive: !overlayOpen && addMode === "chooseKind" });
4781
5489
  useInput((input) => {
@@ -4889,9 +5597,25 @@ function SlideDetailScreen(props) {
4889
5597
  });
4890
5598
  }
4891
5599
  }),
4892
- addMode === "chooseKind" ? /* @__PURE__ */ jsx(Text, {
5600
+ addMode === "chooseKind" ? /* @__PURE__ */ jsxs(Text, {
4893
5601
  color: "cyan",
4894
- children: "Add shape: t textbox, i image, b table, Esc cancel"
5602
+ children: [
5603
+ "Add shape: t textbox, i image, b table",
5604
+ doc.format === "odp" ? ", r rect, e ellipse, n line, p path" : "",
5605
+ ", Esc cancel"
5606
+ ]
5607
+ }) : void 0,
5608
+ addMode === "vector" && vectorKind !== void 0 ? /* @__PURE__ */ jsx(FieldWizard, {
5609
+ fields: fieldsForVectorKind(vectorKind),
5610
+ onCancel: () => {
5611
+ setAddMode("closed");
5612
+ setVectorKind(void 0);
5613
+ },
5614
+ onComplete: (values) => {
5615
+ dispatch(buildVectorAction(vectorKind, slideIndex, values));
5616
+ setAddMode("closed");
5617
+ setVectorKind(void 0);
5618
+ }
4895
5619
  }) : void 0,
4896
5620
  addMode === "textbox" ? /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
4897
5621
  color: "cyan",
@@ -5714,8 +6438,90 @@ const RESERVED_LETTERS = /* @__PURE__ */ new Set([
5714
6438
  "l",
5715
6439
  "p",
5716
6440
  "t",
5717
- "m"
6441
+ "m",
6442
+ "f",
6443
+ "i"
5718
6444
  ]);
6445
+ const IMAGE_EXTENSION_TO_FORMAT = {
6446
+ png: "png",
6447
+ jpg: "jpeg",
6448
+ jpeg: "jpeg"
6449
+ };
6450
+ function inferSheetImageFormat(path) {
6451
+ const extension = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
6452
+ return IMAGE_EXTENSION_TO_FORMAT[extension];
6453
+ }
6454
+ const SHEET_IMAGE_FIELDS = [
6455
+ {
6456
+ key: "path",
6457
+ label: "Image file path (.png/.jpg/.jpeg)",
6458
+ defaultValue: ""
6459
+ },
6460
+ {
6461
+ key: "widthPt",
6462
+ label: "Width (pt)",
6463
+ defaultValue: "100"
6464
+ },
6465
+ {
6466
+ key: "heightPt",
6467
+ label: "Height (pt)",
6468
+ defaultValue: "60"
6469
+ },
6470
+ {
6471
+ key: "offsetXPt",
6472
+ label: "Offset X from anchor cell (pt)",
6473
+ defaultValue: "0"
6474
+ },
6475
+ {
6476
+ key: "offsetYPt",
6477
+ label: "Offset Y from anchor cell (pt)",
6478
+ defaultValue: "0"
6479
+ },
6480
+ {
6481
+ key: "altText",
6482
+ label: "Alt text, blank for none",
6483
+ defaultValue: ""
6484
+ }
6485
+ ];
6486
+ async function applyAddSheetImage(sheetIndex, anchorRow, anchorColumn, values, dispatch) {
6487
+ const path = requireFieldValue(values, "path");
6488
+ const format = inferSheetImageFormat(path);
6489
+ if (format === void 0) {
6490
+ dispatch({
6491
+ type: "SET_STATUS",
6492
+ severity: "warning",
6493
+ text: `${path} is not a .png or .jpg/.jpeg file -- image not added`
6494
+ });
6495
+ return;
6496
+ }
6497
+ try {
6498
+ const bytes = new Uint8Array(await readInput(path));
6499
+ const widthPt = parseNumberField(requireFieldValue(values, "widthPt"), 100);
6500
+ const heightPt = parseNumberField(requireFieldValue(values, "heightPt"), 60);
6501
+ const offsetXPt = parseNumberField(requireFieldValue(values, "offsetXPt"), 0);
6502
+ const offsetYPt = parseNumberField(requireFieldValue(values, "offsetYPt"), 0);
6503
+ const altTextRaw = requireFieldValue(values, "altText").trim();
6504
+ dispatch({
6505
+ type: "ADD_SHEET_IMAGE",
6506
+ sheetIndex,
6507
+ anchorRow,
6508
+ anchorColumn,
6509
+ offsetXPt,
6510
+ offsetYPt,
6511
+ format,
6512
+ bytes,
6513
+ widthPt,
6514
+ heightPt,
6515
+ altText: altTextRaw.length === 0 ? void 0 : altTextRaw
6516
+ });
6517
+ } catch (error) {
6518
+ dispatch({
6519
+ type: "SET_STATUS",
6520
+ severity: "error",
6521
+ text: `Could not read ${path}: ${describeError(error)}`
6522
+ });
6523
+ }
6524
+ }
5719
6525
  function windowStart(cursor, total, viewport) {
5720
6526
  const maxStart = Math.max(0, total - viewport);
5721
6527
  return Math.min(Math.max(cursor - Math.floor(viewport / 2), 0), maxStart);
@@ -5738,6 +6544,9 @@ function OdsSpreadsheetGridScreen() {
5738
6544
  const [viewMode, setViewMode] = useState("grid");
5739
6545
  const [editSession, setEditSession] = useState(void 0);
5740
6546
  const [mergeAnchor, setMergeAnchor] = useState(void 0);
6547
+ const [formulaEditing, setFormulaEditing] = useState(false);
6548
+ const [formulaDraft, setFormulaDraft] = useState("");
6549
+ const [imageWizardOpen, setImageWizardOpen] = useState(false);
5741
6550
  const { columns: terminalColumns, rows: terminalRows } = useWindowSize();
5742
6551
  const sheet = resolveSheet(doc.editor, sheetIndex);
5743
6552
  const { rowCount, columnCount } = sheetExtent(sheet);
@@ -5746,6 +6555,7 @@ function OdsSpreadsheetGridScreen() {
5746
6555
  const clampedColumn = Math.min(cursorColumn, columnCount - 1);
5747
6556
  const overlayOpen = anyOverlayOpen(state);
5748
6557
  const editing = editSession !== void 0;
6558
+ const editingAnything = editing || formulaEditing || imageWizardOpen;
5749
6559
  function moveCursor(deltaRow, deltaColumn) {
5750
6560
  setCursorRow((row) => Math.min(Math.max(row + deltaRow, 0), rowCount - 1));
5751
6561
  setCursorColumn((column) => Math.min(Math.max(column + deltaColumn, 0), columnCount - 1));
@@ -5783,7 +6593,7 @@ function OdsSpreadsheetGridScreen() {
5783
6593
  sheetIndex
5784
6594
  }
5785
6595
  });
5786
- }, { isActive: !overlayOpen && !editing });
6596
+ }, { isActive: !overlayOpen && !editingAnything });
5787
6597
  useInput((input, key) => {
5788
6598
  if (key.upArrow || input === "k") {
5789
6599
  moveCursor(-1, 0);
@@ -5833,6 +6643,16 @@ function OdsSpreadsheetGridScreen() {
5833
6643
  else commitMerge(mergeAnchor);
5834
6644
  return;
5835
6645
  }
6646
+ if (input === "f") {
6647
+ const cell = cells.get(cellKey(clampedRow, clampedColumn));
6648
+ setFormulaDraft(cell?.formula ?? "");
6649
+ setFormulaEditing(true);
6650
+ return;
6651
+ }
6652
+ if (input === "i") {
6653
+ setImageWizardOpen(true);
6654
+ return;
6655
+ }
5836
6656
  if (key.return) {
5837
6657
  if (mergeAnchor !== void 0) {
5838
6658
  commitMerge(mergeAnchor);
@@ -5844,7 +6664,7 @@ function OdsSpreadsheetGridScreen() {
5844
6664
  return;
5845
6665
  }
5846
6666
  if (input.length === 1 && !key.ctrl && !key.meta && !RESERVED_LETTERS.has(input)) beginEdit(input, inferKind(input));
5847
- }, { isActive: !overlayOpen && !editing && viewMode === "grid" });
6667
+ }, { isActive: !overlayOpen && !editingAnything && viewMode === "grid" });
5848
6668
  const compactRows = sheet === void 0 ? [] : sheet.cells.filter((cell) => cell.value.kind !== "empty").map((cell) => ({
5849
6669
  row: cell.row,
5850
6670
  column: cell.column,
@@ -5962,9 +6782,43 @@ function OdsSpreadsheetGridScreen() {
5962
6782
  setEditSession(void 0);
5963
6783
  }
5964
6784
  }),
6785
+ formulaEditing ? /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsxs(Text, {
6786
+ color: "cyan",
6787
+ children: [cursorAddress, " formula: "]
6788
+ }), /* @__PURE__ */ jsx(TextField, {
6789
+ value: formulaDraft,
6790
+ isFocused: !overlayOpen,
6791
+ placeholder: "e.g. of:=[.A1]+[.A2]",
6792
+ onChange: setFormulaDraft,
6793
+ onSubmit: (value) => {
6794
+ const trimmed = value.trim();
6795
+ dispatch({
6796
+ type: "SET_CELL_FORMULA",
6797
+ sheetIndex,
6798
+ row: clampedRow,
6799
+ column: clampedColumn,
6800
+ formula: trimmed.length === 0 ? void 0 : trimmed
6801
+ });
6802
+ setFormulaEditing(false);
6803
+ },
6804
+ onCancel: () => {
6805
+ setFormulaEditing(false);
6806
+ }
6807
+ })] }) : void 0,
6808
+ imageWizardOpen ? /* @__PURE__ */ jsx(FieldWizard, {
6809
+ fields: SHEET_IMAGE_FIELDS,
6810
+ onCancel: () => {
6811
+ setImageWizardOpen(false);
6812
+ },
6813
+ onComplete: (values) => {
6814
+ applyAddSheetImage(sheetIndex, clampedRow, clampedColumn, values, dispatch).then(() => {
6815
+ setImageWizardOpen(false);
6816
+ });
6817
+ }
6818
+ }) : void 0,
5965
6819
  /* @__PURE__ */ jsx(Text, {
5966
6820
  dimColor: true,
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"
6821
+ 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, f formula, i image, Esc back` : "hjkl/arrows to the opposite corner, m/Enter to merge, Esc to cancel"
5968
6822
  })
5969
6823
  ]
5970
6824
  });