pptx-angular-viewer 1.31.2 → 1.31.4

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,10 @@ All notable changes to this project are documented here.
4
4
  This file is generated from [Conventional Commits](https://www.conventionalcommits.org)
5
5
  by [git-cliff](https://git-cliff.org); do not edit it by hand.
6
6
 
7
+ ## [1.31.3](https://github.com/ChristopherVR/pptx-viewer/releases/tag/pptx-angular-viewer@1.31.3) - 2026-07-19
8
+
9
+ ## [1.31.2](https://github.com/ChristopherVR/pptx-viewer/releases/tag/pptx-angular-viewer@1.31.2) - 2026-07-19
10
+
7
11
  ## [1.31.1](https://github.com/ChristopherVR/pptx-viewer/releases/tag/pptx-angular-viewer@1.31.1) - 2026-07-18
8
12
 
9
13
  ## [1.31.0](https://github.com/ChristopherVR/pptx-viewer/releases/tag/pptx-angular-viewer@1.31.0) - 2026-07-18
@@ -450,22 +450,96 @@ function getResolvedShapeClipPathFor(shapeType, width, height, adjustments) {
450
450
  // 4. Final fallback: core's static preset clip-path table.
451
451
  return getShapeClipPath(shapeType);
452
452
  }
453
+ /**
454
+ * Build a CSS `clip-path: path('…')` value from a custom-geometry (`a:custGeom`)
455
+ * SVG path string, rescaling its path-space coordinates (`pathWidth` x
456
+ * `pathHeight`) into the element's pixel box.
457
+ *
458
+ * CSS `path()` coordinates live in the element's own border-box pixel space
459
+ * (there is no viewBox), so freeform coordinates authored against the OOXML path
460
+ * extent must be scaled by `elemW / pathWidth` and `elemH / pathHeight`. Every
461
+ * binding already clips its shape container's background fill with the resolved
462
+ * clip-path, so returning one here lets a themed freeform (e.g. the "Balloons"
463
+ * background) render as its true outline instead of flooding its bounding box.
464
+ *
465
+ * Supports the absolute command set produced by the core geometry engine
466
+ * (M/L/C/Q/Z) plus elliptical arcs (A); unknown commands are passed through
467
+ * unscaled rather than dropped.
468
+ */
469
+ function buildCustomGeometryClipPath(pathData, pathWidth, pathHeight, elemWidth, elemHeight) {
470
+ if (!pathData ||
471
+ !Number.isFinite(pathWidth) ||
472
+ !Number.isFinite(pathHeight) ||
473
+ pathWidth <= 0 ||
474
+ pathHeight <= 0 ||
475
+ !Number.isFinite(elemWidth) ||
476
+ !Number.isFinite(elemHeight) ||
477
+ elemWidth <= 0 ||
478
+ elemHeight <= 0) {
479
+ return undefined;
480
+ }
481
+ const sx = elemWidth / pathWidth;
482
+ const sy = elemHeight / pathHeight;
483
+ const round = (n) => {
484
+ const r = Math.round(n * 100) / 100;
485
+ return Object.is(r, -0) ? '0' : String(r);
486
+ };
487
+ const tokens = pathData.match(/[MLCQZAHVmlcqzahv][^MLCQZAHVmlcqzahv]*/g) ?? [];
488
+ const out = [];
489
+ for (const token of tokens) {
490
+ const cmd = token[0];
491
+ const upper = cmd.toUpperCase();
492
+ if (upper === 'Z') {
493
+ out.push('Z');
494
+ continue;
495
+ }
496
+ const nums = (token.slice(1).match(/-?[\d.]+(?:e-?\d+)?/gi) ?? []).map(Number);
497
+ if (upper === 'A') {
498
+ // rx ry x-axis-rotation large-arc-flag sweep-flag x y (per 7-number group)
499
+ const parts = [];
500
+ for (let i = 0; i + 6 < nums.length; i += 7) {
501
+ parts.push(round(nums[i] * sx), round(nums[i + 1] * sy), String(nums[i + 2]), String(nums[i + 3]), String(nums[i + 4]), round(nums[i + 5] * sx), round(nums[i + 6] * sy));
502
+ }
503
+ out.push(`A ${parts.join(' ')}`);
504
+ continue;
505
+ }
506
+ // M/L/C/Q (and H/V) carry (x, y) pairs; scale x by sx and y by sy.
507
+ const scaled = nums.map((n, i) => round(n * (i % 2 === 0 ? sx : sy)));
508
+ out.push(`${upper} ${scaled.join(' ')}`);
509
+ }
510
+ if (out.length === 0) {
511
+ return undefined;
512
+ }
513
+ return `path('${out.join(' ')}')`;
514
+ }
453
515
  /**
454
516
  * Element-level convenience wrapper. Pulls `shapeType`, `width`, `height`, and
455
517
  * `shapeAdjustments` off a {@link PptxElement} and delegates to
456
518
  * {@link getResolvedShapeClipPathFor}.
457
519
  *
520
+ * Custom-geometry freeforms (which carry `pathData`/`pathWidth`/`pathHeight`
521
+ * rather than a preset `shapeType`) take priority: their outline is rescaled
522
+ * into the element box via {@link buildCustomGeometryClipPath} so the fill clips
523
+ * to the real shape instead of its bounding rectangle.
524
+ *
458
525
  * @param element The PPTX element to resolve a clip-path for.
459
526
  * @param width Optional width override (pixels). Defaults to `element.width`.
460
527
  * @param height Optional height override (pixels). Defaults to `element.height`.
461
528
  */
462
529
  function getResolvedShapeClipPath(element, width, height) {
530
+ const w = typeof width === 'number' ? width : element.width;
531
+ const h = typeof height === 'number' ? height : element.height;
532
+ const custom = element;
533
+ if (custom.pathData && custom.pathWidth && custom.pathHeight) {
534
+ const customClip = buildCustomGeometryClipPath(custom.pathData, custom.pathWidth, custom.pathHeight, w, h);
535
+ if (customClip) {
536
+ return customClip;
537
+ }
538
+ }
463
539
  const shapeType = element.shapeType;
464
540
  if (!shapeType) {
465
541
  return undefined;
466
542
  }
467
- const w = typeof width === 'number' ? width : element.width;
468
- const h = typeof height === 'number' ? height : element.height;
469
543
  const adjustments = element.shapeAdjustments;
470
544
  return getResolvedShapeClipPathFor(shapeType, w, h, adjustments);
471
545
  }
@@ -1450,20 +1524,29 @@ function getSvgStrokeDasharray(dashType, strokeWidth, customDashSegments) {
1450
1524
  /**
1451
1525
  * Builds a CSS `transform` string combining flip and rotation transforms for an element.
1452
1526
  * Flips are expressed as `scaleX(-1)` / `scaleY(-1)`, rotation as `rotate(Ndeg)`.
1527
+ *
1528
+ * Order matters: OOXML `a:xfrm` mirrors the shape *within* its bounding box
1529
+ * (`flipH`/`flipV`) and *then* rotates the box by `rot`. With CSS
1530
+ * `transform-origin: center`, transforms apply right-to-left, so the flips must
1531
+ * come AFTER the rotation in the string (`rotate(θ) scaleX(-1)`) to be applied
1532
+ * first. Emitting `scaleX(-1) rotate(θ)` instead reflects the rotation direction
1533
+ * for any shape that is both flipped and rotated (e.g. the "Balloons" freeforms
1534
+ * render mirrored/tilted the wrong way). This matches the Angular binding's
1535
+ * `getContainerStyle`, which already emits `rotate() scaleX() scaleY()`.
1453
1536
  * @param element - The element whose transforms are read.
1454
1537
  * @returns A CSS transform string, or `undefined` if no transforms apply.
1455
1538
  */
1456
1539
  function getElementTransform(element) {
1457
1540
  const transforms = [];
1541
+ if (element.rotation) {
1542
+ transforms.push(`rotate(${element.rotation}deg)`);
1543
+ }
1458
1544
  if (element.flipHorizontal) {
1459
1545
  transforms.push('scaleX(-1)');
1460
1546
  }
1461
1547
  if (element.flipVertical) {
1462
1548
  transforms.push('scaleY(-1)');
1463
1549
  }
1464
- if (element.rotation) {
1465
- transforms.push(`rotate(${element.rotation}deg)`);
1466
- }
1467
1550
  if (element.skewX) {
1468
1551
  transforms.push(`skewX(${element.skewX}deg)`);
1469
1552
  }
@@ -6250,6 +6333,16 @@ function resolveDataPointFill(series, pointIndex, fallbackColor) {
6250
6333
  const point = findDataPoint(series, pointIndex);
6251
6334
  return point?.spPr?.fillColor ?? series.color ?? fallbackColor;
6252
6335
  }
6336
+ /**
6337
+ * Resolve the fill for a data point in a "vary colours" context, where every
6338
+ * point in a single series is drawn with a distinct palette colour (pie /
6339
+ * doughnut slices, or a bar/column series with `c:varyColors=1`). A per-point
6340
+ * `c:dPt` fill still wins; otherwise the supplied per-point `paletteColor` is
6341
+ * used (NOT the single series colour, which would make every point identical).
6342
+ */
6343
+ function resolveVaryColorFill(series, pointIndex, paletteColor) {
6344
+ return findDataPoint(series, pointIndex)?.spPr?.fillColor ?? paletteColor;
6345
+ }
6253
6346
  /**
6254
6347
  * Resolve the slice explosion (pull-out distance, 0-100) for a pie/doughnut
6255
6348
  * data point: the per-point `c:dPt` explosion when present, otherwise the
@@ -6947,7 +7040,8 @@ function buildBars(chartData, catCount, layout, primaryRange, secondaryRange, se
6947
7040
  y,
6948
7041
  w: singleBarWidth,
6949
7042
  h,
6950
- fill: seriesColor(series[si], si, palette),
7043
+ fill: resolveDataPointFill(series[si], sourceIndex, paletteColor(si, palette)) ??
7044
+ seriesColor(series[si], si, palette),
6951
7045
  rx: 1,
6952
7046
  part: { role: 'dataPoint', seriesIndex: si, pointIndex: sourceIndex },
6953
7047
  });
@@ -6977,22 +7071,14 @@ function buildBars(chartData, catCount, layout, primaryRange, secondaryRange, se
6977
7071
  }));
6978
7072
  const rects = computeStackedBarRects(displaySeries, catCount, layout, primaryRange, palette);
6979
7073
  for (const r of rects) {
6980
- primitives.push({
6981
- kind: 'rect',
6982
- x: r.x,
6983
- y: r.y,
6984
- w: r.w,
6985
- h: r.h,
6986
- fill: r.fill,
6987
- rx: 1,
6988
- part: r.seriesIndex !== undefined && r.pointIndex !== undefined
6989
- ? {
6990
- role: 'dataPoint',
6991
- seriesIndex: r.seriesIndex,
6992
- pointIndex: sourceIndices[r.pointIndex] ?? r.pointIndex,
6993
- }
6994
- : undefined,
6995
- });
7074
+ let fill = r.fill;
7075
+ let part;
7076
+ if (r.seriesIndex !== undefined && r.pointIndex !== undefined) {
7077
+ const sourcePointIndex = sourceIndices[r.pointIndex] ?? r.pointIndex;
7078
+ fill = resolveDataPointFill(series[r.seriesIndex], sourcePointIndex, r.fill) ?? r.fill;
7079
+ part = { role: 'dataPoint', seriesIndex: r.seriesIndex, pointIndex: sourcePointIndex };
7080
+ }
7081
+ primitives.push({ kind: 'rect', x: r.x, y: r.y, w: r.w, h: r.h, fill, rx: 1, part });
6996
7082
  }
6997
7083
  if (showLabels) {
6998
7084
  pushClusteredStackedLabels(series, sourceIndices, catCount, layout, primaryRange, dataLabels);
@@ -7030,7 +7116,8 @@ function buildBars(chartData, catCount, layout, primaryRange, secondaryRange, se
7030
7116
  y,
7031
7117
  w: barW,
7032
7118
  h,
7033
- fill: seriesColor(series[si], si, palette),
7119
+ fill: resolveDataPointFill(series[si], sourceIndex, paletteColor(si, palette)) ??
7120
+ seriesColor(series[si], si, palette),
7034
7121
  part: { role: 'dataPoint', seriesIndex: si, pointIndex: sourceIndex },
7035
7122
  });
7036
7123
  if (showLabels && Math.abs(val) > 0) {
@@ -7123,7 +7210,7 @@ function buildLines(chartData, catCount, layout, primaryRange, secondaryRange, s
7123
7210
  cx: pt.x,
7124
7211
  cy: pt.y,
7125
7212
  r: 2.5,
7126
- fill: c,
7213
+ fill: resolveDataPointFill(series, sourceIndex, c) ?? c,
7127
7214
  part: { role: 'dataPoint', seriesIndex: si, pointIndex: sourceIndex },
7128
7215
  });
7129
7216
  });
@@ -7187,16 +7274,17 @@ function buildAreas(chartData, catCount, layout, range, sourceIndices, xPosition
7187
7274
  part: { role: 'series', seriesIndex: si },
7188
7275
  });
7189
7276
  pts.forEach((pt, displayIndex) => {
7277
+ const sourceIndex = sourceIndices[displayIndex] ?? displayIndex;
7190
7278
  primitives.push({
7191
7279
  kind: 'circle',
7192
7280
  cx: pt.x,
7193
7281
  cy: pt.y,
7194
7282
  r: 2,
7195
- fill: c,
7283
+ fill: resolveDataPointFill(series, sourceIndex, c) ?? c,
7196
7284
  part: {
7197
7285
  role: 'dataPoint',
7198
7286
  seriesIndex: si,
7199
- pointIndex: sourceIndices[displayIndex] ?? displayIndex,
7287
+ pointIndex: sourceIndex,
7200
7288
  },
7201
7289
  });
7202
7290
  });
@@ -7238,7 +7326,7 @@ function buildScatter(chartData, layout, range) {
7238
7326
  cx: dot.cx,
7239
7327
  cy: dot.cy,
7240
7328
  r: 4,
7241
- fill: c,
7329
+ fill: resolveDataPointFill(series, vi, c) ?? c,
7242
7330
  opacity: 0.85,
7243
7331
  part: { role: 'dataPoint', seriesIndex: si, pointIndex: vi },
7244
7332
  });
@@ -7286,7 +7374,7 @@ function buildBubbles(chartData, layout, range) {
7286
7374
  cx: dot.cx,
7287
7375
  cy: dot.cy,
7288
7376
  r,
7289
- fill: c,
7377
+ fill: resolveDataPointFill(series, vi, c) ?? c,
7290
7378
  opacity: 0.6,
7291
7379
  part: { role: 'dataPoint', seriesIndex: si, pointIndex: vi },
7292
7380
  });
@@ -11451,12 +11539,17 @@ function buildPieViewModel(element, chartData, categoryLabels, isDoughnut) {
11451
11539
  const { cx, cy, outerR, innerR, size } = computePieLayout(element.width, element.height, chartData, isDoughnut);
11452
11540
  const svgWidth = Math.max(size, 100);
11453
11541
  const svgHeight = Math.max(size, 60);
11454
- const values = chartData.series[0]?.values ?? [];
11542
+ const pieSeries = chartData.series[0];
11543
+ const values = pieSeries?.values ?? [];
11455
11544
  const slices = computePieSlices(values, cx, cy, outerR, innerR);
11456
11545
  const primitives = slices.map(({ d }, i) => ({
11457
11546
  kind: 'path',
11458
11547
  d,
11459
- fill: chartData.series[0]?.color ?? paletteColor(i, chartData.colorPalette),
11548
+ // Pie/doughnut vary colours per slice (c:varyColors defaults on), so each
11549
+ // slice takes its palette colour, with a per-point c:dPt fill overriding.
11550
+ fill: pieSeries
11551
+ ? resolveVaryColorFill(pieSeries, i, paletteColor(i, chartData.colorPalette))
11552
+ : paletteColor(i, chartData.colorPalette),
11460
11553
  stroke: '#ffffff',
11461
11554
  strokeWidth: 1.5,
11462
11555
  part: { role: 'dataPoint', seriesIndex: 0, pointIndex: i },
@@ -12810,6 +12903,25 @@ const PRESET_ID_TO_EFFECT = {
12810
12903
  26: 'pulse',
12811
12904
  },
12812
12905
  };
12906
+ /**
12907
+ * Map an OOXML `p:cTn/@presetSubtype` code to a {@link FlyEdge} for Fly In and
12908
+ * Fly Out effects. PowerPoint encodes the direction as a bitmask on the object
12909
+ * origin edge: 1=top, 2=right, 4=bottom, 8=left. Corners combine two bits
12910
+ * (e.g. 12 = 8|4 = bottom-left) and fall back to their horizontal edge, which
12911
+ * is the more visually distinct component. Unknown/absent codes are left to the
12912
+ * caller (which keeps the preset default of bottom).
12913
+ */
12914
+ const FLY_SUBTYPE_TO_EDGE = {
12915
+ 1: 'top',
12916
+ 2: 'right',
12917
+ 4: 'bottom',
12918
+ 8: 'left',
12919
+ // Corners -> nearest (horizontal) edge.
12920
+ 3: 'right', // top-right (1|2)
12921
+ 6: 'right', // bottom-right (4|2)
12922
+ 9: 'left', // top-left (8|1)
12923
+ 12: 'left', // bottom-left (8|4)
12924
+ };
12813
12925
 
12814
12926
  /**
12815
12927
  * `animation-keyframes` — CSS `@keyframes` definitions for every static
@@ -13271,10 +13383,10 @@ function resolveEffect(anim) {
13271
13383
  return undefined;
13272
13384
  }
13273
13385
  if (cls === 'entr') {
13274
- return PRESET_ID_TO_EFFECT.entr[id];
13386
+ return applyFlyDirection(PRESET_ID_TO_EFFECT.entr[id], anim.presetSubtype);
13275
13387
  }
13276
13388
  if (cls === 'exit') {
13277
- return PRESET_ID_TO_EFFECT.exit[id];
13389
+ return applyFlyDirection(PRESET_ID_TO_EFFECT.exit[id], anim.presetSubtype);
13278
13390
  }
13279
13391
  if (cls === 'emph') {
13280
13392
  return PRESET_ID_TO_EFFECT.emph[id];
@@ -13282,6 +13394,28 @@ function resolveEffect(anim) {
13282
13394
  // For path/motion/rotation/scale, return undefined — handled dynamically.
13283
13395
  return undefined;
13284
13396
  }
13397
+ /**
13398
+ * Redirect a Fly In / Fly Out effect according to its `presetSubtype` code.
13399
+ * The preset tables default the fly family to the bottom edge; when a subtype
13400
+ * is present and maps to a known edge, swap in the matching directional effect.
13401
+ * A missing or unrecognised subtype preserves the bottom default so existing
13402
+ * decks are unaffected.
13403
+ */
13404
+ function applyFlyDirection(effect, subtype) {
13405
+ if (effect !== 'flyInBottom' && effect !== 'flyOutBottom') {
13406
+ return effect;
13407
+ }
13408
+ if (subtype === undefined) {
13409
+ return effect;
13410
+ }
13411
+ const edge = FLY_SUBTYPE_TO_EDGE[subtype];
13412
+ if (!edge) {
13413
+ return effect;
13414
+ }
13415
+ const prefix = effect === 'flyInBottom' ? 'flyIn' : 'flyOut';
13416
+ const suffix = `${edge.charAt(0).toUpperCase()}${edge.slice(1)}`;
13417
+ return `${prefix}${suffix}`;
13418
+ }
13285
13419
  // ==========================================================================
13286
13420
  // Dynamic keyframe generation (motion path / rotation / scale / colour)
13287
13421
  // ==========================================================================
@@ -16014,6 +16148,122 @@ function getComputed3dStyle(el) {
16014
16148
  return result;
16015
16149
  }
16016
16150
 
16151
+ /**
16152
+ * Map a section's border set onto a cell's four edges given the cell's
16153
+ * position relative to that section's region. Outer region edges use the
16154
+ * `top/bottom/left/right` sides; interior edges use `insideH`/`insideV`.
16155
+ */
16156
+ function edgesFromSection(borders, regionTop, regionBottom, regionLeft, regionRight) {
16157
+ if (!borders) {
16158
+ return undefined;
16159
+ }
16160
+ return {
16161
+ top: regionTop ? (borders.top ?? borders.insideH) : borders.insideH,
16162
+ bottom: regionBottom ? (borders.bottom ?? borders.insideH) : borders.insideH,
16163
+ left: regionLeft ? (borders.left ?? borders.insideV) : borders.insideV,
16164
+ right: regionRight ? (borders.right ?? borders.insideV) : borders.insideV,
16165
+ };
16166
+ }
16167
+ /** Convert a resolved border side to a CSS `border-*` shorthand value. */
16168
+ function borderToCss(border, resolve) {
16169
+ if (!border) {
16170
+ return undefined;
16171
+ }
16172
+ if (border.noFill) {
16173
+ return 'none';
16174
+ }
16175
+ const width = border.width ?? 1;
16176
+ const dash = ooxmlDashToCssBorderStyle(border.dash);
16177
+ const color = border.color ?? resolve(border.fill) ?? '#000000';
16178
+ return `${width}px ${dash} ${color}`;
16179
+ }
16180
+ /**
16181
+ * Build the ordered (low -> high precedence) list of edge candidates from the
16182
+ * sections that apply to this cell.
16183
+ */
16184
+ function collectLayers(entry, tableData, pos) {
16185
+ const { rowIndex, cellIndex, rowCount, columnCount } = pos;
16186
+ const isTop = rowIndex === 0;
16187
+ const isBottom = rowIndex === rowCount - 1;
16188
+ const isLeft = cellIndex === 0;
16189
+ const isRight = cellIndex === columnCount - 1;
16190
+ const layers = [];
16191
+ // Whole table: region spans the entire table.
16192
+ layers.push(edgesFromSection(entry.wholeTblBorders, isTop, isBottom, isLeft, isRight));
16193
+ // Banded rows: treat each banded row as its own single-row region.
16194
+ if (tableData.bandedRows) {
16195
+ const bandStartRow = tableData.firstRowHeader ? 1 : 0;
16196
+ const bandEndRow = tableData.lastRow ? rowCount - 1 : rowCount;
16197
+ if (rowIndex >= bandStartRow && rowIndex < bandEndRow) {
16198
+ const rowCycle = Math.max(tableData.bandRowCycle ?? 1, 1);
16199
+ const bandGroup = Math.floor((rowIndex - bandStartRow) / rowCycle) % 2;
16200
+ const bands = bandGroup === 0 ? entry.band1HBorders : entry.band2HBorders;
16201
+ layers.push(edgesFromSection(bands, true, true, isLeft, isRight));
16202
+ }
16203
+ }
16204
+ // Banded columns: treat each banded column as its own single-column region.
16205
+ if (tableData.bandedColumns) {
16206
+ const colStart = tableData.firstCol ? 1 : 0;
16207
+ const colEnd = tableData.lastCol ? columnCount - 1 : columnCount;
16208
+ if (cellIndex >= colStart && cellIndex < colEnd) {
16209
+ const colCycle = Math.max(tableData.bandColCycle ?? 1, 1);
16210
+ const colGroup = Math.floor((cellIndex - colStart) / colCycle) % 2;
16211
+ const bands = colGroup === 0 ? entry.band1VBorders : entry.band2VBorders;
16212
+ layers.push(edgesFromSection(bands, isTop, isBottom, true, true));
16213
+ }
16214
+ }
16215
+ // First/last row: single-row region spanning all columns.
16216
+ if (tableData.firstRowHeader && isTop) {
16217
+ layers.push(edgesFromSection(entry.firstRowBorders, true, true, isLeft, isRight));
16218
+ }
16219
+ if (tableData.lastRow && isBottom) {
16220
+ layers.push(edgesFromSection(entry.lastRowBorders, true, true, isLeft, isRight));
16221
+ }
16222
+ // First/last column: single-column region spanning all rows.
16223
+ if (tableData.firstCol && isLeft) {
16224
+ layers.push(edgesFromSection(entry.firstColBorders, isTop, isBottom, true, true));
16225
+ }
16226
+ if (tableData.lastCol && isRight) {
16227
+ layers.push(edgesFromSection(entry.lastColBorders, isTop, isBottom, true, true));
16228
+ }
16229
+ return layers.filter((layer) => layer !== undefined);
16230
+ }
16231
+ /**
16232
+ * Resolve the CSS borders a cell inherits from its table style. Returns
16233
+ * `undefined` when the style defines no applicable borders (so the caller can
16234
+ * keep its own fallback border, e.g. the hardcoded total-row line).
16235
+ */
16236
+ function resolveCellBorderCss(entry, tableData, pos, resolve) {
16237
+ if (!entry) {
16238
+ return undefined;
16239
+ }
16240
+ const layers = collectLayers(entry, tableData, pos);
16241
+ if (layers.length === 0) {
16242
+ return undefined;
16243
+ }
16244
+ const pick = (edge) => {
16245
+ let winner;
16246
+ for (const layer of layers) {
16247
+ if (layer[edge]) {
16248
+ winner = layer[edge];
16249
+ }
16250
+ }
16251
+ return winner;
16252
+ };
16253
+ const css = {};
16254
+ let applied = false;
16255
+ const edges = ['top', 'bottom', 'left', 'right'];
16256
+ for (const edge of edges) {
16257
+ const value = borderToCss(pick(edge), resolve);
16258
+ if (value) {
16259
+ const key = `border${edge[0].toUpperCase()}${edge.slice(1)}`;
16260
+ css[key] = value;
16261
+ applied = true;
16262
+ }
16263
+ }
16264
+ return applied ? css : undefined;
16265
+ }
16266
+
16017
16267
  /**
16018
16268
  * Convert a {@link CellTextRun}'s per-run formatting to an inline CSS style
16019
16269
  * object suitable for a `<span>` element.
@@ -16498,6 +16748,16 @@ function getTableCellBandStyle(tableData, rowIndex, cellIndex, rowCount, columnC
16498
16748
  applyStyleText(styleEntry?.lastColText, colorScheme, style);
16499
16749
  applied = true;
16500
16750
  }
16751
+ // ── Table-style borders (issue #71). ──
16752
+ // Resolve gridlines/edges the cell inherits from the table style and let
16753
+ // them supersede the hardcoded total-row line above. Per-cell explicit
16754
+ // `a:lnX` borders (parsed into the cell style) are applied on top of this
16755
+ // layer by the renderer, so they still win.
16756
+ const borderCss = resolveCellBorderCss(styleEntry, tableData, { rowIndex, cellIndex, rowCount, columnCount }, (fill) => resolveStyleFillColor(fill, colorScheme));
16757
+ if (borderCss) {
16758
+ Object.assign(style, borderCss);
16759
+ applied = true;
16760
+ }
16501
16761
  return applied ? style : undefined;
16502
16762
  }
16503
16763
 
@@ -73870,7 +74130,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
73870
74130
  }], propDecorators: { canEdit: [{ type: i0.Input, args: [{ isSignal: true, alias: "canEdit", required: false }] }], slideIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "slideIndex", required: false }] }] } });
73871
74131
 
73872
74132
  // Generated by scripts/inline-shared.mjs from package.json. Do not edit.
73873
- const PPTX_ANGULAR_VIEWER_VERSION = "1.31.1";
74133
+ const PPTX_ANGULAR_VIEWER_VERSION = "1.31.3";
73874
74134
 
73875
74135
  /**
73876
74136
  * account-page.component.ts: File > Account content.