@agent-native/core 0.157.7 → 0.157.9

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.
Files changed (69) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/actions/purge-first-party-analytics-postgres.ts +26 -8
  3. package/corpus/templates/analytics/scripts/first-party-analytics-postgres-retention-dry-run.sql +258 -0
  4. package/corpus/templates/analytics/server/lib/first-party-analytics-purge.ts +59 -27
  5. package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +4 -1
  6. package/corpus/templates/slides/actions/export-pptx.ts +261 -71
  7. package/corpus/templates/slides/actions/import-file.ts +12 -0
  8. package/corpus/templates/slides/actions/import-pptx.ts +9 -0
  9. package/corpus/templates/slides/app/lib/export-pptx-client.ts +45 -5
  10. package/corpus/templates/slides/server/handlers/import/html-converter.ts +69 -1
  11. package/corpus/templates/slides/server/handlers/import/pdf-fidelity-parser.ts +316 -64
  12. package/corpus/templates/slides/server/lib/source-import.ts +10 -1
  13. package/dist/agent/harness/migrations.d.ts +4 -0
  14. package/dist/agent/harness/migrations.js +39 -0
  15. package/dist/agent/production-agent.d.ts +2 -0
  16. package/dist/agent/production-agent.js +4 -0
  17. package/dist/agent/run-loop-with-resume.d.ts +1 -0
  18. package/dist/agent/run-loop-with-resume.js +45 -30
  19. package/dist/agent/run-migrations.d.ts +8 -0
  20. package/dist/agent/run-migrations.js +61 -0
  21. package/dist/chat-threads/migrations.d.ts +1 -0
  22. package/dist/chat-threads/migrations.js +1 -0
  23. package/dist/chat-threads/schema-migrations.d.ts +8 -0
  24. package/dist/chat-threads/schema-migrations.js +60 -0
  25. package/dist/client/AssistantChat.js +4 -0
  26. package/dist/client/chat/message-components.d.ts +2 -0
  27. package/dist/client/chat/message-components.js +11 -1
  28. package/dist/client/observability/ThumbsFeedback.js +35 -22
  29. package/dist/client/observability/useObservability.d.ts +1 -1
  30. package/dist/client/observability/useObservability.js +6 -3
  31. package/dist/collab/awareness.d.ts +2 -2
  32. package/dist/collab/struct-routes.d.ts +1 -1
  33. package/dist/feature-flags/a2a-action-route.js +4 -2
  34. package/dist/feature-flags/permissions.d.ts +2 -0
  35. package/dist/feature-flags/permissions.js +6 -1
  36. package/dist/ingestion/pptx.d.ts +14 -1
  37. package/dist/ingestion/pptx.js +222 -41
  38. package/dist/localization/core-messages/ar-SA.js +4 -0
  39. package/dist/localization/core-messages/de-DE.js +4 -0
  40. package/dist/localization/core-messages/en-US.d.ts +4 -0
  41. package/dist/localization/core-messages/en-US.js +4 -0
  42. package/dist/localization/core-messages/es-ES.js +4 -0
  43. package/dist/localization/core-messages/fr-FR.js +4 -0
  44. package/dist/localization/core-messages/hi-IN.js +4 -0
  45. package/dist/localization/core-messages/ja-JP.js +4 -0
  46. package/dist/localization/core-messages/ko-KR.js +4 -0
  47. package/dist/localization/core-messages/pt-BR.js +4 -0
  48. package/dist/localization/core-messages/zh-CN.js +4 -0
  49. package/dist/localization/core-messages/zh-TW.js +4 -0
  50. package/dist/localization/core-messages.d.ts +4 -0
  51. package/dist/notifications/routes.d.ts +1 -1
  52. package/dist/observability/feedback.js +40 -16
  53. package/dist/observability/routes.d.ts +1 -1
  54. package/dist/observability/routes.js +15 -3
  55. package/dist/observability/store.js +2 -0
  56. package/dist/observability/traces.js +5 -1
  57. package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
  58. package/dist/provider-api/actions/provider-api.d.ts +10 -10
  59. package/dist/resources/handlers.d.ts +1 -1
  60. package/dist/server/action-routes.d.ts +7 -5
  61. package/dist/server/action-routes.js +9 -4
  62. package/dist/server/better-auth-instance.js +4 -0
  63. package/dist/server/poll.d.ts +4 -4
  64. package/dist/server/poll.js +45 -13
  65. package/dist/server/realtime-voice.js +2 -1
  66. package/dist/server/release-migrations.js +16 -0
  67. package/dist/usage/migrations.d.ts +4 -0
  68. package/dist/usage/migrations.js +45 -0
  69. package/package.json +3 -3
@@ -5,6 +5,7 @@ import { defineAction } from "@agent-native/core";
5
5
  import { ssrfSafeFetch } from "@agent-native/core/extensions/url-safety";
6
6
  import { getRequestUserEmail } from "@agent-native/core/server/request-context";
7
7
  import { resolveAccess } from "@agent-native/core/sharing";
8
+ import type PptxGenJS from "pptxgenjs";
8
9
  import { z } from "zod";
9
10
 
10
11
  import "../server/db/index.js"; // ensure registerShareableResource runs
@@ -19,6 +20,9 @@ import {
19
20
  ASPECT_RATIO_VALUES,
20
21
  } from "../shared/aspect-ratios.js";
21
22
 
23
+ type TableCell = PptxGenJS.TableCell;
24
+ type TableRow = PptxGenJS.TableRow;
25
+
22
26
  /**
23
27
  * Extract inline style value for a given property from a style string.
24
28
  */
@@ -29,42 +33,76 @@ function getStyle(style: string, prop: string): string | null {
29
33
  }
30
34
 
31
35
  /**
32
- * Convert a CSS color string to a 6-char hex string (no #).
36
+ * Convert a CSS color string to a 6-char hex string (no #) plus an optional
37
+ * pptxgenjs transparency (0-100, percent transparent) carried from an
38
+ * alpha-bearing CSS color.
33
39
  * Handles #hex, #shortHex, rgb(), rgba(), and named colors.
34
40
  */
35
- function colorToHex(color: string): string {
36
- if (!color) return "FFFFFF";
41
+ function colorToHex(color: string): { hex: string; transparency?: number } {
42
+ if (!color) return { hex: "FFFFFF" };
37
43
 
38
44
  // Strip quotes / trim
39
45
  color = color.replace(/['"]/g, "").trim();
40
46
 
41
47
  // Already hex
42
- if (/^#[0-9a-f]{6}$/i.test(color)) return color.slice(1).toUpperCase();
48
+ if (/^#[0-9a-f]{8}$/i.test(color)) {
49
+ const alpha = parseInt(color.slice(7, 9), 16) / 255;
50
+ return {
51
+ hex: color.slice(1, 7).toUpperCase(),
52
+ transparency: Math.round((1 - alpha) * 100),
53
+ };
54
+ }
55
+ if (/^#[0-9a-f]{6}$/i.test(color))
56
+ return { hex: color.slice(1).toUpperCase() };
43
57
  if (/^#[0-9a-f]{3}$/i.test(color)) {
44
58
  const r = color[1],
45
59
  g = color[2],
46
60
  b = color[3];
47
- return `${r}${r}${g}${g}${b}${b}`.toUpperCase();
61
+ return { hex: `${r}${r}${g}${g}${b}${b}`.toUpperCase() };
48
62
  }
49
63
 
50
64
  // rgb / rgba
51
65
  const rgbMatch = color.match(
52
- /rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*[\d.]+)?\s*\)/,
66
+ /rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)/,
53
67
  );
54
68
  if (rgbMatch) {
55
69
  const hex = (n: string) => parseInt(n).toString(16).padStart(2, "0");
56
- return `${hex(rgbMatch[1])}${hex(rgbMatch[2])}${hex(rgbMatch[3])}`.toUpperCase();
70
+ const result = {
71
+ hex: `${hex(rgbMatch[1])}${hex(rgbMatch[2])}${hex(rgbMatch[3])}`.toUpperCase(),
72
+ };
73
+ if (rgbMatch[4] === undefined) return result;
74
+ const alpha = parseFloat(rgbMatch[4]);
75
+ return { ...result, transparency: Math.round((1 - alpha) * 100) };
57
76
  }
58
77
 
59
- // Common named colors used in the slide templates
78
+ // Named colors used in the slide templates, plus the CSS Level 1 keyword set.
60
79
  const named: Record<string, string> = {
61
80
  white: "FFFFFF",
62
81
  black: "000000",
63
82
  transparent: "000000",
83
+ silver: "C0C0C0",
84
+ gray: "808080",
85
+ grey: "808080",
86
+ maroon: "800000",
87
+ red: "FF0000",
88
+ purple: "800080",
89
+ fuchsia: "FF00FF",
90
+ green: "008000",
91
+ lime: "00FF00",
92
+ olive: "808000",
93
+ yellow: "FFFF00",
94
+ navy: "000080",
95
+ blue: "0000FF",
96
+ teal: "008080",
97
+ aqua: "00FFFF",
64
98
  };
65
- if (named[color.toLowerCase()]) return named[color.toLowerCase()];
99
+ const hex = named[color.toLowerCase()];
100
+ if (hex) return { hex };
66
101
 
67
- return "FFFFFF";
102
+ console.warn(
103
+ `[export-pptx] unrecognized color "${color}", defaulting to white`,
104
+ );
105
+ return { hex: "FFFFFF" };
68
106
  }
69
107
 
70
108
  /**
@@ -79,11 +117,16 @@ function pxToIn(
79
117
  }
80
118
 
81
119
  /**
82
- * Convert CSS font-size px to PowerPoint points.
83
- * 1px CSS ≈ 0.75pt.
120
+ * Convert CSS font-size px to PowerPoint points, using this deck's actual
121
+ * px/inch ratio (like `pxToIn` above) instead of assuming 96 CSS px/inch —
122
+ * the ratio varies by aspect ratio (72 for 16:9/9:16, 108 for 1:1/4:5).
84
123
  */
85
- function pxToPt(px: number): number {
86
- return Math.round(px * 0.75);
124
+ function pxToPt(
125
+ px: number,
126
+ dims: { width: number; pptxInches: { w: number } },
127
+ ): number {
128
+ const pxPerInch = dims.width / dims.pptxInches.w;
129
+ return Math.round((px / pxPerInch) * 72);
87
130
  }
88
131
 
89
132
  interface TextElement {
@@ -91,6 +134,7 @@ interface TextElement {
91
134
  fontSize: number; // in pt
92
135
  fontFace: string;
93
136
  color: string; // 6-char hex
137
+ transparency?: number; // 0-100, percent transparent
94
138
  bold: boolean;
95
139
  x: number; // inches
96
140
  y: number; // inches
@@ -109,6 +153,7 @@ interface TextRunElement {
109
153
  fontSize?: number;
110
154
  fontFace?: string;
111
155
  color?: string;
156
+ transparency?: number; // 0-100, percent transparent
112
157
  bold?: boolean;
113
158
  italic?: boolean;
114
159
  underline?: { style: "sng" };
@@ -130,14 +175,26 @@ interface ShapeElement {
130
175
  w: number;
131
176
  h: number;
132
177
  fill?: string;
178
+ fillTransparency?: number; // 0-100, percent transparent
133
179
  lineColor?: string;
180
+ lineTransparency?: number; // 0-100, percent transparent
134
181
  lineWidth?: number;
135
182
  rounded?: boolean;
136
183
  order?: number;
137
184
  }
138
185
 
186
+ interface TableElement {
187
+ rows: TableRow[];
188
+ x: number;
189
+ y: number;
190
+ w: number;
191
+ h: number;
192
+ order?: number;
193
+ }
194
+
139
195
  interface GridElement {
140
196
  color: string;
197
+ transparency?: number; // 0-100, percent transparent
141
198
  stepX: number;
142
199
  stepY: number;
143
200
  offsetX: number;
@@ -165,6 +222,16 @@ export function assertServerPptxExportable(
165
222
  throw error;
166
223
  }
167
224
 
225
+ interface ParsedSlide {
226
+ texts: TextElement[];
227
+ images: ImageElement[];
228
+ shapes: ShapeElement[];
229
+ tables: TableElement[];
230
+ grid?: GridElement;
231
+ bgColor: string;
232
+ bgTransparency?: number; // 0-100, percent transparent
233
+ }
234
+
168
235
  /**
169
236
  * Parse slide HTML and extract text/image elements with positioning.
170
237
  * We know the exact HTML structure from the slide templates.
@@ -173,13 +240,7 @@ export function parseSlideHtml(
173
240
  html: string,
174
241
  aspectRatio?: AspectRatio,
175
242
  slideNumber = 1,
176
- ): {
177
- texts: TextElement[];
178
- images: ImageElement[];
179
- shapes: ShapeElement[];
180
- grid?: GridElement;
181
- bgColor: string;
182
- } {
243
+ ): ParsedSlide {
183
244
  assertServerPptxExportable(html, slideNumber);
184
245
  const dims = getAspectRatioDims(aspectRatio);
185
246
  if (html.includes('data-imported-pdf="true"')) {
@@ -193,6 +254,7 @@ export function parseSlideHtml(
193
254
  const images: ImageElement[] = [];
194
255
  const shapes: ShapeElement[] = [];
195
256
  let bgColor = "000000";
257
+ let bgTransparency: number | undefined;
196
258
 
197
259
  const slideW = dims.pptxInches.w;
198
260
  const slideH = dims.pptxInches.h;
@@ -201,7 +263,11 @@ export function parseSlideHtml(
201
263
  const slideStyleMatch = html.match(/class="fmd-slide"[^>]*style="([^"]*)"/);
202
264
  if (slideStyleMatch) {
203
265
  const bg = getStyle(slideStyleMatch[1], "background(?:-color)?");
204
- if (bg) bgColor = colorToHex(bg);
266
+ if (bg) {
267
+ const parsedBg = colorToHex(bg);
268
+ bgColor = parsedBg.hex;
269
+ bgTransparency = parsedBg.transparency;
270
+ }
205
271
  }
206
272
 
207
273
  // Extract padding from the .fmd-slide wrapper
@@ -351,47 +417,44 @@ export function parseSlideHtml(
351
417
  const mbStr = getStyle(style, "margin-bottom");
352
418
  if (mbStr) marginBottom = parseInt(mbStr) || 0;
353
419
 
420
+ const parsedColor = colorToHex(color);
354
421
  texts.push({
355
422
  text,
356
- fontSize: pxToPt(fontSize),
423
+ fontSize: pxToPt(fontSize, dims),
357
424
  fontFace: "Poppins",
358
- color: colorToHex(color),
425
+ color: parsedColor.hex,
426
+ transparency: parsedColor.transparency,
359
427
  bold,
360
428
  x: xMargin,
361
429
  y: yPos,
362
430
  w: contentW,
363
431
  h: elHeight + 0.2,
364
432
  letterSpacing: letterSpacing ? parseFloat(letterSpacing) : undefined,
365
- lineSpacing: lineH ? Math.round(lineH * pxToPt(fontSize)) : undefined,
433
+ lineSpacing: lineH
434
+ ? Math.round(lineH * pxToPt(fontSize, dims))
435
+ : undefined,
366
436
  });
367
437
 
368
438
  yPos += elHeight + pxToIn(marginBottom, dims) + 0.1;
369
439
  }
370
440
  }
371
441
 
372
- return { texts, images, shapes, bgColor };
442
+ return { texts, images, shapes, tables: [], bgColor, bgTransparency };
373
443
  }
374
444
 
375
445
  type SlideDims = ReturnType<typeof getAspectRatioDims>;
376
446
 
377
- function parseImportedPdfSlideHtml(
378
- html: string,
379
- dims: SlideDims,
380
- ): {
381
- texts: TextElement[];
382
- images: ImageElement[];
383
- shapes: ShapeElement[];
384
- grid?: GridElement;
385
- bgColor: string;
386
- } {
447
+ function parseImportedPdfSlideHtml(html: string, dims: SlideDims): ParsedSlide {
387
448
  const outerStyle = html.match(
388
449
  /class=["'][^"']*\bfmd-slide\b[^"']*["'][^>]*style=["']([^"']*)["']/i,
389
450
  )?.[1];
390
- const bgColor = colorToHex(
451
+ const parsedBg = colorToHex(
391
452
  outerStyle
392
453
  ? (getStyle(outerStyle, "background(?:-color)?") ?? "#000000") // guard:allow-raw-color - imported PDF fallback
393
454
  : "#000000", // guard:allow-raw-color - imported PDF fallback
394
455
  );
456
+ const bgColor = parsedBg.hex;
457
+ const bgTransparency = parsedBg.transparency;
395
458
  const src = html.match(/<img\b[^>]*\bsrc=["']([^"']+)["'][^>]*>/i)?.[1];
396
459
  const outerAttrs = html.match(/<div\b([^>]*)>/i)?.[1] ?? "";
397
460
  const sourceWidth = Number.parseFloat(
@@ -435,34 +498,30 @@ function parseImportedPdfSlideHtml(
435
498
  ]
436
499
  : [],
437
500
  shapes: [],
501
+ tables: [],
438
502
  bgColor,
503
+ bgTransparency,
439
504
  };
440
505
  }
441
506
 
442
- function parseImportedSlideHtml(
443
- html: string,
444
- dims: SlideDims,
445
- ): {
446
- texts: TextElement[];
447
- images: ImageElement[];
448
- shapes: ShapeElement[];
449
- grid?: GridElement;
450
- bgColor: string;
451
- } {
507
+ function parseImportedSlideHtml(html: string, dims: SlideDims): ParsedSlide {
452
508
  const texts: TextElement[] = [];
453
509
  const images: ImageElement[] = [];
454
510
  const shapes: ShapeElement[] = [];
511
+ const tables: TableElement[] = [];
455
512
  const outerStyle = html.match(
456
513
  /class=["'][^"']*\bfmd-slide\b[^"']*["'][^>]*style=["']([^"']*)["']/i,
457
514
  )?.[1];
458
- const bgColor = colorToHex(
515
+ const parsedBg = colorToHex(
459
516
  outerStyle
460
517
  ? (getStyle(outerStyle, "background(?:-color)?") ?? "#000000") // guard:allow-raw-color - PPTX background fallback
461
518
  : "#000000", // guard:allow-raw-color - PPTX background fallback
462
519
  );
520
+ const bgColor = parsedBg.hex;
521
+ const bgTransparency = parsedBg.transparency;
463
522
  const grid = outerStyle ? parseImportedGrid(outerStyle) : undefined;
464
523
  const elementRegex =
465
- /<div\b([^>]*\bdata-pptx-element-kind=["'](text|image|shape)["'][^>]*)>([\s\S]*?)<\/div>/gi;
524
+ /<div\b([^>]*\bdata-pptx-element-kind=["'](text|image|shape|table)["'][^>]*)>([\s\S]*?)<\/div>/gi;
466
525
  let match: RegExpExecArray | null;
467
526
  while ((match = elementRegex.exec(html)) !== null) {
468
527
  const attrs = match[1];
@@ -485,14 +544,18 @@ function parseImportedSlideHtml(
485
544
  const fill = getStyle(style, "background(?:-color)?");
486
545
  const border = getStyle(style, "border");
487
546
  const borderMatch = border?.match(/([\d.]+)px\s+solid\s+(.+)/i);
547
+ const parsedFill =
548
+ fill && !fill.includes("gradient") ? colorToHex(fill) : undefined;
549
+ const parsedLine = borderMatch ? colorToHex(borderMatch[2]) : undefined;
488
550
  shapes.push({
489
551
  ...geometry,
490
- ...(fill && !fill.includes("gradient")
491
- ? { fill: colorToHex(fill) }
552
+ ...(parsedFill
553
+ ? { fill: parsedFill.hex, fillTransparency: parsedFill.transparency }
492
554
  : {}),
493
- ...(borderMatch
555
+ ...(borderMatch && parsedLine
494
556
  ? {
495
- lineColor: colorToHex(borderMatch[2]),
557
+ lineColor: parsedLine.hex,
558
+ lineTransparency: parsedLine.transparency,
496
559
  lineWidth: Number(borderMatch[1]),
497
560
  }
498
561
  : {}),
@@ -502,7 +565,15 @@ function parseImportedSlideHtml(
502
565
  continue;
503
566
  }
504
567
 
505
- const runs = importedTextRuns(innerHtml);
568
+ if (kind === "table") {
569
+ const rows = importedTableRows(innerHtml, dims);
570
+ if (rows.length > 0) {
571
+ tables.push({ ...geometry, rows, order: match.index });
572
+ }
573
+ continue;
574
+ }
575
+
576
+ const runs = importedTextRuns(innerHtml, dims);
506
577
  const firstRun = runs.find((run) => run.text.trim()) ?? runs[0];
507
578
  const firstParagraph = innerHtml.match(
508
579
  /<p\b[^>]*style=["']([^"']*)["']/i,
@@ -517,6 +588,7 @@ function parseImportedSlideHtml(
517
588
  fontSize,
518
589
  fontFace: firstRun?.options.fontFace ?? "Poppins",
519
590
  color: firstRun?.options.color ?? "FFFFFF",
591
+ transparency: firstRun?.options.transparency,
520
592
  bold: firstRun?.options.bold ?? false,
521
593
  align:
522
594
  alignValue === "center" || alignValue === "right" ? alignValue : "left",
@@ -530,7 +602,7 @@ function parseImportedSlideHtml(
530
602
  });
531
603
  }
532
604
 
533
- return { texts, images, shapes, grid, bgColor };
605
+ return { texts, images, shapes, tables, grid, bgColor, bgTransparency };
534
606
  }
535
607
 
536
608
  function parseImportedGrid(style: string): GridElement | undefined {
@@ -559,8 +631,10 @@ function parseImportedGrid(style: string): GridElement | undefined {
559
631
  ) {
560
632
  return undefined;
561
633
  }
634
+ const parsedColor = colorToHex(color);
562
635
  return {
563
- color: colorToHex(color),
636
+ color: parsedColor.hex,
637
+ transparency: parsedColor.transparency,
564
638
  stepX: size[0],
565
639
  stepY: size[1],
566
640
  offsetX: position[0],
@@ -591,7 +665,7 @@ function pxToInY(px: number, dims: SlideDims): number {
591
665
  return (px / dims.height) * dims.pptxInches.h;
592
666
  }
593
667
 
594
- function importedTextRuns(html: string): TextRunElement[] {
668
+ function importedTextRuns(html: string, dims: SlideDims): TextRunElement[] {
595
669
  const paragraphs = [...html.matchAll(/<p\b[^>]*>([\s\S]*?)<\/p>/gi)].map(
596
670
  (match) => match[1],
597
671
  );
@@ -610,24 +684,105 @@ function importedTextRuns(html: string): TextRunElement[] {
610
684
  const style = getAttribute(attrs, "style") ?? "";
611
685
  const text = decodeHtmlText(stripTags(span[2]));
612
686
  if (!text) continue;
613
- runs.push({ text, options: importedRunOptions(style) });
687
+ runs.push({ text, options: importedRunOptions(style, dims) });
614
688
  }
615
689
  });
616
690
  return runs;
617
691
  }
618
692
 
619
- function importedRunOptions(style: string): TextRunElement["options"] {
693
+ function importedTableRows(html: string, dims: SlideDims): TableRow[] {
694
+ return [...html.matchAll(/<tr\b[^>]*>([\s\S]*?)<\/tr>/gi)].map(
695
+ (rowMatch): TableRow =>
696
+ [...rowMatch[1].matchAll(/<t[dh]\b([^>]*)>([\s\S]*?)<\/t[dh]>/gi)].map(
697
+ (cellMatch): TableCell => {
698
+ const attrs = cellMatch[1];
699
+ const cellHtml = cellMatch[2];
700
+ const style = getAttribute(attrs, "style") ?? "";
701
+ const runs = importedTextRuns(cellHtml, dims);
702
+ const fill = getStyle(style, "background(?:-color)?");
703
+ const parsedFill = fill ? colorToHex(fill) : undefined;
704
+ const border = getStyle(style, "border");
705
+ const borderMatch = border?.match(/([\d.]+)px\s+solid\s+(.+)/i);
706
+ const parsedBorder = borderMatch
707
+ ? colorToHex(borderMatch[2])
708
+ : undefined;
709
+ const colSpan = Number.parseInt(
710
+ getAttribute(attrs, "colspan") ?? "",
711
+ 10,
712
+ );
713
+ const rowSpan = Number.parseInt(
714
+ getAttribute(attrs, "rowspan") ?? "",
715
+ 10,
716
+ );
717
+ const align = getStyle(style, "text-align");
718
+ const verticalAlign = getStyle(style, "vertical-align");
719
+ const options: NonNullable<TableCell["options"]> = {
720
+ ...(align === "center" || align === "right" || align === "justify"
721
+ ? { align }
722
+ : {}),
723
+ ...(verticalAlign === "top" ||
724
+ verticalAlign === "middle" ||
725
+ verticalAlign === "bottom"
726
+ ? { valign: verticalAlign }
727
+ : {}),
728
+ ...(parsedFill
729
+ ? {
730
+ fill: {
731
+ color: parsedFill.hex,
732
+ ...(parsedFill.transparency != null
733
+ ? { transparency: parsedFill.transparency }
734
+ : {}),
735
+ },
736
+ }
737
+ : {}),
738
+ ...(parsedBorder && borderMatch
739
+ ? {
740
+ border: {
741
+ type: "solid" as const,
742
+ color: parsedBorder.hex,
743
+ pt: Math.max(
744
+ 0.5,
745
+ pxToPt(Number.parseFloat(borderMatch[1]), dims),
746
+ ),
747
+ },
748
+ }
749
+ : {}),
750
+ ...(Number.isFinite(colSpan) && colSpan > 1
751
+ ? { colspan: colSpan }
752
+ : {}),
753
+ ...(Number.isFinite(rowSpan) && rowSpan > 1
754
+ ? { rowspan: rowSpan }
755
+ : {}),
756
+ };
757
+ const text: TableCell["text"] =
758
+ runs.length > 0
759
+ ? runs.map((run) => ({ text: run.text, options: run.options }))
760
+ : "";
761
+ return { text, options };
762
+ },
763
+ ),
764
+ );
765
+ }
766
+
767
+ function importedRunOptions(
768
+ style: string,
769
+ dims: SlideDims,
770
+ ): TextRunElement["options"] {
620
771
  const fontSizePx = cssPx(style, "font-size");
621
772
  const fontFamily = getStyle(style, "font-family")
622
773
  ?.replace(/["']/g, "")
623
774
  .split(",")[0]
624
775
  ?.trim();
625
776
  const fontWeight = getStyle(style, "font-weight");
777
+ const colorValue = getStyle(style, "color");
778
+ const parsedColor = colorValue
779
+ ? colorToHex(colorValue) // guard:allow-raw-color - PPTX text fallback
780
+ : undefined;
626
781
  return {
627
- ...(fontSizePx != null ? { fontSize: pxToPt(fontSizePx) } : {}),
782
+ ...(fontSizePx != null ? { fontSize: pxToPt(fontSizePx, dims) } : {}),
628
783
  ...(fontFamily ? { fontFace: fontFamily } : {}),
629
- ...(getStyle(style, "color")
630
- ? { color: colorToHex(getStyle(style, "color") ?? "#FFFFFF") } // guard:allow-raw-color - PPTX text fallback
784
+ ...(parsedColor
785
+ ? { color: parsedColor.hex, transparency: parsedColor.transparency }
631
786
  : {}),
632
787
  ...(fontWeight
633
788
  ? {
@@ -771,13 +926,13 @@ export default defineAction({
771
926
  slide && typeof slide === "object" && typeof slide.content === "string"
772
927
  ? slide.content
773
928
  : "";
774
- const { texts, images, shapes, grid, bgColor } = parseSlideHtml(
775
- slideContent,
776
- aspectRatio,
777
- slideIndex + 1,
778
- );
929
+ const { texts, images, shapes, tables, grid, bgColor, bgTransparency } =
930
+ parseSlideHtml(slideContent, aspectRatio, slideIndex + 1);
779
931
 
780
- pptxSlide.background = { color: bgColor };
932
+ pptxSlide.background = {
933
+ color: bgColor,
934
+ ...(bgTransparency != null ? { transparency: bgTransparency } : {}),
935
+ };
781
936
 
782
937
  if (grid) {
783
938
  const gridWidth = pxToIn(grid.stepX, dims);
@@ -785,6 +940,13 @@ export default defineAction({
785
940
  const gridX = pxToIn(grid.offsetX, dims);
786
941
  const gridY = pxToInY(grid.offsetY, dims);
787
942
  const lineWidth = Math.max(0.5, grid.lineWidth * 0.75);
943
+ const gridLine = {
944
+ color: grid.color,
945
+ width: lineWidth,
946
+ ...(grid.transparency != null
947
+ ? { transparency: grid.transparency }
948
+ : {}),
949
+ };
788
950
 
789
951
  for (let x = gridX; x < dims.pptxInches.w; x += gridWidth) {
790
952
  pptxSlide.addShape(pptx.ShapeType.line, {
@@ -792,7 +954,7 @@ export default defineAction({
792
954
  y: 0,
793
955
  w: 0,
794
956
  h: dims.pptxInches.h,
795
- line: { color: grid.color, width: lineWidth },
957
+ line: gridLine,
796
958
  });
797
959
  }
798
960
  for (let y = gridY; y < dims.pptxInches.h; y += gridHeight) {
@@ -801,7 +963,7 @@ export default defineAction({
801
963
  y,
802
964
  w: dims.pptxInches.w,
803
965
  h: 0,
804
- line: { color: grid.color, width: lineWidth },
966
+ line: gridLine,
805
967
  });
806
968
  }
807
969
  }
@@ -815,6 +977,9 @@ export default defineAction({
815
977
  const orderedShapes = [...shapes].sort(
816
978
  (a, b) => (a.order ?? 0) - (b.order ?? 0),
817
979
  );
980
+ const orderedTables = [...tables].sort(
981
+ (a, b) => (a.order ?? 0) - (b.order ?? 0),
982
+ );
818
983
 
819
984
  // Imported elements are parsed separately because PptxGenJS needs real
820
985
  // slide objects. Keep their source order so overlapping objects retain
@@ -823,6 +988,7 @@ export default defineAction({
823
988
  ...orderedTexts.map((value) => ({ kind: "text" as const, value })),
824
989
  ...orderedImages.map((value) => ({ kind: "image" as const, value })),
825
990
  ...orderedShapes.map((value) => ({ kind: "shape" as const, value })),
991
+ ...orderedTables.map((value) => ({ kind: "table" as const, value })),
826
992
  ].sort((a, b) => (a.value.order ?? 0) - (b.value.order ?? 0));
827
993
 
828
994
  for (const object of orderedObjects) {
@@ -840,6 +1006,7 @@ export default defineAction({
840
1006
  align: t.align || "left",
841
1007
  valign: "top" as const,
842
1008
  wrap: true,
1009
+ ...(t.transparency != null ? { transparency: t.transparency } : {}),
843
1010
  ...(t.letterSpacing != null
844
1011
  ? { charSpacing: t.letterSpacing }
845
1012
  : {}),
@@ -864,6 +1031,17 @@ export default defineAction({
864
1031
  h: img.h,
865
1032
  });
866
1033
  }
1034
+ } else if (object.kind === "table") {
1035
+ const table = object.value;
1036
+ pptxSlide.addTable(table.rows, {
1037
+ x: table.x,
1038
+ y: table.y,
1039
+ w: table.w,
1040
+ h: table.h,
1041
+ autoPage: false,
1042
+ border: { type: "solid", color: "FFFFFF", pt: 0.5 },
1043
+ margin: 0.04,
1044
+ });
867
1045
  } else {
868
1046
  const shape = object.value;
869
1047
  pptxSlide.addShape(
@@ -873,12 +1051,24 @@ export default defineAction({
873
1051
  y: shape.y,
874
1052
  w: shape.w,
875
1053
  h: shape.h,
876
- ...(shape.fill ? { fill: { color: shape.fill } } : {}),
1054
+ ...(shape.fill
1055
+ ? {
1056
+ fill: {
1057
+ color: shape.fill,
1058
+ ...(shape.fillTransparency != null
1059
+ ? { transparency: shape.fillTransparency }
1060
+ : {}),
1061
+ },
1062
+ }
1063
+ : {}),
877
1064
  ...(shape.lineColor
878
1065
  ? {
879
1066
  line: {
880
1067
  color: shape.lineColor,
881
1068
  width: shape.lineWidth ?? 1,
1069
+ ...(shape.lineTransparency != null
1070
+ ? { transparency: shape.lineTransparency }
1071
+ : {}),
882
1072
  },
883
1073
  }
884
1074
  : {}),
@@ -162,6 +162,10 @@ export default defineAction({
162
162
  (total, r) => total + r.imageSkippedCount,
163
163
  0,
164
164
  );
165
+ const tablesDegraded = presentation.slides.reduce(
166
+ (total, s) => total + (s.tablesDegraded ?? 0),
167
+ 0,
168
+ );
165
169
  const sourceImport = buildSourceImportMetadata({
166
170
  format: "pptx",
167
171
  slides: pptxResults.map((result) => ({
@@ -172,6 +176,7 @@ export default defineAction({
172
176
  editableText: true,
173
177
  })),
174
178
  imagesSkipped,
179
+ tablesDegraded,
175
180
  });
176
181
  if (imagesSkipped > 0) {
177
182
  throw new Error(
@@ -202,6 +207,7 @@ export default defineAction({
202
207
  deckId,
203
208
  imported: true,
204
209
  ...(imagesSkipped > 0 ? { imagesSkipped } : {}),
210
+ ...(tablesDegraded > 0 ? { tablesDegraded } : {}),
205
211
  };
206
212
  }
207
213
 
@@ -527,9 +533,14 @@ async function importPdfPagesWithFidelity(args: {
527
533
  ),
528
534
  );
529
535
  const slides = imported.map((entry) => entry.slide);
536
+ const imagesSkipped = fidelityPages.reduce(
537
+ (total, page) => total + page.imagesSkipped,
538
+ 0,
539
+ );
530
540
  const sourceImport = buildSourceImportMetadata({
531
541
  format: "pdf",
532
542
  slides: imported.map((entry) => entry.snapshot),
543
+ imagesSkipped,
533
544
  });
534
545
 
535
546
  const importedTitle = await appendDeckSlides(
@@ -550,6 +561,7 @@ async function importPdfPagesWithFidelity(args: {
550
561
  aspectRatio,
551
562
  deckId,
552
563
  imported: true,
564
+ ...(imagesSkipped > 0 ? { imagesSkipped } : {}),
553
565
  };
554
566
  }
555
567