document-cli 1.1.0 → 1.2.1

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
- import { a as inferFormatFromExtension, i as formatToExtension, t as readInput } from "./io-CKOE9Xda.js";
1
+ import { d as inferFormatFromExtension, i as formatOdbReportLines, l as loadProvidedFonts, n as describeOdbReport, o as readInput, r as formatOdbFormLines, t as describeOdbForm, u as formatToExtension } from "./odb-structure-CX_0zL8_.js";
2
2
  import { readFile, writeFile } from "node:fs/promises";
3
- import { createDocx, createOdg, createOdp, createOds, createOdt, createPptx, decodeMarkdownText, docxToPdf, encodeMarkdownText, hsqldbCellDisplayText, markdownToPdf, odgToPdf, odpToPdf, odsToPdf, odtToPdf, openDocx, openOdg, openOdp, openOds, openOdt, openPptx, pptxToPdf, readOdbTables, readOdgContent, readOdsContent, readPdf, rgbHexToColor } from "documents.js";
3
+ import { createDocx, createOdg, createOdp, createOds, createOdt, createPptx, decodeMarkdownText, docxToPdf, encodeMarkdownText, hsqldbCellDisplayText, markdownToPdf, odgToPdf, odpToPdf, odsToPdf, odtToPdf, openDocx, openOdg, openOdp, openOds, openOdt, openPptx, pptxToPdf, readOdbForms, readOdbReports, readOdbTables, readOdgContent, readOdsContent, readPdf, rgbHexToColor, xlsxToPdf } from "documents.js";
4
4
  import { basename, dirname, extname, join } from "node:path";
5
5
  import { cellReference, columnIndexToLetters, decodePackage } from "odf.js";
6
6
  import { readdirSync } from "node:fs";
@@ -15,9 +15,17 @@ function describeError(error) {
15
15
  }
16
16
  //#endregion
17
17
  //#region src/tui/format/export-pdf.ts
18
- function toPdfOptions(options) {
18
+ function toPdfOptions(options, fonts) {
19
19
  return {
20
20
  signal: options.signal,
21
+ fonts,
22
+ onFontSubstitution: (substitution) => {
23
+ const requested = `${substitution.requestedFamily}${substitution.requestedBold ? " bold" : ""}${substitution.requestedItalic ? " italic" : ""}`;
24
+ options.onDiagnostic({
25
+ severity: "info",
26
+ message: `No "${requested}" face available; drew it in "${substitution.resolvedFamily}"`
27
+ });
28
+ },
21
29
  onSubstitution: (substitution, context) => {
22
30
  options.onDiagnostic({
23
31
  severity: "info",
@@ -29,12 +37,17 @@ function toPdfOptions(options) {
29
37
  }
30
38
  async function exportToPdf(openDocument, destinationPath, options) {
31
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`);
32
- const pdfOptions = toPdfOptions(options);
40
+ const pdfOptions = toPdfOptions(options, await loadProvidedFonts(options.fontFiles ?? [], { signal: options.signal }));
33
41
  if (openDocument.format === "markdown") {
34
42
  const pdfBytes = markdownToPdf(encodeMarkdownText(openDocument.source), pdfOptions);
35
43
  await writeFile(destinationPath, pdfBytes);
36
44
  return;
37
45
  }
46
+ if (openDocument.format === "xlsx") {
47
+ const pdfBytes = xlsxToPdf(openDocument.bytes, pdfOptions);
48
+ await writeFile(destinationPath, pdfBytes);
49
+ return;
50
+ }
38
51
  const bytes = openDocument.editor.toBytes();
39
52
  const pdfBytes = convert(openDocument.format, bytes, pdfOptions);
40
53
  await writeFile(destinationPath, pdfBytes);
@@ -64,11 +77,16 @@ function detectFormat(path) {
64
77
  const ODB_EXTENSION = ".odb";
65
78
  async function openDocumentAtPath(path) {
66
79
  const bytes = new Uint8Array(await readFile(path));
67
- if (path.toLowerCase().endsWith(ODB_EXTENSION)) return {
68
- format: "odb",
69
- tables: readOdbTables(decodePackage(bytes)),
70
- path
71
- };
80
+ if (path.toLowerCase().endsWith(ODB_EXTENSION)) {
81
+ const pkg = decodePackage(bytes);
82
+ return {
83
+ format: "odb",
84
+ tables: readOdbTables(pkg),
85
+ forms: readOdbForms(pkg),
86
+ reports: readOdbReports(pkg),
87
+ path
88
+ };
89
+ }
72
90
  const format = detectFormat(path);
73
91
  if (format === void 0) throw new Error(`Cannot tell what kind of document ${path} is from its extension`);
74
92
  switch (format) {
@@ -112,7 +130,12 @@ async function openDocumentAtPath(path) {
112
130
  source: decodeMarkdownText(bytes),
113
131
  path
114
132
  };
115
- case "xlsx": throw new Error("documents.js has no xlsx editor; convert the workbook to ods first (xlsxToOds) and open that");
133
+ case "xlsx": return {
134
+ format,
135
+ layout: readPdf(xlsxToPdf(bytes)),
136
+ bytes,
137
+ path
138
+ };
116
139
  case "odf": throw new Error("A standalone .odf formula document has no editor; convert it to PDF (odfToPdf) instead");
117
140
  }
118
141
  }
@@ -151,7 +174,7 @@ function createNewDocument(format) {
151
174
  }
152
175
  }
153
176
  async function saveDocumentTo(openDocument, path) {
154
- if (openDocument.format === "odb" || openDocument.format === "pdf") throw new Error(`A ${openDocument.format} document is opened read-only and cannot be written back`);
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`);
155
178
  if (openDocument.format === "markdown") {
156
179
  await writeFile(path, encodeMarkdownText(openDocument.source));
157
180
  return;
@@ -190,6 +213,8 @@ function selectionKeyFor(screen) {
190
213
  case "sheetList":
191
214
  case "pageList":
192
215
  case "odbTableList":
216
+ case "odbFormList":
217
+ case "odbReportList":
193
218
  case "pdfPageList":
194
219
  case "exportOptions":
195
220
  case "saveAsPrompt":
@@ -211,6 +236,8 @@ function selectionKeyFor(screen) {
211
236
  case "shapeOrVectorDetail":
212
237
  case "pdfItemDetail": return `${screen.kind}:${screen.pageIndex}:${screen.itemIndex}`;
213
238
  case "odbTableRows": return `odbTableRows:${screen.tableName}`;
239
+ case "odbFormDetail": return `odbFormDetail:${screen.formName}`;
240
+ case "odbReportDetail": return `odbReportDetail:${screen.reportName}`;
214
241
  case "markdownLineEditor": return `markdownLineEditor:${screen.lineIndex}`;
215
242
  }
216
243
  }
@@ -224,7 +251,8 @@ function rootScreenForFormat(format) {
224
251
  case "odg": return { kind: "pageList" };
225
252
  case "odb": return { kind: "odbTableList" };
226
253
  case "markdown": return { kind: "markdownLineList" };
227
- case "pdf": return { kind: "pdfPageList" };
254
+ case "pdf":
255
+ case "xlsx": return { kind: "pdfPageList" };
228
256
  }
229
257
  }
230
258
  function currentScreen(state) {
@@ -339,6 +367,8 @@ function documentWithPath(doc, path) {
339
367
  case "odb": return {
340
368
  format: "odb",
341
369
  tables: doc.tables,
370
+ forms: doc.forms,
371
+ reports: doc.reports,
342
372
  path
343
373
  };
344
374
  case "markdown": return {
@@ -351,6 +381,12 @@ function documentWithPath(doc, path) {
351
381
  layout: doc.layout,
352
382
  path
353
383
  };
384
+ case "xlsx": return {
385
+ format: "xlsx",
386
+ layout: doc.layout,
387
+ bytes: doc.bytes,
388
+ path
389
+ };
354
390
  }
355
391
  }
356
392
  function reopenEditable(doc, bytes) {
@@ -559,7 +595,7 @@ function appReducer(state, action) {
559
595
  selection: {},
560
596
  errorDetail: void 0,
561
597
  stack: [rootScreenForFormat(action.doc.format)]
562
- }, "info", `Opened ${action.path}`);
598
+ }, "info", action.doc.format === "xlsx" ? `Opened ${action.path} as a read-only PDF preview -- press ':' then 'export pdf' to save it as a real PDF` : `Opened ${action.path}`);
563
599
  case "OPEN_FILE_ERROR": return withStatus({
564
600
  ...state,
565
601
  errorDetail: {
@@ -689,6 +725,21 @@ function appReducer(state, action) {
689
725
  doc.editor.addSlide();
690
726
  });
691
727
  }
728
+ case "ADD_SLIDE_TABLE": {
729
+ const doc = presentationDocument(state);
730
+ if (doc === void 0) return wrongDocument(state, "a pptx or odp document");
731
+ const slide = doc.editor.slides()[action.slideIndex];
732
+ if (slide === void 0) return withStatus(state, "warning", `There is no slide at index ${action.slideIndex}`);
733
+ return mutate(state, doc, () => {
734
+ slide.addTable({
735
+ frame: action.frame,
736
+ table: {
737
+ rows: action.rows,
738
+ columns: action.columns
739
+ }
740
+ });
741
+ });
742
+ }
692
743
  case "ADD_TEXTBOX": {
693
744
  const doc = shapeHostDocument(state);
694
745
  if (doc === void 0) return wrongDocument(state, "a pptx, odp or odg document");
@@ -850,7 +901,7 @@ function appReducer(state, action) {
850
901
  case "UNDO": {
851
902
  const doc = state.openDocument;
852
903
  if (doc === void 0) return withStatus(state, "info", "There is nothing to undo");
853
- if (doc.format === "odb" || doc.format === "pdf") return withStatus(state, "warning", `A ${doc.format} document is read-only, so it has no history to undo`);
904
+ 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`);
854
905
  const snapshot = state.undoStack.at(-1);
855
906
  if (snapshot === void 0) return withStatus(state, "info", "There is nothing to undo");
856
907
  const restored = doc.format === "markdown" ? {
@@ -2349,11 +2400,249 @@ function requireOdbDocument(openDocument) {
2349
2400
  return openDocument;
2350
2401
  }
2351
2402
  //#endregion
2403
+ //#region src/tui/screens/editors/odb/form-detail.tsx
2404
+ const FORM_DETAIL_RESERVED_ROWS = 5;
2405
+ function requireForm(forms, formName) {
2406
+ const form = forms.find((candidate) => candidate.name === formName);
2407
+ if (form === void 0) throw new Error(`odbFormDetail was pushed for form "${formName}", but the open .odb document has no form by that name.`);
2408
+ return form;
2409
+ }
2410
+ function OdbFormDetailScreen() {
2411
+ const state = useAppState();
2412
+ const dispatch = useAppDispatch();
2413
+ const doc = requireOdbDocument(state.openDocument);
2414
+ const screen = currentScreen(state);
2415
+ if (screen.kind !== "odbFormDetail") throw new Error(`OdbFormDetailScreen rendered while the current screen is "${screen.kind}", not "odbFormDetail".`);
2416
+ const form = requireForm(doc.forms, screen.formName);
2417
+ const allLines = formatOdbFormLines(form);
2418
+ const query = state.searchQuery.trim().toLowerCase();
2419
+ const lines = query === "" ? allLines : allLines.filter((line) => line.toLowerCase().includes(query));
2420
+ const { selectedIndex } = useNavigationInput({
2421
+ itemCount: lines.length,
2422
+ onSelect: () => {},
2423
+ onBack: () => {
2424
+ dispatch({ type: "POP_SCREEN" });
2425
+ },
2426
+ isActive: !anyOverlayOpen(state)
2427
+ });
2428
+ return /* @__PURE__ */ jsxs(Box, {
2429
+ flexDirection: "column",
2430
+ children: [
2431
+ /* @__PURE__ */ jsxs(Text, {
2432
+ bold: true,
2433
+ children: [
2434
+ form.name,
2435
+ " (",
2436
+ lines.length,
2437
+ " of ",
2438
+ allLines.length,
2439
+ " lines)"
2440
+ ]
2441
+ }),
2442
+ /* @__PURE__ */ jsx(Text, {
2443
+ dimColor: true,
2444
+ children: form.href
2445
+ }),
2446
+ /* @__PURE__ */ jsx(ListView, {
2447
+ items: lines,
2448
+ selectedIndex,
2449
+ reservedRows: FORM_DETAIL_RESERVED_ROWS,
2450
+ emptyMessage: query === "" ? "This form declares no structure." : `No lines match "${state.searchQuery}".`,
2451
+ renderItem: (line, isSelected) => /* @__PURE__ */ jsx(Text, {
2452
+ color: isSelected ? "cyan" : void 0,
2453
+ inverse: isSelected,
2454
+ children: line
2455
+ })
2456
+ }),
2457
+ /* @__PURE__ */ jsx(Text, {
2458
+ dimColor: true,
2459
+ children: "Esc to go back to the form list"
2460
+ })
2461
+ ]
2462
+ });
2463
+ }
2464
+ //#endregion
2465
+ //#region src/tui/screens/editors/odb/form-list.tsx
2466
+ function OdbFormListScreen() {
2467
+ const state = useAppState();
2468
+ const dispatch = useAppDispatch();
2469
+ const doc = requireOdbDocument(state.openDocument);
2470
+ const query = state.searchQuery.trim().toLowerCase();
2471
+ const forms = query === "" ? doc.forms : doc.forms.filter((form) => form.name.toLowerCase().includes(query));
2472
+ const { selectedIndex } = useNavigationInput({
2473
+ itemCount: forms.length,
2474
+ onSelect: (index) => {
2475
+ const form = forms[index];
2476
+ if (form === void 0) return;
2477
+ dispatch({
2478
+ type: "PUSH_SCREEN",
2479
+ screen: {
2480
+ kind: "odbFormDetail",
2481
+ formName: form.name
2482
+ }
2483
+ });
2484
+ },
2485
+ onBack: () => {
2486
+ dispatch({ type: "POP_SCREEN" });
2487
+ },
2488
+ isActive: !anyOverlayOpen(state)
2489
+ });
2490
+ return /* @__PURE__ */ jsxs(Box, {
2491
+ flexDirection: "column",
2492
+ children: [
2493
+ /* @__PURE__ */ jsxs(Text, {
2494
+ bold: true,
2495
+ children: [
2496
+ "Forms (",
2497
+ forms.length,
2498
+ " of ",
2499
+ doc.forms.length,
2500
+ ")"
2501
+ ]
2502
+ }),
2503
+ /* @__PURE__ */ jsx(ListView, {
2504
+ items: forms,
2505
+ selectedIndex,
2506
+ emptyMessage: query === "" ? "This database declares no forms." : `No forms match "${state.searchQuery}".`,
2507
+ renderItem: (form, isSelected) => /* @__PURE__ */ jsx(Text, {
2508
+ color: isSelected ? "cyan" : void 0,
2509
+ inverse: isSelected,
2510
+ children: describeOdbForm(form)
2511
+ })
2512
+ }),
2513
+ /* @__PURE__ */ jsx(Text, {
2514
+ dimColor: true,
2515
+ children: "Enter to open a form, Esc to go back to the tables"
2516
+ })
2517
+ ]
2518
+ });
2519
+ }
2520
+ //#endregion
2521
+ //#region src/tui/screens/editors/odb/report-detail.tsx
2522
+ const REPORT_DETAIL_RESERVED_ROWS = 5;
2523
+ function requireReport(reports, reportName) {
2524
+ const report = reports.find((candidate) => candidate.name === reportName);
2525
+ if (report === void 0) throw new Error(`odbReportDetail was pushed for report "${reportName}", but the open .odb document has no report by that name.`);
2526
+ return report;
2527
+ }
2528
+ function OdbReportDetailScreen() {
2529
+ const state = useAppState();
2530
+ const dispatch = useAppDispatch();
2531
+ const doc = requireOdbDocument(state.openDocument);
2532
+ const screen = currentScreen(state);
2533
+ if (screen.kind !== "odbReportDetail") throw new Error(`OdbReportDetailScreen rendered while the current screen is "${screen.kind}", not "odbReportDetail".`);
2534
+ const report = requireReport(doc.reports, screen.reportName);
2535
+ const allLines = formatOdbReportLines(report);
2536
+ const query = state.searchQuery.trim().toLowerCase();
2537
+ const lines = query === "" ? allLines : allLines.filter((line) => line.toLowerCase().includes(query));
2538
+ const { selectedIndex } = useNavigationInput({
2539
+ itemCount: lines.length,
2540
+ onSelect: () => {},
2541
+ onBack: () => {
2542
+ dispatch({ type: "POP_SCREEN" });
2543
+ },
2544
+ isActive: !anyOverlayOpen(state)
2545
+ });
2546
+ return /* @__PURE__ */ jsxs(Box, {
2547
+ flexDirection: "column",
2548
+ children: [
2549
+ /* @__PURE__ */ jsxs(Text, {
2550
+ bold: true,
2551
+ children: [
2552
+ report.name,
2553
+ " (",
2554
+ lines.length,
2555
+ " of ",
2556
+ allLines.length,
2557
+ " lines)"
2558
+ ]
2559
+ }),
2560
+ /* @__PURE__ */ jsx(Text, {
2561
+ dimColor: true,
2562
+ children: report.href
2563
+ }),
2564
+ /* @__PURE__ */ jsx(ListView, {
2565
+ items: lines,
2566
+ selectedIndex,
2567
+ reservedRows: REPORT_DETAIL_RESERVED_ROWS,
2568
+ emptyMessage: query === "" ? "This report declares no structure." : `No lines match "${state.searchQuery}".`,
2569
+ renderItem: (line, isSelected) => /* @__PURE__ */ jsx(Text, {
2570
+ color: isSelected ? "cyan" : void 0,
2571
+ inverse: isSelected,
2572
+ children: line
2573
+ })
2574
+ }),
2575
+ /* @__PURE__ */ jsx(Text, {
2576
+ dimColor: true,
2577
+ children: "Esc to go back to the report list"
2578
+ })
2579
+ ]
2580
+ });
2581
+ }
2582
+ //#endregion
2583
+ //#region src/tui/screens/editors/odb/report-list.tsx
2584
+ function OdbReportListScreen() {
2585
+ const state = useAppState();
2586
+ const dispatch = useAppDispatch();
2587
+ const doc = requireOdbDocument(state.openDocument);
2588
+ const query = state.searchQuery.trim().toLowerCase();
2589
+ const reports = query === "" ? doc.reports : doc.reports.filter((report) => report.name.toLowerCase().includes(query));
2590
+ const { selectedIndex } = useNavigationInput({
2591
+ itemCount: reports.length,
2592
+ onSelect: (index) => {
2593
+ const report = reports[index];
2594
+ if (report === void 0) return;
2595
+ dispatch({
2596
+ type: "PUSH_SCREEN",
2597
+ screen: {
2598
+ kind: "odbReportDetail",
2599
+ reportName: report.name
2600
+ }
2601
+ });
2602
+ },
2603
+ onBack: () => {
2604
+ dispatch({ type: "POP_SCREEN" });
2605
+ },
2606
+ isActive: !anyOverlayOpen(state)
2607
+ });
2608
+ return /* @__PURE__ */ jsxs(Box, {
2609
+ flexDirection: "column",
2610
+ children: [
2611
+ /* @__PURE__ */ jsxs(Text, {
2612
+ bold: true,
2613
+ children: [
2614
+ "Reports (",
2615
+ reports.length,
2616
+ " of ",
2617
+ doc.reports.length,
2618
+ ")"
2619
+ ]
2620
+ }),
2621
+ /* @__PURE__ */ jsx(ListView, {
2622
+ items: reports,
2623
+ selectedIndex,
2624
+ emptyMessage: query === "" ? "This database declares no reports." : `No reports match "${state.searchQuery}".`,
2625
+ renderItem: (report, isSelected) => /* @__PURE__ */ jsx(Text, {
2626
+ color: isSelected ? "cyan" : void 0,
2627
+ inverse: isSelected,
2628
+ children: describeOdbReport(report)
2629
+ })
2630
+ }),
2631
+ /* @__PURE__ */ jsx(Text, {
2632
+ dimColor: true,
2633
+ children: "Enter to open a report, Esc to go back to the tables"
2634
+ })
2635
+ ]
2636
+ });
2637
+ }
2638
+ //#endregion
2352
2639
  //#region src/tui/screens/editors/odb/table-list.tsx
2640
+ const TABLE_LIST_RESERVED_ROWS = 5;
2353
2641
  function OdbTableListScreen() {
2354
2642
  const state = useAppState();
2355
2643
  const dispatch = useAppDispatch();
2356
2644
  const doc = requireOdbDocument(state.openDocument);
2645
+ const overlayOpen = anyOverlayOpen(state);
2357
2646
  const query = state.searchQuery.trim().toLowerCase();
2358
2647
  const tables = query === "" ? doc.tables : doc.tables.filter((table) => table.tableName.toLowerCase().includes(query));
2359
2648
  const { selectedIndex } = useNavigationInput({
@@ -2372,36 +2661,63 @@ function OdbTableListScreen() {
2372
2661
  onBack: () => {
2373
2662
  dispatch({ type: "POP_SCREEN" });
2374
2663
  },
2375
- isActive: !anyOverlayOpen(state)
2664
+ isActive: !overlayOpen
2376
2665
  });
2666
+ useInput((input) => {
2667
+ if (input === "f") {
2668
+ dispatch({
2669
+ type: "PUSH_SCREEN",
2670
+ screen: { kind: "odbFormList" }
2671
+ });
2672
+ return;
2673
+ }
2674
+ if (input === "r") dispatch({
2675
+ type: "PUSH_SCREEN",
2676
+ screen: { kind: "odbReportList" }
2677
+ });
2678
+ }, { isActive: !overlayOpen });
2377
2679
  return /* @__PURE__ */ jsxs(Box, {
2378
2680
  flexDirection: "column",
2379
- children: [/* @__PURE__ */ jsxs(Text, {
2380
- bold: true,
2381
- children: [
2382
- "Tables (",
2383
- tables.length,
2384
- " of ",
2385
- doc.tables.length,
2386
- ")"
2387
- ]
2388
- }), /* @__PURE__ */ jsx(ListView, {
2389
- items: tables,
2390
- selectedIndex,
2391
- emptyMessage: query === "" ? "This database has no tables." : `No tables match "${state.searchQuery}".`,
2392
- renderItem: (table, isSelected) => /* @__PURE__ */ jsxs(Text, {
2393
- color: isSelected ? "cyan" : void 0,
2394
- inverse: isSelected,
2681
+ children: [
2682
+ /* @__PURE__ */ jsxs(Text, {
2683
+ bold: true,
2395
2684
  children: [
2396
- table.tableName,
2397
- " (",
2398
- table.columns.length,
2399
- " columns, ",
2400
- table.rows.length,
2401
- " rows)"
2685
+ "Tables (",
2686
+ tables.length,
2687
+ " of ",
2688
+ doc.tables.length,
2689
+ ")"
2690
+ ]
2691
+ }),
2692
+ /* @__PURE__ */ jsx(ListView, {
2693
+ items: tables,
2694
+ selectedIndex,
2695
+ reservedRows: TABLE_LIST_RESERVED_ROWS,
2696
+ emptyMessage: query === "" ? "This database has no tables." : `No tables match "${state.searchQuery}".`,
2697
+ renderItem: (table, isSelected) => /* @__PURE__ */ jsxs(Text, {
2698
+ color: isSelected ? "cyan" : void 0,
2699
+ inverse: isSelected,
2700
+ children: [
2701
+ table.tableName,
2702
+ " (",
2703
+ table.columns.length,
2704
+ " columns, ",
2705
+ table.rows.length,
2706
+ " rows)"
2707
+ ]
2708
+ })
2709
+ }),
2710
+ /* @__PURE__ */ jsxs(Text, {
2711
+ dimColor: true,
2712
+ children: [
2713
+ "Enter to open a table, f for forms (",
2714
+ doc.forms.length,
2715
+ "), r for reports (",
2716
+ doc.reports.length,
2717
+ ")"
2402
2718
  ]
2403
2719
  })
2404
- })]
2720
+ ]
2405
2721
  });
2406
2722
  }
2407
2723
  //#endregion
@@ -3720,6 +4036,12 @@ const IMAGE_EXTENSION_TO_FORMAT = {
3720
4036
  jpg: "jpeg",
3721
4037
  jpeg: "jpeg"
3722
4038
  };
4039
+ const DEFAULT_TABLE_ROWS = 2;
4040
+ const DEFAULT_TABLE_COLUMNS = 2;
4041
+ function parsePositiveIntField(raw, fallback) {
4042
+ const parsed = Number.parseInt(raw, 10);
4043
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
4044
+ }
3723
4045
  function imageFormatFromPath(path) {
3724
4046
  const dotIndex = path.lastIndexOf(".");
3725
4047
  if (dotIndex < 0) return;
@@ -3748,6 +4070,7 @@ function SlideDetailScreen(props) {
3748
4070
  const [addMode, setAddMode] = useState("closed");
3749
4071
  const [draft, setDraft] = useState("");
3750
4072
  const [imageError, setImageError] = useState(void 0);
4073
+ const [tableRows, setTableRows] = useState(DEFAULT_TABLE_ROWS);
3751
4074
  const formIsOpen = addMode !== "closed";
3752
4075
  const { selectedIndex } = useNavigationInput({
3753
4076
  itemCount: rows.length,
@@ -3788,10 +4111,15 @@ function SlideDetailScreen(props) {
3788
4111
  setDraft("");
3789
4112
  setImageError(void 0);
3790
4113
  setAddMode("image");
4114
+ return;
4115
+ }
4116
+ if (input === "b") {
4117
+ setDraft(String(DEFAULT_TABLE_ROWS));
4118
+ setAddMode("tableRows");
3791
4119
  }
3792
4120
  }, { isActive: !overlayOpen && addMode === "chooseKind" });
3793
4121
  useInput((input) => {
3794
- if (input === "n" && doc.format === "odp") dispatch({
4122
+ if (input === "n") dispatch({
3795
4123
  type: "PUSH_SCREEN",
3796
4124
  screen: {
3797
4125
  kind: "notesEditor",
@@ -3826,6 +4154,22 @@ function SlideDetailScreen(props) {
3826
4154
  }
3827
4155
  })();
3828
4156
  };
4157
+ const commitTableRows = (raw) => {
4158
+ setTableRows(parsePositiveIntField(raw, DEFAULT_TABLE_ROWS));
4159
+ setDraft(String(DEFAULT_TABLE_COLUMNS));
4160
+ setAddMode("tableColumns");
4161
+ };
4162
+ const commitTableColumns = (raw) => {
4163
+ const columns = parsePositiveIntField(raw, DEFAULT_TABLE_COLUMNS);
4164
+ dispatch({
4165
+ type: "ADD_SLIDE_TABLE",
4166
+ slideIndex,
4167
+ frame: defaultShapeFrame(doc.editor.slideSize),
4168
+ rows: tableRows,
4169
+ columns
4170
+ });
4171
+ setAddMode("closed");
4172
+ };
3829
4173
  return /* @__PURE__ */ jsxs(Box, {
3830
4174
  flexDirection: "column",
3831
4175
  children: [
@@ -3862,7 +4206,7 @@ function SlideDetailScreen(props) {
3862
4206
  }),
3863
4207
  addMode === "chooseKind" ? /* @__PURE__ */ jsx(Text, {
3864
4208
  color: "cyan",
3865
- children: "Add shape: t textbox, i image, Esc cancel"
4209
+ children: "Add shape: t textbox, i image, b table, Esc cancel"
3866
4210
  }) : void 0,
3867
4211
  addMode === "textbox" ? /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
3868
4212
  color: "cyan",
@@ -3894,13 +4238,33 @@ function SlideDetailScreen(props) {
3894
4238
  children: imageError
3895
4239
  })]
3896
4240
  }) : void 0,
3897
- /* @__PURE__ */ jsxs(Text, {
4241
+ addMode === "tableRows" ? /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
4242
+ color: "cyan",
4243
+ children: "Rows: "
4244
+ }), /* @__PURE__ */ jsx(TextField, {
4245
+ value: draft,
4246
+ isFocused: true,
4247
+ onChange: setDraft,
4248
+ onSubmit: commitTableRows,
4249
+ onCancel: () => {
4250
+ setAddMode("closed");
4251
+ }
4252
+ })] }) : void 0,
4253
+ addMode === "tableColumns" ? /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
4254
+ color: "cyan",
4255
+ children: "Columns: "
4256
+ }), /* @__PURE__ */ jsx(TextField, {
4257
+ value: draft,
4258
+ isFocused: true,
4259
+ onChange: setDraft,
4260
+ onSubmit: commitTableColumns,
4261
+ onCancel: () => {
4262
+ setAddMode("closed");
4263
+ }
4264
+ })] }) : void 0,
4265
+ /* @__PURE__ */ jsx(Text, {
3898
4266
  dimColor: true,
3899
- children: [
3900
- "Enter: edit shape a: add shape",
3901
- doc.format === "odp" ? " n: notes" : "",
3902
- " Esc: back"
3903
- ]
4267
+ children: "Enter: edit shape a: add shape n: notes Esc: back"
3904
4268
  })
3905
4269
  ]
3906
4270
  });
@@ -3914,15 +4278,11 @@ function PptxSlideListScreen() {
3914
4278
  }
3915
4279
  //#endregion
3916
4280
  //#region src/tui/screens/editors/odp/notes-editor.tsx
3917
- function assertOdpDocument(doc) {
3918
- if (doc?.format !== "odp") throw new Error("NotesEditorScreen rendered without an open odp document; check the screen router in app.tsx.");
3919
- return doc;
3920
- }
3921
4281
  function NotesEditorScreen(props) {
3922
4282
  const state = useAppState();
3923
4283
  const dispatch = useAppDispatch();
3924
4284
  const overlayOpen = anyOverlayOpen(state);
3925
- const doc = assertOdpDocument(state.openDocument);
4285
+ const doc = assertPresentationDocument(state.openDocument);
3926
4286
  const { slideIndex } = props.screen;
3927
4287
  const slide = doc.editor.slides()[slideIndex];
3928
4288
  const [draft, setDraft] = useState(() => slide?.notes ?? "");
@@ -4027,6 +4387,7 @@ const KIND_BADGE = {
4027
4387
  boolean: "B",
4028
4388
  date: "D",
4029
4389
  time: "T",
4390
+ dateTime: "@",
4030
4391
  error: "E",
4031
4392
  empty: "."
4032
4393
  };
@@ -4038,6 +4399,7 @@ const CELL_VALUE_KINDS = [
4038
4399
  "boolean",
4039
4400
  "date",
4040
4401
  "time",
4402
+ "dateTime",
4041
4403
  "error"
4042
4404
  ];
4043
4405
  function rawEditableText(value) {
@@ -4046,6 +4408,7 @@ function rawEditableText(value) {
4046
4408
  case "string":
4047
4409
  case "date":
4048
4410
  case "time":
4411
+ case "dateTime":
4049
4412
  case "error": return value.value;
4050
4413
  case "boolean": return value.value ? "TRUE" : "FALSE";
4051
4414
  case "number":
@@ -4116,6 +4479,13 @@ function buildCellValue(kind, text) {
4116
4479
  value: trimmed
4117
4480
  };
4118
4481
  }
4482
+ case "dateTime": {
4483
+ const trimmed = text.trim();
4484
+ return trimmed.length === 0 ? void 0 : {
4485
+ kind: "dateTime",
4486
+ value: trimmed
4487
+ };
4488
+ }
4119
4489
  case "error": {
4120
4490
  const trimmed = text.trim();
4121
4491
  return trimmed.length === 0 ? void 0 : {
@@ -4863,7 +5233,7 @@ function OdtBodyListScreen() {
4863
5233
  //#endregion
4864
5234
  //#region src/tui/screens/editors/pdf/shared.ts
4865
5235
  function requirePdfDocument(openDocument) {
4866
- if (openDocument?.format !== "pdf") throw new Error("A PDF inspection screen rendered without an open PDF document; the app router only reaches this screen group from pdfPageList, which is only ever the root screen of an open PDF document.");
5236
+ 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.");
4867
5237
  return openDocument;
4868
5238
  }
4869
5239
  function formatSize(widthPt, heightPt) {
@@ -5162,12 +5532,17 @@ function PdfPageItemsScreen() {
5162
5532
  function defaultExportDestinationFor(document, cwd) {
5163
5533
  return document.path === void 0 ? join(cwd, "untitled.pdf") : defaultPdfPathFor(document.path);
5164
5534
  }
5535
+ function parseFontFileField(value) {
5536
+ return value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
5537
+ }
5165
5538
  function ExportOptionsScreen() {
5166
5539
  const state = useAppState();
5167
5540
  const dispatch = useAppDispatch();
5168
5541
  const isActive = !anyOverlayOpen(state);
5169
5542
  const document = state.openDocument;
5170
5543
  const [destination, setDestination] = useState(() => document === void 0 ? "" : defaultExportDestinationFor(document, state.cwd));
5544
+ const [fontFiles, setFontFiles] = useState("");
5545
+ const [field, setField] = useState("destination");
5171
5546
  if (document === void 0) return /* @__PURE__ */ jsx(Box, {
5172
5547
  flexDirection: "column",
5173
5548
  children: /* @__PURE__ */ jsx(Text, {
@@ -5175,21 +5550,27 @@ function ExportOptionsScreen() {
5175
5550
  children: "There is no open document to export."
5176
5551
  })
5177
5552
  });
5178
- const submit = (value) => {
5553
+ const cancel = () => {
5554
+ dispatch({ type: "POP_SCREEN" });
5555
+ };
5556
+ const submit = () => {
5179
5557
  (async () => {
5180
5558
  let diagnosticCount = 0;
5181
5559
  try {
5182
- await exportToPdf(document, value, { onDiagnostic: (diagnostic) => {
5183
- diagnosticCount += 1;
5184
- dispatch({
5185
- type: "APPEND_DIAGNOSTIC",
5186
- diagnostic
5187
- });
5188
- } });
5560
+ await exportToPdf(document, destination, {
5561
+ fontFiles: parseFontFileField(fontFiles),
5562
+ onDiagnostic: (diagnostic) => {
5563
+ diagnosticCount += 1;
5564
+ dispatch({
5565
+ type: "APPEND_DIAGNOSTIC",
5566
+ diagnostic
5567
+ });
5568
+ }
5569
+ });
5189
5570
  dispatch({
5190
5571
  type: "SET_STATUS",
5191
5572
  severity: "info",
5192
- text: `Exported ${value}`
5573
+ text: `Exported ${destination}`
5193
5574
  });
5194
5575
  if (diagnosticCount > 0) dispatch({
5195
5576
  type: "OPEN_OVERLAY",
@@ -5199,7 +5580,7 @@ function ExportOptionsScreen() {
5199
5580
  } catch (error) {
5200
5581
  dispatch({
5201
5582
  type: "OPEN_FILE_ERROR",
5202
- message: `Could not export to ${value}`,
5583
+ message: `Could not export to ${destination}`,
5203
5584
  detail: describeError(error)
5204
5585
  });
5205
5586
  }
@@ -5217,17 +5598,28 @@ function ExportOptionsScreen() {
5217
5598
  children: "Path: "
5218
5599
  }), /* @__PURE__ */ jsx(TextField, {
5219
5600
  value: destination,
5220
- isFocused: isActive,
5601
+ isFocused: isActive && field === "destination",
5221
5602
  placeholder: "destination path",
5222
5603
  onChange: setDestination,
5604
+ onSubmit: () => {
5605
+ setField("fonts");
5606
+ },
5607
+ onCancel: cancel
5608
+ })] }),
5609
+ /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
5610
+ color: "cyan",
5611
+ children: "Fonts: "
5612
+ }), /* @__PURE__ */ jsx(TextField, {
5613
+ value: fontFiles,
5614
+ isFocused: isActive && field === "fonts",
5615
+ placeholder: "optional .ttf/.otf paths, comma-separated",
5616
+ onChange: setFontFiles,
5223
5617
  onSubmit: submit,
5224
- onCancel: () => {
5225
- dispatch({ type: "POP_SCREEN" });
5226
- }
5618
+ onCancel: cancel
5227
5619
  })] }),
5228
5620
  /* @__PURE__ */ jsx(Text, {
5229
5621
  dimColor: true,
5230
- children: "Enter to export, Esc to cancel"
5622
+ children: field === "destination" ? "Enter for fonts, Esc to cancel" : "Enter to export, Esc to cancel. Each font's family is read from the file itself"
5231
5623
  })
5232
5624
  ]
5233
5625
  });
@@ -5479,7 +5871,7 @@ function LauncherScreen() {
5479
5871
  }),
5480
5872
  /* @__PURE__ */ jsx(Text, {
5481
5873
  dimColor: true,
5482
- children: "A terminal editor for docx, pptx, odt, odp, ods, odg, markdown, odb and pdf."
5874
+ children: "A terminal editor for docx, pptx, odt, odp, ods, odg, markdown, odb and pdf -- xlsx opens as a read-only PDF preview."
5483
5875
  }),
5484
5876
  /* @__PURE__ */ jsx(Text, { children: " " }),
5485
5877
  /* @__PURE__ */ jsxs(Text, { children: [/* @__PURE__ */ jsx(Text, {
@@ -5665,6 +6057,10 @@ function ScreenBody({ screen }) {
5665
6057
  case "shapeOrVectorDetail": return /* @__PURE__ */ jsx(OdgShapeOrVectorDetailScreen, {});
5666
6058
  case "odbTableList": return /* @__PURE__ */ jsx(OdbTableListScreen, {});
5667
6059
  case "odbTableRows": return /* @__PURE__ */ jsx(OdbTableRowsScreen, {});
6060
+ case "odbFormList": return /* @__PURE__ */ jsx(OdbFormListScreen, {});
6061
+ case "odbFormDetail": return /* @__PURE__ */ jsx(OdbFormDetailScreen, {});
6062
+ case "odbReportList": return /* @__PURE__ */ jsx(OdbReportListScreen, {});
6063
+ case "odbReportDetail": return /* @__PURE__ */ jsx(OdbReportDetailScreen, {});
5668
6064
  case "markdownLineList": return /* @__PURE__ */ jsx(MarkdownLineListScreen, {});
5669
6065
  case "markdownLineEditor": return /* @__PURE__ */ jsx(MarkdownLineEditorScreen, {});
5670
6066
  case "pdfPageList": return /* @__PURE__ */ jsx(PdfPageListScreen, {});