document-cli 1.9.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, bytesToBase64, convertWordprocessingToLayout, createDocx, createFontMeasurer, createFontRegistry, createOdg, createOdp, createOds, createOdt, createPptx, decodeMarkdownText, docxToPdf, encodeMarkdownText, encodePackage, hsqldbCellDisplayText, markdownToPdf, odgToPdf, odpToPdf, odsToPdf, odtToPdf, openDocx, openOdg, openOdp, openOds, openOdt, openPptx, parseXml, pptxToPdf, readDocxContent, readDocxExtras, readMarkdownContent, readOdbForms, readOdbReportContent, readOdbReports, readOdbTables, readOdgContent, readOdpContent, readOdsContent, readOdtContent, readPdf, readPptxContent, rgbHexToColor, writePdf, xlsxToPdf } from "documents.js";
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,40 @@ 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";
511
569
  function vectorHostDocument(state) {
512
570
  const doc = state.openDocument;
513
571
  if (doc === void 0) return;
@@ -1078,6 +1136,129 @@ function appReducer(state, action) {
1078
1136
  action.vector.stroke = action.stroke;
1079
1137
  });
1080
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
+ });
1081
1262
  case "SET_MARKDOWN_SOURCE": {
1082
1263
  const doc = markdownDocument(state);
1083
1264
  if (doc === void 0) return wrongDocument(state, "a markdown document");
@@ -1111,7 +1292,7 @@ function appReducer(state, action) {
1111
1292
  case "UNDO": {
1112
1293
  const doc = state.openDocument;
1113
1294
  if (doc === void 0) return withStatus(state, "info", "There is nothing to undo");
1114
- 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`);
1115
1296
  const snapshot = state.undoStack.at(-1);
1116
1297
  if (snapshot === void 0) return withStatus(state, "info", "There is nothing to undo");
1117
1298
  const restored = doc.format === "markdown" ? {
@@ -1195,7 +1376,7 @@ function TextField(props) {
1195
1376
  }
1196
1377
  //#endregion
1197
1378
  //#region src/tui/components/command-palette.tsx
1198
- const USAGE_COLUMN_WIDTH = 32;
1379
+ const USAGE_COLUMN_WIDTH = 36;
1199
1380
  const COMMANDS = [
1200
1381
  {
1201
1382
  name: "save",
@@ -1214,7 +1395,7 @@ const COMMANDS = [
1214
1395
  },
1215
1396
  {
1216
1397
  name: "new",
1217
- usage: ":new docx|pptx|odt|odp|ods|odg",
1398
+ usage: ":new docx|pptx|odt|odp|ods|odg|pdf",
1218
1399
  description: "Create an empty document"
1219
1400
  },
1220
1401
  {
@@ -1331,7 +1512,7 @@ async function runCommand(line, state, dispatch) {
1331
1512
  case "new": {
1332
1513
  const format = args[0];
1333
1514
  if (format === void 0 || !isEditableFormat(format)) {
1334
- warn(dispatch, "Usage: :new docx|pptx|odt|odp|ods|odg");
1515
+ warn(dispatch, "Usage: :new docx|pptx|odt|odp|ods|odg|pdf");
1335
1516
  return;
1336
1517
  }
1337
1518
  dispatch({
@@ -2522,14 +2703,14 @@ const IMAGE_FIELDS = [
2522
2703
  defaultValue: ""
2523
2704
  }
2524
2705
  ];
2525
- function inferImageFormat$1(path) {
2706
+ function inferImageFormat$2(path) {
2526
2707
  const extension = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
2527
2708
  if (extension === "png") return "png";
2528
2709
  if (extension === "jpg" || extension === "jpeg") return "jpeg";
2529
2710
  }
2530
2711
  async function applyInsertImage(blockIndex, values, dispatch) {
2531
2712
  const path = requireFieldValue(values, "path");
2532
- const format = inferImageFormat$1(path);
2713
+ const format = inferImageFormat$2(path);
2533
2714
  if (format === void 0) {
2534
2715
  dispatch({
2535
2716
  type: "SET_STATUS",
@@ -4012,14 +4193,14 @@ function vectorKindLabel(kind) {
4012
4193
  case "path": return "Path";
4013
4194
  }
4014
4195
  }
4015
- function formatPt(value) {
4196
+ function formatPt$1(value) {
4016
4197
  return value.toFixed(1);
4017
4198
  }
4018
4199
  function formatFrame(box) {
4019
- 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`;
4020
4201
  }
4021
4202
  function formatPoint$1(point) {
4022
- return `${formatPt(point.xPt)},${formatPt(point.yPt)}`;
4203
+ return `${formatPt$1(point.xPt)},${formatPt$1(point.yPt)}`;
4023
4204
  }
4024
4205
  function formatColor$1(color) {
4025
4206
  return `rgb(${color.r.toFixed(2)}, ${color.g.toFixed(2)}, ${color.b.toFixed(2)})`;
@@ -4031,7 +4212,7 @@ function describeVectorGeometry(vector) {
4031
4212
  function describeFillStroke(vector) {
4032
4213
  const parts = [];
4033
4214
  if (vector.kind !== "line" && vector.fill !== void 0) parts.push(`fill ${formatColor$1(vector.fill)}`);
4034
- if (vector.stroke !== void 0) parts.push(`stroke ${formatColor$1(vector.stroke.color)} ${formatPt(vector.stroke.widthPt)}pt`);
4215
+ if (vector.stroke !== void 0) parts.push(`stroke ${formatColor$1(vector.stroke.color)} ${formatPt$1(vector.stroke.widthPt)}pt`);
4035
4216
  return parts.length === 0 ? "no fill or stroke" : parts.join(", ");
4036
4217
  }
4037
4218
  //#endregion
@@ -4098,7 +4279,7 @@ function OdgPageListScreen() {
4098
4279
  }
4099
4280
  //#endregion
4100
4281
  //#region src/tui/screens/editors/odg/page-detail.tsx
4101
- const ADD_KIND_OPTIONS = [
4282
+ const ADD_KIND_OPTIONS$1 = [
4102
4283
  {
4103
4284
  kind: "rect",
4104
4285
  label: "Rectangle"
@@ -4124,7 +4305,7 @@ const ADD_KIND_OPTIONS = [
4124
4305
  label: "Image"
4125
4306
  }
4126
4307
  ];
4127
- const GEOMETRY_FIELDS = [
4308
+ const GEOMETRY_FIELDS$1 = [
4128
4309
  {
4129
4310
  key: "xPt",
4130
4311
  label: "X (pt)",
@@ -4146,24 +4327,24 @@ const GEOMETRY_FIELDS = [
4146
4327
  defaultValue: "100"
4147
4328
  }
4148
4329
  ];
4149
- const FILL_FIELD = {
4330
+ const FILL_FIELD$1 = {
4150
4331
  key: "fill",
4151
4332
  label: "Fill \"r g b\" (0-1 each), blank for none",
4152
4333
  defaultValue: "0.8 0.8 0.8"
4153
4334
  };
4154
- const STROKE_FIELD = {
4335
+ const STROKE_FIELD$1 = {
4155
4336
  key: "stroke",
4156
4337
  label: "Stroke \"r g b widthPt\" (0-1 colour, pt width), blank for none",
4157
4338
  defaultValue: "0 0 0 1"
4158
4339
  };
4159
- function fieldsForAddKind(kind) {
4340
+ function fieldsForAddKind$1(kind) {
4160
4341
  switch (kind) {
4161
4342
  case "rect":
4162
4343
  case "ellipse":
4163
4344
  case "path": return [
4164
- ...GEOMETRY_FIELDS,
4165
- FILL_FIELD,
4166
- STROKE_FIELD
4345
+ ...GEOMETRY_FIELDS$1,
4346
+ FILL_FIELD$1,
4347
+ STROKE_FIELD$1
4167
4348
  ];
4168
4349
  case "line": return [
4169
4350
  {
@@ -4186,15 +4367,15 @@ function fieldsForAddKind(kind) {
4186
4367
  label: "To Y (pt)",
4187
4368
  defaultValue: "40"
4188
4369
  },
4189
- STROKE_FIELD
4370
+ STROKE_FIELD$1
4190
4371
  ];
4191
- case "textbox": return [...GEOMETRY_FIELDS, {
4372
+ case "textbox": return [...GEOMETRY_FIELDS$1, {
4192
4373
  key: "text",
4193
4374
  label: "Text",
4194
4375
  defaultValue: "Text"
4195
4376
  }];
4196
4377
  case "image": return [
4197
- ...GEOMETRY_FIELDS,
4378
+ ...GEOMETRY_FIELDS$1,
4198
4379
  {
4199
4380
  key: "path",
4200
4381
  label: "Image file path (.png/.jpg/.jpeg)",
@@ -4208,7 +4389,7 @@ function fieldsForAddKind(kind) {
4208
4389
  ];
4209
4390
  }
4210
4391
  }
4211
- function readFrame(values) {
4392
+ function readFrame$1(values) {
4212
4393
  return {
4213
4394
  xPt: parseNumberField(requireFieldValue(values, "xPt"), 0),
4214
4395
  yPt: parseNumberField(requireFieldValue(values, "yPt"), 0),
@@ -4227,19 +4408,19 @@ function warnIfVectorAddedReadOnly(doc, pageIndex, dispatch, label) {
4227
4408
  const added = buildPageItems(doc, pageIndex).filter((item) => item.kind === "vector").at(-1);
4228
4409
  if (added !== void 0 && added.liveVector === void 0) warnVectorIsViewOnly(dispatch, label);
4229
4410
  }
4230
- function inferImageFormat(path) {
4411
+ function inferImageFormat$1(path) {
4231
4412
  const extension = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
4232
4413
  if (extension === "png") return "png";
4233
4414
  if (extension === "jpg" || extension === "jpeg") return "jpeg";
4234
4415
  }
4235
- async function applyAddKind(kind, pageIndex, doc, values, dispatch) {
4416
+ async function applyAddKind$1(kind, pageIndex, doc, values, dispatch) {
4236
4417
  switch (kind) {
4237
4418
  case "rect":
4238
4419
  dispatch({
4239
4420
  type: "ADD_RECT",
4240
4421
  containerIndex: pageIndex,
4241
4422
  init: {
4242
- frame: readFrame(values),
4423
+ frame: readFrame$1(values),
4243
4424
  fill: parseColorField(requireFieldValue(values, "fill")),
4244
4425
  stroke: parseStrokeField(requireFieldValue(values, "stroke"))
4245
4426
  }
@@ -4251,7 +4432,7 @@ async function applyAddKind(kind, pageIndex, doc, values, dispatch) {
4251
4432
  type: "ADD_ELLIPSE",
4252
4433
  containerIndex: pageIndex,
4253
4434
  init: {
4254
- frame: readFrame(values),
4435
+ frame: readFrame$1(values),
4255
4436
  fill: parseColorField(requireFieldValue(values, "fill")),
4256
4437
  stroke: parseStrokeField(requireFieldValue(values, "stroke"))
4257
4438
  }
@@ -4284,7 +4465,7 @@ async function applyAddKind(kind, pageIndex, doc, values, dispatch) {
4284
4465
  warnIfVectorAddedReadOnly(doc, pageIndex, dispatch, "Line");
4285
4466
  return;
4286
4467
  case "path": {
4287
- const frame = readFrame(values);
4468
+ const frame = readFrame$1(values);
4288
4469
  dispatch({
4289
4470
  type: "ADD_PATH",
4290
4471
  containerIndex: pageIndex,
@@ -4302,14 +4483,14 @@ async function applyAddKind(kind, pageIndex, doc, values, dispatch) {
4302
4483
  dispatch({
4303
4484
  type: "ADD_TEXTBOX",
4304
4485
  containerIndex: pageIndex,
4305
- frame: readFrame(values),
4486
+ frame: readFrame$1(values),
4306
4487
  text: requireFieldValue(values, "text")
4307
4488
  });
4308
4489
  return;
4309
4490
  case "image": {
4310
- const frame = readFrame(values);
4491
+ const frame = readFrame$1(values);
4311
4492
  const path = requireFieldValue(values, "path");
4312
- const format = inferImageFormat(path);
4493
+ const format = inferImageFormat$1(path);
4313
4494
  if (format === void 0) {
4314
4495
  dispatch({
4315
4496
  type: "SET_STATUS",
@@ -4340,15 +4521,15 @@ async function applyAddKind(kind, pageIndex, doc, values, dispatch) {
4340
4521
  }
4341
4522
  }
4342
4523
  }
4343
- function AddItemFlow(props) {
4524
+ function AddItemFlow$1(props) {
4344
4525
  const dispatch = useAppDispatch();
4345
4526
  const [kind, setKind] = useState(void 0);
4346
4527
  const { selectedIndex } = useNavigationInput({
4347
- itemCount: ADD_KIND_OPTIONS.length,
4528
+ itemCount: ADD_KIND_OPTIONS$1.length,
4348
4529
  isActive: props.isActive && kind === void 0,
4349
4530
  onBack: props.onCancel,
4350
4531
  onSelect: (index) => {
4351
- const option = ADD_KIND_OPTIONS[index];
4532
+ const option = ADD_KIND_OPTIONS$1[index];
4352
4533
  if (option === void 0) return;
4353
4534
  setKind(option.kind);
4354
4535
  }
@@ -4361,7 +4542,7 @@ function AddItemFlow(props) {
4361
4542
  bold: true,
4362
4543
  children: "Add item -- choose a kind"
4363
4544
  }), /* @__PURE__ */ jsx(ListView, {
4364
- items: ADD_KIND_OPTIONS,
4545
+ items: ADD_KIND_OPTIONS$1,
4365
4546
  selectedIndex,
4366
4547
  reservedRows: 6,
4367
4548
  renderItem: (option, isSelected) => /* @__PURE__ */ jsxs(Text, {
@@ -4371,10 +4552,10 @@ function AddItemFlow(props) {
4371
4552
  })]
4372
4553
  });
4373
4554
  return /* @__PURE__ */ jsx(FieldWizard, {
4374
- fields: fieldsForAddKind(kind),
4555
+ fields: fieldsForAddKind$1(kind),
4375
4556
  onCancel: props.onCancel,
4376
4557
  onComplete: (values) => {
4377
- applyAddKind(kind, props.pageIndex, props.doc, values, dispatch).then(props.onCreated);
4558
+ applyAddKind$1(kind, props.pageIndex, props.doc, values, dispatch).then(props.onCreated);
4378
4559
  }
4379
4560
  });
4380
4561
  }
@@ -4426,7 +4607,7 @@ function OdgPageDetailScreen() {
4426
4607
  setIsAdding(true);
4427
4608
  }
4428
4609
  });
4429
- if (isAdding) return /* @__PURE__ */ jsx(AddItemFlow, {
4610
+ if (isAdding) return /* @__PURE__ */ jsx(AddItemFlow$1, {
4430
4611
  pageIndex,
4431
4612
  doc,
4432
4613
  isActive: !overlayOpen,
@@ -4479,7 +4660,7 @@ function buildVectorRows(vector, liveVector, dispatch) {
4479
4660
  });
4480
4661
  }
4481
4662
  rows.push({
4482
- 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`}`,
4483
4664
  currentValue: vector.stroke === void 0 ? "" : `${vector.stroke.color.r} ${vector.stroke.color.g} ${vector.stroke.color.b} ${vector.stroke.widthPt}`,
4484
4665
  commit: (raw) => {
4485
4666
  const stroke = parseStrokeField(raw);
@@ -4611,7 +4792,7 @@ function ShapeDetail(props) {
4611
4792
  }
4612
4793
  },
4613
4794
  {
4614
- label: `X: ${formatPt(frame.xPt)}pt`,
4795
+ label: `X: ${formatPt$1(frame.xPt)}pt`,
4615
4796
  currentValue: String(frame.xPt),
4616
4797
  commit: (raw) => {
4617
4798
  dispatch({
@@ -4626,7 +4807,7 @@ function ShapeDetail(props) {
4626
4807
  }
4627
4808
  },
4628
4809
  {
4629
- label: `Y: ${formatPt(frame.yPt)}pt`,
4810
+ label: `Y: ${formatPt$1(frame.yPt)}pt`,
4630
4811
  currentValue: String(frame.yPt),
4631
4812
  commit: (raw) => {
4632
4813
  dispatch({
@@ -4641,7 +4822,7 @@ function ShapeDetail(props) {
4641
4822
  }
4642
4823
  },
4643
4824
  {
4644
- label: `Width: ${formatPt(frame.widthPt)}pt`,
4825
+ label: `Width: ${formatPt$1(frame.widthPt)}pt`,
4645
4826
  currentValue: String(frame.widthPt),
4646
4827
  commit: (raw) => {
4647
4828
  dispatch({
@@ -4656,7 +4837,7 @@ function ShapeDetail(props) {
4656
4837
  }
4657
4838
  },
4658
4839
  {
4659
- label: `Height: ${formatPt(frame.heightPt)}pt`,
4840
+ label: `Height: ${formatPt$1(frame.heightPt)}pt`,
4660
4841
  currentValue: String(frame.heightPt),
4661
4842
  commit: (raw) => {
4662
4843
  dispatch({
@@ -4671,7 +4852,7 @@ function ShapeDetail(props) {
4671
4852
  }
4672
4853
  },
4673
4854
  {
4674
- label: `Rotation: ${shape.rotationDeg === void 0 ? "none" : `${formatPt(shape.rotationDeg)} deg`}`,
4855
+ label: `Rotation: ${shape.rotationDeg === void 0 ? "none" : `${formatPt$1(shape.rotationDeg)} deg`}`,
4675
4856
  currentValue: shape.rotationDeg === void 0 ? "" : String(shape.rotationDeg),
4676
4857
  commit: (raw) => {
4677
4858
  const trimmed = raw.trim();
@@ -6997,9 +7178,58 @@ function requirePdfDocument(openDocument) {
6997
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.");
6998
7179
  return openDocument;
6999
7180
  }
7181
+ function isEditablePdfDocument(doc) {
7182
+ return doc.format === "pdf";
7183
+ }
7000
7184
  function formatSize(widthPt, heightPt) {
7001
7185
  return `${widthPt.toFixed(0)}×${heightPt.toFixed(0)}pt`;
7002
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
+ }
7003
7233
  //#endregion
7004
7234
  //#region src/tui/screens/editors/pdf/page-list.tsx
7005
7235
  const LAYOUT_ITEM_KIND_ORDER = [
@@ -7088,13 +7318,6 @@ function PdfPageListScreen() {
7088
7318
  function formatPoint(xPt, yPt) {
7089
7319
  return `(${xPt.toFixed(1)}, ${yPt.toFixed(1)})pt`;
7090
7320
  }
7091
- function formatColor(color) {
7092
- const byte = (component) => Math.round(component * 255).toString(16).padStart(2, "0");
7093
- return `#${byte(color.r)}${byte(color.g)}${byte(color.b)}`;
7094
- }
7095
- function formatStroke(stroke) {
7096
- return `${formatColor(stroke.color)} @ ${stroke.widthPt.toFixed(1)}pt`;
7097
- }
7098
7321
  function fieldsFor(item) {
7099
7322
  const fields = [["Kind", item.kind]];
7100
7323
  switch (item.kind) {
@@ -7146,19 +7369,10 @@ function fieldsFor(item) {
7146
7369
  if (item.sourcePath !== void 0) fields.push(["Source path", item.sourcePath]);
7147
7370
  return fields;
7148
7371
  }
7149
- function PdfItemDetailScreen() {
7150
- const state = useAppState();
7151
- const dispatch = useAppDispatch();
7152
- const doc = requirePdfDocument(state.openDocument);
7153
- const screen = currentScreen(state);
7154
- if (screen.kind !== "pdfItemDetail") throw new Error(`PdfItemDetailScreen rendered while the current screen is "${screen.kind}", not "pdfItemDetail".`);
7155
- const page = doc.layout.pages[screen.pageIndex];
7156
- if (page === void 0) throw new Error(`pdfItemDetail was pushed for page ${screen.pageIndex}, but the open PDF has no page at that index.`);
7157
- const item = page.items[screen.itemIndex];
7158
- 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) {
7159
7373
  useInput((input, key) => {
7160
- if (key.escape || key.leftArrow || input === "h") dispatch({ type: "POP_SCREEN" });
7161
- }, { isActive: !anyOverlayOpen(state) });
7374
+ if (key.escape || key.leftArrow || input === "h") props.onBack();
7375
+ }, { isActive: props.isActive });
7162
7376
  return /* @__PURE__ */ jsxs(Box, {
7163
7377
  flexDirection: "column",
7164
7378
  children: [
@@ -7166,12 +7380,12 @@ function PdfItemDetailScreen() {
7166
7380
  bold: true,
7167
7381
  children: [
7168
7382
  "Page ",
7169
- screen.pageIndex + 1,
7383
+ props.pageIndex + 1,
7170
7384
  ", item ",
7171
- screen.itemIndex + 1
7385
+ props.itemIndex + 1
7172
7386
  ]
7173
7387
  }),
7174
- fieldsFor(item).map(([label, value]) => /* @__PURE__ */ jsxs(Text, { children: [
7388
+ fieldsFor(props.item).map(([label, value]) => /* @__PURE__ */ jsxs(Text, { children: [
7175
7389
  label,
7176
7390
  ": ",
7177
7391
  value
@@ -7183,109 +7397,996 @@ function PdfItemDetailScreen() {
7183
7397
  ]
7184
7398
  });
7185
7399
  }
7186
- //#endregion
7187
- //#region src/tui/screens/editors/pdf/page-items.tsx
7188
- const TEXT_PREVIEW_MAX_CHARS = 48;
7189
- function truncate(text, maxChars) {
7190
- return text.length > maxChars ? `${text.slice(0, maxChars)}…` : text;
7191
- }
7192
- function pathDimensions(item) {
7193
- let minX = Number.POSITIVE_INFINITY;
7194
- let minY = Number.POSITIVE_INFINITY;
7195
- let maxX = Number.NEGATIVE_INFINITY;
7196
- let maxY = Number.NEGATIVE_INFINITY;
7197
- const consider = (xPt, yPt) => {
7198
- minX = Math.min(minX, xPt);
7199
- minY = Math.min(minY, yPt);
7200
- maxX = Math.max(maxX, xPt);
7201
- maxY = Math.max(maxY, yPt);
7202
- };
7203
- for (const subpath of item.subpaths) {
7204
- consider(subpath.startXPt, subpath.startYPt);
7205
- for (const segment of subpath.segments) {
7206
- if (segment.kind === "cubic") {
7207
- consider(segment.c1xPt, segment.c1yPt);
7208
- consider(segment.c2xPt, segment.c2yPt);
7209
- }
7210
- 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
+ })
7211
7433
  }
7212
- }
7213
- if (minX > maxX) return "empty path";
7214
- return formatSize(maxX - minX, maxY - minY);
7434
+ ];
7215
7435
  }
7216
- function previewFor(item) {
7217
- switch (item.kind) {
7218
- case "text": return truncate(item.text, TEXT_PREVIEW_MAX_CHARS);
7219
- case "link": return item.uri;
7220
- case "image":
7221
- case "rect":
7222
- case "ellipse": return formatSize(item.widthPt, item.heightPt);
7223
- case "line": return formatSize(Math.abs(item.x2Pt - item.x1Pt), Math.abs(item.y2Pt - item.y1Pt));
7224
- case "path": return pathDimensions(item);
7225
- }
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
+ }];
7226
7446
  }
7227
- function PdfPageItemsScreen() {
7228
- const state = useAppState();
7229
- const dispatch = useAppDispatch();
7230
- const doc = requirePdfDocument(state.openDocument);
7231
- const screen = currentScreen(state);
7232
- if (screen.kind !== "pdfPageItems") throw new Error(`PdfPageItemsScreen rendered while the current screen is "${screen.kind}", not "pdfPageItems".`);
7233
- const page = doc.layout.pages[screen.pageIndex];
7234
- if (page === void 0) throw new Error(`pdfPageItems was pushed for page ${screen.pageIndex}, but the open PDF has no page at that index.`);
7235
- const query = state.searchQuery.trim().toLowerCase();
7236
- const indexed = page.items.map((item, itemIndex) => ({
7237
- item,
7238
- itemIndex
7239
- }));
7240
- const items = query === "" ? indexed : indexed.filter((entry) => `${entry.item.kind} ${previewFor(entry.item)}`.toLowerCase().includes(query));
7241
- const { selectedIndex } = useNavigationInput({
7242
- itemCount: items.length,
7243
- onSelect: (index) => {
7244
- const entry = items[index];
7245
- if (entry === void 0) return;
7246
- dispatch({
7247
- type: "PUSH_SCREEN",
7248
- screen: {
7249
- kind: "pdfItemDetail",
7250
- pageIndex: screen.pageIndex,
7251
- itemIndex: entry.itemIndex
7252
- }
7253
- });
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
+ })
7254
7458
  },
7255
- onBack: () => {
7256
- dispatch({ type: "POP_SCREEN" });
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
+ })
7257
7469
  },
7258
- isActive: !anyOverlayOpen(state)
7259
- });
7260
- return /* @__PURE__ */ jsxs(Box, {
7261
- flexDirection: "column",
7262
- children: [/* @__PURE__ */ jsxs(Text, {
7263
- bold: true,
7264
- children: [
7265
- "Page ",
7266
- screen.pageIndex + 1,
7267
- " items (",
7268
- items.length,
7269
- " of ",
7270
- page.items.length,
7271
- ")"
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"
7507
+ }
7508
+ })
7509
+ },
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
+ })
7522
+ },
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)
7531
+ })
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."
7272
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"
7273
8277
  }), /* @__PURE__ */ jsx(ListView, {
7274
- items,
8278
+ items: ADD_KIND_OPTIONS,
7275
8279
  selectedIndex,
7276
- emptyMessage: query === "" ? "This page has no items." : `No items match "${state.searchQuery}".`,
7277
- renderItem: ({ item, itemIndex }, isSelected) => /* @__PURE__ */ jsxs(Text, {
8280
+ reservedRows: 6,
8281
+ renderItem: (option, isSelected) => /* @__PURE__ */ jsxs(Text, {
7278
8282
  color: isSelected ? "cyan" : void 0,
7279
- inverse: isSelected,
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,
7280
8359
  children: [
7281
- itemIndex + 1,
7282
- ". ",
7283
- item.kind,
7284
- " -- ",
7285
- previewFor(item)
8360
+ "Page ",
8361
+ screen.pageIndex + 1,
8362
+ " items (",
8363
+ items.length,
8364
+ " of ",
8365
+ page.items.length,
8366
+ ")"
7286
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"
7287
8388
  })
7288
- })]
8389
+ ]
7289
8390
  });
7290
8391
  }
7291
8392
  //#endregion