document-cli 1.8.0 → 1.10.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, parseXml, 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, createPdf, createPptx, decodeMarkdownText, docxToPdf, encodeMarkdownText, encodePackage, hsqldbCellDisplayText, markdownToPdf, odgToPdf, odpToPdf, odsToPdf, odtToPdf, openDocx, openOdg, openOdp, openOds, openOdt, openPdf, 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";
@@ -36,7 +36,7 @@ function toPdfOptions(options, fonts) {
36
36
  };
37
37
  }
38
38
  async function exportToPdf(openDocument, destinationPath, options) {
39
- if (openDocument.format === "odb" || openDocument.format === "pdf") throw new Error(`A ${openDocument.format} document is already a read-only source; there is no export-to-PDF path for it`);
39
+ if (openDocument.format === "odb" || openDocument.format === "pdf") throw new Error(`A ${openDocument.format} document has no export-to-PDF path -- ${openDocument.format === "pdf" ? "save it directly instead" : "it is a read-only source with nothing to convert"}`);
40
40
  const pdfOptions = toPdfOptions(options, await loadProvidedFonts(options.fontFiles ?? [], { signal: options.signal }));
41
41
  if (openDocument.format === "markdown") {
42
42
  const pdfBytes = markdownToPdf(encodeMarkdownText(openDocument.source), pdfOptions);
@@ -120,11 +120,15 @@ async function openDocumentAtPath(path) {
120
120
  editor: openOdg(bytes),
121
121
  path
122
122
  };
123
- case "pdf": return {
124
- format,
125
- layout: readPdf(bytes),
126
- path
127
- };
123
+ case "pdf": {
124
+ const editor = openPdf(bytes);
125
+ return {
126
+ format,
127
+ editor,
128
+ layout: editor.toLayoutDocument(),
129
+ path
130
+ };
131
+ }
128
132
  case "markdown": return {
129
133
  format,
130
134
  source: decodeMarkdownText(bytes),
@@ -171,10 +175,19 @@ function createNewDocument(format) {
171
175
  editor: createOdg(),
172
176
  path: void 0
173
177
  };
178
+ case "pdf": {
179
+ const editor = createPdf();
180
+ return {
181
+ format,
182
+ editor,
183
+ layout: editor.toLayoutDocument(),
184
+ path: void 0
185
+ };
186
+ }
174
187
  }
175
188
  }
176
189
  async function saveDocumentTo(openDocument, path) {
177
- if (openDocument.format === "odb" || openDocument.format === "pdf" || openDocument.format === "xlsx") throw new Error(`A ${openDocument.format} document is opened read-only and cannot be written back`);
190
+ if (openDocument.format === "odb" || openDocument.format === "xlsx") throw new Error(`A ${openDocument.format} document is opened read-only and cannot be written back`);
178
191
  if (openDocument.format === "markdown") {
179
192
  await writeFile(path, encodeMarkdownText(openDocument.source));
180
193
  return;
@@ -189,7 +202,8 @@ const EDITABLE_FORMATS = {
189
202
  odt: true,
190
203
  odp: true,
191
204
  ods: true,
192
- odg: true
205
+ odg: true,
206
+ pdf: true
193
207
  };
194
208
  const WRITABLE_FORMATS = {
195
209
  ...EDITABLE_FORMATS,
@@ -368,6 +382,12 @@ function documentWithPath(doc, path) {
368
382
  editor: doc.editor,
369
383
  path
370
384
  };
385
+ case "pdf": return {
386
+ format: "pdf",
387
+ editor: doc.editor,
388
+ layout: doc.layout,
389
+ path
390
+ };
371
391
  case "odb": return {
372
392
  format: "odb",
373
393
  tables: doc.tables,
@@ -380,11 +400,6 @@ function documentWithPath(doc, path) {
380
400
  source: doc.source,
381
401
  path
382
402
  };
383
- case "pdf": return {
384
- format: "pdf",
385
- layout: doc.layout,
386
- path
387
- };
388
403
  case "xlsx": return {
389
404
  format: "xlsx",
390
405
  layout: doc.layout,
@@ -425,6 +440,15 @@ function reopenEditable(doc, bytes) {
425
440
  editor: openOdg(bytes),
426
441
  path: doc.path
427
442
  };
443
+ case "pdf": {
444
+ const editor = openPdf(bytes);
445
+ return {
446
+ format: "pdf",
447
+ editor,
448
+ layout: editor.toLayoutDocument(),
449
+ path: doc.path
450
+ };
451
+ }
428
452
  }
429
453
  }
430
454
  function mutate(state, doc, apply) {
@@ -508,6 +532,45 @@ function drawingDocument(state) {
508
532
  if (doc === void 0) return;
509
533
  return doc.format === "odg" ? doc : void 0;
510
534
  }
535
+ function pdfDocument(state) {
536
+ const doc = state.openDocument;
537
+ if (doc === void 0) return;
538
+ return doc.format === "pdf" ? doc : void 0;
539
+ }
540
+ function pdfItemAt(doc, pageIndex, itemIndex) {
541
+ return doc.editor.page(pageIndex)?.items()[itemIndex];
542
+ }
543
+ function withPdfPage(state, pageIndex, apply) {
544
+ const doc = pdfDocument(state);
545
+ if (doc === void 0) return wrongDocument(state, "a pdf document");
546
+ const page = doc.editor.page(pageIndex);
547
+ if (page === void 0) return withStatus(state, "warning", `There is no page at index ${pageIndex}`);
548
+ return mutate(state, doc, () => {
549
+ apply(page);
550
+ });
551
+ }
552
+ function withPdfItemMatching(state, pageIndex, itemIndex, guard, kindLabel, apply) {
553
+ const doc = pdfDocument(state);
554
+ if (doc === void 0) return wrongDocument(state, "a pdf document");
555
+ const item = pdfItemAt(doc, pageIndex, itemIndex);
556
+ if (item === void 0) return withStatus(state, "warning", `Page ${pageIndex} has no item at index ${itemIndex}`);
557
+ if (!guard(item)) return withStatus(state, "warning", `Item ${itemIndex} on page ${pageIndex} is a ${item.kind} item, not ${kindLabel}`);
558
+ return mutate(state, doc, () => {
559
+ apply(item);
560
+ });
561
+ }
562
+ const isPdfTextItem = (item) => item.kind === "text";
563
+ const isPdfRectItem = (item) => item.kind === "rect";
564
+ const isPdfEllipseItem = (item) => item.kind === "ellipse";
565
+ const isPdfLineItem = (item) => item.kind === "line";
566
+ const isPdfPathItem = (item) => item.kind === "path";
567
+ const isPdfImageItem = (item) => item.kind === "image";
568
+ const isPdfLinkItem = (item) => item.kind === "link";
569
+ function vectorHostDocument(state) {
570
+ const doc = state.openDocument;
571
+ if (doc === void 0) return;
572
+ return doc.format === "odg" || doc.format === "odp" ? doc : void 0;
573
+ }
511
574
  function markdownDocument(state) {
512
575
  const doc = state.openDocument;
513
576
  if (doc === void 0) return;
@@ -740,6 +803,12 @@ function appReducer(state, action) {
740
803
  case "SET_RUN_COLOR": return withRun(state, action.blockIndex, action.runIndex, (run) => {
741
804
  run.color = action.color;
742
805
  });
806
+ case "SET_RUN_FONT_FAMILY": return withRun(state, action.blockIndex, action.runIndex, (run) => {
807
+ run.fontFamily = action.fontFamily;
808
+ });
809
+ case "SET_RUN_FONT_SIZE": return withRun(state, action.blockIndex, action.runIndex, (run) => {
810
+ run.sizePt = action.sizePt;
811
+ });
743
812
  case "APPEND_TABLE": {
744
813
  const doc = wordprocessingDocument(state);
745
814
  if (doc === void 0) return wrongDocument(state, "a docx or odt document");
@@ -800,6 +869,14 @@ function appReducer(state, action) {
800
869
  setTextContainerText(item, action.text);
801
870
  });
802
871
  }
872
+ case "ADD_LIST": {
873
+ const doc = wordprocessingDocument(state);
874
+ if (doc === void 0) return wrongDocument(state, "a docx or odt document");
875
+ if (doc.format !== "odt") return wrongDocument(state, "an odt document (lists are an odt-only concept)");
876
+ return mutate(state, doc, () => {
877
+ doc.editor.body.appendList();
878
+ });
879
+ }
803
880
  case "INSERT_PARAGRAPH_IMAGE": {
804
881
  const doc = wordprocessingDocument(state);
805
882
  if (doc === void 0) return wrongDocument(state, "a docx or odt document");
@@ -943,6 +1020,23 @@ function appReducer(state, action) {
943
1020
  case "SET_CELL_VALUE": return withSheet(state, action.sheetIndex, (sheet) => {
944
1021
  sheet.cell(action.row, action.column).value = action.value;
945
1022
  });
1023
+ case "SET_CELL_FORMULA": return withSheet(state, action.sheetIndex, (sheet) => {
1024
+ sheet.cell(action.row, action.column).formula = action.formula;
1025
+ });
1026
+ case "ADD_SHEET_IMAGE": return withSheet(state, action.sheetIndex, (sheet) => {
1027
+ sheet.addImage({
1028
+ kind: "image",
1029
+ format: action.format,
1030
+ base64: bytesToBase64(action.bytes),
1031
+ widthPt: action.widthPt,
1032
+ heightPt: action.heightPt,
1033
+ altText: action.altText,
1034
+ anchorRow: action.anchorRow,
1035
+ anchorColumn: action.anchorColumn,
1036
+ offsetXPt: action.offsetXPt,
1037
+ offsetYPt: action.offsetYPt
1038
+ });
1039
+ });
946
1040
  case "MERGE_CELLS": {
947
1041
  const doc = spreadsheetDocument(state);
948
1042
  if (doc === void 0) return wrongDocument(state, "an ods document");
@@ -966,23 +1060,64 @@ function appReducer(state, action) {
966
1060
  case "ADD_ELLIPSE":
967
1061
  case "ADD_LINE":
968
1062
  case "ADD_PATH": {
969
- const doc = drawingDocument(state);
970
- if (doc === void 0) return wrongDocument(state, "an odg document");
971
- const page = doc.editor.pages()[action.pageIndex];
972
- if (page === void 0) return withStatus(state, "warning", `There is no page at index ${action.pageIndex}`);
1063
+ const doc = vectorHostDocument(state);
1064
+ if (doc === void 0) return wrongDocument(state, "an odg or odp document");
1065
+ if (doc.format === "odg") {
1066
+ const page = doc.editor.pages()[action.containerIndex];
1067
+ if (page === void 0) return withStatus(state, "warning", `There is no page at index ${action.containerIndex}`);
1068
+ return mutate(state, doc, () => {
1069
+ switch (action.type) {
1070
+ case "ADD_RECT":
1071
+ page.addRect(action.init);
1072
+ return;
1073
+ case "ADD_ELLIPSE":
1074
+ page.addEllipse(action.init);
1075
+ return;
1076
+ case "ADD_LINE":
1077
+ page.addLine(action.init);
1078
+ return;
1079
+ case "ADD_PATH":
1080
+ page.addPath(action.init);
1081
+ return;
1082
+ }
1083
+ });
1084
+ }
1085
+ const slide = doc.editor.slides()[action.containerIndex];
1086
+ if (slide === void 0) return withStatus(state, "warning", `There is no slide at index ${action.containerIndex}`);
973
1087
  return mutate(state, doc, () => {
974
1088
  switch (action.type) {
975
1089
  case "ADD_RECT":
976
- page.addRect(action.init);
1090
+ slide.addVector({
1091
+ kind: "rect",
1092
+ frame: action.init.frame,
1093
+ fill: action.init.fill,
1094
+ stroke: action.init.stroke
1095
+ });
977
1096
  return;
978
1097
  case "ADD_ELLIPSE":
979
- page.addEllipse(action.init);
1098
+ slide.addVector({
1099
+ kind: "ellipse",
1100
+ frame: action.init.frame,
1101
+ fill: action.init.fill,
1102
+ stroke: action.init.stroke
1103
+ });
980
1104
  return;
981
1105
  case "ADD_LINE":
982
- page.addLine(action.init);
1106
+ slide.addVector({
1107
+ kind: "line",
1108
+ from: action.init.from,
1109
+ to: action.init.to,
1110
+ stroke: action.init.stroke
1111
+ });
983
1112
  return;
984
1113
  case "ADD_PATH":
985
- page.addPath(action.init);
1114
+ slide.addVector({
1115
+ kind: "path",
1116
+ frame: action.init.frame,
1117
+ subpaths: [...action.init.subpaths],
1118
+ fill: action.init.fill,
1119
+ stroke: action.init.stroke
1120
+ });
986
1121
  return;
987
1122
  }
988
1123
  });
@@ -1001,6 +1136,129 @@ function appReducer(state, action) {
1001
1136
  action.vector.stroke = action.stroke;
1002
1137
  });
1003
1138
  }
1139
+ case "ADD_PDF_TEXT": return withPdfPage(state, action.pageIndex, (page) => {
1140
+ page.appendText(action.init);
1141
+ });
1142
+ case "ADD_PDF_RECT": return withPdfPage(state, action.pageIndex, (page) => {
1143
+ page.appendRect(action.init);
1144
+ });
1145
+ case "ADD_PDF_ELLIPSE": return withPdfPage(state, action.pageIndex, (page) => {
1146
+ page.appendEllipse(action.init);
1147
+ });
1148
+ case "ADD_PDF_LINE": return withPdfPage(state, action.pageIndex, (page) => {
1149
+ page.appendLine(action.init);
1150
+ });
1151
+ case "ADD_PDF_PATH": return withPdfPage(state, action.pageIndex, (page) => {
1152
+ page.appendPath(action.init);
1153
+ });
1154
+ case "ADD_PDF_IMAGE": return withPdfPage(state, action.pageIndex, (page) => {
1155
+ page.appendImage(action.init);
1156
+ });
1157
+ case "ADD_PDF_LINK": return withPdfPage(state, action.pageIndex, (page) => {
1158
+ page.appendLink(action.init);
1159
+ });
1160
+ case "REMOVE_PDF_ITEM": {
1161
+ const doc = pdfDocument(state);
1162
+ if (doc === void 0) return wrongDocument(state, "a pdf document");
1163
+ const item = pdfItemAt(doc, action.pageIndex, action.itemIndex);
1164
+ if (item === void 0) return withStatus(state, "warning", `Page ${action.pageIndex} has no item at index ${action.itemIndex}`);
1165
+ return mutate(state, doc, () => {
1166
+ item.remove();
1167
+ });
1168
+ }
1169
+ case "SET_PDF_TEXT_TEXT": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfTextItem, "text", (item) => {
1170
+ item.text = action.text;
1171
+ });
1172
+ case "SET_PDF_TEXT_POSITION": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfTextItem, "text", (item) => {
1173
+ item.xPt = action.xPt;
1174
+ item.yPt = action.yPt;
1175
+ });
1176
+ case "SET_PDF_TEXT_FONT": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfTextItem, "text", (item) => {
1177
+ item.font = action.font;
1178
+ });
1179
+ case "SET_PDF_TEXT_SIZE": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfTextItem, "text", (item) => {
1180
+ item.sizePt = action.sizePt;
1181
+ });
1182
+ case "SET_PDF_TEXT_COLOR": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfTextItem, "text", (item) => {
1183
+ item.color = action.color;
1184
+ });
1185
+ case "SET_PDF_TEXT_ROTATION": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfTextItem, "text", (item) => {
1186
+ item.rotationDeg = action.rotationDeg;
1187
+ });
1188
+ case "SET_PDF_TEXT_WIDTH": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfTextItem, "text", (item) => {
1189
+ item.widthPt = action.widthPt;
1190
+ });
1191
+ case "TOGGLE_PDF_TEXT_UNDERLINE": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfTextItem, "text", (item) => {
1192
+ item.underline = !(item.underline ?? false);
1193
+ });
1194
+ case "SET_PDF_RECT_FRAME": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfRectItem, "rect", (item) => {
1195
+ item.xPt = action.xPt;
1196
+ item.yPt = action.yPt;
1197
+ item.widthPt = action.widthPt;
1198
+ item.heightPt = action.heightPt;
1199
+ });
1200
+ case "SET_PDF_RECT_FILL": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfRectItem, "rect", (item) => {
1201
+ item.fill = action.fill;
1202
+ });
1203
+ case "SET_PDF_RECT_STROKE": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfRectItem, "rect", (item) => {
1204
+ item.stroke = action.stroke;
1205
+ });
1206
+ case "SET_PDF_ELLIPSE_FRAME": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfEllipseItem, "ellipse", (item) => {
1207
+ item.xPt = action.xPt;
1208
+ item.yPt = action.yPt;
1209
+ item.widthPt = action.widthPt;
1210
+ item.heightPt = action.heightPt;
1211
+ });
1212
+ case "SET_PDF_ELLIPSE_FILL": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfEllipseItem, "ellipse", (item) => {
1213
+ item.fill = action.fill;
1214
+ });
1215
+ case "SET_PDF_ELLIPSE_STROKE": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfEllipseItem, "ellipse", (item) => {
1216
+ item.stroke = action.stroke;
1217
+ });
1218
+ case "SET_PDF_LINE_FROM": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfLineItem, "line", (item) => {
1219
+ item.x1Pt = action.x1Pt;
1220
+ item.y1Pt = action.y1Pt;
1221
+ });
1222
+ case "SET_PDF_LINE_TO": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfLineItem, "line", (item) => {
1223
+ item.x2Pt = action.x2Pt;
1224
+ item.y2Pt = action.y2Pt;
1225
+ });
1226
+ case "SET_PDF_LINE_COLOR": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfLineItem, "line", (item) => {
1227
+ item.color = action.color;
1228
+ });
1229
+ case "SET_PDF_LINE_WIDTH": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfLineItem, "line", (item) => {
1230
+ item.widthPt = action.widthPt;
1231
+ });
1232
+ case "SET_PDF_PATH_FILL": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfPathItem, "path", (item) => {
1233
+ item.fill = action.fill;
1234
+ });
1235
+ case "SET_PDF_PATH_FILL_RULE": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfPathItem, "path", (item) => {
1236
+ item.fillRule = action.fillRule;
1237
+ });
1238
+ case "SET_PDF_PATH_STROKE": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfPathItem, "path", (item) => {
1239
+ item.stroke = action.stroke;
1240
+ });
1241
+ case "SET_PDF_IMAGE_FRAME": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfImageItem, "image", (item) => {
1242
+ item.xPt = action.xPt;
1243
+ item.yPt = action.yPt;
1244
+ item.widthPt = action.widthPt;
1245
+ item.heightPt = action.heightPt;
1246
+ });
1247
+ case "SET_PDF_IMAGE_ROTATION": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfImageItem, "image", (item) => {
1248
+ item.rotationDeg = action.rotationDeg;
1249
+ });
1250
+ case "SET_PDF_IMAGE_SOURCE": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfImageItem, "image", (item) => {
1251
+ item.setImage(action.bytes, action.format);
1252
+ });
1253
+ case "SET_PDF_LINK_URI": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfLinkItem, "link", (item) => {
1254
+ item.uri = action.uri;
1255
+ });
1256
+ case "SET_PDF_LINK_FRAME": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfLinkItem, "link", (item) => {
1257
+ item.xPt = action.xPt;
1258
+ item.yPt = action.yPt;
1259
+ item.widthPt = action.widthPt;
1260
+ item.heightPt = action.heightPt;
1261
+ });
1004
1262
  case "SET_MARKDOWN_SOURCE": {
1005
1263
  const doc = markdownDocument(state);
1006
1264
  if (doc === void 0) return wrongDocument(state, "a markdown document");
@@ -1034,7 +1292,7 @@ function appReducer(state, action) {
1034
1292
  case "UNDO": {
1035
1293
  const doc = state.openDocument;
1036
1294
  if (doc === void 0) return withStatus(state, "info", "There is nothing to undo");
1037
- if (doc.format === "odb" || doc.format === "pdf" || doc.format === "xlsx") return withStatus(state, "warning", `A ${doc.format} document is read-only, so it has no history to undo`);
1295
+ if (doc.format === "odb" || doc.format === "xlsx") return withStatus(state, "warning", `A ${doc.format} document is read-only, so it has no history to undo`);
1038
1296
  const snapshot = state.undoStack.at(-1);
1039
1297
  if (snapshot === void 0) return withStatus(state, "info", "There is nothing to undo");
1040
1298
  const restored = doc.format === "markdown" ? {
@@ -1118,7 +1376,7 @@ function TextField(props) {
1118
1376
  }
1119
1377
  //#endregion
1120
1378
  //#region src/tui/components/command-palette.tsx
1121
- const USAGE_COLUMN_WIDTH = 32;
1379
+ const USAGE_COLUMN_WIDTH = 36;
1122
1380
  const COMMANDS = [
1123
1381
  {
1124
1382
  name: "save",
@@ -1137,7 +1395,7 @@ const COMMANDS = [
1137
1395
  },
1138
1396
  {
1139
1397
  name: "new",
1140
- usage: ":new docx|pptx|odt|odp|ods|odg",
1398
+ usage: ":new docx|pptx|odt|odp|ods|odg|pdf",
1141
1399
  description: "Create an empty document"
1142
1400
  },
1143
1401
  {
@@ -1254,7 +1512,7 @@ async function runCommand(line, state, dispatch) {
1254
1512
  case "new": {
1255
1513
  const format = args[0];
1256
1514
  if (format === void 0 || !isEditableFormat(format)) {
1257
- warn(dispatch, "Usage: :new docx|pptx|odt|odp|ods|odg");
1515
+ warn(dispatch, "Usage: :new docx|pptx|odt|odp|ods|odg|pdf");
1258
1516
  return;
1259
1517
  }
1260
1518
  dispatch({
@@ -2132,6 +2390,19 @@ function ParagraphFamilyBodyList(props) {
2132
2390
  setTableWizard("rows");
2133
2391
  }
2134
2392
  }, { isActive: !anyOverlayOpen(state) && !wizardOpen && !formulaFlowOpen });
2393
+ useInput((input) => {
2394
+ if (input === "L" && adapter.lists !== void 0) {
2395
+ const newIndex = adapter.lists().length;
2396
+ dispatch({ type: "ADD_LIST" });
2397
+ dispatch({
2398
+ type: "PUSH_SCREEN",
2399
+ screen: {
2400
+ kind: "listEditor",
2401
+ blockIndex: newIndex
2402
+ }
2403
+ });
2404
+ }
2405
+ }, { isActive: !anyOverlayOpen(state) && !wizardOpen && !formulaFlowOpen });
2135
2406
  useInput((input) => {
2136
2407
  if (input === "m" && adapter.formatLabel === "odt") setFormulaFlow("picking");
2137
2408
  }, { isActive: !anyOverlayOpen(state) && !wizardOpen && !formulaFlowOpen });
@@ -2386,6 +2657,7 @@ function ParagraphFamilyBodyList(props) {
2386
2657
  dimColor: true,
2387
2658
  children: [
2388
2659
  "Enter to open, a to append a paragraph, T to append a table",
2660
+ adapter.lists !== void 0 ? ", L to add a list" : "",
2389
2661
  adapter.formatLabel === "odt" ? ", m to insert a formula" : "",
2390
2662
  ", Esc back"
2391
2663
  ]
@@ -2408,6 +2680,7 @@ function isValidHexColorInput(input) {
2408
2680
  }
2409
2681
  //#endregion
2410
2682
  //#region src/tui/screens/editors/docx/paragraph-detail.tsx
2683
+ const DEFAULT_RUN_SIZE_PT = 12;
2411
2684
  const IMAGE_FIELDS = [
2412
2685
  {
2413
2686
  key: "path",
@@ -2430,14 +2703,14 @@ const IMAGE_FIELDS = [
2430
2703
  defaultValue: ""
2431
2704
  }
2432
2705
  ];
2433
- function inferImageFormat$1(path) {
2706
+ function inferImageFormat$2(path) {
2434
2707
  const extension = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
2435
2708
  if (extension === "png") return "png";
2436
2709
  if (extension === "jpg" || extension === "jpeg") return "jpeg";
2437
2710
  }
2438
2711
  async function applyInsertImage(blockIndex, values, dispatch) {
2439
2712
  const path = requireFieldValue(values, "path");
2440
- const format = inferImageFormat$1(path);
2713
+ const format = inferImageFormat$2(path);
2441
2714
  if (format === void 0) {
2442
2715
  dispatch({
2443
2716
  type: "SET_STATUS",
@@ -2487,6 +2760,8 @@ function ParagraphDetailScreen() {
2487
2760
  const dispatch = useAppDispatch();
2488
2761
  const [runIndex, setRunIndex] = useState(0);
2489
2762
  const [colorInput, setColorInput] = useState(void 0);
2763
+ const [fontFamilyInput, setFontFamilyInput] = useState(void 0);
2764
+ const [fontSizeInput, setFontSizeInput] = useState(void 0);
2490
2765
  const [imageWizardOpen, setImageWizardOpen] = useState(false);
2491
2766
  const [formulaPickerOpen, setFormulaPickerOpen] = useState(false);
2492
2767
  const screen = currentScreen(state);
@@ -2584,8 +2859,16 @@ function ParagraphDetailScreen() {
2584
2859
  });
2585
2860
  return;
2586
2861
  }
2587
- if (input === "c") setColorInput(selectedRun.color === void 0 ? "" : layoutColorToHex(selectedRun.color).slice(1));
2588
- }, { isActive: !anyOverlayOpen(state) && colorInput === void 0 && !imageWizardOpen && !formulaPickerOpen });
2862
+ if (input === "c") {
2863
+ setColorInput(selectedRun.color === void 0 ? "" : layoutColorToHex(selectedRun.color).slice(1));
2864
+ return;
2865
+ }
2866
+ if (input === "f") {
2867
+ setFontFamilyInput(selectedRun.fontFamily ?? "");
2868
+ return;
2869
+ }
2870
+ if (input === "s") setFontSizeInput(selectedRun.sizePt === void 0 ? "" : String(selectedRun.sizePt));
2871
+ }, { isActive: !anyOverlayOpen(state) && colorInput === void 0 && fontFamilyInput === void 0 && fontSizeInput === void 0 && !imageWizardOpen && !formulaPickerOpen });
2589
2872
  if (screen.kind !== "paragraphDetail") return /* @__PURE__ */ jsx(Text, {
2590
2873
  color: "red",
2591
2874
  children: "ParagraphDetailScreen rendered outside a paragraphDetail screen."
@@ -2643,6 +2926,60 @@ function ParagraphDetailScreen() {
2643
2926
  setColorInput(void 0);
2644
2927
  }
2645
2928
  })] }),
2929
+ fontFamilyInput === void 0 ? void 0 : /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
2930
+ color: "cyan",
2931
+ children: "Font family: "
2932
+ }), /* @__PURE__ */ jsx(TextField, {
2933
+ value: fontFamilyInput,
2934
+ isFocused: true,
2935
+ placeholder: "e.g. Calibri",
2936
+ onChange: setFontFamilyInput,
2937
+ onSubmit: (value) => {
2938
+ const trimmed = value.trim();
2939
+ if (trimmed.length === 0) dispatch({
2940
+ type: "SET_STATUS",
2941
+ severity: "warning",
2942
+ text: "A font family name cannot be blank"
2943
+ });
2944
+ else dispatch({
2945
+ type: "SET_RUN_FONT_FAMILY",
2946
+ blockIndex,
2947
+ runIndex: clampedRunIndex,
2948
+ fontFamily: trimmed
2949
+ });
2950
+ setFontFamilyInput(void 0);
2951
+ },
2952
+ onCancel: () => {
2953
+ setFontFamilyInput(void 0);
2954
+ }
2955
+ })] }),
2956
+ fontSizeInput === void 0 ? void 0 : /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
2957
+ color: "cyan",
2958
+ children: "Font size (pt): "
2959
+ }), /* @__PURE__ */ jsx(TextField, {
2960
+ value: fontSizeInput,
2961
+ isFocused: true,
2962
+ placeholder: "e.g. 12",
2963
+ onChange: setFontSizeInput,
2964
+ onSubmit: (value) => {
2965
+ const sizePt = parseNumberField(value, selectedRun?.sizePt ?? DEFAULT_RUN_SIZE_PT);
2966
+ if (sizePt <= 0) dispatch({
2967
+ type: "SET_STATUS",
2968
+ severity: "warning",
2969
+ text: `"${value}" is not a positive font size`
2970
+ });
2971
+ else dispatch({
2972
+ type: "SET_RUN_FONT_SIZE",
2973
+ blockIndex,
2974
+ runIndex: clampedRunIndex,
2975
+ sizePt
2976
+ });
2977
+ setFontSizeInput(void 0);
2978
+ },
2979
+ onCancel: () => {
2980
+ setFontSizeInput(void 0);
2981
+ }
2982
+ })] }),
2646
2983
  imageWizardOpen ? /* @__PURE__ */ jsx(FieldWizard, {
2647
2984
  fields: IMAGE_FIELDS,
2648
2985
  onCancel: () => {
@@ -2678,7 +3015,7 @@ function ParagraphDetailScreen() {
2678
3015
  /* @__PURE__ */ jsxs(Text, {
2679
3016
  dimColor: true,
2680
3017
  children: [
2681
- "<- / -> move, Enter edit text, b/i/u toggle, c colour, a append run, I image",
3018
+ "<- / -> move, Enter edit text, b/i/u toggle, c colour, f font, s size, a append run, I image",
2682
3019
  doc.format === "docx" ? ", m formula" : "",
2683
3020
  ", Esc back"
2684
3021
  ]
@@ -3747,6 +4084,64 @@ function OdbTableRowsScreen() {
3747
4084
  });
3748
4085
  }
3749
4086
  //#endregion
4087
+ //#region src/tui/screens/shared/vector-fields.ts
4088
+ function parseColorField(raw) {
4089
+ const trimmed = raw.trim();
4090
+ if (trimmed.length === 0) return;
4091
+ const [r, g, b] = trimmed.split(/\s+/).map((part) => Number.parseFloat(part));
4092
+ if (r === void 0 || g === void 0 || b === void 0 || ![
4093
+ r,
4094
+ g,
4095
+ b
4096
+ ].every((value) => Number.isFinite(value))) return;
4097
+ return {
4098
+ r,
4099
+ g,
4100
+ b
4101
+ };
4102
+ }
4103
+ function parseStrokeField(raw) {
4104
+ const trimmed = raw.trim();
4105
+ if (trimmed.length === 0) return;
4106
+ const [r, g, b, widthPt] = trimmed.split(/\s+/).map((part) => Number.parseFloat(part));
4107
+ if (r === void 0 || g === void 0 || b === void 0 || widthPt === void 0 || ![
4108
+ r,
4109
+ g,
4110
+ b,
4111
+ widthPt
4112
+ ].every((value) => Number.isFinite(value))) return;
4113
+ return {
4114
+ color: {
4115
+ r,
4116
+ g,
4117
+ b
4118
+ },
4119
+ widthPt
4120
+ };
4121
+ }
4122
+ function defaultTriangleSubpaths(widthPt, heightPt) {
4123
+ return [{
4124
+ start: {
4125
+ xPt: 0,
4126
+ yPt: heightPt
4127
+ },
4128
+ segments: [{
4129
+ kind: "line",
4130
+ to: {
4131
+ xPt: widthPt / 2,
4132
+ yPt: 0
4133
+ }
4134
+ }, {
4135
+ kind: "line",
4136
+ to: {
4137
+ xPt: widthPt,
4138
+ yPt: heightPt
4139
+ }
4140
+ }],
4141
+ closed: true
4142
+ }];
4143
+ }
4144
+ //#endregion
3750
4145
  //#region src/tui/screens/editors/odg/shared.ts
3751
4146
  function requireOdgDocument(state) {
3752
4147
  const doc = state.openDocument;
@@ -3798,14 +4193,14 @@ function vectorKindLabel(kind) {
3798
4193
  case "path": return "Path";
3799
4194
  }
3800
4195
  }
3801
- function formatPt(value) {
4196
+ function formatPt$1(value) {
3802
4197
  return value.toFixed(1);
3803
4198
  }
3804
4199
  function formatFrame(box) {
3805
- return `${formatPt(box.xPt)},${formatPt(box.yPt)} ${formatPt(box.widthPt)}x${formatPt(box.heightPt)}pt`;
4200
+ return `${formatPt$1(box.xPt)},${formatPt$1(box.yPt)} ${formatPt$1(box.widthPt)}x${formatPt$1(box.heightPt)}pt`;
3806
4201
  }
3807
4202
  function formatPoint$1(point) {
3808
- return `${formatPt(point.xPt)},${formatPt(point.yPt)}`;
4203
+ return `${formatPt$1(point.xPt)},${formatPt$1(point.yPt)}`;
3809
4204
  }
3810
4205
  function formatColor$1(color) {
3811
4206
  return `rgb(${color.r.toFixed(2)}, ${color.g.toFixed(2)}, ${color.b.toFixed(2)})`;
@@ -3817,78 +4212,22 @@ function describeVectorGeometry(vector) {
3817
4212
  function describeFillStroke(vector) {
3818
4213
  const parts = [];
3819
4214
  if (vector.kind !== "line" && vector.fill !== void 0) parts.push(`fill ${formatColor$1(vector.fill)}`);
3820
- if (vector.stroke !== void 0) parts.push(`stroke ${formatColor$1(vector.stroke.color)} ${formatPt(vector.stroke.widthPt)}pt`);
4215
+ if (vector.stroke !== void 0) parts.push(`stroke ${formatColor$1(vector.stroke.color)} ${formatPt$1(vector.stroke.widthPt)}pt`);
3821
4216
  return parts.length === 0 ? "no fill or stroke" : parts.join(", ");
3822
4217
  }
3823
- function parseColorField(raw) {
3824
- const trimmed = raw.trim();
3825
- if (trimmed.length === 0) return;
3826
- const [r, g, b] = trimmed.split(/\s+/).map((part) => Number.parseFloat(part));
3827
- if (r === void 0 || g === void 0 || b === void 0 || ![
3828
- r,
3829
- g,
3830
- b
3831
- ].every((value) => Number.isFinite(value))) return;
3832
- return {
3833
- r,
3834
- g,
3835
- b
3836
- };
3837
- }
3838
- function parseStrokeField(raw) {
3839
- const trimmed = raw.trim();
3840
- if (trimmed.length === 0) return;
3841
- const [r, g, b, widthPt] = trimmed.split(/\s+/).map((part) => Number.parseFloat(part));
3842
- if (r === void 0 || g === void 0 || b === void 0 || widthPt === void 0 || ![
3843
- r,
3844
- g,
3845
- b,
3846
- widthPt
3847
- ].every((value) => Number.isFinite(value))) return;
3848
- return {
3849
- color: {
3850
- r,
3851
- g,
3852
- b
3853
- },
3854
- widthPt
3855
- };
3856
- }
3857
- function defaultTriangleSubpaths(widthPt, heightPt) {
3858
- return [{
3859
- start: {
3860
- xPt: 0,
3861
- yPt: heightPt
3862
- },
3863
- segments: [{
3864
- kind: "line",
3865
- to: {
3866
- xPt: widthPt / 2,
3867
- yPt: 0
3868
- }
3869
- }, {
3870
- kind: "line",
3871
- to: {
3872
- xPt: widthPt,
3873
- yPt: heightPt
3874
- }
3875
- }],
3876
- closed: true
3877
- }];
3878
- }
3879
- //#endregion
3880
- //#region src/tui/screens/editors/odg/page-list.tsx
3881
- function OdgPageListScreen() {
3882
- const state = useAppState();
3883
- const dispatch = useAppDispatch();
3884
- const pages = requireOdgDocument(state).editor.pages();
3885
- const query = state.searchQuery.trim().toLowerCase();
3886
- const pageIndices = pages.map((_, index) => index).filter((index) => query.length === 0 || `page ${index + 1}`.includes(query));
3887
- const { selectedIndex } = useNavigationInput({
3888
- itemCount: pageIndices.length,
3889
- isActive: !anyOverlayOpen(state),
3890
- onBack: () => {
3891
- dispatch({ type: "POP_SCREEN" });
4218
+ //#endregion
4219
+ //#region src/tui/screens/editors/odg/page-list.tsx
4220
+ function OdgPageListScreen() {
4221
+ const state = useAppState();
4222
+ const dispatch = useAppDispatch();
4223
+ const pages = requireOdgDocument(state).editor.pages();
4224
+ const query = state.searchQuery.trim().toLowerCase();
4225
+ const pageIndices = pages.map((_, index) => index).filter((index) => query.length === 0 || `page ${index + 1}`.includes(query));
4226
+ const { selectedIndex } = useNavigationInput({
4227
+ itemCount: pageIndices.length,
4228
+ isActive: !anyOverlayOpen(state),
4229
+ onBack: () => {
4230
+ dispatch({ type: "POP_SCREEN" });
3892
4231
  },
3893
4232
  onSelect: (index) => {
3894
4233
  const pageIndex = pageIndices[index];
@@ -3940,7 +4279,7 @@ function OdgPageListScreen() {
3940
4279
  }
3941
4280
  //#endregion
3942
4281
  //#region src/tui/screens/editors/odg/page-detail.tsx
3943
- const ADD_KIND_OPTIONS = [
4282
+ const ADD_KIND_OPTIONS$1 = [
3944
4283
  {
3945
4284
  kind: "rect",
3946
4285
  label: "Rectangle"
@@ -3966,7 +4305,7 @@ const ADD_KIND_OPTIONS = [
3966
4305
  label: "Image"
3967
4306
  }
3968
4307
  ];
3969
- const GEOMETRY_FIELDS = [
4308
+ const GEOMETRY_FIELDS$1 = [
3970
4309
  {
3971
4310
  key: "xPt",
3972
4311
  label: "X (pt)",
@@ -3988,24 +4327,24 @@ const GEOMETRY_FIELDS = [
3988
4327
  defaultValue: "100"
3989
4328
  }
3990
4329
  ];
3991
- const FILL_FIELD = {
4330
+ const FILL_FIELD$1 = {
3992
4331
  key: "fill",
3993
4332
  label: "Fill \"r g b\" (0-1 each), blank for none",
3994
4333
  defaultValue: "0.8 0.8 0.8"
3995
4334
  };
3996
- const STROKE_FIELD = {
4335
+ const STROKE_FIELD$1 = {
3997
4336
  key: "stroke",
3998
4337
  label: "Stroke \"r g b widthPt\" (0-1 colour, pt width), blank for none",
3999
4338
  defaultValue: "0 0 0 1"
4000
4339
  };
4001
- function fieldsForAddKind(kind) {
4340
+ function fieldsForAddKind$1(kind) {
4002
4341
  switch (kind) {
4003
4342
  case "rect":
4004
4343
  case "ellipse":
4005
4344
  case "path": return [
4006
- ...GEOMETRY_FIELDS,
4007
- FILL_FIELD,
4008
- STROKE_FIELD
4345
+ ...GEOMETRY_FIELDS$1,
4346
+ FILL_FIELD$1,
4347
+ STROKE_FIELD$1
4009
4348
  ];
4010
4349
  case "line": return [
4011
4350
  {
@@ -4028,15 +4367,15 @@ function fieldsForAddKind(kind) {
4028
4367
  label: "To Y (pt)",
4029
4368
  defaultValue: "40"
4030
4369
  },
4031
- STROKE_FIELD
4370
+ STROKE_FIELD$1
4032
4371
  ];
4033
- case "textbox": return [...GEOMETRY_FIELDS, {
4372
+ case "textbox": return [...GEOMETRY_FIELDS$1, {
4034
4373
  key: "text",
4035
4374
  label: "Text",
4036
4375
  defaultValue: "Text"
4037
4376
  }];
4038
4377
  case "image": return [
4039
- ...GEOMETRY_FIELDS,
4378
+ ...GEOMETRY_FIELDS$1,
4040
4379
  {
4041
4380
  key: "path",
4042
4381
  label: "Image file path (.png/.jpg/.jpeg)",
@@ -4050,7 +4389,7 @@ function fieldsForAddKind(kind) {
4050
4389
  ];
4051
4390
  }
4052
4391
  }
4053
- function readFrame(values) {
4392
+ function readFrame$1(values) {
4054
4393
  return {
4055
4394
  xPt: parseNumberField(requireFieldValue(values, "xPt"), 0),
4056
4395
  yPt: parseNumberField(requireFieldValue(values, "yPt"), 0),
@@ -4069,19 +4408,19 @@ function warnIfVectorAddedReadOnly(doc, pageIndex, dispatch, label) {
4069
4408
  const added = buildPageItems(doc, pageIndex).filter((item) => item.kind === "vector").at(-1);
4070
4409
  if (added !== void 0 && added.liveVector === void 0) warnVectorIsViewOnly(dispatch, label);
4071
4410
  }
4072
- function inferImageFormat(path) {
4411
+ function inferImageFormat$1(path) {
4073
4412
  const extension = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
4074
4413
  if (extension === "png") return "png";
4075
4414
  if (extension === "jpg" || extension === "jpeg") return "jpeg";
4076
4415
  }
4077
- async function applyAddKind(kind, pageIndex, doc, values, dispatch) {
4416
+ async function applyAddKind$1(kind, pageIndex, doc, values, dispatch) {
4078
4417
  switch (kind) {
4079
4418
  case "rect":
4080
4419
  dispatch({
4081
4420
  type: "ADD_RECT",
4082
- pageIndex,
4421
+ containerIndex: pageIndex,
4083
4422
  init: {
4084
- frame: readFrame(values),
4423
+ frame: readFrame$1(values),
4085
4424
  fill: parseColorField(requireFieldValue(values, "fill")),
4086
4425
  stroke: parseStrokeField(requireFieldValue(values, "stroke"))
4087
4426
  }
@@ -4091,9 +4430,9 @@ async function applyAddKind(kind, pageIndex, doc, values, dispatch) {
4091
4430
  case "ellipse":
4092
4431
  dispatch({
4093
4432
  type: "ADD_ELLIPSE",
4094
- pageIndex,
4433
+ containerIndex: pageIndex,
4095
4434
  init: {
4096
- frame: readFrame(values),
4435
+ frame: readFrame$1(values),
4097
4436
  fill: parseColorField(requireFieldValue(values, "fill")),
4098
4437
  stroke: parseStrokeField(requireFieldValue(values, "stroke"))
4099
4438
  }
@@ -4103,7 +4442,7 @@ async function applyAddKind(kind, pageIndex, doc, values, dispatch) {
4103
4442
  case "line":
4104
4443
  dispatch({
4105
4444
  type: "ADD_LINE",
4106
- pageIndex,
4445
+ containerIndex: pageIndex,
4107
4446
  init: {
4108
4447
  from: {
4109
4448
  xPt: parseNumberField(requireFieldValue(values, "fromXPt"), 0),
@@ -4126,10 +4465,10 @@ async function applyAddKind(kind, pageIndex, doc, values, dispatch) {
4126
4465
  warnIfVectorAddedReadOnly(doc, pageIndex, dispatch, "Line");
4127
4466
  return;
4128
4467
  case "path": {
4129
- const frame = readFrame(values);
4468
+ const frame = readFrame$1(values);
4130
4469
  dispatch({
4131
4470
  type: "ADD_PATH",
4132
- pageIndex,
4471
+ containerIndex: pageIndex,
4133
4472
  init: {
4134
4473
  frame,
4135
4474
  subpaths: defaultTriangleSubpaths(frame.widthPt, frame.heightPt),
@@ -4144,14 +4483,14 @@ async function applyAddKind(kind, pageIndex, doc, values, dispatch) {
4144
4483
  dispatch({
4145
4484
  type: "ADD_TEXTBOX",
4146
4485
  containerIndex: pageIndex,
4147
- frame: readFrame(values),
4486
+ frame: readFrame$1(values),
4148
4487
  text: requireFieldValue(values, "text")
4149
4488
  });
4150
4489
  return;
4151
4490
  case "image": {
4152
- const frame = readFrame(values);
4491
+ const frame = readFrame$1(values);
4153
4492
  const path = requireFieldValue(values, "path");
4154
- const format = inferImageFormat(path);
4493
+ const format = inferImageFormat$1(path);
4155
4494
  if (format === void 0) {
4156
4495
  dispatch({
4157
4496
  type: "SET_STATUS",
@@ -4182,15 +4521,15 @@ async function applyAddKind(kind, pageIndex, doc, values, dispatch) {
4182
4521
  }
4183
4522
  }
4184
4523
  }
4185
- function AddItemFlow(props) {
4524
+ function AddItemFlow$1(props) {
4186
4525
  const dispatch = useAppDispatch();
4187
4526
  const [kind, setKind] = useState(void 0);
4188
4527
  const { selectedIndex } = useNavigationInput({
4189
- itemCount: ADD_KIND_OPTIONS.length,
4528
+ itemCount: ADD_KIND_OPTIONS$1.length,
4190
4529
  isActive: props.isActive && kind === void 0,
4191
4530
  onBack: props.onCancel,
4192
4531
  onSelect: (index) => {
4193
- const option = ADD_KIND_OPTIONS[index];
4532
+ const option = ADD_KIND_OPTIONS$1[index];
4194
4533
  if (option === void 0) return;
4195
4534
  setKind(option.kind);
4196
4535
  }
@@ -4203,7 +4542,7 @@ function AddItemFlow(props) {
4203
4542
  bold: true,
4204
4543
  children: "Add item -- choose a kind"
4205
4544
  }), /* @__PURE__ */ jsx(ListView, {
4206
- items: ADD_KIND_OPTIONS,
4545
+ items: ADD_KIND_OPTIONS$1,
4207
4546
  selectedIndex,
4208
4547
  reservedRows: 6,
4209
4548
  renderItem: (option, isSelected) => /* @__PURE__ */ jsxs(Text, {
@@ -4213,10 +4552,10 @@ function AddItemFlow(props) {
4213
4552
  })]
4214
4553
  });
4215
4554
  return /* @__PURE__ */ jsx(FieldWizard, {
4216
- fields: fieldsForAddKind(kind),
4555
+ fields: fieldsForAddKind$1(kind),
4217
4556
  onCancel: props.onCancel,
4218
4557
  onComplete: (values) => {
4219
- applyAddKind(kind, props.pageIndex, props.doc, values, dispatch).then(props.onCreated);
4558
+ applyAddKind$1(kind, props.pageIndex, props.doc, values, dispatch).then(props.onCreated);
4220
4559
  }
4221
4560
  });
4222
4561
  }
@@ -4268,7 +4607,7 @@ function OdgPageDetailScreen() {
4268
4607
  setIsAdding(true);
4269
4608
  }
4270
4609
  });
4271
- if (isAdding) return /* @__PURE__ */ jsx(AddItemFlow, {
4610
+ if (isAdding) return /* @__PURE__ */ jsx(AddItemFlow$1, {
4272
4611
  pageIndex,
4273
4612
  doc,
4274
4613
  isActive: !overlayOpen,
@@ -4321,7 +4660,7 @@ function buildVectorRows(vector, liveVector, dispatch) {
4321
4660
  });
4322
4661
  }
4323
4662
  rows.push({
4324
- label: `Stroke: ${vector.stroke === void 0 ? "none" : `${formatColor$1(vector.stroke.color)} ${formatPt(vector.stroke.widthPt)}pt`}`,
4663
+ label: `Stroke: ${vector.stroke === void 0 ? "none" : `${formatColor$1(vector.stroke.color)} ${formatPt$1(vector.stroke.widthPt)}pt`}`,
4325
4664
  currentValue: vector.stroke === void 0 ? "" : `${vector.stroke.color.r} ${vector.stroke.color.g} ${vector.stroke.color.b} ${vector.stroke.widthPt}`,
4326
4665
  commit: (raw) => {
4327
4666
  const stroke = parseStrokeField(raw);
@@ -4453,7 +4792,7 @@ function ShapeDetail(props) {
4453
4792
  }
4454
4793
  },
4455
4794
  {
4456
- label: `X: ${formatPt(frame.xPt)}pt`,
4795
+ label: `X: ${formatPt$1(frame.xPt)}pt`,
4457
4796
  currentValue: String(frame.xPt),
4458
4797
  commit: (raw) => {
4459
4798
  dispatch({
@@ -4468,7 +4807,7 @@ function ShapeDetail(props) {
4468
4807
  }
4469
4808
  },
4470
4809
  {
4471
- label: `Y: ${formatPt(frame.yPt)}pt`,
4810
+ label: `Y: ${formatPt$1(frame.yPt)}pt`,
4472
4811
  currentValue: String(frame.yPt),
4473
4812
  commit: (raw) => {
4474
4813
  dispatch({
@@ -4483,7 +4822,7 @@ function ShapeDetail(props) {
4483
4822
  }
4484
4823
  },
4485
4824
  {
4486
- label: `Width: ${formatPt(frame.widthPt)}pt`,
4825
+ label: `Width: ${formatPt$1(frame.widthPt)}pt`,
4487
4826
  currentValue: String(frame.widthPt),
4488
4827
  commit: (raw) => {
4489
4828
  dispatch({
@@ -4498,7 +4837,7 @@ function ShapeDetail(props) {
4498
4837
  }
4499
4838
  },
4500
4839
  {
4501
- label: `Height: ${formatPt(frame.heightPt)}pt`,
4840
+ label: `Height: ${formatPt$1(frame.heightPt)}pt`,
4502
4841
  currentValue: String(frame.heightPt),
4503
4842
  commit: (raw) => {
4504
4843
  dispatch({
@@ -4513,7 +4852,7 @@ function ShapeDetail(props) {
4513
4852
  }
4514
4853
  },
4515
4854
  {
4516
- label: `Rotation: ${shape.rotationDeg === void 0 ? "none" : `${formatPt(shape.rotationDeg)} deg`}`,
4855
+ label: `Rotation: ${shape.rotationDeg === void 0 ? "none" : `${formatPt$1(shape.rotationDeg)} deg`}`,
4517
4856
  currentValue: shape.rotationDeg === void 0 ? "" : String(shape.rotationDeg),
4518
4857
  commit: (raw) => {
4519
4858
  const trimmed = raw.trim();
@@ -5061,17 +5400,149 @@ function summarizeSlideTables(doc, slideIndex) {
5061
5400
  }
5062
5401
  //#endregion
5063
5402
  //#region src/tui/screens/editors/pptx/slide-detail.tsx
5064
- const IMAGE_EXTENSION_TO_FORMAT = {
5403
+ const IMAGE_EXTENSION_TO_FORMAT$1 = {
5065
5404
  png: "png",
5066
5405
  jpg: "jpeg",
5067
5406
  jpeg: "jpeg"
5068
5407
  };
5069
5408
  const DEFAULT_TABLE_ROWS = 2;
5070
5409
  const DEFAULT_TABLE_COLUMNS = 2;
5410
+ const VECTOR_GEOMETRY_FIELDS = [
5411
+ {
5412
+ key: "xPt",
5413
+ label: "X (pt)",
5414
+ defaultValue: "40"
5415
+ },
5416
+ {
5417
+ key: "yPt",
5418
+ label: "Y (pt)",
5419
+ defaultValue: "40"
5420
+ },
5421
+ {
5422
+ key: "widthPt",
5423
+ label: "Width (pt)",
5424
+ defaultValue: "160"
5425
+ },
5426
+ {
5427
+ key: "heightPt",
5428
+ label: "Height (pt)",
5429
+ defaultValue: "100"
5430
+ }
5431
+ ];
5432
+ const VECTOR_FILL_FIELD = {
5433
+ key: "fill",
5434
+ label: "Fill \"r g b\" (0-1 each), blank for none",
5435
+ defaultValue: "0.8 0.8 0.8"
5436
+ };
5437
+ const VECTOR_STROKE_FIELD = {
5438
+ key: "stroke",
5439
+ label: "Stroke \"r g b widthPt\" (0-1 colour, pt width), blank for none",
5440
+ defaultValue: "0 0 0 1"
5441
+ };
5442
+ const VECTOR_LINE_FIELDS = [
5443
+ {
5444
+ key: "fromXPt",
5445
+ label: "From X (pt)",
5446
+ defaultValue: "40"
5447
+ },
5448
+ {
5449
+ key: "fromYPt",
5450
+ label: "From Y (pt)",
5451
+ defaultValue: "40"
5452
+ },
5453
+ {
5454
+ key: "toXPt",
5455
+ label: "To X (pt)",
5456
+ defaultValue: "200"
5457
+ },
5458
+ {
5459
+ key: "toYPt",
5460
+ label: "To Y (pt)",
5461
+ defaultValue: "40"
5462
+ },
5463
+ VECTOR_STROKE_FIELD
5464
+ ];
5465
+ function fieldsForVectorKind(kind) {
5466
+ switch (kind) {
5467
+ case "rect":
5468
+ case "ellipse":
5469
+ case "path": return [
5470
+ ...VECTOR_GEOMETRY_FIELDS,
5471
+ VECTOR_FILL_FIELD,
5472
+ VECTOR_STROKE_FIELD
5473
+ ];
5474
+ case "line": return VECTOR_LINE_FIELDS;
5475
+ }
5476
+ }
5477
+ function readVectorFrame(values) {
5478
+ return {
5479
+ xPt: parseNumberField(requireFieldValue(values, "xPt"), 0),
5480
+ yPt: parseNumberField(requireFieldValue(values, "yPt"), 0),
5481
+ widthPt: parseNumberField(requireFieldValue(values, "widthPt"), 160),
5482
+ heightPt: parseNumberField(requireFieldValue(values, "heightPt"), 100)
5483
+ };
5484
+ }
5485
+ function buildVectorAction(kind, slideIndex, values) {
5486
+ switch (kind) {
5487
+ case "rect": return {
5488
+ type: "ADD_RECT",
5489
+ containerIndex: slideIndex,
5490
+ init: {
5491
+ frame: readVectorFrame(values),
5492
+ fill: parseColorField(requireFieldValue(values, "fill")),
5493
+ stroke: parseStrokeField(requireFieldValue(values, "stroke"))
5494
+ }
5495
+ };
5496
+ case "ellipse": return {
5497
+ type: "ADD_ELLIPSE",
5498
+ containerIndex: slideIndex,
5499
+ init: {
5500
+ frame: readVectorFrame(values),
5501
+ fill: parseColorField(requireFieldValue(values, "fill")),
5502
+ stroke: parseStrokeField(requireFieldValue(values, "stroke"))
5503
+ }
5504
+ };
5505
+ case "line": return {
5506
+ type: "ADD_LINE",
5507
+ containerIndex: slideIndex,
5508
+ init: {
5509
+ from: {
5510
+ xPt: parseNumberField(requireFieldValue(values, "fromXPt"), 0),
5511
+ yPt: parseNumberField(requireFieldValue(values, "fromYPt"), 0)
5512
+ },
5513
+ to: {
5514
+ xPt: parseNumberField(requireFieldValue(values, "toXPt"), 100),
5515
+ yPt: parseNumberField(requireFieldValue(values, "toYPt"), 0)
5516
+ },
5517
+ stroke: parseStrokeField(requireFieldValue(values, "stroke")) ?? {
5518
+ color: {
5519
+ r: 0,
5520
+ g: 0,
5521
+ b: 0
5522
+ },
5523
+ widthPt: 1
5524
+ }
5525
+ }
5526
+ };
5527
+ case "path": {
5528
+ const frame = readVectorFrame(values);
5529
+ return {
5530
+ type: "ADD_PATH",
5531
+ containerIndex: slideIndex,
5532
+ init: {
5533
+ frame,
5534
+ subpaths: defaultTriangleSubpaths(frame.widthPt, frame.heightPt),
5535
+ fill: parseColorField(requireFieldValue(values, "fill")),
5536
+ stroke: parseStrokeField(requireFieldValue(values, "stroke"))
5537
+ }
5538
+ };
5539
+ }
5540
+ }
5541
+ }
5071
5542
  function imageFormatFromPath(path) {
5072
5543
  const dotIndex = path.lastIndexOf(".");
5073
5544
  if (dotIndex < 0) return;
5074
- return IMAGE_EXTENSION_TO_FORMAT[path.slice(dotIndex + 1).toLowerCase()];
5545
+ return IMAGE_EXTENSION_TO_FORMAT$1[path.slice(dotIndex + 1).toLowerCase()];
5075
5546
  }
5076
5547
  async function readImageForShape(path) {
5077
5548
  const format = imageFormatFromPath(path);
@@ -5112,6 +5583,7 @@ function SlideDetailScreen(props) {
5112
5583
  const [draft, setDraft] = useState("");
5113
5584
  const [imageError, setImageError] = useState(void 0);
5114
5585
  const [tableRows, setTableRows] = useState(DEFAULT_TABLE_ROWS);
5586
+ const [vectorKind, setVectorKind] = useState(void 0);
5115
5587
  const formIsOpen = addMode !== "closed";
5116
5588
  const { selectedIndex } = useNavigationInput({
5117
5589
  itemCount: selectableRowIndices.length,
@@ -5172,6 +5644,27 @@ function SlideDetailScreen(props) {
5172
5644
  if (input === "b") {
5173
5645
  setDraft(String(DEFAULT_TABLE_ROWS));
5174
5646
  setAddMode("tableRows");
5647
+ return;
5648
+ }
5649
+ if (doc.format !== "odp") return;
5650
+ if (input === "r") {
5651
+ setVectorKind("rect");
5652
+ setAddMode("vector");
5653
+ return;
5654
+ }
5655
+ if (input === "e") {
5656
+ setVectorKind("ellipse");
5657
+ setAddMode("vector");
5658
+ return;
5659
+ }
5660
+ if (input === "n") {
5661
+ setVectorKind("line");
5662
+ setAddMode("vector");
5663
+ return;
5664
+ }
5665
+ if (input === "p") {
5666
+ setVectorKind("path");
5667
+ setAddMode("vector");
5175
5668
  }
5176
5669
  }, { isActive: !overlayOpen && addMode === "chooseKind" });
5177
5670
  useInput((input) => {
@@ -5285,9 +5778,25 @@ function SlideDetailScreen(props) {
5285
5778
  });
5286
5779
  }
5287
5780
  }),
5288
- addMode === "chooseKind" ? /* @__PURE__ */ jsx(Text, {
5781
+ addMode === "chooseKind" ? /* @__PURE__ */ jsxs(Text, {
5289
5782
  color: "cyan",
5290
- children: "Add shape: t textbox, i image, b table, Esc cancel"
5783
+ children: [
5784
+ "Add shape: t textbox, i image, b table",
5785
+ doc.format === "odp" ? ", r rect, e ellipse, n line, p path" : "",
5786
+ ", Esc cancel"
5787
+ ]
5788
+ }) : void 0,
5789
+ addMode === "vector" && vectorKind !== void 0 ? /* @__PURE__ */ jsx(FieldWizard, {
5790
+ fields: fieldsForVectorKind(vectorKind),
5791
+ onCancel: () => {
5792
+ setAddMode("closed");
5793
+ setVectorKind(void 0);
5794
+ },
5795
+ onComplete: (values) => {
5796
+ dispatch(buildVectorAction(vectorKind, slideIndex, values));
5797
+ setAddMode("closed");
5798
+ setVectorKind(void 0);
5799
+ }
5291
5800
  }) : void 0,
5292
5801
  addMode === "textbox" ? /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
5293
5802
  color: "cyan",
@@ -6110,8 +6619,90 @@ const RESERVED_LETTERS = /* @__PURE__ */ new Set([
6110
6619
  "l",
6111
6620
  "p",
6112
6621
  "t",
6113
- "m"
6622
+ "m",
6623
+ "f",
6624
+ "i"
6114
6625
  ]);
6626
+ const IMAGE_EXTENSION_TO_FORMAT = {
6627
+ png: "png",
6628
+ jpg: "jpeg",
6629
+ jpeg: "jpeg"
6630
+ };
6631
+ function inferSheetImageFormat(path) {
6632
+ const extension = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
6633
+ return IMAGE_EXTENSION_TO_FORMAT[extension];
6634
+ }
6635
+ const SHEET_IMAGE_FIELDS = [
6636
+ {
6637
+ key: "path",
6638
+ label: "Image file path (.png/.jpg/.jpeg)",
6639
+ defaultValue: ""
6640
+ },
6641
+ {
6642
+ key: "widthPt",
6643
+ label: "Width (pt)",
6644
+ defaultValue: "100"
6645
+ },
6646
+ {
6647
+ key: "heightPt",
6648
+ label: "Height (pt)",
6649
+ defaultValue: "60"
6650
+ },
6651
+ {
6652
+ key: "offsetXPt",
6653
+ label: "Offset X from anchor cell (pt)",
6654
+ defaultValue: "0"
6655
+ },
6656
+ {
6657
+ key: "offsetYPt",
6658
+ label: "Offset Y from anchor cell (pt)",
6659
+ defaultValue: "0"
6660
+ },
6661
+ {
6662
+ key: "altText",
6663
+ label: "Alt text, blank for none",
6664
+ defaultValue: ""
6665
+ }
6666
+ ];
6667
+ async function applyAddSheetImage(sheetIndex, anchorRow, anchorColumn, values, dispatch) {
6668
+ const path = requireFieldValue(values, "path");
6669
+ const format = inferSheetImageFormat(path);
6670
+ if (format === void 0) {
6671
+ dispatch({
6672
+ type: "SET_STATUS",
6673
+ severity: "warning",
6674
+ text: `${path} is not a .png or .jpg/.jpeg file -- image not added`
6675
+ });
6676
+ return;
6677
+ }
6678
+ try {
6679
+ const bytes = new Uint8Array(await readInput(path));
6680
+ const widthPt = parseNumberField(requireFieldValue(values, "widthPt"), 100);
6681
+ const heightPt = parseNumberField(requireFieldValue(values, "heightPt"), 60);
6682
+ const offsetXPt = parseNumberField(requireFieldValue(values, "offsetXPt"), 0);
6683
+ const offsetYPt = parseNumberField(requireFieldValue(values, "offsetYPt"), 0);
6684
+ const altTextRaw = requireFieldValue(values, "altText").trim();
6685
+ dispatch({
6686
+ type: "ADD_SHEET_IMAGE",
6687
+ sheetIndex,
6688
+ anchorRow,
6689
+ anchorColumn,
6690
+ offsetXPt,
6691
+ offsetYPt,
6692
+ format,
6693
+ bytes,
6694
+ widthPt,
6695
+ heightPt,
6696
+ altText: altTextRaw.length === 0 ? void 0 : altTextRaw
6697
+ });
6698
+ } catch (error) {
6699
+ dispatch({
6700
+ type: "SET_STATUS",
6701
+ severity: "error",
6702
+ text: `Could not read ${path}: ${describeError(error)}`
6703
+ });
6704
+ }
6705
+ }
6115
6706
  function windowStart(cursor, total, viewport) {
6116
6707
  const maxStart = Math.max(0, total - viewport);
6117
6708
  return Math.min(Math.max(cursor - Math.floor(viewport / 2), 0), maxStart);
@@ -6134,6 +6725,9 @@ function OdsSpreadsheetGridScreen() {
6134
6725
  const [viewMode, setViewMode] = useState("grid");
6135
6726
  const [editSession, setEditSession] = useState(void 0);
6136
6727
  const [mergeAnchor, setMergeAnchor] = useState(void 0);
6728
+ const [formulaEditing, setFormulaEditing] = useState(false);
6729
+ const [formulaDraft, setFormulaDraft] = useState("");
6730
+ const [imageWizardOpen, setImageWizardOpen] = useState(false);
6137
6731
  const { columns: terminalColumns, rows: terminalRows } = useWindowSize();
6138
6732
  const sheet = resolveSheet(doc.editor, sheetIndex);
6139
6733
  const { rowCount, columnCount } = sheetExtent(sheet);
@@ -6142,6 +6736,7 @@ function OdsSpreadsheetGridScreen() {
6142
6736
  const clampedColumn = Math.min(cursorColumn, columnCount - 1);
6143
6737
  const overlayOpen = anyOverlayOpen(state);
6144
6738
  const editing = editSession !== void 0;
6739
+ const editingAnything = editing || formulaEditing || imageWizardOpen;
6145
6740
  function moveCursor(deltaRow, deltaColumn) {
6146
6741
  setCursorRow((row) => Math.min(Math.max(row + deltaRow, 0), rowCount - 1));
6147
6742
  setCursorColumn((column) => Math.min(Math.max(column + deltaColumn, 0), columnCount - 1));
@@ -6179,7 +6774,7 @@ function OdsSpreadsheetGridScreen() {
6179
6774
  sheetIndex
6180
6775
  }
6181
6776
  });
6182
- }, { isActive: !overlayOpen && !editing });
6777
+ }, { isActive: !overlayOpen && !editingAnything });
6183
6778
  useInput((input, key) => {
6184
6779
  if (key.upArrow || input === "k") {
6185
6780
  moveCursor(-1, 0);
@@ -6229,6 +6824,16 @@ function OdsSpreadsheetGridScreen() {
6229
6824
  else commitMerge(mergeAnchor);
6230
6825
  return;
6231
6826
  }
6827
+ if (input === "f") {
6828
+ const cell = cells.get(cellKey(clampedRow, clampedColumn));
6829
+ setFormulaDraft(cell?.formula ?? "");
6830
+ setFormulaEditing(true);
6831
+ return;
6832
+ }
6833
+ if (input === "i") {
6834
+ setImageWizardOpen(true);
6835
+ return;
6836
+ }
6232
6837
  if (key.return) {
6233
6838
  if (mergeAnchor !== void 0) {
6234
6839
  commitMerge(mergeAnchor);
@@ -6240,7 +6845,7 @@ function OdsSpreadsheetGridScreen() {
6240
6845
  return;
6241
6846
  }
6242
6847
  if (input.length === 1 && !key.ctrl && !key.meta && !RESERVED_LETTERS.has(input)) beginEdit(input, inferKind(input));
6243
- }, { isActive: !overlayOpen && !editing && viewMode === "grid" });
6848
+ }, { isActive: !overlayOpen && !editingAnything && viewMode === "grid" });
6244
6849
  const compactRows = sheet === void 0 ? [] : sheet.cells.filter((cell) => cell.value.kind !== "empty").map((cell) => ({
6245
6850
  row: cell.row,
6246
6851
  column: cell.column,
@@ -6358,25 +6963,59 @@ function OdsSpreadsheetGridScreen() {
6358
6963
  setEditSession(void 0);
6359
6964
  }
6360
6965
  }),
6361
- /* @__PURE__ */ jsx(Text, {
6362
- dimColor: true,
6363
- children: mergeAnchor === void 0 ? `hjkl/arrows move, Enter/type to edit, m to anchor a merge, p print settings, t ${viewMode === "grid" ? "compact list" : "grid"} view, Esc back` : "hjkl/arrows to the opposite corner, m/Enter to merge, Esc to cancel"
6364
- })
6365
- ]
6366
- });
6367
- }
6368
- //#endregion
6369
- //#region src/tui/screens/editors/odt/list-editor.tsx
6370
- function ListEditorScreen() {
6371
- const state = useAppState();
6372
- const dispatch = useAppDispatch();
6373
- const overlayOpen = anyOverlayOpen(state);
6374
- const [isAdding, setIsAdding] = useState(false);
6375
- const [newItemText, setNewItemText] = useState("");
6376
- const [editingIndex, setEditingIndex] = useState(void 0);
6377
- const screen = currentScreen(state);
6378
- const doc = paragraphFamilyDocument(state.openDocument);
6379
- const list = screen.kind === "listEditor" && doc?.format === "odt" ? doc.editor.lists()[screen.blockIndex] : void 0;
6966
+ formulaEditing ? /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsxs(Text, {
6967
+ color: "cyan",
6968
+ children: [cursorAddress, " formula: "]
6969
+ }), /* @__PURE__ */ jsx(TextField, {
6970
+ value: formulaDraft,
6971
+ isFocused: !overlayOpen,
6972
+ placeholder: "e.g. of:=[.A1]+[.A2]",
6973
+ onChange: setFormulaDraft,
6974
+ onSubmit: (value) => {
6975
+ const trimmed = value.trim();
6976
+ dispatch({
6977
+ type: "SET_CELL_FORMULA",
6978
+ sheetIndex,
6979
+ row: clampedRow,
6980
+ column: clampedColumn,
6981
+ formula: trimmed.length === 0 ? void 0 : trimmed
6982
+ });
6983
+ setFormulaEditing(false);
6984
+ },
6985
+ onCancel: () => {
6986
+ setFormulaEditing(false);
6987
+ }
6988
+ })] }) : void 0,
6989
+ imageWizardOpen ? /* @__PURE__ */ jsx(FieldWizard, {
6990
+ fields: SHEET_IMAGE_FIELDS,
6991
+ onCancel: () => {
6992
+ setImageWizardOpen(false);
6993
+ },
6994
+ onComplete: (values) => {
6995
+ applyAddSheetImage(sheetIndex, clampedRow, clampedColumn, values, dispatch).then(() => {
6996
+ setImageWizardOpen(false);
6997
+ });
6998
+ }
6999
+ }) : void 0,
7000
+ /* @__PURE__ */ jsx(Text, {
7001
+ dimColor: true,
7002
+ 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"
7003
+ })
7004
+ ]
7005
+ });
7006
+ }
7007
+ //#endregion
7008
+ //#region src/tui/screens/editors/odt/list-editor.tsx
7009
+ function ListEditorScreen() {
7010
+ const state = useAppState();
7011
+ const dispatch = useAppDispatch();
7012
+ const overlayOpen = anyOverlayOpen(state);
7013
+ const [isAdding, setIsAdding] = useState(false);
7014
+ const [newItemText, setNewItemText] = useState("");
7015
+ const [editingIndex, setEditingIndex] = useState(void 0);
7016
+ const screen = currentScreen(state);
7017
+ const doc = paragraphFamilyDocument(state.openDocument);
7018
+ const list = screen.kind === "listEditor" && doc?.format === "odt" ? doc.editor.lists()[screen.blockIndex] : void 0;
6380
7019
  const items = list === void 0 ? [] : list.items();
6381
7020
  const rows = items.map((item, index) => ({
6382
7021
  item,
@@ -6539,9 +7178,58 @@ function requirePdfDocument(openDocument) {
6539
7178
  if (openDocument?.format !== "pdf" && openDocument?.format !== "xlsx") throw new Error("A PDF inspection screen rendered without an open PDF or xlsx document; the app router only reaches this screen group from pdfPageList, which is only ever the root screen of one of those two formats.");
6540
7179
  return openDocument;
6541
7180
  }
7181
+ function isEditablePdfDocument(doc) {
7182
+ return doc.format === "pdf";
7183
+ }
6542
7184
  function formatSize(widthPt, heightPt) {
6543
7185
  return `${widthPt.toFixed(0)}×${heightPt.toFixed(0)}pt`;
6544
7186
  }
7187
+ function formatPt(value) {
7188
+ return value.toFixed(1);
7189
+ }
7190
+ function formatColor(color) {
7191
+ const byte = (component) => Math.round(component * 255).toString(16).padStart(2, "0");
7192
+ return `#${byte(color.r)}${byte(color.g)}${byte(color.b)}`;
7193
+ }
7194
+ function formatStroke(stroke) {
7195
+ return `${formatColor(stroke.color)} @ ${stroke.widthPt.toFixed(1)}pt`;
7196
+ }
7197
+ function parseRequiredColorField(raw, fallback) {
7198
+ return parseColorField(raw) ?? fallback;
7199
+ }
7200
+ function parseFontWeight(raw) {
7201
+ return raw.trim().toLowerCase() === "bold" ? "bold" : "normal";
7202
+ }
7203
+ function parseFontStyle(raw) {
7204
+ return raw.trim().toLowerCase() === "italic" ? "italic" : "normal";
7205
+ }
7206
+ function parseOptionalNumberField(raw) {
7207
+ const trimmed = raw.trim();
7208
+ if (trimmed.length === 0) return;
7209
+ const parsed = Number.parseFloat(trimmed);
7210
+ return Number.isFinite(parsed) ? parsed : void 0;
7211
+ }
7212
+ function defaultTriangleLayoutSubpaths(widthPt, heightPt) {
7213
+ return [{
7214
+ startXPt: 0,
7215
+ startYPt: heightPt,
7216
+ segments: [{
7217
+ kind: "line",
7218
+ xPt: widthPt / 2,
7219
+ yPt: 0
7220
+ }, {
7221
+ kind: "line",
7222
+ xPt: widthPt,
7223
+ yPt: heightPt
7224
+ }],
7225
+ closed: true
7226
+ }];
7227
+ }
7228
+ function inferImageFormat(path) {
7229
+ const extension = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
7230
+ if (extension === "png") return "png";
7231
+ if (extension === "jpg" || extension === "jpeg") return "jpeg";
7232
+ }
6545
7233
  //#endregion
6546
7234
  //#region src/tui/screens/editors/pdf/page-list.tsx
6547
7235
  const LAYOUT_ITEM_KIND_ORDER = [
@@ -6630,13 +7318,6 @@ function PdfPageListScreen() {
6630
7318
  function formatPoint(xPt, yPt) {
6631
7319
  return `(${xPt.toFixed(1)}, ${yPt.toFixed(1)})pt`;
6632
7320
  }
6633
- function formatColor(color) {
6634
- const byte = (component) => Math.round(component * 255).toString(16).padStart(2, "0");
6635
- return `#${byte(color.r)}${byte(color.g)}${byte(color.b)}`;
6636
- }
6637
- function formatStroke(stroke) {
6638
- return `${formatColor(stroke.color)} @ ${stroke.widthPt.toFixed(1)}pt`;
6639
- }
6640
7321
  function fieldsFor(item) {
6641
7322
  const fields = [["Kind", item.kind]];
6642
7323
  switch (item.kind) {
@@ -6688,19 +7369,10 @@ function fieldsFor(item) {
6688
7369
  if (item.sourcePath !== void 0) fields.push(["Source path", item.sourcePath]);
6689
7370
  return fields;
6690
7371
  }
6691
- function PdfItemDetailScreen() {
6692
- const state = useAppState();
6693
- const dispatch = useAppDispatch();
6694
- const doc = requirePdfDocument(state.openDocument);
6695
- const screen = currentScreen(state);
6696
- if (screen.kind !== "pdfItemDetail") throw new Error(`PdfItemDetailScreen rendered while the current screen is "${screen.kind}", not "pdfItemDetail".`);
6697
- const page = doc.layout.pages[screen.pageIndex];
6698
- if (page === void 0) throw new Error(`pdfItemDetail was pushed for page ${screen.pageIndex}, but the open PDF has no page at that index.`);
6699
- const item = page.items[screen.itemIndex];
6700
- if (item === void 0) throw new Error(`pdfItemDetail was pushed for item ${screen.itemIndex} on page ${screen.pageIndex}, but that page has no item at that index.`);
7372
+ function ReadOnlyItemDetail(props) {
6701
7373
  useInput((input, key) => {
6702
- if (key.escape || key.leftArrow || input === "h") dispatch({ type: "POP_SCREEN" });
6703
- }, { isActive: !anyOverlayOpen(state) });
7374
+ if (key.escape || key.leftArrow || input === "h") props.onBack();
7375
+ }, { isActive: props.isActive });
6704
7376
  return /* @__PURE__ */ jsxs(Box, {
6705
7377
  flexDirection: "column",
6706
7378
  children: [
@@ -6708,12 +7380,12 @@ function PdfItemDetailScreen() {
6708
7380
  bold: true,
6709
7381
  children: [
6710
7382
  "Page ",
6711
- screen.pageIndex + 1,
7383
+ props.pageIndex + 1,
6712
7384
  ", item ",
6713
- screen.itemIndex + 1
7385
+ props.itemIndex + 1
6714
7386
  ]
6715
7387
  }),
6716
- fieldsFor(item).map(([label, value]) => /* @__PURE__ */ jsxs(Text, { children: [
7388
+ fieldsFor(props.item).map(([label, value]) => /* @__PURE__ */ jsxs(Text, { children: [
6717
7389
  label,
6718
7390
  ": ",
6719
7391
  value
@@ -6725,109 +7397,996 @@ function PdfItemDetailScreen() {
6725
7397
  ]
6726
7398
  });
6727
7399
  }
6728
- //#endregion
6729
- //#region src/tui/screens/editors/pdf/page-items.tsx
6730
- const TEXT_PREVIEW_MAX_CHARS = 48;
6731
- function truncate(text, maxChars) {
6732
- return text.length > maxChars ? `${text.slice(0, maxChars)}…` : text;
6733
- }
6734
- function pathDimensions(item) {
6735
- let minX = Number.POSITIVE_INFINITY;
6736
- let minY = Number.POSITIVE_INFINITY;
6737
- let maxX = Number.NEGATIVE_INFINITY;
6738
- let maxY = Number.NEGATIVE_INFINITY;
6739
- const consider = (xPt, yPt) => {
6740
- minX = Math.min(minX, xPt);
6741
- minY = Math.min(minY, yPt);
6742
- maxX = Math.max(maxX, xPt);
6743
- maxY = Math.max(maxY, yPt);
6744
- };
6745
- for (const subpath of item.subpaths) {
6746
- consider(subpath.startXPt, subpath.startYPt);
6747
- for (const segment of subpath.segments) {
6748
- if (segment.kind === "cubic") {
6749
- consider(segment.c1xPt, segment.c1yPt);
6750
- consider(segment.c2xPt, segment.c2yPt);
6751
- }
6752
- consider(segment.xPt, segment.yPt);
7400
+ function buildFrameRows(frame, onFrameChange) {
7401
+ return [
7402
+ {
7403
+ label: `X: ${formatPt(frame.xPt)}pt`,
7404
+ currentValue: String(frame.xPt),
7405
+ commit: (raw) => onFrameChange({
7406
+ ...frame,
7407
+ xPt: parseNumberField(raw, frame.xPt)
7408
+ })
7409
+ },
7410
+ {
7411
+ label: `Y: ${formatPt(frame.yPt)}pt`,
7412
+ currentValue: String(frame.yPt),
7413
+ commit: (raw) => onFrameChange({
7414
+ ...frame,
7415
+ yPt: parseNumberField(raw, frame.yPt)
7416
+ })
7417
+ },
7418
+ {
7419
+ label: `Width: ${formatPt(frame.widthPt)}pt`,
7420
+ currentValue: String(frame.widthPt),
7421
+ commit: (raw) => onFrameChange({
7422
+ ...frame,
7423
+ widthPt: parseNumberField(raw, frame.widthPt)
7424
+ })
7425
+ },
7426
+ {
7427
+ label: `Height: ${formatPt(frame.heightPt)}pt`,
7428
+ currentValue: String(frame.heightPt),
7429
+ commit: (raw) => onFrameChange({
7430
+ ...frame,
7431
+ heightPt: parseNumberField(raw, frame.heightPt)
7432
+ })
6753
7433
  }
6754
- }
6755
- if (minX > maxX) return "empty path";
6756
- return formatSize(maxX - minX, maxY - minY);
7434
+ ];
6757
7435
  }
6758
- function previewFor(item) {
6759
- switch (item.kind) {
6760
- case "text": return truncate(item.text, TEXT_PREVIEW_MAX_CHARS);
6761
- case "link": return item.uri;
6762
- case "image":
6763
- case "rect":
6764
- case "ellipse": return formatSize(item.widthPt, item.heightPt);
6765
- case "line": return formatSize(Math.abs(item.x2Pt - item.x1Pt), Math.abs(item.y2Pt - item.y1Pt));
6766
- case "path": return pathDimensions(item);
6767
- }
7436
+ function buildFillStrokeRows(fill, stroke, onFillChange, onStrokeChange) {
7437
+ return [{
7438
+ label: `Fill: ${fill === void 0 ? "none" : formatColor(fill)}`,
7439
+ currentValue: fill === void 0 ? "" : `${fill.r} ${fill.g} ${fill.b}`,
7440
+ commit: (raw) => onFillChange(parseColorField(raw))
7441
+ }, {
7442
+ label: `Stroke: ${stroke === void 0 ? "none" : formatStroke(stroke)}`,
7443
+ currentValue: stroke === void 0 ? "" : `${stroke.color.r} ${stroke.color.g} ${stroke.color.b} ${stroke.widthPt}`,
7444
+ commit: (raw) => onStrokeChange(parseStrokeField(raw))
7445
+ }];
6768
7446
  }
6769
- function PdfPageItemsScreen() {
6770
- const state = useAppState();
6771
- const dispatch = useAppDispatch();
6772
- const doc = requirePdfDocument(state.openDocument);
6773
- const screen = currentScreen(state);
6774
- if (screen.kind !== "pdfPageItems") throw new Error(`PdfPageItemsScreen rendered while the current screen is "${screen.kind}", not "pdfPageItems".`);
6775
- const page = doc.layout.pages[screen.pageIndex];
6776
- if (page === void 0) throw new Error(`pdfPageItems was pushed for page ${screen.pageIndex}, but the open PDF has no page at that index.`);
6777
- const query = state.searchQuery.trim().toLowerCase();
6778
- const indexed = page.items.map((item, itemIndex) => ({
6779
- item,
6780
- itemIndex
6781
- }));
6782
- const items = query === "" ? indexed : indexed.filter((entry) => `${entry.item.kind} ${previewFor(entry.item)}`.toLowerCase().includes(query));
6783
- const { selectedIndex } = useNavigationInput({
6784
- itemCount: items.length,
6785
- onSelect: (index) => {
6786
- const entry = items[index];
6787
- if (entry === void 0) return;
6788
- dispatch({
6789
- type: "PUSH_SCREEN",
6790
- screen: {
6791
- kind: "pdfItemDetail",
6792
- pageIndex: screen.pageIndex,
6793
- itemIndex: entry.itemIndex
7447
+ function buildTextRows(item, pageIndex, itemIndex, dispatch) {
7448
+ return [
7449
+ {
7450
+ label: `Text: ${item.text}`,
7451
+ currentValue: item.text,
7452
+ commit: (raw) => dispatch({
7453
+ type: "SET_PDF_TEXT_TEXT",
7454
+ pageIndex,
7455
+ itemIndex,
7456
+ text: raw
7457
+ })
7458
+ },
7459
+ {
7460
+ label: `X: ${formatPt(item.xPt)}pt`,
7461
+ currentValue: String(item.xPt),
7462
+ commit: (raw) => dispatch({
7463
+ type: "SET_PDF_TEXT_POSITION",
7464
+ pageIndex,
7465
+ itemIndex,
7466
+ xPt: parseNumberField(raw, item.xPt),
7467
+ yPt: item.yPt
7468
+ })
7469
+ },
7470
+ {
7471
+ label: `Y: ${formatPt(item.yPt)}pt`,
7472
+ currentValue: String(item.yPt),
7473
+ commit: (raw) => dispatch({
7474
+ type: "SET_PDF_TEXT_POSITION",
7475
+ pageIndex,
7476
+ itemIndex,
7477
+ xPt: item.xPt,
7478
+ yPt: parseNumberField(raw, item.yPt)
7479
+ })
7480
+ },
7481
+ {
7482
+ label: `Font family: ${item.font.family}`,
7483
+ currentValue: item.font.family,
7484
+ commit: (raw) => {
7485
+ const family = raw.trim();
7486
+ dispatch({
7487
+ type: "SET_PDF_TEXT_FONT",
7488
+ pageIndex,
7489
+ itemIndex,
7490
+ font: {
7491
+ ...item.font,
7492
+ family: family.length === 0 ? item.font.family : family
7493
+ }
7494
+ });
7495
+ }
7496
+ },
7497
+ {
7498
+ label: `Font weight: ${item.font.weight} (Enter to toggle)`,
7499
+ currentValue: "",
7500
+ activate: () => dispatch({
7501
+ type: "SET_PDF_TEXT_FONT",
7502
+ pageIndex,
7503
+ itemIndex,
7504
+ font: {
7505
+ ...item.font,
7506
+ weight: item.font.weight === "bold" ? "normal" : "bold"
6794
7507
  }
6795
- });
7508
+ })
6796
7509
  },
6797
- onBack: () => {
6798
- dispatch({ type: "POP_SCREEN" });
7510
+ {
7511
+ label: `Font style: ${item.font.style} (Enter to toggle)`,
7512
+ currentValue: "",
7513
+ activate: () => dispatch({
7514
+ type: "SET_PDF_TEXT_FONT",
7515
+ pageIndex,
7516
+ itemIndex,
7517
+ font: {
7518
+ ...item.font,
7519
+ style: item.font.style === "italic" ? "normal" : "italic"
7520
+ }
7521
+ })
6799
7522
  },
6800
- isActive: !anyOverlayOpen(state)
6801
- });
6802
- return /* @__PURE__ */ jsxs(Box, {
6803
- flexDirection: "column",
6804
- children: [/* @__PURE__ */ jsxs(Text, {
6805
- bold: true,
6806
- children: [
6807
- "Page ",
6808
- screen.pageIndex + 1,
6809
- " items (",
6810
- items.length,
6811
- " of ",
6812
- page.items.length,
6813
- ")"
6814
- ]
6815
- }), /* @__PURE__ */ jsx(ListView, {
6816
- items,
6817
- selectedIndex,
6818
- emptyMessage: query === "" ? "This page has no items." : `No items match "${state.searchQuery}".`,
6819
- renderItem: ({ item, itemIndex }, isSelected) => /* @__PURE__ */ jsxs(Text, {
6820
- color: isSelected ? "cyan" : void 0,
6821
- inverse: isSelected,
6822
- children: [
6823
- itemIndex + 1,
6824
- ". ",
6825
- item.kind,
6826
- " -- ",
6827
- previewFor(item)
6828
- ]
7523
+ {
7524
+ label: `Size: ${item.sizePt}pt`,
7525
+ currentValue: String(item.sizePt),
7526
+ commit: (raw) => dispatch({
7527
+ type: "SET_PDF_TEXT_SIZE",
7528
+ pageIndex,
7529
+ itemIndex,
7530
+ sizePt: parseNumberField(raw, item.sizePt)
6829
7531
  })
6830
- })]
7532
+ },
7533
+ {
7534
+ label: `Colour: ${formatColor(item.color)}`,
7535
+ currentValue: `${item.color.r} ${item.color.g} ${item.color.b}`,
7536
+ commit: (raw) => dispatch({
7537
+ type: "SET_PDF_TEXT_COLOR",
7538
+ pageIndex,
7539
+ itemIndex,
7540
+ color: parseRequiredColorField(raw, item.color)
7541
+ })
7542
+ },
7543
+ {
7544
+ label: `Width: ${item.widthPt === void 0 ? "unset" : `${item.widthPt}pt`}`,
7545
+ currentValue: item.widthPt === void 0 ? "" : String(item.widthPt),
7546
+ commit: (raw) => dispatch({
7547
+ type: "SET_PDF_TEXT_WIDTH",
7548
+ pageIndex,
7549
+ itemIndex,
7550
+ widthPt: parseOptionalNumberField(raw)
7551
+ })
7552
+ },
7553
+ {
7554
+ label: `Rotation: ${item.rotationDeg === void 0 ? "unset" : `${item.rotationDeg}°`}`,
7555
+ currentValue: item.rotationDeg === void 0 ? "" : String(item.rotationDeg),
7556
+ commit: (raw) => dispatch({
7557
+ type: "SET_PDF_TEXT_ROTATION",
7558
+ pageIndex,
7559
+ itemIndex,
7560
+ rotationDeg: parseOptionalNumberField(raw)
7561
+ })
7562
+ },
7563
+ {
7564
+ label: `Underline: ${item.underline === true ? "yes" : "no"} (Enter to toggle)`,
7565
+ currentValue: "",
7566
+ activate: () => dispatch({
7567
+ type: "TOGGLE_PDF_TEXT_UNDERLINE",
7568
+ pageIndex,
7569
+ itemIndex
7570
+ })
7571
+ }
7572
+ ];
7573
+ }
7574
+ function buildRectRows(item, pageIndex, itemIndex, dispatch) {
7575
+ return [...buildFrameRows(item, (frame) => dispatch({
7576
+ type: "SET_PDF_RECT_FRAME",
7577
+ pageIndex,
7578
+ itemIndex,
7579
+ ...frame
7580
+ })), ...buildFillStrokeRows(item.fill, item.stroke, (fill) => dispatch({
7581
+ type: "SET_PDF_RECT_FILL",
7582
+ pageIndex,
7583
+ itemIndex,
7584
+ fill
7585
+ }), (stroke) => dispatch({
7586
+ type: "SET_PDF_RECT_STROKE",
7587
+ pageIndex,
7588
+ itemIndex,
7589
+ stroke
7590
+ }))];
7591
+ }
7592
+ function buildEllipseRows(item, pageIndex, itemIndex, dispatch) {
7593
+ return [...buildFrameRows(item, (frame) => dispatch({
7594
+ type: "SET_PDF_ELLIPSE_FRAME",
7595
+ pageIndex,
7596
+ itemIndex,
7597
+ ...frame
7598
+ })), ...buildFillStrokeRows(item.fill, item.stroke, (fill) => dispatch({
7599
+ type: "SET_PDF_ELLIPSE_FILL",
7600
+ pageIndex,
7601
+ itemIndex,
7602
+ fill
7603
+ }), (stroke) => dispatch({
7604
+ type: "SET_PDF_ELLIPSE_STROKE",
7605
+ pageIndex,
7606
+ itemIndex,
7607
+ stroke
7608
+ }))];
7609
+ }
7610
+ function buildLineRows(item, pageIndex, itemIndex, dispatch) {
7611
+ return [
7612
+ {
7613
+ label: `From X: ${formatPt(item.x1Pt)}pt`,
7614
+ currentValue: String(item.x1Pt),
7615
+ commit: (raw) => dispatch({
7616
+ type: "SET_PDF_LINE_FROM",
7617
+ pageIndex,
7618
+ itemIndex,
7619
+ x1Pt: parseNumberField(raw, item.x1Pt),
7620
+ y1Pt: item.y1Pt
7621
+ })
7622
+ },
7623
+ {
7624
+ label: `From Y: ${formatPt(item.y1Pt)}pt`,
7625
+ currentValue: String(item.y1Pt),
7626
+ commit: (raw) => dispatch({
7627
+ type: "SET_PDF_LINE_FROM",
7628
+ pageIndex,
7629
+ itemIndex,
7630
+ x1Pt: item.x1Pt,
7631
+ y1Pt: parseNumberField(raw, item.y1Pt)
7632
+ })
7633
+ },
7634
+ {
7635
+ label: `To X: ${formatPt(item.x2Pt)}pt`,
7636
+ currentValue: String(item.x2Pt),
7637
+ commit: (raw) => dispatch({
7638
+ type: "SET_PDF_LINE_TO",
7639
+ pageIndex,
7640
+ itemIndex,
7641
+ x2Pt: parseNumberField(raw, item.x2Pt),
7642
+ y2Pt: item.y2Pt
7643
+ })
7644
+ },
7645
+ {
7646
+ label: `To Y: ${formatPt(item.y2Pt)}pt`,
7647
+ currentValue: String(item.y2Pt),
7648
+ commit: (raw) => dispatch({
7649
+ type: "SET_PDF_LINE_TO",
7650
+ pageIndex,
7651
+ itemIndex,
7652
+ x2Pt: item.x2Pt,
7653
+ y2Pt: parseNumberField(raw, item.y2Pt)
7654
+ })
7655
+ },
7656
+ {
7657
+ label: `Colour: ${formatColor(item.color)}`,
7658
+ currentValue: `${item.color.r} ${item.color.g} ${item.color.b}`,
7659
+ commit: (raw) => dispatch({
7660
+ type: "SET_PDF_LINE_COLOR",
7661
+ pageIndex,
7662
+ itemIndex,
7663
+ color: parseRequiredColorField(raw, item.color)
7664
+ })
7665
+ },
7666
+ {
7667
+ label: `Width: ${item.widthPt}pt`,
7668
+ currentValue: String(item.widthPt),
7669
+ commit: (raw) => dispatch({
7670
+ type: "SET_PDF_LINE_WIDTH",
7671
+ pageIndex,
7672
+ itemIndex,
7673
+ widthPt: Math.max(parseNumberField(raw, item.widthPt), Number.EPSILON)
7674
+ })
7675
+ }
7676
+ ];
7677
+ }
7678
+ function buildPathRows(item, pageIndex, itemIndex, dispatch) {
7679
+ return [{
7680
+ label: `Fill rule: ${item.fillRule ?? "nonzero (default)"} (Enter to cycle)`,
7681
+ currentValue: "",
7682
+ activate: () => dispatch({
7683
+ type: "SET_PDF_PATH_FILL_RULE",
7684
+ pageIndex,
7685
+ itemIndex,
7686
+ fillRule: item.fillRule === "evenodd" ? void 0 : item.fillRule === "nonzero" ? "evenodd" : "nonzero"
7687
+ })
7688
+ }, ...buildFillStrokeRows(item.fill, item.stroke, (fill) => dispatch({
7689
+ type: "SET_PDF_PATH_FILL",
7690
+ pageIndex,
7691
+ itemIndex,
7692
+ fill
7693
+ }), (stroke) => dispatch({
7694
+ type: "SET_PDF_PATH_STROKE",
7695
+ pageIndex,
7696
+ itemIndex,
7697
+ stroke
7698
+ }))];
7699
+ }
7700
+ function buildImageRows(item, pageIndex, itemIndex, dispatch, onReplaceImage) {
7701
+ return [
7702
+ ...buildFrameRows(item, (frame) => dispatch({
7703
+ type: "SET_PDF_IMAGE_FRAME",
7704
+ pageIndex,
7705
+ itemIndex,
7706
+ ...frame
7707
+ })),
7708
+ {
7709
+ label: `Rotation: ${item.rotationDeg === void 0 ? "unset" : `${item.rotationDeg}°`}`,
7710
+ currentValue: item.rotationDeg === void 0 ? "" : String(item.rotationDeg),
7711
+ commit: (raw) => dispatch({
7712
+ type: "SET_PDF_IMAGE_ROTATION",
7713
+ pageIndex,
7714
+ itemIndex,
7715
+ rotationDeg: parseOptionalNumberField(raw)
7716
+ })
7717
+ },
7718
+ {
7719
+ label: "Replace image...",
7720
+ currentValue: "",
7721
+ activate: onReplaceImage
7722
+ }
7723
+ ];
7724
+ }
7725
+ function buildLinkRows(item, pageIndex, itemIndex, dispatch) {
7726
+ return [{
7727
+ label: `URI: ${item.uri}`,
7728
+ currentValue: item.uri,
7729
+ commit: (raw) => dispatch({
7730
+ type: "SET_PDF_LINK_URI",
7731
+ pageIndex,
7732
+ itemIndex,
7733
+ uri: raw
7734
+ })
7735
+ }, ...buildFrameRows(item, (frame) => dispatch({
7736
+ type: "SET_PDF_LINK_FRAME",
7737
+ pageIndex,
7738
+ itemIndex,
7739
+ ...frame
7740
+ }))];
7741
+ }
7742
+ function buildRowsFor(item, pageIndex, itemIndex, dispatch, onReplaceImage) {
7743
+ switch (item.kind) {
7744
+ case "text": return buildTextRows(item, pageIndex, itemIndex, dispatch);
7745
+ case "rect": return buildRectRows(item, pageIndex, itemIndex, dispatch);
7746
+ case "ellipse": return buildEllipseRows(item, pageIndex, itemIndex, dispatch);
7747
+ case "line": return buildLineRows(item, pageIndex, itemIndex, dispatch);
7748
+ case "path": return buildPathRows(item, pageIndex, itemIndex, dispatch);
7749
+ case "image": return buildImageRows(item, pageIndex, itemIndex, dispatch, onReplaceImage);
7750
+ case "link": return buildLinkRows(item, pageIndex, itemIndex, dispatch);
7751
+ }
7752
+ }
7753
+ function pathSummary(item) {
7754
+ const segmentCount = item.subpaths.reduce((total, subpath) => total + subpath.segments.length, 0);
7755
+ return `${item.subpaths.length} subpath${item.subpaths.length === 1 ? "" : "s"}, ${segmentCount} segment${segmentCount === 1 ? "" : "s"} (not editable here -- see documents.js's own PdfPathItem doc comment)`;
7756
+ }
7757
+ async function applyImageReplace(pageIndex, itemIndex, path, dispatch) {
7758
+ const format = inferImageFormat(path);
7759
+ if (format === void 0) {
7760
+ dispatch({
7761
+ type: "SET_STATUS",
7762
+ severity: "warning",
7763
+ text: `${path} is not a .png or .jpg/.jpeg file -- image not replaced`
7764
+ });
7765
+ return;
7766
+ }
7767
+ try {
7768
+ dispatch({
7769
+ type: "SET_PDF_IMAGE_SOURCE",
7770
+ pageIndex,
7771
+ itemIndex,
7772
+ format,
7773
+ bytes: new Uint8Array(await readInput(path))
7774
+ });
7775
+ } catch (error) {
7776
+ dispatch({
7777
+ type: "SET_STATUS",
7778
+ severity: "error",
7779
+ text: `Could not read ${path}: ${describeError(error)}`
7780
+ });
7781
+ }
7782
+ }
7783
+ function EditableItemDetail(props) {
7784
+ const dispatch = useAppDispatch();
7785
+ const { doc, pageIndex, itemIndex } = props;
7786
+ const [editingField, setEditingField] = useState(void 0);
7787
+ const [draft, setDraft] = useState("");
7788
+ const [replacingImage, setReplacingImage] = useState(false);
7789
+ const item = doc.editor.page(pageIndex)?.items()[itemIndex];
7790
+ const rows = item === void 0 ? [] : buildRowsFor(item, pageIndex, itemIndex, dispatch, () => setReplacingImage(true));
7791
+ const { selectedIndex } = useNavigationInput({
7792
+ itemCount: rows.length,
7793
+ isActive: props.isActive && editingField === void 0 && !replacingImage,
7794
+ onBack: () => {
7795
+ dispatch({ type: "POP_SCREEN" });
7796
+ },
7797
+ onSelect: (index) => {
7798
+ const row = rows[index];
7799
+ if (row === void 0) return;
7800
+ if (row.activate !== void 0) {
7801
+ row.activate();
7802
+ return;
7803
+ }
7804
+ setDraft(row.currentValue);
7805
+ setEditingField(index);
7806
+ }
7807
+ });
7808
+ if (item === void 0) return /* @__PURE__ */ jsxs(Box, {
7809
+ flexDirection: "column",
7810
+ children: [/* @__PURE__ */ jsxs(Text, {
7811
+ color: "yellow",
7812
+ children: [
7813
+ "There is no item ",
7814
+ itemIndex + 1,
7815
+ " on page ",
7816
+ pageIndex + 1,
7817
+ " any more."
7818
+ ]
7819
+ }), /* @__PURE__ */ jsx(Text, {
7820
+ dimColor: true,
7821
+ children: "Esc to go back"
7822
+ })]
7823
+ });
7824
+ if (replacingImage) return /* @__PURE__ */ jsx(FieldWizard, {
7825
+ fields: [{
7826
+ key: "path",
7827
+ label: "Image file path (.png/.jpg/.jpeg)",
7828
+ defaultValue: ""
7829
+ }],
7830
+ onCancel: () => {
7831
+ setReplacingImage(false);
7832
+ },
7833
+ onComplete: (values) => {
7834
+ applyImageReplace(pageIndex, itemIndex, requireFieldValue(values, "path"), dispatch).then(() => {
7835
+ setReplacingImage(false);
7836
+ });
7837
+ }
7838
+ });
7839
+ if (editingField !== void 0) {
7840
+ const row = rows[editingField];
7841
+ if (row === void 0) throw new Error(`EditableItemDetail 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.`);
7842
+ return /* @__PURE__ */ jsxs(Box, {
7843
+ flexDirection: "column",
7844
+ borderStyle: "round",
7845
+ paddingX: 1,
7846
+ children: [/* @__PURE__ */ jsx(Text, {
7847
+ bold: true,
7848
+ children: row.label
7849
+ }), /* @__PURE__ */ jsx(TextField, {
7850
+ value: draft,
7851
+ isFocused: true,
7852
+ onChange: setDraft,
7853
+ onCancel: () => {
7854
+ setEditingField(void 0);
7855
+ },
7856
+ onSubmit: (value) => {
7857
+ row.commit?.(value);
7858
+ setEditingField(void 0);
7859
+ }
7860
+ })]
7861
+ });
7862
+ }
7863
+ return /* @__PURE__ */ jsxs(Box, {
7864
+ flexDirection: "column",
7865
+ children: [
7866
+ /* @__PURE__ */ jsxs(Text, {
7867
+ bold: true,
7868
+ children: [
7869
+ "Page ",
7870
+ pageIndex + 1,
7871
+ ", item ",
7872
+ itemIndex + 1,
7873
+ " -- ",
7874
+ item.kind
7875
+ ]
7876
+ }),
7877
+ item.kind === "path" && /* @__PURE__ */ jsx(Text, {
7878
+ dimColor: true,
7879
+ children: pathSummary(item)
7880
+ }),
7881
+ item.kind === "image" && /* @__PURE__ */ jsxs(Text, {
7882
+ dimColor: true,
7883
+ children: ["Image ID: ", item.imageId]
7884
+ }),
7885
+ /* @__PURE__ */ jsx(ListView, {
7886
+ items: rows,
7887
+ selectedIndex,
7888
+ reservedRows: 5,
7889
+ renderItem: (row, isSelected) => /* @__PURE__ */ jsx(Text, {
7890
+ color: isSelected ? "cyan" : void 0,
7891
+ inverse: isSelected,
7892
+ children: row.label
7893
+ })
7894
+ }),
7895
+ /* @__PURE__ */ jsx(Text, {
7896
+ dimColor: true,
7897
+ children: "Enter to edit a field, Esc to go back"
7898
+ })
7899
+ ]
7900
+ });
7901
+ }
7902
+ function PdfItemDetailScreen() {
7903
+ const state = useAppState();
7904
+ const dispatch = useAppDispatch();
7905
+ const doc = requirePdfDocument(state.openDocument);
7906
+ const screen = currentScreen(state);
7907
+ if (screen.kind !== "pdfItemDetail") throw new Error(`PdfItemDetailScreen rendered while the current screen is "${screen.kind}", not "pdfItemDetail".`);
7908
+ const isActive = !anyOverlayOpen(state);
7909
+ if (!isEditablePdfDocument(doc)) {
7910
+ const page = doc.layout.pages[screen.pageIndex];
7911
+ if (page === void 0) throw new Error(`pdfItemDetail was pushed for page ${screen.pageIndex}, but the open PDF has no page at that index.`);
7912
+ const item = page.items[screen.itemIndex];
7913
+ if (item === void 0) throw new Error(`pdfItemDetail was pushed for item ${screen.itemIndex} on page ${screen.pageIndex}, but that page has no item at that index.`);
7914
+ return /* @__PURE__ */ jsx(ReadOnlyItemDetail, {
7915
+ item,
7916
+ pageIndex: screen.pageIndex,
7917
+ itemIndex: screen.itemIndex,
7918
+ isActive,
7919
+ onBack: () => {
7920
+ dispatch({ type: "POP_SCREEN" });
7921
+ }
7922
+ });
7923
+ }
7924
+ return /* @__PURE__ */ jsx(EditableItemDetail, {
7925
+ doc,
7926
+ pageIndex: screen.pageIndex,
7927
+ itemIndex: screen.itemIndex,
7928
+ isActive
7929
+ });
7930
+ }
7931
+ //#endregion
7932
+ //#region src/tui/screens/editors/pdf/page-items.tsx
7933
+ const TEXT_PREVIEW_MAX_CHARS = 48;
7934
+ function truncate(text, maxChars) {
7935
+ return text.length > maxChars ? `${text.slice(0, maxChars)}…` : text;
7936
+ }
7937
+ function pathDimensions(item) {
7938
+ let minX = Number.POSITIVE_INFINITY;
7939
+ let minY = Number.POSITIVE_INFINITY;
7940
+ let maxX = Number.NEGATIVE_INFINITY;
7941
+ let maxY = Number.NEGATIVE_INFINITY;
7942
+ const consider = (xPt, yPt) => {
7943
+ minX = Math.min(minX, xPt);
7944
+ minY = Math.min(minY, yPt);
7945
+ maxX = Math.max(maxX, xPt);
7946
+ maxY = Math.max(maxY, yPt);
7947
+ };
7948
+ for (const subpath of item.subpaths) {
7949
+ consider(subpath.startXPt, subpath.startYPt);
7950
+ for (const segment of subpath.segments) {
7951
+ if (segment.kind === "cubic") {
7952
+ consider(segment.c1xPt, segment.c1yPt);
7953
+ consider(segment.c2xPt, segment.c2yPt);
7954
+ }
7955
+ consider(segment.xPt, segment.yPt);
7956
+ }
7957
+ }
7958
+ if (minX > maxX) return "empty path";
7959
+ return formatSize(maxX - minX, maxY - minY);
7960
+ }
7961
+ function previewFor(item) {
7962
+ switch (item.kind) {
7963
+ case "text": return truncate(item.text, TEXT_PREVIEW_MAX_CHARS);
7964
+ case "link": return item.uri;
7965
+ case "image":
7966
+ case "rect":
7967
+ case "ellipse": return formatSize(item.widthPt, item.heightPt);
7968
+ case "line": return formatSize(Math.abs(item.x2Pt - item.x1Pt), Math.abs(item.y2Pt - item.y1Pt));
7969
+ case "path": return pathDimensions(item);
7970
+ }
7971
+ }
7972
+ const ADD_KIND_OPTIONS = [
7973
+ {
7974
+ kind: "text",
7975
+ label: "Text"
7976
+ },
7977
+ {
7978
+ kind: "rect",
7979
+ label: "Rectangle"
7980
+ },
7981
+ {
7982
+ kind: "ellipse",
7983
+ label: "Ellipse"
7984
+ },
7985
+ {
7986
+ kind: "line",
7987
+ label: "Line"
7988
+ },
7989
+ {
7990
+ kind: "path",
7991
+ label: "Path (fixed triangle shape)"
7992
+ },
7993
+ {
7994
+ kind: "image",
7995
+ label: "Image"
7996
+ },
7997
+ {
7998
+ kind: "link",
7999
+ label: "Link"
8000
+ }
8001
+ ];
8002
+ const GEOMETRY_FIELDS = [
8003
+ {
8004
+ key: "xPt",
8005
+ label: "X (pt)",
8006
+ defaultValue: "40"
8007
+ },
8008
+ {
8009
+ key: "yPt",
8010
+ label: "Y (pt)",
8011
+ defaultValue: "40"
8012
+ },
8013
+ {
8014
+ key: "widthPt",
8015
+ label: "Width (pt)",
8016
+ defaultValue: "160"
8017
+ },
8018
+ {
8019
+ key: "heightPt",
8020
+ label: "Height (pt)",
8021
+ defaultValue: "100"
8022
+ }
8023
+ ];
8024
+ const FILL_FIELD = {
8025
+ key: "fill",
8026
+ label: "Fill \"r g b\" (0-1 each), blank for none",
8027
+ defaultValue: "0.8 0.8 0.8"
8028
+ };
8029
+ const STROKE_FIELD = {
8030
+ key: "stroke",
8031
+ label: "Stroke \"r g b widthPt\" (0-1 colour, pt width), blank for none",
8032
+ defaultValue: "0 0 0 1"
8033
+ };
8034
+ const REQUIRED_COLOR_FIELD = {
8035
+ key: "color",
8036
+ label: "Colour \"r g b\" (0-1 each)",
8037
+ defaultValue: "0 0 0"
8038
+ };
8039
+ function fieldsForAddKind(kind) {
8040
+ switch (kind) {
8041
+ case "text": return [
8042
+ {
8043
+ key: "xPt",
8044
+ label: "X (pt)",
8045
+ defaultValue: "40"
8046
+ },
8047
+ {
8048
+ key: "yPt",
8049
+ label: "Y (pt)",
8050
+ defaultValue: "40"
8051
+ },
8052
+ {
8053
+ key: "text",
8054
+ label: "Text",
8055
+ defaultValue: "Text"
8056
+ },
8057
+ {
8058
+ key: "fontFamily",
8059
+ label: "Font family",
8060
+ defaultValue: "Helvetica"
8061
+ },
8062
+ {
8063
+ key: "fontWeight",
8064
+ label: "Font weight (normal/bold)",
8065
+ defaultValue: "normal"
8066
+ },
8067
+ {
8068
+ key: "fontStyle",
8069
+ label: "Font style (normal/italic)",
8070
+ defaultValue: "normal"
8071
+ },
8072
+ {
8073
+ key: "sizePt",
8074
+ label: "Size (pt)",
8075
+ defaultValue: "12"
8076
+ },
8077
+ REQUIRED_COLOR_FIELD
8078
+ ];
8079
+ case "rect":
8080
+ case "ellipse":
8081
+ case "path": return [
8082
+ ...GEOMETRY_FIELDS,
8083
+ FILL_FIELD,
8084
+ STROKE_FIELD
8085
+ ];
8086
+ case "line": return [
8087
+ {
8088
+ key: "fromXPt",
8089
+ label: "From X (pt)",
8090
+ defaultValue: "40"
8091
+ },
8092
+ {
8093
+ key: "fromYPt",
8094
+ label: "From Y (pt)",
8095
+ defaultValue: "40"
8096
+ },
8097
+ {
8098
+ key: "toXPt",
8099
+ label: "To X (pt)",
8100
+ defaultValue: "200"
8101
+ },
8102
+ {
8103
+ key: "toYPt",
8104
+ label: "To Y (pt)",
8105
+ defaultValue: "40"
8106
+ },
8107
+ REQUIRED_COLOR_FIELD,
8108
+ {
8109
+ key: "widthPt",
8110
+ label: "Width (pt)",
8111
+ defaultValue: "1"
8112
+ }
8113
+ ];
8114
+ case "image": return [...GEOMETRY_FIELDS, {
8115
+ key: "path",
8116
+ label: "Image file path (.png/.jpg/.jpeg)",
8117
+ defaultValue: ""
8118
+ }];
8119
+ case "link": return [{
8120
+ key: "uri",
8121
+ label: "URI",
8122
+ defaultValue: "https://example.com"
8123
+ }, ...GEOMETRY_FIELDS];
8124
+ }
8125
+ }
8126
+ function readFrame(values) {
8127
+ return {
8128
+ xPt: parseNumberField(requireFieldValue(values, "xPt"), 0),
8129
+ yPt: parseNumberField(requireFieldValue(values, "yPt"), 0),
8130
+ widthPt: parseNumberField(requireFieldValue(values, "widthPt"), 100),
8131
+ heightPt: parseNumberField(requireFieldValue(values, "heightPt"), 60)
8132
+ };
8133
+ }
8134
+ async function applyAddKind(kind, pageIndex, values, dispatch) {
8135
+ switch (kind) {
8136
+ case "text":
8137
+ dispatch({
8138
+ type: "ADD_PDF_TEXT",
8139
+ pageIndex,
8140
+ init: {
8141
+ xPt: parseNumberField(requireFieldValue(values, "xPt"), 0),
8142
+ yPt: parseNumberField(requireFieldValue(values, "yPt"), 0),
8143
+ text: requireFieldValue(values, "text"),
8144
+ font: {
8145
+ family: requireFieldValue(values, "fontFamily").trim() || "Helvetica",
8146
+ weight: parseFontWeight(requireFieldValue(values, "fontWeight")),
8147
+ style: parseFontStyle(requireFieldValue(values, "fontStyle"))
8148
+ },
8149
+ sizePt: Math.max(parseNumberField(requireFieldValue(values, "sizePt"), 12), Number.EPSILON),
8150
+ color: parseColorField(requireFieldValue(values, "color")) ?? {
8151
+ r: 0,
8152
+ g: 0,
8153
+ b: 0
8154
+ }
8155
+ }
8156
+ });
8157
+ return;
8158
+ case "rect":
8159
+ dispatch({
8160
+ type: "ADD_PDF_RECT",
8161
+ pageIndex,
8162
+ init: {
8163
+ ...readFrame(values),
8164
+ fill: parseColorField(requireFieldValue(values, "fill")),
8165
+ stroke: parseStrokeField(requireFieldValue(values, "stroke"))
8166
+ }
8167
+ });
8168
+ return;
8169
+ case "ellipse":
8170
+ dispatch({
8171
+ type: "ADD_PDF_ELLIPSE",
8172
+ pageIndex,
8173
+ init: {
8174
+ ...readFrame(values),
8175
+ fill: parseColorField(requireFieldValue(values, "fill")),
8176
+ stroke: parseStrokeField(requireFieldValue(values, "stroke"))
8177
+ }
8178
+ });
8179
+ return;
8180
+ case "line":
8181
+ dispatch({
8182
+ type: "ADD_PDF_LINE",
8183
+ pageIndex,
8184
+ init: {
8185
+ x1Pt: parseNumberField(requireFieldValue(values, "fromXPt"), 0),
8186
+ y1Pt: parseNumberField(requireFieldValue(values, "fromYPt"), 0),
8187
+ x2Pt: parseNumberField(requireFieldValue(values, "toXPt"), 100),
8188
+ y2Pt: parseNumberField(requireFieldValue(values, "toYPt"), 0),
8189
+ color: parseColorField(requireFieldValue(values, "color")) ?? {
8190
+ r: 0,
8191
+ g: 0,
8192
+ b: 0
8193
+ },
8194
+ widthPt: Math.max(parseNumberField(requireFieldValue(values, "widthPt"), 1), Number.EPSILON)
8195
+ }
8196
+ });
8197
+ return;
8198
+ case "path": {
8199
+ const frame = readFrame(values);
8200
+ dispatch({
8201
+ type: "ADD_PDF_PATH",
8202
+ pageIndex,
8203
+ init: {
8204
+ subpaths: defaultTriangleLayoutSubpaths(frame.widthPt, frame.heightPt),
8205
+ fill: parseColorField(requireFieldValue(values, "fill")),
8206
+ stroke: parseStrokeField(requireFieldValue(values, "stroke"))
8207
+ }
8208
+ });
8209
+ return;
8210
+ }
8211
+ case "image": {
8212
+ const frame = readFrame(values);
8213
+ const path = requireFieldValue(values, "path");
8214
+ const format = inferImageFormat(path);
8215
+ if (format === void 0) {
8216
+ dispatch({
8217
+ type: "SET_STATUS",
8218
+ severity: "warning",
8219
+ text: `${path} is not a .png or .jpg/.jpeg file -- image not added`
8220
+ });
8221
+ return;
8222
+ }
8223
+ try {
8224
+ const bytes = new Uint8Array(await readInput(path));
8225
+ dispatch({
8226
+ type: "ADD_PDF_IMAGE",
8227
+ pageIndex,
8228
+ init: {
8229
+ ...frame,
8230
+ format,
8231
+ bytes
8232
+ }
8233
+ });
8234
+ } catch (error) {
8235
+ dispatch({
8236
+ type: "SET_STATUS",
8237
+ severity: "error",
8238
+ text: `Could not read ${path}: ${describeError(error)}`
8239
+ });
8240
+ }
8241
+ return;
8242
+ }
8243
+ case "link": {
8244
+ const frame = readFrame(values);
8245
+ dispatch({
8246
+ type: "ADD_PDF_LINK",
8247
+ pageIndex,
8248
+ init: {
8249
+ uri: requireFieldValue(values, "uri"),
8250
+ ...frame
8251
+ }
8252
+ });
8253
+ return;
8254
+ }
8255
+ }
8256
+ }
8257
+ function AddItemFlow(props) {
8258
+ const dispatch = useAppDispatch();
8259
+ const [kind, setKind] = useState(void 0);
8260
+ const { selectedIndex } = useNavigationInput({
8261
+ itemCount: ADD_KIND_OPTIONS.length,
8262
+ isActive: props.isActive && kind === void 0,
8263
+ onBack: props.onCancel,
8264
+ onSelect: (index) => {
8265
+ const option = ADD_KIND_OPTIONS[index];
8266
+ if (option === void 0) return;
8267
+ setKind(option.kind);
8268
+ }
8269
+ });
8270
+ if (kind === void 0) return /* @__PURE__ */ jsxs(Box, {
8271
+ flexDirection: "column",
8272
+ borderStyle: "round",
8273
+ paddingX: 1,
8274
+ children: [/* @__PURE__ */ jsx(Text, {
8275
+ bold: true,
8276
+ children: "Add item -- choose a kind"
8277
+ }), /* @__PURE__ */ jsx(ListView, {
8278
+ items: ADD_KIND_OPTIONS,
8279
+ selectedIndex,
8280
+ reservedRows: 6,
8281
+ renderItem: (option, isSelected) => /* @__PURE__ */ jsxs(Text, {
8282
+ color: isSelected ? "cyan" : void 0,
8283
+ children: [isSelected ? "> " : " ", option.label]
8284
+ })
8285
+ })]
8286
+ });
8287
+ return /* @__PURE__ */ jsx(FieldWizard, {
8288
+ fields: fieldsForAddKind(kind),
8289
+ onCancel: props.onCancel,
8290
+ onComplete: (values) => {
8291
+ applyAddKind(kind, props.pageIndex, values, dispatch).then(props.onCreated);
8292
+ }
8293
+ });
8294
+ }
8295
+ function PdfPageItemsScreen() {
8296
+ const state = useAppState();
8297
+ const dispatch = useAppDispatch();
8298
+ const doc = requirePdfDocument(state.openDocument);
8299
+ const editable = isEditablePdfDocument(doc);
8300
+ const screen = currentScreen(state);
8301
+ if (screen.kind !== "pdfPageItems") throw new Error(`PdfPageItemsScreen rendered while the current screen is "${screen.kind}", not "pdfPageItems".`);
8302
+ const page = doc.layout.pages[screen.pageIndex];
8303
+ if (page === void 0) throw new Error(`pdfPageItems was pushed for page ${screen.pageIndex}, but the open PDF has no page at that index.`);
8304
+ const [isAdding, setIsAdding] = useState(false);
8305
+ const overlayOpen = anyOverlayOpen(state);
8306
+ const query = state.searchQuery.trim().toLowerCase();
8307
+ const indexed = page.items.map((item, itemIndex) => ({
8308
+ item,
8309
+ itemIndex
8310
+ }));
8311
+ const items = query === "" ? indexed : indexed.filter((entry) => `${entry.item.kind} ${previewFor(entry.item)}`.toLowerCase().includes(query));
8312
+ const { selectedIndex } = useNavigationInput({
8313
+ itemCount: items.length,
8314
+ isActive: !overlayOpen && !isAdding,
8315
+ onSelect: (index) => {
8316
+ const entry = items[index];
8317
+ if (entry === void 0) return;
8318
+ dispatch({
8319
+ type: "PUSH_SCREEN",
8320
+ screen: {
8321
+ kind: "pdfItemDetail",
8322
+ pageIndex: screen.pageIndex,
8323
+ itemIndex: entry.itemIndex
8324
+ }
8325
+ });
8326
+ },
8327
+ onBack: () => {
8328
+ dispatch({ type: "POP_SCREEN" });
8329
+ },
8330
+ onAppend: editable ? () => {
8331
+ setIsAdding(true);
8332
+ } : void 0
8333
+ });
8334
+ useInput((input) => {
8335
+ if (input !== "d") return;
8336
+ const entry = items[selectedIndex];
8337
+ if (entry === void 0) return;
8338
+ dispatch({
8339
+ type: "REMOVE_PDF_ITEM",
8340
+ pageIndex: screen.pageIndex,
8341
+ itemIndex: entry.itemIndex
8342
+ });
8343
+ }, { isActive: editable && !overlayOpen && !isAdding && items.length > 0 });
8344
+ if (isAdding) return /* @__PURE__ */ jsx(AddItemFlow, {
8345
+ pageIndex: screen.pageIndex,
8346
+ isActive: !overlayOpen,
8347
+ onCancel: () => {
8348
+ setIsAdding(false);
8349
+ },
8350
+ onCreated: () => {
8351
+ setIsAdding(false);
8352
+ }
8353
+ });
8354
+ return /* @__PURE__ */ jsxs(Box, {
8355
+ flexDirection: "column",
8356
+ children: [
8357
+ /* @__PURE__ */ jsxs(Text, {
8358
+ bold: true,
8359
+ children: [
8360
+ "Page ",
8361
+ screen.pageIndex + 1,
8362
+ " items (",
8363
+ items.length,
8364
+ " of ",
8365
+ page.items.length,
8366
+ ")"
8367
+ ]
8368
+ }),
8369
+ /* @__PURE__ */ jsx(ListView, {
8370
+ items,
8371
+ selectedIndex,
8372
+ emptyMessage: query === "" ? editable ? "This page has no items -- press 'a' to add one" : "This page has no items." : `No items match "${state.searchQuery}".`,
8373
+ renderItem: ({ item, itemIndex }, isSelected) => /* @__PURE__ */ jsxs(Text, {
8374
+ color: isSelected ? "cyan" : void 0,
8375
+ inverse: isSelected,
8376
+ children: [
8377
+ itemIndex + 1,
8378
+ ". ",
8379
+ item.kind,
8380
+ " -- ",
8381
+ previewFor(item)
8382
+ ]
8383
+ })
8384
+ }),
8385
+ editable && /* @__PURE__ */ jsx(Text, {
8386
+ dimColor: true,
8387
+ children: "a to add an item, d to delete the selected item"
8388
+ })
8389
+ ]
6831
8390
  });
6832
8391
  }
6833
8392
  //#endregion