@productcloudos/editor 1.0.5 → 1.0.7

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 (51) hide show
  1. package/dist/pc-editor.esm.js +2403 -279
  2. package/dist/pc-editor.esm.js.map +1 -1
  3. package/dist/pc-editor.js +2406 -278
  4. package/dist/pc-editor.js.map +1 -1
  5. package/dist/pc-editor.min.js +1 -1
  6. package/dist/pc-editor.min.js.map +1 -1
  7. package/dist/types/lib/core/PCEditor.d.ts +89 -1
  8. package/dist/types/lib/core/PCEditor.d.ts.map +1 -1
  9. package/dist/types/lib/fonts/FontManager.d.ts +71 -0
  10. package/dist/types/lib/fonts/FontManager.d.ts.map +1 -0
  11. package/dist/types/lib/fonts/index.d.ts +3 -0
  12. package/dist/types/lib/fonts/index.d.ts.map +1 -0
  13. package/dist/types/lib/index.d.ts +6 -4
  14. package/dist/types/lib/index.d.ts.map +1 -1
  15. package/dist/types/lib/objects/table/TableObject.d.ts +72 -1
  16. package/dist/types/lib/objects/table/TableObject.d.ts.map +1 -1
  17. package/dist/types/lib/objects/table/types.d.ts +20 -0
  18. package/dist/types/lib/objects/table/types.d.ts.map +1 -1
  19. package/dist/types/lib/panes/ConditionalSectionPane.d.ts +62 -0
  20. package/dist/types/lib/panes/ConditionalSectionPane.d.ts.map +1 -0
  21. package/dist/types/lib/panes/FormattingPane.d.ts +1 -0
  22. package/dist/types/lib/panes/FormattingPane.d.ts.map +1 -1
  23. package/dist/types/lib/panes/ImagePane.d.ts.map +1 -1
  24. package/dist/types/lib/panes/RepeatingSectionPane.d.ts.map +1 -1
  25. package/dist/types/lib/panes/TablePane.d.ts +4 -2
  26. package/dist/types/lib/panes/TablePane.d.ts.map +1 -1
  27. package/dist/types/lib/panes/TableRowLoopPane.d.ts.map +1 -1
  28. package/dist/types/lib/panes/TextBoxPane.d.ts.map +1 -1
  29. package/dist/types/lib/panes/index.d.ts +2 -0
  30. package/dist/types/lib/panes/index.d.ts.map +1 -1
  31. package/dist/types/lib/rendering/CanvasManager.d.ts +2 -0
  32. package/dist/types/lib/rendering/CanvasManager.d.ts.map +1 -1
  33. package/dist/types/lib/rendering/FlowingTextRenderer.d.ts +17 -1
  34. package/dist/types/lib/rendering/FlowingTextRenderer.d.ts.map +1 -1
  35. package/dist/types/lib/rendering/PDFGenerator.d.ts +13 -0
  36. package/dist/types/lib/rendering/PDFGenerator.d.ts.map +1 -1
  37. package/dist/types/lib/text/ConditionalSectionManager.d.ts +101 -0
  38. package/dist/types/lib/text/ConditionalSectionManager.d.ts.map +1 -0
  39. package/dist/types/lib/text/FlowingTextContent.d.ts +44 -6
  40. package/dist/types/lib/text/FlowingTextContent.d.ts.map +1 -1
  41. package/dist/types/lib/text/ParagraphFormatting.d.ts +1 -1
  42. package/dist/types/lib/text/ParagraphFormatting.d.ts.map +1 -1
  43. package/dist/types/lib/text/PredicateEvaluator.d.ts +23 -0
  44. package/dist/types/lib/text/PredicateEvaluator.d.ts.map +1 -0
  45. package/dist/types/lib/text/index.d.ts +3 -1
  46. package/dist/types/lib/text/index.d.ts.map +1 -1
  47. package/dist/types/lib/text/types.d.ts +21 -0
  48. package/dist/types/lib/text/types.d.ts.map +1 -1
  49. package/dist/types/lib/types/index.d.ts +13 -0
  50. package/dist/types/lib/types/index.d.ts.map +1 -1
  51. package/package.json +2 -1
package/dist/pc-editor.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var pdfLib = require('pdf-lib');
4
+ var fontkit = require('@pdf-lib/fontkit');
4
5
  var pdfjsLib = require('pdfjs-dist');
5
6
 
6
7
  function _interopNamespaceDefault(e) {
@@ -2307,6 +2308,291 @@ class RepeatingSectionManager extends EventEmitter {
2307
2308
  }
2308
2309
  }
2309
2310
 
2311
+ /**
2312
+ * Manages conditional sections within text content.
2313
+ * Conditional sections define ranges of content that are shown or hidden
2314
+ * based on a boolean predicate evaluated against merge data.
2315
+ * They start and end at paragraph boundaries.
2316
+ */
2317
+ class ConditionalSectionManager extends EventEmitter {
2318
+ constructor() {
2319
+ super();
2320
+ this.sections = new Map();
2321
+ this.nextId = 1;
2322
+ }
2323
+ /**
2324
+ * Create a new conditional section.
2325
+ * @param startIndex Text index at paragraph start (must be 0 or immediately after a newline)
2326
+ * @param endIndex Text index at closing paragraph start (must be immediately after a newline)
2327
+ * @param predicate The predicate expression to evaluate (e.g., "isActive")
2328
+ */
2329
+ create(startIndex, endIndex, predicate) {
2330
+ const id = `cond-${this.nextId++}`;
2331
+ const section = {
2332
+ id,
2333
+ predicate,
2334
+ startIndex,
2335
+ endIndex
2336
+ };
2337
+ this.sections.set(id, section);
2338
+ this.emit('section-added', { section });
2339
+ return section;
2340
+ }
2341
+ /**
2342
+ * Remove a conditional section by ID.
2343
+ */
2344
+ remove(id) {
2345
+ const section = this.sections.get(id);
2346
+ if (section) {
2347
+ this.sections.delete(id);
2348
+ this.emit('section-removed', { section });
2349
+ }
2350
+ return section;
2351
+ }
2352
+ /**
2353
+ * Get a conditional section by ID.
2354
+ */
2355
+ getSection(id) {
2356
+ return this.sections.get(id);
2357
+ }
2358
+ /**
2359
+ * Get all conditional sections.
2360
+ */
2361
+ getSections() {
2362
+ return Array.from(this.sections.values());
2363
+ }
2364
+ /**
2365
+ * Get all conditional sections sorted by startIndex.
2366
+ */
2367
+ getSectionsSorted() {
2368
+ return this.getSections().sort((a, b) => a.startIndex - b.startIndex);
2369
+ }
2370
+ /**
2371
+ * Get all conditional sections sorted by startIndex in descending order.
2372
+ * Useful for processing sections end-to-start during merge.
2373
+ */
2374
+ getSectionsDescending() {
2375
+ return this.getSections().sort((a, b) => b.startIndex - a.startIndex);
2376
+ }
2377
+ /**
2378
+ * Find a conditional section that contains the given text index.
2379
+ */
2380
+ getSectionContaining(textIndex) {
2381
+ for (const section of this.sections.values()) {
2382
+ if (textIndex >= section.startIndex && textIndex < section.endIndex) {
2383
+ return section;
2384
+ }
2385
+ }
2386
+ return undefined;
2387
+ }
2388
+ /**
2389
+ * Find a conditional section that has a boundary at the given text index.
2390
+ * Returns the section if textIndex matches startIndex or endIndex.
2391
+ */
2392
+ getSectionAtBoundary(textIndex) {
2393
+ for (const section of this.sections.values()) {
2394
+ if (section.startIndex === textIndex || section.endIndex === textIndex) {
2395
+ return section;
2396
+ }
2397
+ }
2398
+ return undefined;
2399
+ }
2400
+ /**
2401
+ * Update a section's predicate.
2402
+ */
2403
+ updatePredicate(id, predicate) {
2404
+ const section = this.sections.get(id);
2405
+ if (!section) {
2406
+ return false;
2407
+ }
2408
+ section.predicate = predicate;
2409
+ this.emit('section-updated', { section });
2410
+ return true;
2411
+ }
2412
+ /**
2413
+ * Update a section's visual state (called during rendering).
2414
+ */
2415
+ updateVisualState(id, visualState) {
2416
+ const section = this.sections.get(id);
2417
+ if (!section) {
2418
+ return false;
2419
+ }
2420
+ section.visualState = visualState;
2421
+ return true;
2422
+ }
2423
+ /**
2424
+ * Shift section positions when text is inserted.
2425
+ * @param fromIndex The position where text was inserted
2426
+ * @param delta The number of characters inserted (positive)
2427
+ */
2428
+ shiftSections(fromIndex, delta) {
2429
+ let changed = false;
2430
+ for (const section of this.sections.values()) {
2431
+ if (fromIndex <= section.startIndex) {
2432
+ section.startIndex += delta;
2433
+ section.endIndex += delta;
2434
+ changed = true;
2435
+ }
2436
+ else if (fromIndex < section.endIndex) {
2437
+ section.endIndex += delta;
2438
+ changed = true;
2439
+ }
2440
+ }
2441
+ if (changed) {
2442
+ this.emit('sections-shifted', { fromIndex, delta });
2443
+ }
2444
+ }
2445
+ /**
2446
+ * Handle deletion of text range.
2447
+ * Sections entirely within the deleted range are removed.
2448
+ * Sections partially overlapping are adjusted or removed.
2449
+ * @returns Array of removed sections
2450
+ */
2451
+ handleDeletion(start, length) {
2452
+ const end = start + length;
2453
+ const removedSections = [];
2454
+ const sectionsToUpdate = [];
2455
+ for (const section of this.sections.values()) {
2456
+ if (section.startIndex >= start && section.endIndex <= end) {
2457
+ removedSections.push(section);
2458
+ continue;
2459
+ }
2460
+ if (section.startIndex < end && section.endIndex > start) {
2461
+ if (start <= section.startIndex) {
2462
+ removedSections.push(section);
2463
+ continue;
2464
+ }
2465
+ if (start < section.endIndex) {
2466
+ if (end >= section.endIndex) {
2467
+ const newEnd = start;
2468
+ if (newEnd <= section.startIndex) {
2469
+ removedSections.push(section);
2470
+ continue;
2471
+ }
2472
+ sectionsToUpdate.push({
2473
+ id: section.id,
2474
+ newStart: section.startIndex,
2475
+ newEnd: newEnd
2476
+ });
2477
+ }
2478
+ else {
2479
+ const newEnd = section.endIndex - length;
2480
+ sectionsToUpdate.push({
2481
+ id: section.id,
2482
+ newStart: section.startIndex,
2483
+ newEnd: newEnd
2484
+ });
2485
+ }
2486
+ continue;
2487
+ }
2488
+ }
2489
+ if (section.startIndex >= end) {
2490
+ sectionsToUpdate.push({
2491
+ id: section.id,
2492
+ newStart: section.startIndex - length,
2493
+ newEnd: section.endIndex - length
2494
+ });
2495
+ }
2496
+ }
2497
+ for (const section of removedSections) {
2498
+ this.sections.delete(section.id);
2499
+ this.emit('section-removed', { section });
2500
+ }
2501
+ for (const update of sectionsToUpdate) {
2502
+ const section = this.sections.get(update.id);
2503
+ if (section) {
2504
+ section.startIndex = update.newStart;
2505
+ section.endIndex = update.newEnd;
2506
+ }
2507
+ }
2508
+ if (removedSections.length > 0 || sectionsToUpdate.length > 0) {
2509
+ this.emit('sections-changed');
2510
+ }
2511
+ return removedSections;
2512
+ }
2513
+ /**
2514
+ * Validate that the given boundaries are at paragraph boundaries.
2515
+ * Also checks that conditionals don't partially overlap repeating sections.
2516
+ * @param start The proposed start index
2517
+ * @param end The proposed end index
2518
+ * @param content The text content to validate against
2519
+ * @returns true if valid, false otherwise
2520
+ */
2521
+ validateBoundaries(start, end, content) {
2522
+ if (start !== 0 && content[start - 1] !== '\n') {
2523
+ return false;
2524
+ }
2525
+ if (end !== 0 && end < content.length && content[end - 1] !== '\n') {
2526
+ return false;
2527
+ }
2528
+ if (end <= start) {
2529
+ return false;
2530
+ }
2531
+ // Check for overlapping conditional sections
2532
+ for (const existing of this.sections.values()) {
2533
+ if ((start >= existing.startIndex && start < existing.endIndex) ||
2534
+ (end > existing.startIndex && end <= existing.endIndex) ||
2535
+ (start <= existing.startIndex && end >= existing.endIndex)) {
2536
+ return false;
2537
+ }
2538
+ }
2539
+ return true;
2540
+ }
2541
+ /**
2542
+ * Get the number of conditional sections.
2543
+ */
2544
+ get count() {
2545
+ return this.sections.size;
2546
+ }
2547
+ /**
2548
+ * Check if there are any conditional sections.
2549
+ */
2550
+ get isEmpty() {
2551
+ return this.sections.size === 0;
2552
+ }
2553
+ /**
2554
+ * Clear all conditional sections.
2555
+ */
2556
+ clear() {
2557
+ const hadSections = this.sections.size > 0;
2558
+ this.sections.clear();
2559
+ if (hadSections) {
2560
+ this.emit('sections-cleared');
2561
+ }
2562
+ }
2563
+ /**
2564
+ * Serialize all sections to JSON.
2565
+ */
2566
+ toJSON() {
2567
+ return this.getSectionsSorted().map(section => ({
2568
+ id: section.id,
2569
+ predicate: section.predicate,
2570
+ startIndex: section.startIndex,
2571
+ endIndex: section.endIndex
2572
+ }));
2573
+ }
2574
+ /**
2575
+ * Load sections from serialized data.
2576
+ */
2577
+ fromJSON(data) {
2578
+ this.clear();
2579
+ for (const sectionData of data) {
2580
+ const section = {
2581
+ id: sectionData.id,
2582
+ predicate: sectionData.predicate,
2583
+ startIndex: sectionData.startIndex,
2584
+ endIndex: sectionData.endIndex
2585
+ };
2586
+ this.sections.set(section.id, section);
2587
+ const idNum = parseInt(sectionData.id.replace('cond-', ''), 10);
2588
+ if (!isNaN(idNum) && idNum >= this.nextId) {
2589
+ this.nextId = idNum + 1;
2590
+ }
2591
+ }
2592
+ this.emit('sections-loaded', { count: this.sections.size });
2593
+ }
2594
+ }
2595
+
2310
2596
  /**
2311
2597
  * HyperlinkManager - Manages hyperlinks within flowing text content
2312
2598
  */
@@ -6212,12 +6498,20 @@ class TableObject extends BaseEmbeddedObject {
6212
6498
  this._coveredCells = new Map();
6213
6499
  // Row loops for merge expansion
6214
6500
  this._rowLoops = new Map();
6501
+ // Row conditionals for conditional display
6502
+ this._rowConditionals = new Map();
6215
6503
  // Layout caching for performance
6216
6504
  this._layoutDirty = true;
6217
6505
  this._cachedRowHeights = [];
6218
6506
  this._cachedRowPositions = [];
6219
6507
  // Multi-page rendering info: pageIndex -> slice render info
6220
6508
  this._renderedSlices = new Map();
6509
+ // ============================================
6510
+ // Table Row Conditionals
6511
+ // ============================================
6512
+ this._nextCondId = 1;
6513
+ this._selectedRowLoopId = null;
6514
+ this._selectedRowConditionalId = null;
6221
6515
  // Tables ONLY support block positioning - force it regardless of config
6222
6516
  this._position = 'block';
6223
6517
  // Initialize defaults
@@ -6911,6 +7205,85 @@ class TableObject extends BaseEmbeddedObject {
6911
7205
  loopRangesOverlap(start1, end1, start2, end2) {
6912
7206
  return start1 <= end2 && start2 <= end1;
6913
7207
  }
7208
+ generateRowConditionalId() {
7209
+ return `row-cond-${this._nextCondId++}`;
7210
+ }
7211
+ /**
7212
+ * Create a row conditional.
7213
+ */
7214
+ createRowConditional(startRowIndex, endRowIndex, predicate) {
7215
+ if (startRowIndex < 0 || endRowIndex >= this._rows.length) {
7216
+ Logger.warn('[pc-editor:TableObject.createRowConditional] Invalid row range');
7217
+ return null;
7218
+ }
7219
+ if (startRowIndex > endRowIndex) {
7220
+ Logger.warn('[pc-editor:TableObject.createRowConditional] Start index must be <= end index');
7221
+ return null;
7222
+ }
7223
+ // Check for overlapping conditionals
7224
+ for (const existing of this._rowConditionals.values()) {
7225
+ if (this.loopRangesOverlap(startRowIndex, endRowIndex, existing.startRowIndex, existing.endRowIndex)) {
7226
+ Logger.warn('[pc-editor:TableObject.createRowConditional] Range overlaps with existing conditional');
7227
+ return null;
7228
+ }
7229
+ }
7230
+ const cond = {
7231
+ id: this.generateRowConditionalId(),
7232
+ predicate,
7233
+ startRowIndex,
7234
+ endRowIndex
7235
+ };
7236
+ this._rowConditionals.set(cond.id, cond);
7237
+ this.emit('row-conditional-created', { conditional: cond });
7238
+ this.emit('content-changed', {});
7239
+ return cond;
7240
+ }
7241
+ /**
7242
+ * Remove a row conditional by ID.
7243
+ */
7244
+ removeRowConditional(id) {
7245
+ const cond = this._rowConditionals.get(id);
7246
+ if (!cond)
7247
+ return false;
7248
+ this._rowConditionals.delete(id);
7249
+ this.emit('row-conditional-removed', { conditionalId: id });
7250
+ return true;
7251
+ }
7252
+ /**
7253
+ * Get a row conditional by ID.
7254
+ */
7255
+ getRowConditional(id) {
7256
+ return this._rowConditionals.get(id);
7257
+ }
7258
+ /**
7259
+ * Get all row conditionals.
7260
+ */
7261
+ getAllRowConditionals() {
7262
+ return Array.from(this._rowConditionals.values());
7263
+ }
7264
+ /**
7265
+ * Get the row conditional at a given row index.
7266
+ */
7267
+ getRowConditionalAtRow(rowIndex) {
7268
+ for (const cond of this._rowConditionals.values()) {
7269
+ if (rowIndex >= cond.startRowIndex && rowIndex <= cond.endRowIndex) {
7270
+ return cond;
7271
+ }
7272
+ }
7273
+ return undefined;
7274
+ }
7275
+ /**
7276
+ * Update a row conditional's predicate.
7277
+ */
7278
+ updateRowConditionalPredicate(id, predicate) {
7279
+ const cond = this._rowConditionals.get(id);
7280
+ if (!cond)
7281
+ return false;
7282
+ cond.predicate = predicate;
7283
+ this.emit('row-conditional-updated', { conditional: cond });
7284
+ this.emit('content-changed', {});
7285
+ return true;
7286
+ }
6914
7287
  /**
6915
7288
  * Shift row loop indices when rows are inserted or removed.
6916
7289
  * @param fromIndex The row index where insertion/removal occurred
@@ -6951,6 +7324,41 @@ class TableObject extends BaseEmbeddedObject {
6951
7324
  this._rowLoops.delete(id);
6952
7325
  this.emit('row-loop-removed', { loopId: id, reason: 'row-deleted' });
6953
7326
  }
7327
+ // Also shift row conditional indices
7328
+ this.shiftRowConditionalIndices(fromIndex, delta);
7329
+ }
7330
+ /**
7331
+ * Shift row conditional indices when rows are inserted or removed.
7332
+ */
7333
+ shiftRowConditionalIndices(fromIndex, delta) {
7334
+ const condsToRemove = [];
7335
+ for (const cond of this._rowConditionals.values()) {
7336
+ if (delta < 0) {
7337
+ const removeCount = Math.abs(delta);
7338
+ const removeEnd = fromIndex + removeCount - 1;
7339
+ if (fromIndex <= cond.endRowIndex && removeEnd >= cond.startRowIndex) {
7340
+ condsToRemove.push(cond.id);
7341
+ continue;
7342
+ }
7343
+ if (fromIndex < cond.startRowIndex) {
7344
+ cond.startRowIndex += delta;
7345
+ cond.endRowIndex += delta;
7346
+ }
7347
+ }
7348
+ else {
7349
+ if (fromIndex <= cond.startRowIndex) {
7350
+ cond.startRowIndex += delta;
7351
+ cond.endRowIndex += delta;
7352
+ }
7353
+ else if (fromIndex <= cond.endRowIndex) {
7354
+ cond.endRowIndex += delta;
7355
+ }
7356
+ }
7357
+ }
7358
+ for (const id of condsToRemove) {
7359
+ this._rowConditionals.delete(id);
7360
+ this.emit('row-conditional-removed', { conditionalId: id, reason: 'row-deleted' });
7361
+ }
6954
7362
  }
6955
7363
  /**
6956
7364
  * Get rows in a range (for loop expansion).
@@ -7758,6 +8166,9 @@ class TableObject extends BaseEmbeddedObject {
7758
8166
  result.mergedCell.markReflowDirty();
7759
8167
  }
7760
8168
  this.clearSelection();
8169
+ // Focus the anchor (top-left) cell of the merged range
8170
+ const normalized = TableCellMerger.normalizeRange(mergeRange);
8171
+ this.focusCell(normalized.start.row, normalized.start.col);
7761
8172
  this.emit('cells-merged', { range: mergeRange });
7762
8173
  this.emit('content-changed', {});
7763
8174
  }
@@ -7848,6 +8259,10 @@ class TableObject extends BaseEmbeddedObject {
7848
8259
  if (this._rowLoops.size > 0) {
7849
8260
  this.renderRowLoopIndicators(ctx);
7850
8261
  }
8262
+ // Render row conditional indicators
8263
+ if (this._rowConditionals.size > 0) {
8264
+ this.renderRowConditionalIndicators(ctx);
8265
+ }
7851
8266
  // Render cell range selection highlight
7852
8267
  if (this._selectedRange) {
7853
8268
  this.renderRangeSelection(ctx);
@@ -7862,11 +8277,54 @@ class TableObject extends BaseEmbeddedObject {
7862
8277
  }
7863
8278
  }
7864
8279
  /**
7865
- * Render row loop indicators (colored stripe on left side of loop rows).
8280
+ * Select a row loop by ID (for pane display).
7866
8281
  */
8282
+ selectRowLoop(loopId) {
8283
+ this._selectedRowLoopId = loopId;
8284
+ }
8285
+ /**
8286
+ * Get the currently selected row loop ID.
8287
+ */
8288
+ get selectedRowLoopId() {
8289
+ return this._selectedRowLoopId;
8290
+ }
8291
+ /**
8292
+ * Hit-test a point against row loop labels.
8293
+ * Point should be in table-local coordinates.
8294
+ * Returns the loop if a label was clicked, null otherwise.
8295
+ */
8296
+ getRowLoopAtPoint(point) {
8297
+ let rowPositions = this._cachedRowPositions;
8298
+ if (rowPositions.length === 0) {
8299
+ rowPositions = [];
8300
+ let y = 0;
8301
+ for (const row of this._rows) {
8302
+ rowPositions.push(y);
8303
+ y += row.calculatedHeight;
8304
+ }
8305
+ }
8306
+ for (const loop of this._rowLoops.values()) {
8307
+ const startY = rowPositions[loop.startRowIndex] || 0;
8308
+ let _endY = startY;
8309
+ for (let i = loop.startRowIndex; i <= loop.endRowIndex && i < this._rows.length; i++) {
8310
+ _endY += this._rows[i].calculatedHeight;
8311
+ }
8312
+ // Label bounds (matches rendering)
8313
+ const labelWidth = 30; // approximate for "Loop" at 10px
8314
+ const labelHeight = 10 + TableObject.LOOP_LABEL_PADDING * 2;
8315
+ const labelX = -6 - labelWidth - 4;
8316
+ const labelY = startY - labelHeight - 2;
8317
+ if (point.x >= labelX && point.x <= labelX + labelWidth + 4 &&
8318
+ point.y >= labelY && point.y <= labelY + labelHeight + 4) {
8319
+ return loop;
8320
+ }
8321
+ }
8322
+ return null;
8323
+ }
7867
8324
  renderRowLoopIndicators(ctx) {
7868
- const indicatorWidth = 4;
7869
- const labelPadding = 4;
8325
+ const color = TableObject.LOOP_COLOR;
8326
+ const padding = TableObject.LOOP_LABEL_PADDING;
8327
+ const radius = TableObject.LOOP_LABEL_RADIUS;
7870
8328
  // Calculate row Y positions if not cached
7871
8329
  let rowPositions = this._cachedRowPositions;
7872
8330
  if (rowPositions.length === 0) {
@@ -7877,12 +8335,8 @@ class TableObject extends BaseEmbeddedObject {
7877
8335
  y += row.calculatedHeight;
7878
8336
  }
7879
8337
  }
7880
- // Colors for different loops (cycle through these)
7881
- const loopColors = ['#9b59b6', '#3498db', '#e67e22', '#1abc9c', '#e74c3c'];
7882
- let colorIndex = 0;
7883
8338
  for (const loop of this._rowLoops.values()) {
7884
- const color = loopColors[colorIndex % loopColors.length];
7885
- colorIndex++;
8339
+ const isSelected = this._selectedRowLoopId === loop.id;
7886
8340
  // Calculate the Y range for this loop
7887
8341
  const startY = rowPositions[loop.startRowIndex] || 0;
7888
8342
  let endY = startY;
@@ -7892,31 +8346,149 @@ class TableObject extends BaseEmbeddedObject {
7892
8346
  const loopHeight = endY - startY;
7893
8347
  // Draw colored stripe on left side
7894
8348
  ctx.fillStyle = color;
7895
- ctx.fillRect(-indicatorWidth - 2, startY, indicatorWidth, loopHeight);
7896
- // Draw loop label on the first row
8349
+ ctx.fillRect(-6, startY, 4, loopHeight);
8350
+ // Draw vertical connector line
8351
+ ctx.strokeStyle = color;
8352
+ ctx.lineWidth = 1;
8353
+ ctx.beginPath();
8354
+ ctx.moveTo(-4, startY);
8355
+ ctx.lineTo(-4, endY);
8356
+ ctx.stroke();
8357
+ // Draw "Loop" label — matches text flow style
7897
8358
  ctx.save();
7898
8359
  ctx.font = '10px Arial';
7899
- ctx.fillStyle = color;
7900
- // Rotate text to be vertical along the stripe
7901
- const labelText = `⟳ ${loop.fieldPath}`;
7902
- const textMetrics = ctx.measureText(labelText);
7903
- // Position label to the left of the stripe
7904
- ctx.translate(-indicatorWidth - labelPadding - textMetrics.width - 4, startY + loopHeight / 2);
7905
- ctx.fillText(labelText, 0, 4);
8360
+ const labelText = 'Loop';
8361
+ const metrics = ctx.measureText(labelText);
8362
+ const boxWidth = metrics.width + padding * 2;
8363
+ const boxHeight = 10 + padding * 2;
8364
+ const labelX = -6 - boxWidth - 4;
8365
+ const labelY = startY - boxHeight - 2;
8366
+ ctx.beginPath();
8367
+ ctx.roundRect(labelX, labelY, boxWidth, boxHeight, radius);
8368
+ if (isSelected) {
8369
+ // Selected: filled background with white text
8370
+ ctx.fillStyle = color;
8371
+ ctx.fill();
8372
+ ctx.fillStyle = '#ffffff';
8373
+ }
8374
+ else {
8375
+ // Not selected: white background, outlined with colored text
8376
+ ctx.fillStyle = '#ffffff';
8377
+ ctx.fill();
8378
+ ctx.strokeStyle = color;
8379
+ ctx.lineWidth = 1.5;
8380
+ ctx.stroke();
8381
+ ctx.fillStyle = color;
8382
+ }
8383
+ ctx.textBaseline = 'middle';
8384
+ ctx.fillText(labelText, labelX + padding, labelY + boxHeight / 2);
7906
8385
  ctx.restore();
7907
- // Draw top and bottom brackets
8386
+ }
8387
+ }
8388
+ /**
8389
+ * Select a row conditional by ID (for pane display).
8390
+ */
8391
+ selectRowConditional(conditionalId) {
8392
+ this._selectedRowConditionalId = conditionalId;
8393
+ }
8394
+ /**
8395
+ * Get the currently selected row conditional ID.
8396
+ */
8397
+ get selectedRowConditionalId() {
8398
+ return this._selectedRowConditionalId;
8399
+ }
8400
+ /**
8401
+ * Hit-test a point against row conditional labels.
8402
+ * Point should be in table-local coordinates.
8403
+ */
8404
+ getRowConditionalAtPoint(point) {
8405
+ let rowPositions = this._cachedRowPositions;
8406
+ if (rowPositions.length === 0) {
8407
+ rowPositions = [];
8408
+ let y = 0;
8409
+ for (const row of this._rows) {
8410
+ rowPositions.push(y);
8411
+ y += row.calculatedHeight;
8412
+ }
8413
+ }
8414
+ for (const cond of this._rowConditionals.values()) {
8415
+ const startY = rowPositions[cond.startRowIndex] || 0;
8416
+ let _endY = startY;
8417
+ for (let i = cond.startRowIndex; i <= cond.endRowIndex && i < this._rows.length; i++) {
8418
+ _endY += this._rows[i].calculatedHeight;
8419
+ }
8420
+ // Label bounds (right side of table, offset from loop labels)
8421
+ const totalWidth = this._columns.reduce((sum, col) => sum + col.width, 0);
8422
+ const labelWidth = 22; // approximate for "If" at 10px
8423
+ const labelHeight = 10 + TableObject.LOOP_LABEL_PADDING * 2;
8424
+ const labelX = totalWidth + 10;
8425
+ const labelY = startY - labelHeight - 2;
8426
+ if (point.x >= labelX && point.x <= labelX + labelWidth + 4 &&
8427
+ point.y >= labelY && point.y <= labelY + labelHeight + 4) {
8428
+ return cond;
8429
+ }
8430
+ }
8431
+ return null;
8432
+ }
8433
+ renderRowConditionalIndicators(ctx) {
8434
+ const color = TableObject.COND_COLOR;
8435
+ const padding = TableObject.LOOP_LABEL_PADDING;
8436
+ const radius = TableObject.LOOP_LABEL_RADIUS;
8437
+ let rowPositions = this._cachedRowPositions;
8438
+ if (rowPositions.length === 0) {
8439
+ rowPositions = [];
8440
+ let y = 0;
8441
+ for (const row of this._rows) {
8442
+ rowPositions.push(y);
8443
+ y += row.calculatedHeight;
8444
+ }
8445
+ }
8446
+ const totalWidth = this._columns.reduce((sum, col) => sum + col.width, 0);
8447
+ for (const cond of this._rowConditionals.values()) {
8448
+ const isSelected = this._selectedRowConditionalId === cond.id;
8449
+ const startY = rowPositions[cond.startRowIndex] || 0;
8450
+ let endY = startY;
8451
+ for (let i = cond.startRowIndex; i <= cond.endRowIndex && i < this._rows.length; i++) {
8452
+ endY += this._rows[i].calculatedHeight;
8453
+ }
8454
+ const condHeight = endY - startY;
8455
+ // Draw colored stripe on right side
8456
+ ctx.fillStyle = color;
8457
+ ctx.fillRect(totalWidth + 2, startY, 4, condHeight);
8458
+ // Draw vertical connector line
7908
8459
  ctx.strokeStyle = color;
7909
8460
  ctx.lineWidth = 1;
7910
8461
  ctx.beginPath();
7911
- // Top bracket
7912
- ctx.moveTo(-indicatorWidth - 2, startY);
7913
- ctx.lineTo(-indicatorWidth - 6, startY);
7914
- ctx.lineTo(-indicatorWidth - 6, startY + 6);
7915
- // Bottom bracket
7916
- ctx.moveTo(-indicatorWidth - 2, endY);
7917
- ctx.lineTo(-indicatorWidth - 6, endY);
7918
- ctx.lineTo(-indicatorWidth - 6, endY - 6);
8462
+ ctx.moveTo(totalWidth + 4, startY);
8463
+ ctx.lineTo(totalWidth + 4, endY);
7919
8464
  ctx.stroke();
8465
+ // Draw "If" label
8466
+ ctx.save();
8467
+ ctx.font = '10px Arial';
8468
+ const labelText = 'If';
8469
+ const metrics = ctx.measureText(labelText);
8470
+ const boxWidth = metrics.width + padding * 2;
8471
+ const boxHeight = 10 + padding * 2;
8472
+ const labelX = totalWidth + 10;
8473
+ const labelY = startY - boxHeight - 2;
8474
+ ctx.beginPath();
8475
+ ctx.roundRect(labelX, labelY, boxWidth, boxHeight, radius);
8476
+ if (isSelected) {
8477
+ ctx.fillStyle = color;
8478
+ ctx.fill();
8479
+ ctx.fillStyle = '#ffffff';
8480
+ }
8481
+ else {
8482
+ ctx.fillStyle = '#ffffff';
8483
+ ctx.fill();
8484
+ ctx.strokeStyle = color;
8485
+ ctx.lineWidth = 1.5;
8486
+ ctx.stroke();
8487
+ ctx.fillStyle = color;
8488
+ }
8489
+ ctx.textBaseline = 'middle';
8490
+ ctx.fillText(labelText, labelX + padding, labelY + boxHeight / 2);
8491
+ ctx.restore();
7920
8492
  }
7921
8493
  }
7922
8494
  /**
@@ -8035,6 +8607,14 @@ class TableObject extends BaseEmbeddedObject {
8035
8607
  columns: this._columns.map(col => ({ ...col })),
8036
8608
  rows: this._rows.map(row => row.toData()),
8037
8609
  rowLoops,
8610
+ rowConditionals: this._rowConditionals.size > 0
8611
+ ? Array.from(this._rowConditionals.values()).map(c => ({
8612
+ id: c.id,
8613
+ predicate: c.predicate,
8614
+ startRowIndex: c.startRowIndex,
8615
+ endRowIndex: c.endRowIndex
8616
+ }))
8617
+ : undefined,
8038
8618
  defaultCellPadding: this._defaultCellPadding,
8039
8619
  defaultBorderColor: this._defaultBorderColor,
8040
8620
  defaultBorderWidth: this._defaultBorderWidth,
@@ -8078,6 +8658,17 @@ class TableObject extends BaseEmbeddedObject {
8078
8658
  });
8079
8659
  }
8080
8660
  }
8661
+ // Load row conditionals if present
8662
+ if (data.data.rowConditionals) {
8663
+ for (const condData of data.data.rowConditionals) {
8664
+ table._rowConditionals.set(condData.id, {
8665
+ id: condData.id,
8666
+ predicate: condData.predicate,
8667
+ startRowIndex: condData.startRowIndex,
8668
+ endRowIndex: condData.endRowIndex
8669
+ });
8670
+ }
8671
+ }
8081
8672
  table.updateCoveredCells();
8082
8673
  return table;
8083
8674
  }
@@ -8107,6 +8698,18 @@ class TableObject extends BaseEmbeddedObject {
8107
8698
  });
8108
8699
  }
8109
8700
  }
8701
+ // Restore row conditionals if any
8702
+ this._rowConditionals.clear();
8703
+ if (data.data.rowConditionals) {
8704
+ for (const condData of data.data.rowConditionals) {
8705
+ this._rowConditionals.set(condData.id, {
8706
+ id: condData.id,
8707
+ predicate: condData.predicate,
8708
+ startRowIndex: condData.startRowIndex,
8709
+ endRowIndex: condData.endRowIndex
8710
+ });
8711
+ }
8712
+ }
8110
8713
  // Restore defaults
8111
8714
  if (data.data.defaultCellPadding !== undefined) {
8112
8715
  this._defaultCellPadding = data.data.defaultCellPadding;
@@ -8126,6 +8729,13 @@ class TableObject extends BaseEmbeddedObject {
8126
8729
  return TableObject.fromData(this.toData());
8127
8730
  }
8128
8731
  }
8732
+ /**
8733
+ * Render row loop indicators (colored stripe on left side of loop rows).
8734
+ */
8735
+ TableObject.LOOP_COLOR = '#6B46C1';
8736
+ TableObject.LOOP_LABEL_PADDING = 4;
8737
+ TableObject.LOOP_LABEL_RADIUS = 4;
8738
+ TableObject.COND_COLOR = '#D97706'; // Orange
8129
8739
 
8130
8740
  /**
8131
8741
  * TableResizeHandler - Handles column and row resize operations for tables.
@@ -8511,6 +9121,7 @@ class FlowingTextContent extends EventEmitter {
8511
9121
  this.substitutionFields = new SubstitutionFieldManager();
8512
9122
  this.embeddedObjects = new EmbeddedObjectManager();
8513
9123
  this.repeatingSections = new RepeatingSectionManager();
9124
+ this.conditionalSections = new ConditionalSectionManager();
8514
9125
  this.hyperlinks = new HyperlinkManager();
8515
9126
  this.layout = new TextLayout();
8516
9127
  this.setupEventForwarding();
@@ -8548,6 +9159,7 @@ class FlowingTextContent extends EventEmitter {
8548
9159
  this.substitutionFields.handleDeletion(data.start, data.length);
8549
9160
  this.embeddedObjects.handleDeletion(data.start, data.length);
8550
9161
  this.repeatingSections.handleDeletion(data.start, data.length);
9162
+ this.conditionalSections.handleDeletion(data.start, data.length);
8551
9163
  this.paragraphFormatting.handleDeletion(data.start, data.length);
8552
9164
  this.hyperlinks.handleDeletion(data.start, data.length);
8553
9165
  this.emit('content-changed', {
@@ -8599,6 +9211,16 @@ class FlowingTextContent extends EventEmitter {
8599
9211
  this.repeatingSections.on('section-updated', (data) => {
8600
9212
  this.emit('repeating-section-updated', data);
8601
9213
  });
9214
+ // Forward conditional section events
9215
+ this.conditionalSections.on('section-added', (data) => {
9216
+ this.emit('conditional-section-added', data);
9217
+ });
9218
+ this.conditionalSections.on('section-removed', (data) => {
9219
+ this.emit('conditional-section-removed', data);
9220
+ });
9221
+ this.conditionalSections.on('section-updated', (data) => {
9222
+ this.emit('conditional-section-updated', data);
9223
+ });
8602
9224
  // Forward hyperlink events
8603
9225
  this.hyperlinks.on('hyperlink-added', (data) => {
8604
9226
  this.emit('hyperlink-added', data);
@@ -8656,6 +9278,7 @@ class FlowingTextContent extends EventEmitter {
8656
9278
  this.substitutionFields.shiftFields(insertAt, text.length);
8657
9279
  this.embeddedObjects.shiftObjects(insertAt, text.length);
8658
9280
  this.repeatingSections.shiftSections(insertAt, text.length);
9281
+ this.conditionalSections.shiftSections(insertAt, text.length);
8659
9282
  this.hyperlinks.shiftHyperlinks(insertAt, text.length);
8660
9283
  // Insert the text first so we have the full content
8661
9284
  this.textState.insertText(text, insertAt);
@@ -8728,6 +9351,7 @@ class FlowingTextContent extends EventEmitter {
8728
9351
  this.substitutionFields.shiftFields(position, text.length);
8729
9352
  this.embeddedObjects.shiftObjects(position, text.length);
8730
9353
  this.repeatingSections.shiftSections(position, text.length);
9354
+ this.conditionalSections.shiftSections(position, text.length);
8731
9355
  this.hyperlinks.shiftHyperlinks(position, text.length);
8732
9356
  // Insert the text
8733
9357
  const content = this.textState.getText();
@@ -8745,6 +9369,7 @@ class FlowingTextContent extends EventEmitter {
8745
9369
  this.substitutionFields.handleDeletion(position, length);
8746
9370
  this.embeddedObjects.handleDeletion(position, length);
8747
9371
  this.repeatingSections.handleDeletion(position, length);
9372
+ this.conditionalSections.handleDeletion(position, length);
8748
9373
  this.paragraphFormatting.handleDeletion(position, length);
8749
9374
  this.hyperlinks.handleDeletion(position, length);
8750
9375
  // Delete the text
@@ -9134,6 +9759,7 @@ class FlowingTextContent extends EventEmitter {
9134
9759
  this.substitutionFields.shiftFields(insertAt, 1);
9135
9760
  this.embeddedObjects.shiftObjects(insertAt, 1);
9136
9761
  this.repeatingSections.shiftSections(insertAt, 1);
9762
+ this.conditionalSections.shiftSections(insertAt, 1);
9137
9763
  // Insert the placeholder character
9138
9764
  this.textState.insertText(OBJECT_REPLACEMENT_CHAR, insertAt);
9139
9765
  // Shift paragraph formatting with the complete content
@@ -9381,6 +10007,7 @@ class FlowingTextContent extends EventEmitter {
9381
10007
  this.substitutionFields.clear();
9382
10008
  this.embeddedObjects.clear();
9383
10009
  this.repeatingSections.clear();
10010
+ this.conditionalSections.clear();
9384
10011
  this.hyperlinks.clear();
9385
10012
  }
9386
10013
  // ============================================
@@ -9737,44 +10364,60 @@ class FlowingTextContent extends EventEmitter {
9737
10364
  // List Operations
9738
10365
  // ============================================
9739
10366
  /**
9740
- * Toggle bullet list for the current paragraph (or selection).
10367
+ * Get paragraph starts affected by the current selection or cursor position.
9741
10368
  */
9742
- toggleBulletList() {
10369
+ getAffectedParagraphStarts() {
10370
+ const content = this.textState.getText();
10371
+ const selection = this.getSelection();
10372
+ if (selection && selection.start !== selection.end) {
10373
+ return this.paragraphFormatting.getParagraphBoundariesInRange(selection.start, selection.end, content);
10374
+ }
9743
10375
  const cursorPos = this.textState.getCursorPosition();
10376
+ return [this.paragraphFormatting.getParagraphStart(cursorPos, content)];
10377
+ }
10378
+ /**
10379
+ * Toggle bullet list for the current paragraph(s) in selection.
10380
+ */
10381
+ toggleBulletList() {
9744
10382
  const content = this.textState.getText();
9745
- const paragraphStart = this.paragraphFormatting.getParagraphStart(cursorPos, content);
9746
- this.paragraphFormatting.toggleList(paragraphStart, 'bullet');
9747
- this.emit('content-changed', { text: content, cursorPosition: cursorPos });
10383
+ const paragraphStarts = this.getAffectedParagraphStarts();
10384
+ for (const start of paragraphStarts) {
10385
+ this.paragraphFormatting.toggleList(start, 'bullet');
10386
+ }
10387
+ this.emit('content-changed', { text: content, cursorPosition: this.textState.getCursorPosition() });
9748
10388
  }
9749
10389
  /**
9750
- * Toggle numbered list for the current paragraph (or selection).
10390
+ * Toggle numbered list for the current paragraph(s) in selection.
9751
10391
  */
9752
10392
  toggleNumberedList() {
9753
- const cursorPos = this.textState.getCursorPosition();
9754
10393
  const content = this.textState.getText();
9755
- const paragraphStart = this.paragraphFormatting.getParagraphStart(cursorPos, content);
9756
- this.paragraphFormatting.toggleList(paragraphStart, 'number');
9757
- this.emit('content-changed', { text: content, cursorPosition: cursorPos });
10394
+ const paragraphStarts = this.getAffectedParagraphStarts();
10395
+ for (const start of paragraphStarts) {
10396
+ this.paragraphFormatting.toggleList(start, 'number');
10397
+ }
10398
+ this.emit('content-changed', { text: content, cursorPosition: this.textState.getCursorPosition() });
9758
10399
  }
9759
10400
  /**
9760
- * Indent the current paragraph (increase list nesting level).
10401
+ * Indent the current paragraph(s) in selection.
9761
10402
  */
9762
10403
  indentParagraph() {
9763
- const cursorPos = this.textState.getCursorPosition();
9764
10404
  const content = this.textState.getText();
9765
- const paragraphStart = this.paragraphFormatting.getParagraphStart(cursorPos, content);
9766
- this.paragraphFormatting.indentParagraph(paragraphStart);
9767
- this.emit('content-changed', { text: content, cursorPosition: cursorPos });
10405
+ const paragraphStarts = this.getAffectedParagraphStarts();
10406
+ for (const start of paragraphStarts) {
10407
+ this.paragraphFormatting.indentParagraph(start);
10408
+ }
10409
+ this.emit('content-changed', { text: content, cursorPosition: this.textState.getCursorPosition() });
9768
10410
  }
9769
10411
  /**
9770
- * Outdent the current paragraph (decrease list nesting level).
10412
+ * Outdent the current paragraph(s) in selection.
9771
10413
  */
9772
10414
  outdentParagraph() {
9773
- const cursorPos = this.textState.getCursorPosition();
9774
10415
  const content = this.textState.getText();
9775
- const paragraphStart = this.paragraphFormatting.getParagraphStart(cursorPos, content);
9776
- this.paragraphFormatting.outdentParagraph(paragraphStart);
9777
- this.emit('content-changed', { text: content, cursorPosition: cursorPos });
10416
+ const paragraphStarts = this.getAffectedParagraphStarts();
10417
+ for (const start of paragraphStarts) {
10418
+ this.paragraphFormatting.outdentParagraph(start);
10419
+ }
10420
+ this.emit('content-changed', { text: content, cursorPosition: this.textState.getCursorPosition() });
9778
10421
  }
9779
10422
  /**
9780
10423
  * Get the list formatting for the current paragraph.
@@ -9936,6 +10579,79 @@ class FlowingTextContent extends EventEmitter {
9936
10579
  return result;
9937
10580
  }
9938
10581
  // ============================================
10582
+ // Conditional Section Operations
10583
+ // ============================================
10584
+ /**
10585
+ * Get the conditional section manager.
10586
+ */
10587
+ getConditionalSectionManager() {
10588
+ return this.conditionalSections;
10589
+ }
10590
+ /**
10591
+ * Get all conditional sections.
10592
+ */
10593
+ getConditionalSections() {
10594
+ return this.conditionalSections.getSections();
10595
+ }
10596
+ /**
10597
+ * Create a conditional section.
10598
+ * @param startIndex Text index at paragraph start (must be at a paragraph boundary)
10599
+ * @param endIndex Text index at closing paragraph start (must be at a paragraph boundary)
10600
+ * @param predicate The predicate expression to evaluate
10601
+ * @returns The created section, or null if boundaries are invalid
10602
+ */
10603
+ createConditionalSection(startIndex, endIndex, predicate) {
10604
+ const content = this.textState.getText();
10605
+ if (!this.conditionalSections.validateBoundaries(startIndex, endIndex, content)) {
10606
+ return null;
10607
+ }
10608
+ const section = this.conditionalSections.create(startIndex, endIndex, predicate);
10609
+ this.emit('content-changed', {
10610
+ text: content,
10611
+ cursorPosition: this.textState.getCursorPosition()
10612
+ });
10613
+ return section;
10614
+ }
10615
+ /**
10616
+ * Remove a conditional section by ID.
10617
+ */
10618
+ removeConditionalSection(id) {
10619
+ const section = this.conditionalSections.remove(id);
10620
+ if (section) {
10621
+ this.emit('content-changed', {
10622
+ text: this.textState.getText(),
10623
+ cursorPosition: this.textState.getCursorPosition()
10624
+ });
10625
+ return true;
10626
+ }
10627
+ return false;
10628
+ }
10629
+ /**
10630
+ * Get a conditional section by ID.
10631
+ */
10632
+ getConditionalSection(id) {
10633
+ return this.conditionalSections.getSection(id);
10634
+ }
10635
+ /**
10636
+ * Find a conditional section that has a boundary at the given text index.
10637
+ */
10638
+ getConditionalSectionAtBoundary(textIndex) {
10639
+ return this.conditionalSections.getSectionAtBoundary(textIndex);
10640
+ }
10641
+ /**
10642
+ * Update a conditional section's predicate.
10643
+ */
10644
+ updateConditionalSectionPredicate(id, predicate) {
10645
+ const result = this.conditionalSections.updatePredicate(id, predicate);
10646
+ if (result) {
10647
+ this.emit('content-changed', {
10648
+ text: this.textState.getText(),
10649
+ cursorPosition: this.textState.getCursorPosition()
10650
+ });
10651
+ }
10652
+ return result;
10653
+ }
10654
+ // ============================================
9939
10655
  // Serialization
9940
10656
  // ============================================
9941
10657
  /**
@@ -9989,6 +10705,8 @@ class FlowingTextContent extends EventEmitter {
9989
10705
  }));
9990
10706
  // Serialize repeating sections
9991
10707
  const repeatingSectionsData = this.repeatingSections.toJSON();
10708
+ // Serialize conditional sections
10709
+ const conditionalSectionsData = this.conditionalSections.toJSON();
9992
10710
  // Serialize embedded objects
9993
10711
  const embeddedObjects = [];
9994
10712
  const objectsMap = this.embeddedObjects.getObjects();
@@ -10006,6 +10724,7 @@ class FlowingTextContent extends EventEmitter {
10006
10724
  paragraphFormatting: paragraphFormatting.length > 0 ? paragraphFormatting : undefined,
10007
10725
  substitutionFields: substitutionFieldsData.length > 0 ? substitutionFieldsData : undefined,
10008
10726
  repeatingSections: repeatingSectionsData.length > 0 ? repeatingSectionsData : undefined,
10727
+ conditionalSections: conditionalSectionsData.length > 0 ? conditionalSectionsData : undefined,
10009
10728
  embeddedObjects: embeddedObjects.length > 0 ? embeddedObjects : undefined,
10010
10729
  hyperlinks: hyperlinksData.length > 0 ? hyperlinksData : undefined
10011
10730
  };
@@ -10044,6 +10763,10 @@ class FlowingTextContent extends EventEmitter {
10044
10763
  if (data.repeatingSections && data.repeatingSections.length > 0) {
10045
10764
  content.getRepeatingSectionManager().fromJSON(data.repeatingSections);
10046
10765
  }
10766
+ // Restore conditional sections
10767
+ if (data.conditionalSections && data.conditionalSections.length > 0) {
10768
+ content.getConditionalSectionManager().fromJSON(data.conditionalSections);
10769
+ }
10047
10770
  // Restore embedded objects using factory
10048
10771
  if (data.embeddedObjects && data.embeddedObjects.length > 0) {
10049
10772
  for (const ref of data.embeddedObjects) {
@@ -10098,6 +10821,10 @@ class FlowingTextContent extends EventEmitter {
10098
10821
  if (data.repeatingSections && data.repeatingSections.length > 0) {
10099
10822
  this.repeatingSections.fromJSON(data.repeatingSections);
10100
10823
  }
10824
+ // Restore conditional sections
10825
+ if (data.conditionalSections && data.conditionalSections.length > 0) {
10826
+ this.conditionalSections.fromJSON(data.conditionalSections);
10827
+ }
10101
10828
  // Restore embedded objects
10102
10829
  if (data.embeddedObjects && data.embeddedObjects.length > 0) {
10103
10830
  for (const ref of data.embeddedObjects) {
@@ -10117,6 +10844,349 @@ class FlowingTextContent extends EventEmitter {
10117
10844
  }
10118
10845
  }
10119
10846
 
10847
+ /**
10848
+ * Simple recursive-descent predicate evaluator.
10849
+ * Supports:
10850
+ * - Truthiness: `isActive`
10851
+ * - Negation: `!isActive`
10852
+ * - Comparisons: ==, !=, >, <, >=, <=
10853
+ * - Logical: &&, ||, parentheses
10854
+ * - Literals: "approved", 100, true/false
10855
+ * - Dot notation: customer.isVIP
10856
+ */
10857
+ function tokenize(input) {
10858
+ const tokens = [];
10859
+ let i = 0;
10860
+ while (i < input.length) {
10861
+ const ch = input[i];
10862
+ // Skip whitespace
10863
+ if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
10864
+ i++;
10865
+ continue;
10866
+ }
10867
+ // Parentheses
10868
+ if (ch === '(' || ch === ')') {
10869
+ tokens.push({ type: 'paren', value: ch });
10870
+ i++;
10871
+ continue;
10872
+ }
10873
+ // Two-character operators
10874
+ if (i + 1 < input.length) {
10875
+ const two = input[i] + input[i + 1];
10876
+ if (two === '==' || two === '!=' || two === '>=' || two === '<=' || two === '&&' || two === '||' || two === '=~' || two === '!~') {
10877
+ tokens.push({ type: 'operator', value: two });
10878
+ i += 2;
10879
+ continue;
10880
+ }
10881
+ }
10882
+ // Single-character operators
10883
+ if (ch === '>' || ch === '<') {
10884
+ tokens.push({ type: 'operator', value: ch });
10885
+ i++;
10886
+ continue;
10887
+ }
10888
+ // Not operator
10889
+ if (ch === '!') {
10890
+ tokens.push({ type: 'not' });
10891
+ i++;
10892
+ continue;
10893
+ }
10894
+ // String literals
10895
+ if (ch === '"' || ch === "'") {
10896
+ const quote = ch;
10897
+ i++;
10898
+ let str = '';
10899
+ while (i < input.length && input[i] !== quote) {
10900
+ if (input[i] === '\\' && i + 1 < input.length) {
10901
+ i++;
10902
+ str += input[i];
10903
+ }
10904
+ else {
10905
+ str += input[i];
10906
+ }
10907
+ i++;
10908
+ }
10909
+ i++; // skip closing quote
10910
+ tokens.push({ type: 'string', value: str });
10911
+ continue;
10912
+ }
10913
+ // Numbers
10914
+ if (ch >= '0' && ch <= '9') {
10915
+ let num = '';
10916
+ while (i < input.length && ((input[i] >= '0' && input[i] <= '9') || input[i] === '.')) {
10917
+ num += input[i];
10918
+ i++;
10919
+ }
10920
+ tokens.push({ type: 'number', value: parseFloat(num) });
10921
+ continue;
10922
+ }
10923
+ // Identifiers (including dot notation: customer.isVIP)
10924
+ if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '$') {
10925
+ let ident = '';
10926
+ while (i < input.length &&
10927
+ ((input[i] >= 'a' && input[i] <= 'z') ||
10928
+ (input[i] >= 'A' && input[i] <= 'Z') ||
10929
+ (input[i] >= '0' && input[i] <= '9') ||
10930
+ input[i] === '_' || input[i] === '$' || input[i] === '.')) {
10931
+ ident += input[i];
10932
+ i++;
10933
+ }
10934
+ if (ident === 'true') {
10935
+ tokens.push({ type: 'boolean', value: true });
10936
+ }
10937
+ else if (ident === 'false') {
10938
+ tokens.push({ type: 'boolean', value: false });
10939
+ }
10940
+ else {
10941
+ tokens.push({ type: 'identifier', value: ident });
10942
+ }
10943
+ continue;
10944
+ }
10945
+ // Unknown character — skip
10946
+ i++;
10947
+ }
10948
+ tokens.push({ type: 'eof' });
10949
+ return tokens;
10950
+ }
10951
+ class Parser {
10952
+ constructor(tokens, data) {
10953
+ this.pos = 0;
10954
+ this.tokens = tokens;
10955
+ this.data = data;
10956
+ }
10957
+ peek() {
10958
+ return this.tokens[this.pos];
10959
+ }
10960
+ advance() {
10961
+ const token = this.tokens[this.pos];
10962
+ this.pos++;
10963
+ return token;
10964
+ }
10965
+ /**
10966
+ * Parse the full expression.
10967
+ * Grammar:
10968
+ * expr → or_expr
10969
+ * or_expr → and_expr ('||' and_expr)*
10970
+ * and_expr → unary (('==' | '!=' | '>' | '<' | '>=' | '<=') unary)?
10971
+ * | unary ('&&' unary_or_comparison)*
10972
+ * unary → '!' unary | primary
10973
+ * primary → '(' expr ')' | literal | identifier
10974
+ */
10975
+ parse() {
10976
+ const result = this.parseOr();
10977
+ return result;
10978
+ }
10979
+ parseOr() {
10980
+ let left = this.parseAnd();
10981
+ while (this.peek().type === 'operator' && this.peek().value === '||') {
10982
+ this.advance();
10983
+ const right = this.parseAnd();
10984
+ left = this.isTruthy(left) || this.isTruthy(right);
10985
+ }
10986
+ return left;
10987
+ }
10988
+ parseAnd() {
10989
+ let left = this.parseComparison();
10990
+ while (this.peek().type === 'operator' && this.peek().value === '&&') {
10991
+ this.advance();
10992
+ const right = this.parseComparison();
10993
+ left = this.isTruthy(left) && this.isTruthy(right);
10994
+ }
10995
+ return left;
10996
+ }
10997
+ parseComparison() {
10998
+ const left = this.parseUnary();
10999
+ const token = this.peek();
11000
+ if (token.type === 'operator') {
11001
+ const op = token.value;
11002
+ if (op === '==' || op === '!=' || op === '>' || op === '<' || op === '>=' || op === '<=' || op === '=~' || op === '!~') {
11003
+ this.advance();
11004
+ const right = this.parseUnary();
11005
+ return this.compare(left, op, right);
11006
+ }
11007
+ }
11008
+ return left;
11009
+ }
11010
+ parseUnary() {
11011
+ if (this.peek().type === 'not') {
11012
+ this.advance();
11013
+ const value = this.parseUnary();
11014
+ return !this.isTruthy(value);
11015
+ }
11016
+ return this.parsePrimary();
11017
+ }
11018
+ parsePrimary() {
11019
+ const token = this.peek();
11020
+ if (token.type === 'paren' && token.value === '(') {
11021
+ this.advance();
11022
+ const value = this.parseOr();
11023
+ // Consume closing paren
11024
+ if (this.peek().type === 'paren' && this.peek().value === ')') {
11025
+ this.advance();
11026
+ }
11027
+ return value;
11028
+ }
11029
+ if (token.type === 'string') {
11030
+ this.advance();
11031
+ return token.value;
11032
+ }
11033
+ if (token.type === 'number') {
11034
+ this.advance();
11035
+ return token.value;
11036
+ }
11037
+ if (token.type === 'boolean') {
11038
+ this.advance();
11039
+ return token.value;
11040
+ }
11041
+ if (token.type === 'identifier') {
11042
+ this.advance();
11043
+ return this.resolveIdentifier(token.value);
11044
+ }
11045
+ // EOF or unexpected — return undefined
11046
+ this.advance();
11047
+ return undefined;
11048
+ }
11049
+ resolveIdentifier(path) {
11050
+ const parts = path.split('.');
11051
+ let current = this.data;
11052
+ for (const part of parts) {
11053
+ if (current === null || current === undefined) {
11054
+ return undefined;
11055
+ }
11056
+ if (typeof current === 'object') {
11057
+ current = current[part];
11058
+ }
11059
+ else {
11060
+ return undefined;
11061
+ }
11062
+ }
11063
+ return current;
11064
+ }
11065
+ compare(left, op, right) {
11066
+ // Regex match: left is coerced to string, right is the pattern string
11067
+ if (op === '=~' || op === '!~') {
11068
+ const str = this.toString(left);
11069
+ const pattern = this.toString(right);
11070
+ try {
11071
+ const regex = new RegExp(pattern);
11072
+ const matches = regex.test(str);
11073
+ return op === '=~' ? matches : !matches;
11074
+ }
11075
+ catch {
11076
+ // Invalid regex pattern — treat as no match
11077
+ return op === '!~';
11078
+ }
11079
+ }
11080
+ // For ordering operators, coerce both sides to numbers if either side is numeric
11081
+ if (op === '>' || op === '<' || op === '>=' || op === '<=') {
11082
+ const l = this.toNumber(left);
11083
+ const r = this.toNumber(right);
11084
+ switch (op) {
11085
+ case '>': return l > r;
11086
+ case '<': return l < r;
11087
+ case '>=': return l >= r;
11088
+ case '<=': return l <= r;
11089
+ }
11090
+ }
11091
+ // For equality, coerce to numbers if both sides look numeric
11092
+ const ln = this.toNumberIfNumeric(left);
11093
+ const rn = this.toNumberIfNumeric(right);
11094
+ switch (op) {
11095
+ case '==': return ln == rn; // eslint-disable-line eqeqeq
11096
+ case '!=': return ln != rn; // eslint-disable-line eqeqeq
11097
+ default: return false;
11098
+ }
11099
+ }
11100
+ /**
11101
+ * Convert a value to a string for regex matching.
11102
+ */
11103
+ toString(value) {
11104
+ if (value === null || value === undefined)
11105
+ return '';
11106
+ if (typeof value === 'string')
11107
+ return value;
11108
+ return String(value);
11109
+ }
11110
+ /**
11111
+ * Convert a value to a number. Strings that look like numbers are parsed.
11112
+ * Non-numeric values become NaN.
11113
+ */
11114
+ toNumber(value) {
11115
+ if (typeof value === 'number')
11116
+ return value;
11117
+ if (typeof value === 'string') {
11118
+ const n = Number(value);
11119
+ return isNaN(n) ? NaN : n;
11120
+ }
11121
+ if (typeof value === 'boolean')
11122
+ return value ? 1 : 0;
11123
+ return NaN;
11124
+ }
11125
+ /**
11126
+ * If a value is a string that looks like a number, convert it.
11127
+ * Otherwise return the value as-is. Used for == / != so that
11128
+ * "5" == 5 is true but "hello" == "hello" still works.
11129
+ */
11130
+ toNumberIfNumeric(value) {
11131
+ if (typeof value === 'string' && value.length > 0) {
11132
+ const n = Number(value);
11133
+ if (!isNaN(n))
11134
+ return n;
11135
+ }
11136
+ return value;
11137
+ }
11138
+ isTruthy(value) {
11139
+ if (value === null || value === undefined)
11140
+ return false;
11141
+ if (typeof value === 'boolean')
11142
+ return value;
11143
+ if (typeof value === 'number')
11144
+ return value !== 0;
11145
+ if (typeof value === 'string')
11146
+ return value.length > 0;
11147
+ if (Array.isArray(value))
11148
+ return value.length > 0;
11149
+ return true;
11150
+ }
11151
+ }
11152
+ /**
11153
+ * Static predicate evaluator for conditional sections.
11154
+ */
11155
+ class PredicateEvaluator {
11156
+ /**
11157
+ * Evaluate a predicate expression against data.
11158
+ * @param predicate The predicate string (e.g., "isActive", "count > 0")
11159
+ * @param data The data context to evaluate against
11160
+ * @returns true if the predicate is truthy, false otherwise
11161
+ */
11162
+ static evaluate(predicate, data) {
11163
+ if (!predicate || predicate.trim().length === 0) {
11164
+ return false;
11165
+ }
11166
+ try {
11167
+ const tokens = tokenize(predicate.trim());
11168
+ const parser = new Parser(tokens, data);
11169
+ const result = parser.parse();
11170
+ // Convert result to boolean
11171
+ if (result === null || result === undefined)
11172
+ return false;
11173
+ if (typeof result === 'boolean')
11174
+ return result;
11175
+ if (typeof result === 'number')
11176
+ return result !== 0;
11177
+ if (typeof result === 'string')
11178
+ return result.length > 0;
11179
+ if (Array.isArray(result))
11180
+ return result.length > 0;
11181
+ return true;
11182
+ }
11183
+ catch {
11184
+ // If evaluation fails, treat as false
11185
+ return false;
11186
+ }
11187
+ }
11188
+ }
11189
+
10120
11190
  /**
10121
11191
  * Abstract base class providing common functionality for regions.
10122
11192
  */
@@ -11152,6 +12222,11 @@ const LOOP_INDICATOR_COLOR = '#6B46C1'; // Purple
11152
12222
  const LOOP_LABEL_PADDING = 4;
11153
12223
  const LOOP_LABEL_RADIUS = 4;
11154
12224
  const LOOP_LINE_DASH = [4, 4];
12225
+ // Conditional section indicator styling
12226
+ const COND_INDICATOR_COLOR = '#D97706'; // Orange
12227
+ const COND_LABEL_PADDING = 4;
12228
+ const COND_LABEL_RADIUS = 4;
12229
+ const COND_LINE_DASH = [4, 4];
11155
12230
  // Hyperlink styling
11156
12231
  const DEFAULT_HYPERLINK_COLOR = '#0066CC'; // Blue
11157
12232
  class FlowingTextRenderer extends EventEmitter {
@@ -11409,8 +12484,6 @@ class FlowingTextRenderer extends EventEmitter {
11409
12484
  if (pageIndex === 0) {
11410
12485
  // Clear table continuations when starting a new render cycle
11411
12486
  this.clearTableContinuations();
11412
- // Clear content hit targets - they will be re-registered during render
11413
- this._hitTestManager.clearCategory('content');
11414
12487
  // This is the first page, flow all text
11415
12488
  const flowedPages = this.flowTextForPage(page, ctx, contentBounds);
11416
12489
  this.flowedPages.set(page.id, flowedPages);
@@ -11710,6 +12783,8 @@ class FlowingTextRenderer extends EventEmitter {
11710
12783
  const pageCount = firstPage ? (this.flowedPages.get(firstPage.id)?.length || 1) : 1;
11711
12784
  // Get hyperlinks for rendering
11712
12785
  const hyperlinks = flowingContent.getAllHyperlinks();
12786
+ // Track relative objects to render after all lines (so they appear on top)
12787
+ const relativeObjects = [];
11713
12788
  // Render each line
11714
12789
  let y = bounds.y;
11715
12790
  for (let lineIndex = 0; lineIndex < flowedLines.length; lineIndex++) {
@@ -11722,6 +12797,18 @@ class FlowingTextRenderer extends EventEmitter {
11722
12797
  if (clipToBounds && y > bounds.y + bounds.height) {
11723
12798
  break;
11724
12799
  }
12800
+ // Collect relative objects from this line
12801
+ if (line.embeddedObjects) {
12802
+ for (const embeddedObj of line.embeddedObjects) {
12803
+ if (embeddedObj.isAnchor && embeddedObj.object.position === 'relative') {
12804
+ relativeObjects.push({
12805
+ object: embeddedObj.object,
12806
+ anchorX: bounds.x,
12807
+ anchorY: y
12808
+ });
12809
+ }
12810
+ }
12811
+ }
11725
12812
  this.renderFlowedLine(line, ctx, { x: bounds.x, y }, maxWidth, pageIndex, cursorTextIndex, pageCount, hyperlinks);
11726
12813
  y += line.height;
11727
12814
  }
@@ -11732,6 +12819,10 @@ class FlowingTextRenderer extends EventEmitter {
11732
12819
  if (clipToBounds) {
11733
12820
  ctx.restore();
11734
12821
  }
12822
+ // Render relative objects on top of text (outside clip region)
12823
+ if (relativeObjects.length > 0) {
12824
+ this.renderRelativeObjects(relativeObjects, ctx, pageIndex);
12825
+ }
11735
12826
  }
11736
12827
  /**
11737
12828
  * Render selection highlight for a region.
@@ -13887,43 +14978,256 @@ class FlowingTextRenderer extends EventEmitter {
13887
14978
  }
13888
14979
  y += line.height;
13889
14980
  }
13890
- // Check if text index is just past the last line (end of content)
13891
- if (flowedPage.lines.length > 0) {
13892
- const lastLine = flowedPage.lines[flowedPage.lines.length - 1];
13893
- if (textIndex === lastLine.endIndex + 1) {
13894
- return { y, lineIndex: flowedPage.lines.length - 1 };
13895
- }
14981
+ // Check if text index is just past the last line (end of content)
14982
+ if (flowedPage.lines.length > 0) {
14983
+ const lastLine = flowedPage.lines[flowedPage.lines.length - 1];
14984
+ if (textIndex === lastLine.endIndex + 1) {
14985
+ return { y, lineIndex: flowedPage.lines.length - 1 };
14986
+ }
14987
+ }
14988
+ return null;
14989
+ }
14990
+ /**
14991
+ * Check if a section spans across a flowed page (starts before and ends after).
14992
+ */
14993
+ sectionSpansPage(section, flowedPage) {
14994
+ if (flowedPage.lines.length === 0)
14995
+ return false;
14996
+ const pageStart = flowedPage.startIndex;
14997
+ const pageEnd = flowedPage.endIndex;
14998
+ // Section spans this page if it started before and ends after
14999
+ return section.startIndex < pageStart && section.endIndex > pageEnd;
15000
+ }
15001
+ /**
15002
+ * Get a repeating section at a point (for click detection).
15003
+ * Checks if the point is on the Loop label or vertical connector.
15004
+ */
15005
+ getRepeatingSectionAtPoint(point, sections, _pageIndex, pageBounds, contentBounds, flowedPage) {
15006
+ const labelX = pageBounds.x + 5;
15007
+ const labelWidth = 32;
15008
+ const connectorX = labelX + labelWidth / 2;
15009
+ const hitRadius = 10; // Pixels for click detection
15010
+ for (const section of sections) {
15011
+ const startInfo = this.findLineYForTextIndex(flowedPage, section.startIndex, contentBounds);
15012
+ const endInfo = this.findLineYForTextIndex(flowedPage, section.endIndex, contentBounds);
15013
+ const sectionSpansThisPage = this.sectionSpansPage(section, flowedPage);
15014
+ if (!startInfo && !endInfo && !sectionSpansThisPage) {
15015
+ continue;
15016
+ }
15017
+ // Check if click is on the Loop label
15018
+ if (startInfo) {
15019
+ const labelY = startInfo.y - 10;
15020
+ const labelHeight = 18;
15021
+ if (point.x >= labelX &&
15022
+ point.x <= labelX + labelWidth &&
15023
+ point.y >= labelY &&
15024
+ point.y <= labelY + labelHeight) {
15025
+ return section;
15026
+ }
15027
+ }
15028
+ // Check if click is on the vertical connector line
15029
+ let verticalStartY;
15030
+ let verticalEndY;
15031
+ if (startInfo) {
15032
+ verticalStartY = startInfo.y;
15033
+ }
15034
+ else {
15035
+ verticalStartY = contentBounds.y;
15036
+ }
15037
+ if (endInfo) {
15038
+ verticalEndY = endInfo.y;
15039
+ }
15040
+ else if (sectionSpansThisPage) {
15041
+ verticalEndY = contentBounds.y + flowedPage.height;
15042
+ }
15043
+ else {
15044
+ continue;
15045
+ }
15046
+ if (Math.abs(point.x - connectorX) <= hitRadius &&
15047
+ point.y >= verticalStartY &&
15048
+ point.y <= verticalEndY) {
15049
+ return section;
15050
+ }
15051
+ }
15052
+ return null;
15053
+ }
15054
+ // ============================================
15055
+ // Conditional Section Indicators
15056
+ // ============================================
15057
+ /**
15058
+ * Render conditional section indicators for a page.
15059
+ */
15060
+ renderConditionalSectionIndicators(sections, pageIndex, ctx, contentBounds, flowedPage, pageBounds, selectedSectionId = null) {
15061
+ for (const section of sections) {
15062
+ this.renderConditionalIndicator(section, pageIndex, ctx, contentBounds, flowedPage, pageBounds, section.id === selectedSectionId);
15063
+ }
15064
+ }
15065
+ /**
15066
+ * Render a single conditional section indicator.
15067
+ */
15068
+ renderConditionalIndicator(section, pageIndex, ctx, contentBounds, flowedPage, _pageBounds, isSelected = false) {
15069
+ const startInfo = this.findLineYForTextIndex(flowedPage, section.startIndex, contentBounds);
15070
+ const endInfo = this.findLineYForTextIndex(flowedPage, section.endIndex, contentBounds);
15071
+ const sectionOverlapsPage = section.startIndex < flowedPage.endIndex &&
15072
+ section.endIndex > flowedPage.startIndex;
15073
+ if (!sectionOverlapsPage) {
15074
+ return;
15075
+ }
15076
+ const hasStart = startInfo !== null;
15077
+ const hasEnd = endInfo !== null;
15078
+ const startsBeforePage = section.startIndex < flowedPage.startIndex;
15079
+ const endsAfterPage = section.endIndex > flowedPage.endIndex;
15080
+ ctx.save();
15081
+ ctx.strokeStyle = COND_INDICATOR_COLOR;
15082
+ ctx.fillStyle = COND_INDICATOR_COLOR;
15083
+ ctx.lineWidth = 1;
15084
+ // Position on the right side of the content area
15085
+ const labelWidth = 22;
15086
+ const labelX = contentBounds.x + contentBounds.width + 5;
15087
+ const connectorX = labelX + labelWidth / 2;
15088
+ // Draw start indicator lines
15089
+ if (hasStart) {
15090
+ const startY = startInfo.y;
15091
+ ctx.setLineDash(COND_LINE_DASH);
15092
+ ctx.beginPath();
15093
+ ctx.moveTo(contentBounds.x, startY);
15094
+ ctx.lineTo(contentBounds.x + contentBounds.width, startY);
15095
+ ctx.stroke();
15096
+ ctx.setLineDash([]);
15097
+ ctx.beginPath();
15098
+ ctx.moveTo(contentBounds.x + contentBounds.width, startY);
15099
+ ctx.lineTo(labelX, startY);
15100
+ ctx.stroke();
15101
+ }
15102
+ else if (startsBeforePage) {
15103
+ const topY = contentBounds.y;
15104
+ ctx.setLineDash(COND_LINE_DASH);
15105
+ ctx.beginPath();
15106
+ ctx.moveTo(contentBounds.x, topY);
15107
+ ctx.lineTo(contentBounds.x + contentBounds.width, topY);
15108
+ ctx.stroke();
15109
+ ctx.setLineDash([]);
15110
+ ctx.beginPath();
15111
+ ctx.moveTo(connectorX, topY);
15112
+ ctx.lineTo(contentBounds.x + contentBounds.width, topY);
15113
+ ctx.stroke();
15114
+ }
15115
+ // Draw end indicator
15116
+ if (hasEnd) {
15117
+ const endY = endInfo.y;
15118
+ ctx.setLineDash(COND_LINE_DASH);
15119
+ ctx.beginPath();
15120
+ ctx.moveTo(contentBounds.x, endY);
15121
+ ctx.lineTo(contentBounds.x + contentBounds.width, endY);
15122
+ ctx.stroke();
15123
+ ctx.setLineDash([]);
15124
+ ctx.beginPath();
15125
+ ctx.moveTo(connectorX, endY);
15126
+ ctx.lineTo(contentBounds.x + contentBounds.width, endY);
15127
+ ctx.stroke();
15128
+ }
15129
+ else if (endsAfterPage) {
15130
+ const bottomY = contentBounds.y + contentBounds.height;
15131
+ ctx.setLineDash(COND_LINE_DASH);
15132
+ ctx.beginPath();
15133
+ ctx.moveTo(contentBounds.x, bottomY);
15134
+ ctx.lineTo(contentBounds.x + contentBounds.width, bottomY);
15135
+ ctx.stroke();
15136
+ ctx.setLineDash([]);
15137
+ ctx.beginPath();
15138
+ ctx.moveTo(connectorX, bottomY);
15139
+ ctx.lineTo(contentBounds.x + contentBounds.width, bottomY);
15140
+ ctx.stroke();
15141
+ }
15142
+ // Draw vertical connector line
15143
+ let verticalStartY;
15144
+ let verticalEndY;
15145
+ if (hasStart) {
15146
+ verticalStartY = startInfo.y;
15147
+ }
15148
+ else if (startsBeforePage) {
15149
+ verticalStartY = contentBounds.y;
15150
+ }
15151
+ else {
15152
+ verticalStartY = contentBounds.y;
15153
+ }
15154
+ if (hasEnd) {
15155
+ verticalEndY = endInfo.y;
15156
+ }
15157
+ else if (endsAfterPage) {
15158
+ verticalEndY = contentBounds.y + contentBounds.height;
15159
+ }
15160
+ else {
15161
+ verticalEndY = verticalStartY;
13896
15162
  }
13897
- return null;
15163
+ if (verticalEndY > verticalStartY) {
15164
+ ctx.beginPath();
15165
+ ctx.moveTo(connectorX, verticalStartY);
15166
+ ctx.lineTo(connectorX, verticalEndY);
15167
+ ctx.stroke();
15168
+ }
15169
+ // Draw "If" label
15170
+ if (hasStart) {
15171
+ const startY = startInfo.y;
15172
+ this.drawCondLabel(ctx, labelX, startY - 10, 'If', isSelected);
15173
+ }
15174
+ // Update visual state
15175
+ section.visualState = {
15176
+ startPageIndex: hasStart ? pageIndex : -1,
15177
+ startY: hasStart ? startInfo.y : 0,
15178
+ endPageIndex: hasEnd ? pageIndex : -1,
15179
+ endY: hasEnd ? endInfo.y : 0,
15180
+ spansMultiplePages: !hasStart || !hasEnd
15181
+ };
15182
+ ctx.restore();
13898
15183
  }
13899
15184
  /**
13900
- * Check if a section spans across a flowed page (starts before and ends after).
15185
+ * Draw the "If" label in a rounded rectangle.
13901
15186
  */
13902
- sectionSpansPage(section, flowedPage) {
13903
- if (flowedPage.lines.length === 0)
13904
- return false;
13905
- const pageStart = flowedPage.startIndex;
13906
- const pageEnd = flowedPage.endIndex;
13907
- // Section spans this page if it started before and ends after
13908
- return section.startIndex < pageStart && section.endIndex > pageEnd;
15187
+ drawCondLabel(ctx, x, y, text, isSelected = false) {
15188
+ ctx.save();
15189
+ ctx.font = '10px Arial';
15190
+ const metrics = ctx.measureText(text);
15191
+ const textWidth = metrics.width;
15192
+ const textHeight = 10;
15193
+ const boxWidth = textWidth + COND_LABEL_PADDING * 2;
15194
+ const boxHeight = textHeight + COND_LABEL_PADDING * 2;
15195
+ ctx.beginPath();
15196
+ this.roundRect(ctx, x, y, boxWidth, boxHeight, COND_LABEL_RADIUS);
15197
+ if (isSelected) {
15198
+ ctx.fillStyle = COND_INDICATOR_COLOR;
15199
+ ctx.fill();
15200
+ ctx.fillStyle = '#ffffff';
15201
+ }
15202
+ else {
15203
+ ctx.fillStyle = '#ffffff';
15204
+ ctx.fill();
15205
+ ctx.strokeStyle = COND_INDICATOR_COLOR;
15206
+ ctx.lineWidth = 1.5;
15207
+ ctx.stroke();
15208
+ ctx.fillStyle = COND_INDICATOR_COLOR;
15209
+ }
15210
+ ctx.textBaseline = 'middle';
15211
+ ctx.fillText(text, x + COND_LABEL_PADDING, y + boxHeight / 2);
15212
+ ctx.restore();
13909
15213
  }
13910
15214
  /**
13911
- * Get a repeating section at a point (for click detection).
13912
- * Checks if the point is on the Loop label or vertical connector.
15215
+ * Get a conditional section at a point (for click detection).
13913
15216
  */
13914
- getRepeatingSectionAtPoint(point, sections, _pageIndex, pageBounds, contentBounds, flowedPage) {
13915
- const labelX = pageBounds.x + 5;
13916
- const labelWidth = 32;
15217
+ getConditionalSectionAtPoint(point, sections, _pageIndex, _pageBounds, contentBounds, flowedPage) {
15218
+ const labelWidth = 22;
15219
+ const labelX = contentBounds.x + contentBounds.width + 5;
13917
15220
  const connectorX = labelX + labelWidth / 2;
13918
- const hitRadius = 10; // Pixels for click detection
15221
+ const hitRadius = 10;
13919
15222
  for (const section of sections) {
13920
15223
  const startInfo = this.findLineYForTextIndex(flowedPage, section.startIndex, contentBounds);
13921
15224
  const endInfo = this.findLineYForTextIndex(flowedPage, section.endIndex, contentBounds);
13922
- const sectionSpansThisPage = this.sectionSpansPage(section, flowedPage);
15225
+ const sectionSpansThisPage = section.startIndex < flowedPage.startIndex &&
15226
+ section.endIndex > flowedPage.endIndex;
13923
15227
  if (!startInfo && !endInfo && !sectionSpansThisPage) {
13924
15228
  continue;
13925
15229
  }
13926
- // Check if click is on the Loop label
15230
+ // Check if click is on the "If" label
13927
15231
  if (startInfo) {
13928
15232
  const labelY = startInfo.y - 10;
13929
15233
  const labelHeight = 18;
@@ -13988,6 +15292,7 @@ class CanvasManager extends EventEmitter {
13988
15292
  this.isSelectingText = false;
13989
15293
  this.textSelectionStartPageId = null;
13990
15294
  this.selectedSectionId = null;
15295
+ this.selectedConditionalSectionId = null;
13991
15296
  this._activeSection = 'body';
13992
15297
  this.lastClickTime = 0;
13993
15298
  this.lastClickPosition = null;
@@ -14127,6 +15432,11 @@ class CanvasManager extends EventEmitter {
14127
15432
  }
14128
15433
  // 2. CONTENT: Render all text and elements
14129
15434
  const pageIndex = this.document.pages.findIndex(p => p.id === page.id);
15435
+ // Clear content hit targets before rendering all sections (header, body, footer)
15436
+ // so that each section's hit targets are re-registered during render
15437
+ if (pageIndex === 0) {
15438
+ this.flowingTextRenderer.hitTestManager.clearCategory('content');
15439
+ }
14130
15440
  // Render header content
14131
15441
  const headerRegion = this.regionManager.getHeaderRegion();
14132
15442
  this.flowingTextRenderer.renderHeaderText(page, ctx, this._activeSection === 'header', headerRegion ?? undefined, pageIndex);
@@ -14154,6 +15464,16 @@ class CanvasManager extends EventEmitter {
14154
15464
  this.flowingTextRenderer.renderRepeatingSectionIndicators(sections, pageIndex, ctx, contentRect, flowedPages[pageIndex], pageBounds, this.selectedSectionId);
14155
15465
  }
14156
15466
  }
15467
+ // Render conditional section indicators (only in body)
15468
+ const condSections = bodyFlowingContent?.getConditionalSections() ?? [];
15469
+ if (condSections.length > 0) {
15470
+ const flowedPages = this.flowingTextRenderer.getFlowedPagesForPage(this.document.pages[0].id);
15471
+ if (flowedPages && flowedPages[pageIndex]) {
15472
+ const pageDimensions = page.getPageDimensions();
15473
+ const pageBounds = { x: 0, y: 0, width: pageDimensions.width, height: pageDimensions.height };
15474
+ this.flowingTextRenderer.renderConditionalSectionIndicators(condSections, pageIndex, ctx, contentRect, flowedPages[pageIndex], pageBounds, this.selectedConditionalSectionId);
15475
+ }
15476
+ }
14157
15477
  // Render all elements (without selection marks)
14158
15478
  this.renderPageElements(page, ctx);
14159
15479
  // 3. DISABLEMENT OVERLAYS: Draw overlays on inactive sections
@@ -14314,7 +15634,8 @@ class CanvasManager extends EventEmitter {
14314
15634
  const pageIndex = this.document.pages.findIndex(p => p.id === pageId);
14315
15635
  // Get the slice for this page (for multi-page tables)
14316
15636
  const slice = table.getRenderedSlice(pageIndex);
14317
- const tablePosition = slice?.position || table.renderedPosition;
15637
+ const tablePosition = slice?.position ||
15638
+ (table.renderedPageIndex === pageIndex ? table.renderedPosition : null);
14318
15639
  const sliceHeight = slice?.height || table.height;
14319
15640
  // Check if point is within the table slice on this page
14320
15641
  const isInsideTable = tablePosition &&
@@ -14347,6 +15668,7 @@ class CanvasManager extends EventEmitter {
14347
15668
  end: cellAddr
14348
15669
  });
14349
15670
  this.render();
15671
+ this.emit('table-cell-selection-changed', { table });
14350
15672
  e.preventDefault();
14351
15673
  return;
14352
15674
  }
@@ -14449,11 +15771,10 @@ class CanvasManager extends EventEmitter {
14449
15771
  const embeddedObjectHit = hitTestManager.queryByType(mouseDownPageIndex, point, 'embedded-object');
14450
15772
  if (embeddedObjectHit && embeddedObjectHit.data.type === 'embedded-object') {
14451
15773
  const object = embeddedObjectHit.data.object;
14452
- // Check which section the object belongs to - only interact if in active section
15774
+ // If object is in a different section, switch to that section first
14453
15775
  const objectSection = this.getSectionForEmbeddedObject(object);
14454
15776
  if (objectSection && objectSection !== this._activeSection) {
14455
- // Object is in a different section - ignore the interaction
14456
- return;
15777
+ this.setActiveSection(objectSection);
14457
15778
  }
14458
15779
  // For relative-positioned objects, prepare for potential drag
14459
15780
  // Don't start drag immediately - wait for threshold to allow double-click
@@ -14626,7 +15947,8 @@ class CanvasManager extends EventEmitter {
14626
15947
  const currentPageIndex = this.document.pages.findIndex(p => p.id === pageId);
14627
15948
  // Get the slice for the current page (for multi-page tables)
14628
15949
  const slice = table.getRenderedSlice(currentPageIndex);
14629
- const tablePosition = slice?.position || table.renderedPosition;
15950
+ const tablePosition = slice?.position ||
15951
+ (table.renderedPageIndex === currentPageIndex ? table.renderedPosition : null);
14630
15952
  const sliceHeight = slice?.height || table.height;
14631
15953
  if (tablePosition) {
14632
15954
  // Check if point is within the table slice on this page
@@ -14656,6 +15978,7 @@ class CanvasManager extends EventEmitter {
14656
15978
  end: cellAddr
14657
15979
  });
14658
15980
  this.render();
15981
+ this.emit('table-cell-selection-changed', { table });
14659
15982
  }
14660
15983
  }
14661
15984
  }
@@ -14938,14 +16261,12 @@ class CanvasManager extends EventEmitter {
14938
16261
  const embeddedObjectHit = hitTestManager.queryByType(clickedPageIndex, point, 'embedded-object');
14939
16262
  if (embeddedObjectHit && embeddedObjectHit.data.type === 'embedded-object') {
14940
16263
  const clickedObject = embeddedObjectHit.data.object;
14941
- // Check which section the object belongs to
16264
+ // If object is in a different section, switch to that section first
14942
16265
  const objectSection = this.getSectionForEmbeddedObject(clickedObject);
14943
- // Only allow selection if object is in the active section
14944
16266
  if (objectSection && objectSection !== this._activeSection) {
14945
- // Object is in a different section - ignore the click
14946
- return;
16267
+ this.setActiveSection(objectSection);
14947
16268
  }
14948
- // Clicked on embedded object in the active section - clear text selection and select it
16269
+ // Clicked on embedded object - clear text selection and select it
14949
16270
  const activeFlowingContent = this.getFlowingContentForActiveSection();
14950
16271
  if (activeFlowingContent) {
14951
16272
  activeFlowingContent.clearSelection();
@@ -14982,6 +16303,64 @@ class CanvasManager extends EventEmitter {
14982
16303
  }
14983
16304
  }
14984
16305
  }
16306
+ // Check if we clicked on a conditional section indicator
16307
+ if (bodyFlowingContent) {
16308
+ const condSections = bodyFlowingContent.getConditionalSections();
16309
+ if (condSections.length > 0 && page) {
16310
+ const pageIndex = this.document.pages.findIndex(p => p.id === pageId);
16311
+ const flowedPages = this.flowingTextRenderer.getFlowedPagesForPage(this.document.pages[0].id);
16312
+ if (flowedPages && flowedPages[pageIndex]) {
16313
+ const contentBounds = page.getContentBounds();
16314
+ const contentRect = {
16315
+ x: contentBounds.position.x,
16316
+ y: contentBounds.position.y,
16317
+ width: contentBounds.size.width,
16318
+ height: contentBounds.size.height
16319
+ };
16320
+ const pageDimensions = page.getPageDimensions();
16321
+ const pageBounds = { x: 0, y: 0, width: pageDimensions.width, height: pageDimensions.height };
16322
+ const clickedCondSection = this.flowingTextRenderer.getConditionalSectionAtPoint(point, condSections, pageIndex, pageBounds, contentRect, flowedPages[pageIndex]);
16323
+ if (clickedCondSection) {
16324
+ this.clearSelection();
16325
+ this.selectedConditionalSectionId = clickedCondSection.id;
16326
+ this.render();
16327
+ this.emit('conditional-section-clicked', { section: clickedCondSection });
16328
+ return;
16329
+ }
16330
+ }
16331
+ }
16332
+ }
16333
+ // Check if we clicked on a table row loop label
16334
+ const clickedPageIdx = this.document.pages.findIndex(p => p.id === pageId);
16335
+ const bodyContent = this.document.bodyFlowingContent;
16336
+ if (bodyContent) {
16337
+ const embeddedObjects = bodyContent.getEmbeddedObjects();
16338
+ for (const [, obj] of embeddedObjects.entries()) {
16339
+ if (obj instanceof TableObject && obj.renderedPosition && obj.renderedPageIndex === clickedPageIdx) {
16340
+ // Convert to table-local coordinates
16341
+ const localPoint = {
16342
+ x: point.x - obj.renderedPosition.x,
16343
+ y: point.y - obj.renderedPosition.y
16344
+ };
16345
+ const clickedLoop = obj.getRowLoopAtPoint(localPoint);
16346
+ if (clickedLoop) {
16347
+ // Select this loop
16348
+ obj.selectRowLoop(clickedLoop.id);
16349
+ this.render();
16350
+ this.emit('table-row-loop-clicked', { table: obj, loop: clickedLoop });
16351
+ return;
16352
+ }
16353
+ // Check for row conditional click
16354
+ const clickedCond = obj.getRowConditionalAtPoint(localPoint);
16355
+ if (clickedCond) {
16356
+ obj.selectRowConditional(clickedCond.id);
16357
+ this.render();
16358
+ this.emit('table-row-conditional-clicked', { table: obj, conditional: clickedCond });
16359
+ return;
16360
+ }
16361
+ }
16362
+ }
16363
+ }
14985
16364
  // If no regular element was clicked, try flowing text using unified region click handler
14986
16365
  const ctx = this.contexts.get(pageId);
14987
16366
  const pageIndex = this.document.pages.findIndex(p => p.id === pageId);
@@ -15191,48 +16570,49 @@ class CanvasManager extends EventEmitter {
15191
16570
  const embeddedObjectHit = hitTestManager.queryByType(pageIndex, point, 'embedded-object');
15192
16571
  if (embeddedObjectHit && embeddedObjectHit.data.type === 'embedded-object') {
15193
16572
  const object = embeddedObjectHit.data.object;
15194
- // Only show interactive cursors for objects in the active section
15195
- const objectSection = this.getSectionForEmbeddedObject(object);
15196
- if (objectSection && objectSection !== this._activeSection) ;
16573
+ if (object.position === 'relative') {
16574
+ canvas.style.cursor = 'move';
16575
+ return;
16576
+ }
16577
+ // Show text cursor for objects in edit mode, arrow otherwise
16578
+ if (object instanceof TextBoxObject && this.editingTextBox === object) {
16579
+ canvas.style.cursor = CanvasManager.TEXT_CURSOR;
16580
+ }
16581
+ else if (object instanceof TableObject && this._focusedControl === object) {
16582
+ canvas.style.cursor = CanvasManager.TEXT_CURSOR;
16583
+ }
15197
16584
  else {
15198
- if (object.position === 'relative') {
15199
- canvas.style.cursor = 'move';
15200
- return;
15201
- }
15202
- // Show text cursor for text boxes
15203
- if (object instanceof TextBoxObject) {
15204
- canvas.style.cursor = 'text';
15205
- return;
15206
- }
16585
+ canvas.style.cursor = 'default';
15207
16586
  }
16587
+ return;
15208
16588
  }
15209
16589
  // Check for table cells (show text cursor)
15210
16590
  const tableCellHit = hitTestManager.queryByType(pageIndex, point, 'table-cell');
15211
16591
  if (tableCellHit && tableCellHit.data.type === 'table-cell') {
15212
- canvas.style.cursor = 'text';
16592
+ canvas.style.cursor = CanvasManager.TEXT_CURSOR;
15213
16593
  return;
15214
16594
  }
15215
16595
  // Check for text regions (body, header, footer - show text cursor)
15216
16596
  const textRegionHit = hitTestManager.queryByType(pageIndex, point, 'text-region');
15217
16597
  if (textRegionHit && textRegionHit.data.type === 'text-region') {
15218
- canvas.style.cursor = 'text';
16598
+ canvas.style.cursor = CanvasManager.TEXT_CURSOR;
15219
16599
  return;
15220
16600
  }
15221
16601
  // Also check if point is within any editable region (body, header, footer)
15222
16602
  // This catches cases where text region hit targets may not cover empty space
15223
16603
  const bodyRegion = this.regionManager.getBodyRegion();
15224
16604
  if (bodyRegion && bodyRegion.containsPointInRegion(point, pageIndex)) {
15225
- canvas.style.cursor = 'text';
16605
+ canvas.style.cursor = CanvasManager.TEXT_CURSOR;
15226
16606
  return;
15227
16607
  }
15228
16608
  const headerRegion = this.regionManager.getHeaderRegion();
15229
16609
  if (headerRegion && headerRegion.containsPointInRegion(point, pageIndex)) {
15230
- canvas.style.cursor = 'text';
16610
+ canvas.style.cursor = CanvasManager.TEXT_CURSOR;
15231
16611
  return;
15232
16612
  }
15233
16613
  const footerRegion = this.regionManager.getFooterRegion();
15234
16614
  if (footerRegion && footerRegion.containsPointInRegion(point, pageIndex)) {
15235
- canvas.style.cursor = 'text';
16615
+ canvas.style.cursor = CanvasManager.TEXT_CURSOR;
15236
16616
  return;
15237
16617
  }
15238
16618
  canvas.style.cursor = 'default';
@@ -15250,7 +16630,8 @@ class CanvasManager extends EventEmitter {
15250
16630
  const { table, dividerType, index } = target.data;
15251
16631
  // Get the table position from slice info
15252
16632
  const slice = table.getRenderedSlice(pageIndex);
15253
- const tablePosition = slice?.position || table.renderedPosition;
16633
+ const tablePosition = slice?.position ||
16634
+ (table.renderedPageIndex === pageIndex ? table.renderedPosition : null);
15254
16635
  if (tablePosition) {
15255
16636
  // Calculate the divider position based on type and index
15256
16637
  let position;
@@ -15393,6 +16774,7 @@ class CanvasManager extends EventEmitter {
15393
16774
  });
15394
16775
  this.selectedElements.clear();
15395
16776
  this.selectedSectionId = null;
16777
+ this.selectedConditionalSectionId = null;
15396
16778
  Logger.log('[pc-editor:CanvasManager] About to render after clearing selection...');
15397
16779
  this.render();
15398
16780
  this.updateResizeHandleHitTargets();
@@ -15978,7 +17360,9 @@ class CanvasManager extends EventEmitter {
15978
17360
  if (obj instanceof TableObject) {
15979
17361
  // For multi-page tables, check if this page has a rendered slice
15980
17362
  const slice = obj.getRenderedSlice(pageIndex);
15981
- const tablePosition = slice?.position || obj.renderedPosition;
17363
+ // Only use renderedPosition if the table was actually rendered on this page
17364
+ const tablePosition = slice?.position ||
17365
+ (obj.renderedPageIndex === pageIndex ? obj.renderedPosition : null);
15982
17366
  if (tablePosition) {
15983
17367
  // Check if point is inside the table slice on this page
15984
17368
  const sliceHeight = slice?.height || obj.height;
@@ -16222,6 +17606,10 @@ class CanvasManager extends EventEmitter {
16222
17606
  }
16223
17607
  CanvasManager.CELL_SELECTION_THRESHOLD = 5; // Minimum pixels to drag before cell selection starts
16224
17608
  CanvasManager.RELATIVE_DRAG_THRESHOLD = 3; // Minimum pixels to drag before moving starts
17609
+ // Custom text cursor as a black I-beam SVG data URI.
17610
+ // The native 'text' cursor can render as white on Windows browsers,
17611
+ // making it invisible over the white canvas background.
17612
+ CanvasManager.TEXT_CURSOR = "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='20' viewBox='0 0 16 20'%3E%3Cpath d='M5 1h2v1h2V1h2v2h-2v6h2v2h-2v6h2v2h-2v-1H7v1H5v-2h2v-6H5V9h2V3H5z' fill='%23000'/%3E%3C/svg%3E\") 8 10, text";
16225
17613
 
16226
17614
  /**
16227
17615
  * DataBinder handles binding data to documents.
@@ -16469,8 +17857,10 @@ function drawLine(page, x1, y1, x2, y2, color, thickness, pageHeight) {
16469
17857
  * - Repeating section indicators, loop markers
16470
17858
  */
16471
17859
  class PDFGenerator {
16472
- constructor() {
17860
+ constructor(fontManager) {
16473
17861
  this.fontCache = new Map();
17862
+ this.customFontCache = new Map();
17863
+ this.fontManager = fontManager;
16474
17864
  }
16475
17865
  /**
16476
17866
  * Generate a PDF from the document.
@@ -16481,9 +17871,13 @@ class PDFGenerator {
16481
17871
  */
16482
17872
  async generate(document, flowedContent, _options) {
16483
17873
  const pdfDoc = await pdfLib.PDFDocument.create();
17874
+ pdfDoc.registerFontkit(fontkit);
16484
17875
  this.fontCache.clear();
17876
+ this.customFontCache.clear();
16485
17877
  // Embed standard fonts we'll need
16486
17878
  await this.embedStandardFonts(pdfDoc);
17879
+ // Embed any custom fonts that have font data
17880
+ await this.embedCustomFonts(pdfDoc);
16487
17881
  // Render each page
16488
17882
  for (let pageIndex = 0; pageIndex < document.pages.length; pageIndex++) {
16489
17883
  try {
@@ -16605,11 +17999,59 @@ class PDFGenerator {
16605
17999
  }
16606
18000
  return result;
16607
18001
  }
18002
+ /**
18003
+ * Embed custom fonts that have raw font data available.
18004
+ */
18005
+ async embedCustomFonts(pdfDoc) {
18006
+ const fonts = this.fontManager.getAvailableFonts();
18007
+ for (const font of fonts) {
18008
+ if (font.source !== 'custom')
18009
+ continue;
18010
+ for (const variant of font.variants) {
18011
+ if (!variant.fontData)
18012
+ continue;
18013
+ const cacheKey = `custom:${font.family.toLowerCase()}:${variant.weight}:${variant.style}`;
18014
+ try {
18015
+ // Ensure we pass Uint8Array (some pdf-lib versions need it)
18016
+ const fontBytes = variant.fontData instanceof Uint8Array
18017
+ ? variant.fontData
18018
+ : new Uint8Array(variant.fontData);
18019
+ const embedded = await pdfDoc.embedFont(fontBytes, { subset: true });
18020
+ this.customFontCache.set(cacheKey, embedded);
18021
+ Logger.log('[pc-editor:PDFGenerator] Embedded custom font:', font.family, variant.weight, variant.style);
18022
+ }
18023
+ catch (e) {
18024
+ Logger.warn('[pc-editor:PDFGenerator] Failed to embed custom font:', font.family, e);
18025
+ }
18026
+ }
18027
+ }
18028
+ }
18029
+ /**
18030
+ * Check if a font family is a custom font with embedded data.
18031
+ */
18032
+ isCustomFont(family) {
18033
+ return !this.fontManager.isBuiltIn(family) && this.fontManager.isRegistered(family);
18034
+ }
16608
18035
  /**
16609
18036
  * Get a font from cache by formatting style.
18037
+ * Checks custom fonts first, then falls back to standard fonts.
16610
18038
  */
16611
18039
  getFont(formatting) {
16612
- const standardFont = getStandardFont(formatting.fontFamily || 'Arial', formatting.fontWeight, formatting.fontStyle);
18040
+ const family = formatting.fontFamily || 'Arial';
18041
+ const weight = formatting.fontWeight || 'normal';
18042
+ const style = formatting.fontStyle || 'normal';
18043
+ // Try custom font first
18044
+ const customKey = `custom:${family.toLowerCase()}:${weight}:${style}`;
18045
+ const customFont = this.customFontCache.get(customKey);
18046
+ if (customFont)
18047
+ return customFont;
18048
+ // Try custom font with normal variant as fallback
18049
+ const customNormalKey = `custom:${family.toLowerCase()}:normal:normal`;
18050
+ const customNormalFont = this.customFontCache.get(customNormalKey);
18051
+ if (customNormalFont)
18052
+ return customNormalFont;
18053
+ // Fall back to standard fonts
18054
+ const standardFont = getStandardFont(family, weight, style);
16613
18055
  return this.fontCache.get(standardFont) || this.fontCache.get(pdfLib.StandardFonts.Helvetica);
16614
18056
  }
16615
18057
  /**
@@ -16642,12 +18084,14 @@ class PDFGenerator {
16642
18084
  for (const run of line.runs) {
16643
18085
  if (!run.text)
16644
18086
  continue;
16645
- // Filter text to WinAnsi-compatible characters (standard PDF fonts limitation)
16646
- const safeText = this.filterToWinAnsi(run.text);
16647
- if (!safeText)
16648
- continue;
16649
18087
  // Ensure formatting has required properties with defaults
16650
18088
  const formatting = run.formatting || {};
18089
+ // Custom fonts support full Unicode; standard fonts need WinAnsi filtering
18090
+ const safeText = this.isCustomFont(formatting.fontFamily || 'Arial')
18091
+ ? run.text
18092
+ : this.filterToWinAnsi(run.text);
18093
+ if (!safeText)
18094
+ continue;
16651
18095
  const font = this.getFont(formatting);
16652
18096
  const fontSize = formatting.fontSize || 14;
16653
18097
  const color = parseColor(formatting.color || '#000000');
@@ -21322,6 +22766,156 @@ class PDFImporter {
21322
22766
  }
21323
22767
  }
21324
22768
 
22769
+ /**
22770
+ * FontManager - Manages font registration and availability for the editor.
22771
+ *
22772
+ * Built-in fonts are web-safe and map to pdf-lib StandardFonts.
22773
+ * Custom fonts are loaded via the FontFace API for canvas rendering
22774
+ * and their raw bytes are stored for PDF embedding.
22775
+ */
22776
+ /**
22777
+ * Built-in web-safe fonts that need no loading.
22778
+ */
22779
+ const BUILT_IN_FONTS = [
22780
+ 'Arial',
22781
+ 'Times New Roman',
22782
+ 'Courier New',
22783
+ 'Georgia',
22784
+ 'Verdana'
22785
+ ];
22786
+ class FontManager extends EventEmitter {
22787
+ constructor() {
22788
+ super();
22789
+ this.fonts = new Map();
22790
+ // Register built-in fonts
22791
+ for (const family of BUILT_IN_FONTS) {
22792
+ this.fonts.set(family.toLowerCase(), {
22793
+ family,
22794
+ source: 'built-in',
22795
+ variants: [{
22796
+ weight: 'normal',
22797
+ style: 'normal',
22798
+ fontData: null,
22799
+ loaded: true
22800
+ }]
22801
+ });
22802
+ }
22803
+ }
22804
+ /**
22805
+ * Register a custom font. Fetches the font data if a URL is provided,
22806
+ * creates a FontFace for canvas rendering, and stores the raw bytes for PDF embedding.
22807
+ */
22808
+ async registerFont(options) {
22809
+ const { family, url, data, weight = 'normal', style = 'normal' } = options;
22810
+ Logger.log('[pc-editor:FontManager] registerFont', family, weight, style);
22811
+ let fontData = null;
22812
+ // Get font bytes
22813
+ if (data) {
22814
+ fontData = data;
22815
+ }
22816
+ else if (url) {
22817
+ try {
22818
+ const response = await fetch(url);
22819
+ if (!response.ok) {
22820
+ throw new Error(`Failed to fetch font: ${response.status} ${response.statusText}`);
22821
+ }
22822
+ fontData = await response.arrayBuffer();
22823
+ }
22824
+ catch (e) {
22825
+ Logger.error(`[pc-editor:FontManager] Failed to fetch font "${family}" from ${url}:`, e);
22826
+ throw e;
22827
+ }
22828
+ }
22829
+ // Create FontFace for canvas rendering
22830
+ if (fontData && typeof FontFace !== 'undefined') {
22831
+ try {
22832
+ const fontFace = new FontFace(family, fontData, {
22833
+ weight,
22834
+ style
22835
+ });
22836
+ await fontFace.load();
22837
+ document.fonts.add(fontFace);
22838
+ Logger.log('[pc-editor:FontManager] FontFace loaded:', family, weight, style);
22839
+ }
22840
+ catch (e) {
22841
+ Logger.error(`[pc-editor:FontManager] Failed to load FontFace "${family}":`, e);
22842
+ throw e;
22843
+ }
22844
+ }
22845
+ // Register in our map
22846
+ const key = family.toLowerCase();
22847
+ let registration = this.fonts.get(key);
22848
+ if (!registration) {
22849
+ registration = {
22850
+ family,
22851
+ source: 'custom',
22852
+ variants: []
22853
+ };
22854
+ this.fonts.set(key, registration);
22855
+ }
22856
+ else if (registration.source === 'built-in') {
22857
+ // Upgrading a built-in font with custom data (e.g., for PDF embedding)
22858
+ registration.source = 'custom';
22859
+ }
22860
+ // Add or update variant
22861
+ const existingVariant = registration.variants.find(v => v.weight === weight && v.style === style);
22862
+ if (existingVariant) {
22863
+ existingVariant.fontData = fontData;
22864
+ existingVariant.loaded = true;
22865
+ }
22866
+ else {
22867
+ registration.variants.push({
22868
+ weight,
22869
+ style,
22870
+ fontData,
22871
+ loaded: true
22872
+ });
22873
+ }
22874
+ this.emit('font-registered', { family, weight, style });
22875
+ }
22876
+ /**
22877
+ * Get all registered font families.
22878
+ */
22879
+ getAvailableFonts() {
22880
+ return Array.from(this.fonts.values());
22881
+ }
22882
+ /**
22883
+ * Get all available font family names.
22884
+ */
22885
+ getAvailableFontFamilies() {
22886
+ return Array.from(this.fonts.values()).map(f => f.family);
22887
+ }
22888
+ /**
22889
+ * Check if a font family is built-in.
22890
+ */
22891
+ isBuiltIn(family) {
22892
+ const reg = this.fonts.get(family.toLowerCase());
22893
+ return reg?.source === 'built-in';
22894
+ }
22895
+ /**
22896
+ * Check if a font family is registered (built-in or custom).
22897
+ */
22898
+ isRegistered(family) {
22899
+ return this.fonts.has(family.toLowerCase());
22900
+ }
22901
+ /**
22902
+ * Get raw font bytes for PDF embedding.
22903
+ * Returns null for built-in fonts (they use StandardFonts in pdf-lib).
22904
+ */
22905
+ getFontData(family, weight = 'normal', style = 'normal') {
22906
+ const reg = this.fonts.get(family.toLowerCase());
22907
+ if (!reg)
22908
+ return null;
22909
+ // Try exact match first
22910
+ const exact = reg.variants.find(v => v.weight === weight && v.style === style);
22911
+ if (exact?.fontData)
22912
+ return exact.fontData;
22913
+ // Fall back to normal variant
22914
+ const normal = reg.variants.find(v => v.weight === 'normal' && v.style === 'normal');
22915
+ return normal?.fontData || null;
22916
+ }
22917
+ }
22918
+
21325
22919
  class PCEditor extends EventEmitter {
21326
22920
  constructor(container, options) {
21327
22921
  super();
@@ -21348,7 +22942,8 @@ class PCEditor extends EventEmitter {
21348
22942
  units: this.options.units
21349
22943
  });
21350
22944
  this.dataBinder = new DataBinder();
21351
- this.pdfGenerator = new PDFGenerator();
22945
+ this.fontManager = new FontManager();
22946
+ this.pdfGenerator = new PDFGenerator(this.fontManager);
21352
22947
  this.clipboardManager = new ClipboardManager();
21353
22948
  this.initialize();
21354
22949
  }
@@ -21510,6 +23105,14 @@ class PCEditor extends EventEmitter {
21510
23105
  this.canvasManager.on('tablecell-cursor-changed', (data) => {
21511
23106
  this.emit('tablecell-cursor-changed', data);
21512
23107
  });
23108
+ // Forward table cell selection changes (multi-cell drag/shift-click)
23109
+ this.canvasManager.on('table-cell-selection-changed', (data) => {
23110
+ this.emit('table-cell-selection-changed', data);
23111
+ });
23112
+ // Forward table row loop clicks
23113
+ this.canvasManager.on('table-row-loop-clicked', (data) => {
23114
+ this.emit('table-row-loop-clicked', data);
23115
+ });
21513
23116
  this.canvasManager.on('repeating-section-clicked', (data) => {
21514
23117
  // Repeating section clicked - update selection state
21515
23118
  if (data.section && data.section.id) {
@@ -21520,6 +23123,16 @@ class PCEditor extends EventEmitter {
21520
23123
  this.emitSelectionChange();
21521
23124
  }
21522
23125
  });
23126
+ this.canvasManager.on('conditional-section-clicked', (data) => {
23127
+ // Conditional section clicked - update selection state
23128
+ if (data.section && data.section.id) {
23129
+ this.currentSelection = {
23130
+ type: 'conditional-section',
23131
+ sectionId: data.section.id
23132
+ };
23133
+ this.emitSelectionChange();
23134
+ }
23135
+ });
21523
23136
  // Listen for section focus changes from CanvasManager (double-click)
21524
23137
  this.canvasManager.on('section-focus-changed', (data) => {
21525
23138
  // Update our internal state to match the canvas manager
@@ -22348,17 +23961,24 @@ class PCEditor extends EventEmitter {
22348
23961
  this.selectAll();
22349
23962
  return;
22350
23963
  }
22351
- // If an embedded object is selected (but not being edited), arrow keys should deselect it
22352
- // and move the cursor in the text flow
22353
- const isArrowKey = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key);
22354
- if (isArrowKey && this.canvasManager.hasSelectedElements()) {
22355
- // Check if we're not in editing mode
23964
+ // If an embedded object is selected (but not being edited), handle special keys
23965
+ if (this.canvasManager.hasSelectedElements()) {
22356
23966
  const editingTextBox = this.canvasManager.getEditingTextBox();
22357
23967
  const focusedTable = this.canvasManager.getFocusedControl();
22358
23968
  const isEditing = editingTextBox?.editing || (focusedTable instanceof TableObject && focusedTable.editing);
22359
23969
  if (!isEditing) {
22360
- // Clear the selection and let the key be handled by the body content
22361
- this.canvasManager.clearSelection();
23970
+ // Arrow keys: deselect and move cursor in text flow
23971
+ const isArrowKey = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key);
23972
+ if (isArrowKey) {
23973
+ this.canvasManager.clearSelection();
23974
+ // Fall through to normal key handling
23975
+ }
23976
+ // Backspace/Delete: delete the selected object from the text flow
23977
+ if (e.key === 'Backspace' || e.key === 'Delete') {
23978
+ e.preventDefault();
23979
+ this.deleteSelectedElements();
23980
+ return;
23981
+ }
22362
23982
  }
22363
23983
  }
22364
23984
  // Use the unified focus system to get the currently focused control
@@ -22461,6 +24081,32 @@ class PCEditor extends EventEmitter {
22461
24081
  this.canvasManager.clearSelection();
22462
24082
  this.canvasManager.render();
22463
24083
  }
24084
+ /**
24085
+ * Delete all currently selected embedded objects from the text flow.
24086
+ */
24087
+ deleteSelectedElements() {
24088
+ const selectedElements = this.canvasManager.getSelectedElements();
24089
+ if (selectedElements.length === 0)
24090
+ return;
24091
+ for (const elementId of selectedElements) {
24092
+ const objectInfo = this.findEmbeddedObjectInfo(elementId);
24093
+ if (objectInfo) {
24094
+ // Delete the placeholder character at the object's text index
24095
+ // This removes the object from the text flow
24096
+ objectInfo.content.deleteText(objectInfo.textIndex, 1);
24097
+ // Return focus to the parent flowing content
24098
+ const cursorPos = Math.min(objectInfo.textIndex, objectInfo.content.getText().length);
24099
+ objectInfo.content.setCursorPosition(cursorPos);
24100
+ this.canvasManager.setFocus(objectInfo.content);
24101
+ if (objectInfo.section !== this.canvasManager.getActiveSection()) {
24102
+ this.canvasManager.setActiveSection(objectInfo.section);
24103
+ }
24104
+ }
24105
+ }
24106
+ this.canvasManager.clearSelection();
24107
+ this.canvasManager.render();
24108
+ this.emit('content-changed', {});
24109
+ }
22464
24110
  /**
22465
24111
  * Find embedded object info by ID across all flowing content sources.
22466
24112
  */
@@ -23479,6 +25125,39 @@ class PCEditor extends EventEmitter {
23479
25125
  table.removeColumn(colIndex);
23480
25126
  this.canvasManager.render();
23481
25127
  }
25128
+ /**
25129
+ * Merge selected cells in a table.
25130
+ * Uses the table's current cell selection range.
25131
+ * @param table The table containing the cells to merge
25132
+ * @returns true if cells were merged successfully
25133
+ */
25134
+ tableMergeCells(table) {
25135
+ Logger.log('[pc-editor] tableMergeCells');
25136
+ if (!this._isReady)
25137
+ return false;
25138
+ const result = table.mergeCells();
25139
+ if (result.success) {
25140
+ this.canvasManager.render();
25141
+ }
25142
+ return result.success;
25143
+ }
25144
+ /**
25145
+ * Split a merged cell back into individual cells.
25146
+ * @param table The table containing the merged cell
25147
+ * @param row Row index of the merged cell
25148
+ * @param col Column index of the merged cell
25149
+ * @returns true if the cell was split successfully
25150
+ */
25151
+ tableSplitCell(table, row, col) {
25152
+ Logger.log('[pc-editor] tableSplitCell', row, col);
25153
+ if (!this._isReady)
25154
+ return false;
25155
+ const result = table.splitCell(row, col);
25156
+ if (result.success) {
25157
+ this.canvasManager.render();
25158
+ }
25159
+ return result.success;
25160
+ }
23482
25161
  /**
23483
25162
  * Begin a compound operation. Groups multiple mutations into a single undo entry.
23484
25163
  * Call endCompoundOperation after making changes.
@@ -23545,11 +25224,17 @@ class PCEditor extends EventEmitter {
23545
25224
  let totalFieldCount = 0;
23546
25225
  // Step 1: Expand repeating sections in body (header/footer don't support them)
23547
25226
  this.expandRepeatingSections(bodyContent, data);
23548
- // Step 2: Expand table row loops in body, header, and footer
25227
+ // Step 2: Evaluate conditional sections in body (remove content where predicate is false)
25228
+ this.evaluateConditionalSections(bodyContent, data);
25229
+ // Step 3: Expand table row loops in body, header, and footer
23549
25230
  this.expandTableRowLoops(bodyContent, data);
23550
25231
  this.expandTableRowLoops(this.document.headerFlowingContent, data);
23551
25232
  this.expandTableRowLoops(this.document.footerFlowingContent, data);
23552
- // Step 3: Substitute all fields in body
25233
+ // Step 4: Evaluate table row conditionals in body, header, and footer
25234
+ this.evaluateTableRowConditionals(bodyContent, data);
25235
+ this.evaluateTableRowConditionals(this.document.headerFlowingContent, data);
25236
+ this.evaluateTableRowConditionals(this.document.footerFlowingContent, data);
25237
+ // Step 5: Substitute all fields in body
23553
25238
  totalFieldCount += this.substituteFieldsInContent(bodyContent, data);
23554
25239
  // Step 4: Substitute all fields in embedded objects in body
23555
25240
  totalFieldCount += this.substituteFieldsInEmbeddedObjects(bodyContent, data);
@@ -23757,9 +25442,62 @@ class PCEditor extends EventEmitter {
23757
25442
  const newFieldName = this.rewriteFieldNameWithIndex(field.fieldName, section.fieldPath, 0);
23758
25443
  fieldManager.updateFieldConfig(field.textIndex, { fieldName: newFieldName });
23759
25444
  }
23760
- // Remove the section after expansion
23761
- sectionManager.remove(section.id);
25445
+ // Remove the section after expansion
25446
+ sectionManager.remove(section.id);
25447
+ }
25448
+ }
25449
+ /**
25450
+ * Evaluate conditional sections by removing content where predicate is false.
25451
+ * Processes sections from end to start to preserve text indices.
25452
+ */
25453
+ evaluateConditionalSections(flowingContent, data) {
25454
+ const sectionManager = flowingContent.getConditionalSectionManager();
25455
+ // Get sections in descending order (process end-to-start)
25456
+ const sections = sectionManager.getSectionsDescending();
25457
+ for (const section of sections) {
25458
+ const result = PredicateEvaluator.evaluate(section.predicate, data);
25459
+ if (!result) {
25460
+ // Predicate is false — remove the content within this section
25461
+ const deleteStart = section.startIndex;
25462
+ const deleteLength = section.endIndex - section.startIndex;
25463
+ flowingContent.deleteText(deleteStart, deleteLength);
25464
+ }
25465
+ // Remove the conditional section marker regardless
25466
+ sectionManager.remove(section.id);
25467
+ }
25468
+ }
25469
+ /**
25470
+ * Evaluate table row conditionals in embedded tables within a FlowingTextContent.
25471
+ * For each table with row conditionals, removes rows where predicate is false.
25472
+ */
25473
+ evaluateTableRowConditionals(flowingContent, data) {
25474
+ const embeddedObjects = flowingContent.getEmbeddedObjects();
25475
+ for (const [, obj] of embeddedObjects.entries()) {
25476
+ if (obj instanceof TableObject) {
25477
+ this.evaluateTableRowConditionalsInTable(obj, data);
25478
+ }
25479
+ }
25480
+ }
25481
+ /**
25482
+ * Evaluate row conditionals in a single table.
25483
+ * Processes conditionals from end to start to preserve row indices.
25484
+ */
25485
+ evaluateTableRowConditionalsInTable(table, data) {
25486
+ const conditionals = table.getAllRowConditionals();
25487
+ if (conditionals.length === 0)
25488
+ return;
25489
+ // Sort by startRowIndex descending (process end-to-start)
25490
+ const sorted = [...conditionals].sort((a, b) => b.startRowIndex - a.startRowIndex);
25491
+ for (const cond of sorted) {
25492
+ const result = PredicateEvaluator.evaluate(cond.predicate, data);
25493
+ if (!result) {
25494
+ // Predicate is false — remove the rows
25495
+ table.removeRowsInRange(cond.startRowIndex, cond.endRowIndex);
25496
+ }
25497
+ // Remove the conditional marker regardless
25498
+ table.removeRowConditional(cond.id);
23762
25499
  }
25500
+ table.markLayoutDirty();
23763
25501
  }
23764
25502
  /**
23765
25503
  * Get a value at a path without array defaulting.
@@ -24008,7 +25746,7 @@ class PCEditor extends EventEmitter {
24008
25746
  toggleBulletList() {
24009
25747
  if (!this._isReady)
24010
25748
  return;
24011
- const flowingContent = this.getActiveFlowingContent();
25749
+ const flowingContent = this.getEditingFlowingContent() || this.getActiveFlowingContent();
24012
25750
  if (!flowingContent)
24013
25751
  return;
24014
25752
  flowingContent.toggleBulletList();
@@ -24021,7 +25759,7 @@ class PCEditor extends EventEmitter {
24021
25759
  toggleNumberedList() {
24022
25760
  if (!this._isReady)
24023
25761
  return;
24024
- const flowingContent = this.getActiveFlowingContent();
25762
+ const flowingContent = this.getEditingFlowingContent() || this.getActiveFlowingContent();
24025
25763
  if (!flowingContent)
24026
25764
  return;
24027
25765
  flowingContent.toggleNumberedList();
@@ -24034,7 +25772,7 @@ class PCEditor extends EventEmitter {
24034
25772
  indentParagraph() {
24035
25773
  if (!this._isReady)
24036
25774
  return;
24037
- const flowingContent = this.getActiveFlowingContent();
25775
+ const flowingContent = this.getEditingFlowingContent() || this.getActiveFlowingContent();
24038
25776
  if (!flowingContent)
24039
25777
  return;
24040
25778
  flowingContent.indentParagraph();
@@ -24047,7 +25785,7 @@ class PCEditor extends EventEmitter {
24047
25785
  outdentParagraph() {
24048
25786
  if (!this._isReady)
24049
25787
  return;
24050
- const flowingContent = this.getActiveFlowingContent();
25788
+ const flowingContent = this.getEditingFlowingContent() || this.getActiveFlowingContent();
24051
25789
  if (!flowingContent)
24052
25790
  return;
24053
25791
  flowingContent.outdentParagraph();
@@ -24060,7 +25798,7 @@ class PCEditor extends EventEmitter {
24060
25798
  getListFormatting() {
24061
25799
  if (!this._isReady)
24062
25800
  return undefined;
24063
- const flowingContent = this.getActiveFlowingContent();
25801
+ const flowingContent = this.getEditingFlowingContent() || this.getActiveFlowingContent();
24064
25802
  if (!flowingContent)
24065
25803
  return undefined;
24066
25804
  return flowingContent.getListFormatting();
@@ -24271,9 +26009,12 @@ class PCEditor extends EventEmitter {
24271
26009
  // If a table is focused, create a row loop instead of a text repeating section
24272
26010
  const focusedTable = this.getFocusedTable();
24273
26011
  if (focusedTable && focusedTable.focusedCell) {
24274
- Logger.log('[pc-editor] createRepeatingSection table row loop', fieldPath);
24275
- const row = focusedTable.focusedCell.row;
24276
- const loop = focusedTable.createRowLoop(row, row, fieldPath);
26012
+ // Use the selected range if multiple rows are selected, otherwise use the focused cell's row
26013
+ const selectedRange = focusedTable.selectedRange;
26014
+ const startRow = selectedRange ? selectedRange.start.row : focusedTable.focusedCell.row;
26015
+ const endRow = selectedRange ? selectedRange.end.row : focusedTable.focusedCell.row;
26016
+ Logger.log('[pc-editor] createRepeatingSection → table row loop', startRow, endRow, fieldPath);
26017
+ const loop = focusedTable.createRowLoop(startRow, endRow, fieldPath);
24277
26018
  if (loop) {
24278
26019
  this.canvasManager.render();
24279
26020
  this.emit('table-row-loop-added', { table: focusedTable, loop });
@@ -24345,6 +26086,103 @@ class PCEditor extends EventEmitter {
24345
26086
  return this.document.bodyFlowingContent.getRepeatingSectionAtBoundary(textIndex) || null;
24346
26087
  }
24347
26088
  // ============================================
26089
+ // Conditional Section API
26090
+ // ============================================
26091
+ /**
26092
+ * Create a conditional section.
26093
+ *
26094
+ * If a table is currently being edited (focused), creates a table row conditional
26095
+ * based on the focused cell's row.
26096
+ *
26097
+ * Otherwise, creates a body text conditional section at the given paragraph boundaries.
26098
+ *
26099
+ * @param startIndex Text index at paragraph start (ignored for table row conditionals)
26100
+ * @param endIndex Text index at closing paragraph start (ignored for table row conditionals)
26101
+ * @param predicate The predicate expression to evaluate (e.g., "isActive")
26102
+ * @returns The created section, or null if creation failed
26103
+ */
26104
+ addConditionalSection(startIndex, endIndex, predicate) {
26105
+ if (!this._isReady) {
26106
+ throw new Error('Editor is not ready');
26107
+ }
26108
+ // If a table is focused, create a row conditional instead
26109
+ const focusedTable = this.getFocusedTable();
26110
+ if (focusedTable && focusedTable.focusedCell) {
26111
+ const selectedRange = focusedTable.selectedRange;
26112
+ const startRow = selectedRange ? selectedRange.start.row : focusedTable.focusedCell.row;
26113
+ const endRow = selectedRange ? selectedRange.end.row : focusedTable.focusedCell.row;
26114
+ Logger.log('[pc-editor] addConditionalSection → table row conditional', startRow, endRow, predicate);
26115
+ const cond = focusedTable.createRowConditional(startRow, endRow, predicate);
26116
+ if (cond) {
26117
+ this.canvasManager.render();
26118
+ this.emit('table-row-conditional-added', { table: focusedTable, conditional: cond });
26119
+ }
26120
+ return null; // Row conditionals are not ConditionalSections, return null
26121
+ }
26122
+ Logger.log('[pc-editor] addConditionalSection', startIndex, endIndex, predicate);
26123
+ const section = this.document.bodyFlowingContent.createConditionalSection(startIndex, endIndex, predicate);
26124
+ if (section) {
26125
+ this.canvasManager.render();
26126
+ this.emit('conditional-section-added', { section });
26127
+ }
26128
+ return section;
26129
+ }
26130
+ /**
26131
+ * Get a conditional section by ID.
26132
+ */
26133
+ getConditionalSection(id) {
26134
+ if (!this._isReady) {
26135
+ return null;
26136
+ }
26137
+ return this.document.bodyFlowingContent.getConditionalSection(id) || null;
26138
+ }
26139
+ /**
26140
+ * Get all conditional sections.
26141
+ */
26142
+ getConditionalSections() {
26143
+ if (!this._isReady) {
26144
+ return [];
26145
+ }
26146
+ return this.document.bodyFlowingContent.getConditionalSections();
26147
+ }
26148
+ /**
26149
+ * Update a conditional section's predicate.
26150
+ */
26151
+ updateConditionalSectionPredicate(id, predicate) {
26152
+ if (!this._isReady) {
26153
+ return false;
26154
+ }
26155
+ const success = this.document.bodyFlowingContent.updateConditionalSectionPredicate(id, predicate);
26156
+ if (success) {
26157
+ this.canvasManager.render();
26158
+ this.emit('conditional-section-updated', { id, predicate });
26159
+ }
26160
+ return success;
26161
+ }
26162
+ /**
26163
+ * Remove a conditional section by ID.
26164
+ */
26165
+ removeConditionalSection(id) {
26166
+ if (!this._isReady) {
26167
+ return false;
26168
+ }
26169
+ const success = this.document.bodyFlowingContent.removeConditionalSection(id);
26170
+ if (success) {
26171
+ this.canvasManager.render();
26172
+ this.emit('conditional-section-removed', { id });
26173
+ }
26174
+ return success;
26175
+ }
26176
+ /**
26177
+ * Find a conditional section that has a boundary at the given text index.
26178
+ */
26179
+ getConditionalSectionAtBoundary(textIndex) {
26180
+ if (!this._isReady) {
26181
+ return null;
26182
+ }
26183
+ return this.document.bodyFlowingContent.getConditionalSectionAtBoundary(textIndex) || null;
26184
+ }
26185
+ // ============================================
24348
26186
  // Header/Footer API
24349
26187
  // ============================================
24350
26188
  /**
@@ -24695,6 +26533,39 @@ class PCEditor extends EventEmitter {
24695
26533
  setLogging(enabled) {
24696
26534
  Logger.setEnabled(enabled);
24697
26535
  }
26536
+ // ============================================
26537
+ // Font Management
26538
+ // ============================================
26539
+ /**
26540
+ * Register a custom font for use in the editor and PDF export.
26541
+ * The font will be loaded via the FontFace API for canvas rendering
26542
+ * and its raw bytes stored for PDF embedding.
26543
+ * @param options Font registration options (family + url or data)
26544
+ */
26545
+ async registerFont(options) {
26546
+ Logger.log('[pc-editor] registerFont', options.family);
26547
+ await this.fontManager.registerFont(options);
26548
+ this.emit('font-registered', { family: options.family });
26549
+ // Re-render to pick up the new font if it's already in use
26550
+ if (this._isReady) {
26551
+ this.canvasManager.render();
26552
+ }
26553
+ }
26554
+ /**
26555
+ * Get all registered fonts (built-in and custom).
26556
+ */
26557
+ getAvailableFonts() {
26558
+ return this.fontManager.getAvailableFonts().map(f => ({
26559
+ family: f.family,
26560
+ source: f.source
26561
+ }));
26562
+ }
26563
+ /**
26564
+ * Get all available font family names.
26565
+ */
26566
+ getAvailableFontFamilies() {
26567
+ return this.fontManager.getAvailableFontFamilies();
26568
+ }
24698
26569
  destroy() {
24699
26570
  this.disableTextInput();
24700
26571
  if (this.canvasManager) {
@@ -26211,7 +28082,7 @@ class MergeDataPane extends BasePane {
26211
28082
  createContent() {
26212
28083
  const container = document.createElement('div');
26213
28084
  // Textarea for JSON
26214
- const textareaGroup = this.createFormGroup('JSON Data', this.createTextarea());
28085
+ const textareaGroup = this.createFormGroup('JSON Data:', this.createTextarea());
26215
28086
  container.appendChild(textareaGroup);
26216
28087
  // Error hint (hidden by default)
26217
28088
  this.errorHint = this.createHint('');
@@ -26357,17 +28228,29 @@ class FormattingPane extends BasePane {
26357
28228
  attach(options) {
26358
28229
  super.attach(options);
26359
28230
  if (this.editor) {
28231
+ // Populate font list from editor if no explicit list was provided
28232
+ if (this.fontFamilies === DEFAULT_FONT_FAMILIES) {
28233
+ this.fontFamilies = this.editor.getAvailableFontFamilies();
28234
+ this.rebuildFontSelect();
28235
+ }
26360
28236
  // Update on cursor/selection changes
26361
28237
  const updateHandler = () => this.updateFromEditor();
26362
28238
  this.editor.on('cursor-changed', updateHandler);
26363
28239
  this.editor.on('selection-changed', updateHandler);
26364
28240
  this.editor.on('text-changed', updateHandler);
26365
28241
  this.editor.on('formatting-changed', updateHandler);
28242
+ // Update font list when new fonts are registered
28243
+ const fontHandler = () => {
28244
+ this.fontFamilies = this.editor.getAvailableFontFamilies();
28245
+ this.rebuildFontSelect();
28246
+ };
28247
+ this.editor.on('font-registered', fontHandler);
26366
28248
  this.eventCleanup.push(() => {
26367
28249
  this.editor?.off('cursor-changed', updateHandler);
26368
28250
  this.editor?.off('selection-changed', updateHandler);
26369
28251
  this.editor?.off('text-changed', updateHandler);
26370
28252
  this.editor?.off('formatting-changed', updateHandler);
28253
+ this.editor?.off('font-registered', fontHandler);
26371
28254
  });
26372
28255
  // Initial update
26373
28256
  this.updateFromEditor();
@@ -26436,38 +28319,82 @@ class FormattingPane extends BasePane {
26436
28319
  listsGroup.appendChild(this.outdentBtn);
26437
28320
  listsSection.appendChild(listsGroup);
26438
28321
  container.appendChild(listsSection);
26439
- // Font section
28322
+ // Font section - label-value grid with right-aligned labels
26440
28323
  const fontSection = this.createSection('Font');
28324
+ const fontGrid = document.createElement('div');
28325
+ fontGrid.className = 'pc-pane-label-value-grid';
28326
+ // Family row
28327
+ const familyLabel = document.createElement('label');
28328
+ familyLabel.className = 'pc-pane-label pc-pane-margin-label';
28329
+ familyLabel.textContent = 'Family:';
26441
28330
  this.fontFamilySelect = this.createSelect(this.fontFamilies.map(f => ({ value: f, label: f })), 'Arial');
26442
28331
  this.addImmediateApplyListener(this.fontFamilySelect, () => this.applyFontFamily());
26443
- fontSection.appendChild(this.createFormGroup('Family', this.fontFamilySelect));
28332
+ fontGrid.appendChild(familyLabel);
28333
+ fontGrid.appendChild(this.fontFamilySelect);
28334
+ fontGrid.appendChild(document.createElement('div'));
28335
+ // Size row
28336
+ const sizeLabel = document.createElement('label');
28337
+ sizeLabel.className = 'pc-pane-label pc-pane-margin-label';
28338
+ sizeLabel.textContent = 'Size:';
26444
28339
  this.fontSizeSelect = this.createSelect(this.fontSizes.map(s => ({ value: s.toString(), label: s.toString() })), '14');
26445
28340
  this.addImmediateApplyListener(this.fontSizeSelect, () => this.applyFontSize());
26446
- fontSection.appendChild(this.createFormGroup('Size', this.fontSizeSelect));
28341
+ fontGrid.appendChild(sizeLabel);
28342
+ fontGrid.appendChild(this.fontSizeSelect);
28343
+ fontGrid.appendChild(document.createElement('div'));
28344
+ fontSection.appendChild(fontGrid);
26447
28345
  container.appendChild(fontSection);
26448
- // Color section
28346
+ // Color section - label-value grid with right-aligned labels
26449
28347
  const colorSection = this.createSection('Color');
26450
- const colorRow = this.createRow();
26451
- const colorGroup = document.createElement('div');
28348
+ const colorGrid = document.createElement('div');
28349
+ colorGrid.className = 'pc-pane-label-value-grid';
28350
+ // Text color row: label | picker | spacer
28351
+ const textColorLabel = document.createElement('label');
28352
+ textColorLabel.className = 'pc-pane-label pc-pane-margin-label';
28353
+ textColorLabel.textContent = 'Text:';
26452
28354
  this.colorInput = this.createColorInput('#000000');
26453
28355
  this.addImmediateApplyListener(this.colorInput, () => this.applyTextColor());
26454
- colorGroup.appendChild(this.createFormGroup('Text', this.colorInput));
26455
- colorRow.appendChild(colorGroup);
26456
- const highlightGroup = document.createElement('div');
28356
+ colorGrid.appendChild(textColorLabel);
28357
+ colorGrid.appendChild(this.colorInput);
28358
+ colorGrid.appendChild(document.createElement('div'));
28359
+ // Highlight row: label | picker + clear button | spacer
28360
+ const highlightLabel = document.createElement('label');
28361
+ highlightLabel.className = 'pc-pane-label pc-pane-margin-label';
28362
+ highlightLabel.textContent = 'Highlight:';
26457
28363
  this.highlightInput = this.createColorInput('#ffff00');
26458
28364
  this.addImmediateApplyListener(this.highlightInput, () => this.applyHighlight());
26459
- const highlightForm = this.createFormGroup('Highlight', this.highlightInput);
28365
+ const highlightControls = document.createElement('div');
28366
+ highlightControls.style.display = 'flex';
28367
+ highlightControls.style.alignItems = 'center';
28368
+ highlightControls.style.gap = '4px';
28369
+ highlightControls.appendChild(this.highlightInput);
26460
28370
  const clearHighlightBtn = this.createButton('Clear');
26461
28371
  clearHighlightBtn.className = 'pc-pane-button';
26462
- clearHighlightBtn.style.marginLeft = '4px';
26463
28372
  this.addButtonListener(clearHighlightBtn, () => this.clearHighlight());
26464
- highlightForm.appendChild(clearHighlightBtn);
26465
- highlightGroup.appendChild(highlightForm);
26466
- colorRow.appendChild(highlightGroup);
26467
- colorSection.appendChild(colorRow);
28373
+ highlightControls.appendChild(clearHighlightBtn);
28374
+ colorGrid.appendChild(highlightLabel);
28375
+ colorGrid.appendChild(highlightControls);
28376
+ colorGrid.appendChild(document.createElement('div'));
28377
+ colorSection.appendChild(colorGrid);
26468
28378
  container.appendChild(colorSection);
26469
28379
  return container;
26470
28380
  }
28381
+ rebuildFontSelect() {
28382
+ if (!this.fontFamilySelect)
28383
+ return;
28384
+ const currentValue = this.fontFamilySelect.value;
28385
+ this.fontFamilySelect.innerHTML = '';
28386
+ for (const family of this.fontFamilies) {
28387
+ const option = document.createElement('option');
28388
+ option.value = family;
28389
+ option.textContent = family;
28390
+ option.style.fontFamily = family;
28391
+ this.fontFamilySelect.appendChild(option);
28392
+ }
28393
+ // Restore selection if the font still exists
28394
+ if (this.fontFamilies.includes(currentValue)) {
28395
+ this.fontFamilySelect.value = currentValue;
28396
+ }
28397
+ }
26471
28398
  updateFromEditor() {
26472
28399
  if (!this.editor)
26473
28400
  return;
@@ -26521,9 +28448,15 @@ class FormattingPane extends BasePane {
26521
28448
  this.bulletListBtn?.classList.toggle('pc-pane-button--active', listFormatting.listType === 'bullet');
26522
28449
  this.numberedListBtn?.classList.toggle('pc-pane-button--active', listFormatting.listType === 'number');
26523
28450
  }
28451
+ else {
28452
+ this.bulletListBtn?.classList.remove('pc-pane-button--active');
28453
+ this.numberedListBtn?.classList.remove('pc-pane-button--active');
28454
+ }
26524
28455
  }
26525
28456
  catch {
26526
28457
  // No text editing active
28458
+ this.bulletListBtn?.classList.remove('pc-pane-button--active');
28459
+ this.numberedListBtn?.classList.remove('pc-pane-button--active');
26527
28460
  }
26528
28461
  }
26529
28462
  getSelection() {
@@ -26685,10 +28618,10 @@ class HyperlinkPane extends BasePane {
26685
28618
  const container = document.createElement('div');
26686
28619
  // URL input
26687
28620
  this.urlInput = this.createTextInput({ placeholder: 'https://example.com' });
26688
- container.appendChild(this.createFormGroup('URL', this.urlInput));
28621
+ container.appendChild(this.createFormGroup('URL:', this.urlInput));
26689
28622
  // Title input
26690
28623
  this.titleInput = this.createTextInput({ placeholder: 'Link title (optional)' });
26691
- container.appendChild(this.createFormGroup('Title', this.titleInput));
28624
+ container.appendChild(this.createFormGroup('Title:', this.titleInput));
26692
28625
  // Apply button
26693
28626
  const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
26694
28627
  this.addButtonListener(applyBtn, () => this.applyChanges());
@@ -26839,10 +28772,10 @@ class SubstitutionFieldPane extends BasePane {
26839
28772
  const container = document.createElement('div');
26840
28773
  // Field name input
26841
28774
  this.fieldNameInput = this.createTextInput({ placeholder: 'Field name' });
26842
- container.appendChild(this.createFormGroup('Field Name', this.fieldNameInput));
28775
+ container.appendChild(this.createFormGroup('Field Name:', this.fieldNameInput));
26843
28776
  // Default value input
26844
28777
  this.fieldDefaultInput = this.createTextInput({ placeholder: 'Default value (optional)' });
26845
- container.appendChild(this.createFormGroup('Default Value', this.fieldDefaultInput));
28778
+ container.appendChild(this.createFormGroup('Default Value:', this.fieldDefaultInput));
26846
28779
  // Value type select
26847
28780
  this.valueTypeSelect = this.createSelect([
26848
28781
  { value: '', label: '(None)' },
@@ -26851,7 +28784,7 @@ class SubstitutionFieldPane extends BasePane {
26851
28784
  { value: 'date', label: 'Date' }
26852
28785
  ]);
26853
28786
  this.addImmediateApplyListener(this.valueTypeSelect, () => this.updateFormatGroups());
26854
- container.appendChild(this.createFormGroup('Value Type', this.valueTypeSelect));
28787
+ container.appendChild(this.createFormGroup('Value Type:', this.valueTypeSelect));
26855
28788
  // Number format group
26856
28789
  this.numberFormatGroup = this.createSection();
26857
28790
  this.numberFormatGroup.style.display = 'none';
@@ -26861,7 +28794,7 @@ class SubstitutionFieldPane extends BasePane {
26861
28794
  { value: '0,0', label: 'Thousands separator (0,0)' },
26862
28795
  { value: '0,0.00', label: 'Thousands + decimals (0,0.00)' }
26863
28796
  ]);
26864
- this.numberFormatGroup.appendChild(this.createFormGroup('Number Format', this.numberFormatSelect));
28797
+ this.numberFormatGroup.appendChild(this.createFormGroup('Number Format:', this.numberFormatSelect));
26865
28798
  container.appendChild(this.numberFormatGroup);
26866
28799
  // Currency format group
26867
28800
  this.currencyFormatGroup = this.createSection();
@@ -26872,7 +28805,7 @@ class SubstitutionFieldPane extends BasePane {
26872
28805
  { value: 'GBP', label: 'GBP' },
26873
28806
  { value: 'JPY', label: 'JPY' }
26874
28807
  ]);
26875
- this.currencyFormatGroup.appendChild(this.createFormGroup('Currency', this.currencyFormatSelect));
28808
+ this.currencyFormatGroup.appendChild(this.createFormGroup('Currency:', this.currencyFormatSelect));
26876
28809
  container.appendChild(this.currencyFormatGroup);
26877
28810
  // Date format group
26878
28811
  this.dateFormatGroup = this.createSection();
@@ -26883,7 +28816,7 @@ class SubstitutionFieldPane extends BasePane {
26883
28816
  { value: 'DD/MM/YYYY', label: '01/01/2026 (EU)' },
26884
28817
  { value: 'YYYY-MM-DD', label: '2026-01-01 (ISO)' }
26885
28818
  ]);
26886
- this.dateFormatGroup.appendChild(this.createFormGroup('Date Format', this.dateFormatSelect));
28819
+ this.dateFormatGroup.appendChild(this.createFormGroup('Date Format:', this.dateFormatSelect));
26887
28820
  container.appendChild(this.dateFormatGroup);
26888
28821
  // Apply button
26889
28822
  const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
@@ -27045,12 +28978,17 @@ class RepeatingSectionPane extends BasePane {
27045
28978
  if (this.editor) {
27046
28979
  // Listen for repeating section selection
27047
28980
  const selectionHandler = (event) => {
27048
- if (event.type === 'repeating-section' && event.sectionId) {
27049
- const section = this.editor?.getRepeatingSection(event.sectionId);
28981
+ const sel = event.selection || event;
28982
+ if (sel.type === 'repeating-section' && sel.sectionId) {
28983
+ const section = this.editor?.getRepeatingSection(sel.sectionId);
27050
28984
  if (section) {
27051
28985
  this.showSection(section);
27052
28986
  }
27053
28987
  }
28988
+ else {
28989
+ // Selection changed away from repeating section — hide pane
28990
+ this.hideSection();
28991
+ }
27054
28992
  };
27055
28993
  const removedHandler = () => {
27056
28994
  this.hideSection();
@@ -27067,7 +29005,7 @@ class RepeatingSectionPane extends BasePane {
27067
29005
  const container = document.createElement('div');
27068
29006
  // Field path input
27069
29007
  this.fieldPathInput = this.createTextInput({ placeholder: 'items' });
27070
- container.appendChild(this.createFormGroup('Array Field Path', this.fieldPathInput, {
29008
+ container.appendChild(this.createFormGroup('Array Field Path:', this.fieldPathInput, {
27071
29009
  hint: 'Path to array in merge data (e.g., "items" or "contact.addresses")'
27072
29010
  }));
27073
29011
  // Apply button
@@ -27167,6 +29105,158 @@ class RepeatingSectionPane extends BasePane {
27167
29105
  }
27168
29106
  }
27169
29107
 
29108
+ /**
29109
+ * ConditionalSectionPane - Edit conditional section properties.
29110
+ *
29111
+ * Shows:
29112
+ * - Predicate input (boolean expression in merge data)
29113
+ * - Position information
29114
+ *
29115
+ * Uses the PCEditor public API:
29116
+ * - editor.getConditionalSection()
29117
+ * - editor.updateConditionalSectionPredicate()
29118
+ * - editor.removeConditionalSection()
29119
+ */
29120
+ class ConditionalSectionPane extends BasePane {
29121
+ constructor(id = 'conditional-section', options = {}) {
29122
+ super(id, { className: 'pc-pane-conditional-section', ...options });
29123
+ this.predicateInput = null;
29124
+ this.positionHint = null;
29125
+ this.currentSection = null;
29126
+ this.onApplyCallback = options.onApply;
29127
+ this.onRemoveCallback = options.onRemove;
29128
+ }
29129
+ attach(options) {
29130
+ super.attach(options);
29131
+ if (this.editor) {
29132
+ // Listen for conditional section selection
29133
+ const selectionHandler = (event) => {
29134
+ const sel = event.selection || event;
29135
+ if (sel.type === 'conditional-section' && sel.sectionId) {
29136
+ const section = this.editor?.getConditionalSection(sel.sectionId);
29137
+ if (section) {
29138
+ this.showSection(section);
29139
+ }
29140
+ }
29141
+ else {
29142
+ // Selection changed away from conditional section — hide pane
29143
+ this.hideSection();
29144
+ }
29145
+ };
29146
+ const removedHandler = () => {
29147
+ this.hideSection();
29148
+ };
29149
+ this.editor.on('selection-change', selectionHandler);
29150
+ this.editor.on('conditional-section-removed', removedHandler);
29151
+ this.eventCleanup.push(() => {
29152
+ this.editor?.off('selection-change', selectionHandler);
29153
+ this.editor?.off('conditional-section-removed', removedHandler);
29154
+ });
29155
+ }
29156
+ }
29157
+ createContent() {
29158
+ const container = document.createElement('div');
29159
+ // Predicate input
29160
+ this.predicateInput = this.createTextInput({ placeholder: 'isActive' });
29161
+ container.appendChild(this.createFormGroup('Condition:', this.predicateInput, {
29162
+ hint: 'Boolean expression evaluated against merge data (e.g., "isActive", "count > 0")'
29163
+ }));
29164
+ // Apply button
29165
+ const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
29166
+ this.addButtonListener(applyBtn, () => this.applyChanges());
29167
+ container.appendChild(applyBtn);
29168
+ // Remove button
29169
+ const removeBtn = this.createButton('Remove Condition', { variant: 'danger' });
29170
+ removeBtn.style.marginTop = '0.5rem';
29171
+ this.addButtonListener(removeBtn, () => this.removeSection());
29172
+ container.appendChild(removeBtn);
29173
+ // Position hint
29174
+ this.positionHint = this.createHint('');
29175
+ container.appendChild(this.positionHint);
29176
+ return container;
29177
+ }
29178
+ /**
29179
+ * Show the pane with the given section.
29180
+ */
29181
+ showSection(section) {
29182
+ this.currentSection = section;
29183
+ if (this.predicateInput) {
29184
+ this.predicateInput.value = section.predicate;
29185
+ }
29186
+ if (this.positionHint) {
29187
+ this.positionHint.textContent = `Condition from position ${section.startIndex} to ${section.endIndex}`;
29188
+ }
29189
+ this.show();
29190
+ }
29191
+ /**
29192
+ * Hide the pane and clear the current section.
29193
+ */
29194
+ hideSection() {
29195
+ this.currentSection = null;
29196
+ this.hide();
29197
+ }
29198
+ applyChanges() {
29199
+ if (!this.editor || !this.currentSection) {
29200
+ this.onApplyCallback?.(false, new Error('No section selected'));
29201
+ return;
29202
+ }
29203
+ const predicate = this.predicateInput?.value.trim();
29204
+ if (!predicate) {
29205
+ this.onApplyCallback?.(false, new Error('Predicate cannot be empty'));
29206
+ return;
29207
+ }
29208
+ if (predicate === this.currentSection.predicate) {
29209
+ return; // No changes
29210
+ }
29211
+ try {
29212
+ const success = this.editor.updateConditionalSectionPredicate(this.currentSection.id, predicate);
29213
+ if (success) {
29214
+ this.currentSection = this.editor.getConditionalSection(this.currentSection.id) || null;
29215
+ if (this.currentSection) {
29216
+ this.showSection(this.currentSection);
29217
+ }
29218
+ this.onApplyCallback?.(true);
29219
+ }
29220
+ else {
29221
+ this.onApplyCallback?.(false, new Error('Failed to update section'));
29222
+ }
29223
+ }
29224
+ catch (error) {
29225
+ this.onApplyCallback?.(false, error instanceof Error ? error : new Error(String(error)));
29226
+ }
29227
+ }
29228
+ removeSection() {
29229
+ if (!this.editor || !this.currentSection)
29230
+ return;
29231
+ try {
29232
+ this.editor.removeConditionalSection(this.currentSection.id);
29233
+ this.hideSection();
29234
+ this.onRemoveCallback?.(true);
29235
+ }
29236
+ catch {
29237
+ this.onRemoveCallback?.(false);
29238
+ }
29239
+ }
29240
+ /**
29241
+ * Get the currently selected section.
29242
+ */
29243
+ getCurrentSection() {
29244
+ return this.currentSection;
29245
+ }
29246
+ /**
29247
+ * Check if a section is currently selected.
29248
+ */
29249
+ hasSection() {
29250
+ return this.currentSection !== null;
29251
+ }
29252
+ /**
29253
+ * Update the pane from current editor state.
29254
+ */
29255
+ update() {
29256
+ // Section pane doesn't auto-update - it's driven by selection events
29257
+ }
29258
+ }
29259
+
27170
29260
  /**
27171
29261
  * TableRowLoopPane - Edit table row loop properties.
27172
29262
  *
@@ -27191,14 +29281,28 @@ class TableRowLoopPane extends BasePane {
27191
29281
  }
27192
29282
  attach(options) {
27193
29283
  super.attach(options);
27194
- // Table row loop pane is typically shown manually when a table's row loop is selected
27195
- // The consumer is responsible for calling showLoop() with the table and loop
29284
+ if (this.editor) {
29285
+ // Auto-show when a table row loop is clicked
29286
+ const loopClickHandler = (data) => {
29287
+ this.showLoop(data.table, data.loop);
29288
+ };
29289
+ // Hide when selection changes away from a loop
29290
+ const selectionHandler = () => {
29291
+ this.hideLoop();
29292
+ };
29293
+ this.editor.on('table-row-loop-clicked', loopClickHandler);
29294
+ this.editor.on('selection-change', selectionHandler);
29295
+ this.eventCleanup.push(() => {
29296
+ this.editor?.off('table-row-loop-clicked', loopClickHandler);
29297
+ this.editor?.off('selection-change', selectionHandler);
29298
+ });
29299
+ }
27196
29300
  }
27197
29301
  createContent() {
27198
29302
  const container = document.createElement('div');
27199
29303
  // Field path input
27200
29304
  this.fieldPathInput = this.createTextInput({ placeholder: 'items' });
27201
- container.appendChild(this.createFormGroup('Array Field Path', this.fieldPathInput, {
29305
+ container.appendChild(this.createFormGroup('Array Field Path:', this.fieldPathInput, {
27202
29306
  hint: 'Path to array in merge data (e.g., "items" or "orders")'
27203
29307
  }));
27204
29308
  // Apply button
@@ -27358,56 +29462,63 @@ class TextBoxPane extends BasePane {
27358
29462
  }
27359
29463
  createContent() {
27360
29464
  const container = document.createElement('div');
27361
- // Position section
29465
+ // Position section - Type on same row as label
27362
29466
  const positionSection = this.createSection('Position');
27363
29467
  this.positionSelect = this.createSelect([
27364
29468
  { value: 'inline', label: 'Inline' },
27365
29469
  { value: 'block', label: 'Block' },
27366
29470
  { value: 'relative', label: 'Relative' }
27367
29471
  ], 'inline');
27368
- this.addImmediateApplyListener(this.positionSelect, () => this.updateOffsetVisibility());
27369
- positionSection.appendChild(this.createFormGroup('Type', this.positionSelect));
29472
+ this.addImmediateApplyListener(this.positionSelect, () => {
29473
+ this.updateOffsetVisibility();
29474
+ this.applyChanges();
29475
+ });
29476
+ positionSection.appendChild(this.createFormGroup('Type:', this.positionSelect, { inline: true }));
27370
29477
  // Offset group (only visible for relative positioning)
27371
29478
  this.offsetGroup = document.createElement('div');
27372
29479
  this.offsetGroup.style.display = 'none';
27373
29480
  const offsetRow = this.createRow();
27374
29481
  this.offsetXInput = this.createNumberInput({ value: 0 });
27375
29482
  this.offsetYInput = this.createNumberInput({ value: 0 });
27376
- offsetRow.appendChild(this.createFormGroup('X', this.offsetXInput, { inline: true }));
27377
- offsetRow.appendChild(this.createFormGroup('Y', this.offsetYInput, { inline: true }));
29483
+ this.addImmediateApplyListener(this.offsetXInput, () => this.applyChanges());
29484
+ this.addImmediateApplyListener(this.offsetYInput, () => this.applyChanges());
29485
+ offsetRow.appendChild(this.createFormGroup('X:', this.offsetXInput, { inline: true }));
29486
+ offsetRow.appendChild(this.createFormGroup('Y:', this.offsetYInput, { inline: true }));
27378
29487
  this.offsetGroup.appendChild(offsetRow);
27379
29488
  positionSection.appendChild(this.offsetGroup);
27380
29489
  container.appendChild(positionSection);
27381
- // Background section
27382
- const bgSection = this.createSection('Background');
29490
+ // Background - color on same row as label
29491
+ const bgSection = this.createSection();
27383
29492
  this.bgColorInput = this.createColorInput('#ffffff');
27384
- bgSection.appendChild(this.createFormGroup('Color', this.bgColorInput));
29493
+ this.addImmediateApplyListener(this.bgColorInput, () => this.applyChanges());
29494
+ bgSection.appendChild(this.createFormGroup('Background:', this.bgColorInput, { inline: true }));
27385
29495
  container.appendChild(bgSection);
27386
29496
  // Border section
27387
29497
  const borderSection = this.createSection('Border');
27388
29498
  const borderRow = this.createRow();
27389
29499
  this.borderWidthInput = this.createNumberInput({ min: 0, max: 10, value: 1 });
27390
29500
  this.borderColorInput = this.createColorInput('#cccccc');
27391
- borderRow.appendChild(this.createFormGroup('Width', this.borderWidthInput, { inline: true }));
27392
- borderRow.appendChild(this.createFormGroup('Color', this.borderColorInput, { inline: true }));
29501
+ this.addImmediateApplyListener(this.borderWidthInput, () => this.applyChanges());
29502
+ this.addImmediateApplyListener(this.borderColorInput, () => this.applyChanges());
29503
+ borderRow.appendChild(this.createFormGroup('Width:', this.borderWidthInput, { inline: true }));
29504
+ borderRow.appendChild(this.createFormGroup('Color:', this.borderColorInput, { inline: true }));
27393
29505
  borderSection.appendChild(borderRow);
29506
+ // Border style on same row as label
27394
29507
  this.borderStyleSelect = this.createSelect([
27395
29508
  { value: 'solid', label: 'Solid' },
27396
29509
  { value: 'dashed', label: 'Dashed' },
27397
29510
  { value: 'dotted', label: 'Dotted' },
27398
29511
  { value: 'none', label: 'None' }
27399
29512
  ], 'solid');
27400
- borderSection.appendChild(this.createFormGroup('Style', this.borderStyleSelect));
29513
+ this.addImmediateApplyListener(this.borderStyleSelect, () => this.applyChanges());
29514
+ borderSection.appendChild(this.createFormGroup('Style:', this.borderStyleSelect, { inline: true }));
27401
29515
  container.appendChild(borderSection);
27402
- // Padding section
27403
- const paddingSection = this.createSection('Padding');
29516
+ // Padding on same row as label
29517
+ const paddingSection = this.createSection();
27404
29518
  this.paddingInput = this.createNumberInput({ min: 0, max: 50, value: 8 });
27405
- paddingSection.appendChild(this.createFormGroup('All sides (px)', this.paddingInput));
29519
+ this.addImmediateApplyListener(this.paddingInput, () => this.applyChanges());
29520
+ paddingSection.appendChild(this.createFormGroup('Padding:', this.paddingInput, { inline: true }));
27406
29521
  container.appendChild(paddingSection);
27407
- // Apply button
27408
- const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
27409
- this.addButtonListener(applyBtn, () => this.applyChanges());
27410
- container.appendChild(applyBtn);
27411
29522
  return container;
27412
29523
  }
27413
29524
  updateFromSelection() {
@@ -27483,7 +29594,6 @@ class TextBoxPane extends BasePane {
27483
29594
  }
27484
29595
  applyChanges() {
27485
29596
  if (!this.editor || !this.currentTextBox) {
27486
- this.onApplyCallback?.(false, new Error('No text box selected'));
27487
29597
  return;
27488
29598
  }
27489
29599
  const updates = {};
@@ -27519,12 +29629,7 @@ class TextBoxPane extends BasePane {
27519
29629
  }
27520
29630
  try {
27521
29631
  const success = this.editor.updateTextBox(this.currentTextBox.id, updates);
27522
- if (success) {
27523
- this.onApplyCallback?.(true);
27524
- }
27525
- else {
27526
- this.onApplyCallback?.(false, new Error('Failed to update text box'));
27527
- }
29632
+ this.onApplyCallback?.(success);
27528
29633
  }
27529
29634
  catch (error) {
27530
29635
  this.onApplyCallback?.(false, error instanceof Error ? error : new Error(String(error)));
@@ -27600,28 +29705,35 @@ class ImagePane extends BasePane {
27600
29705
  }
27601
29706
  createContent() {
27602
29707
  const container = document.createElement('div');
27603
- // Position section
29708
+ // Position section — with heading, matching TextBoxPane
27604
29709
  const positionSection = this.createSection('Position');
27605
29710
  this.positionSelect = this.createSelect([
27606
29711
  { value: 'inline', label: 'Inline' },
27607
29712
  { value: 'block', label: 'Block' },
27608
29713
  { value: 'relative', label: 'Relative' }
27609
29714
  ], 'inline');
27610
- this.addImmediateApplyListener(this.positionSelect, () => this.updateOffsetVisibility());
27611
- positionSection.appendChild(this.createFormGroup('Type', this.positionSelect));
29715
+ this.addImmediateApplyListener(this.positionSelect, () => {
29716
+ this.updateOffsetVisibility();
29717
+ this.applyChanges();
29718
+ });
29719
+ positionSection.appendChild(this.createFormGroup('Type:', this.positionSelect, { inline: true }));
27612
29720
  // Offset group (only visible for relative positioning)
27613
29721
  this.offsetGroup = document.createElement('div');
27614
29722
  this.offsetGroup.style.display = 'none';
27615
29723
  const offsetRow = this.createRow();
27616
29724
  this.offsetXInput = this.createNumberInput({ value: 0 });
27617
29725
  this.offsetYInput = this.createNumberInput({ value: 0 });
27618
- offsetRow.appendChild(this.createFormGroup('X', this.offsetXInput, { inline: true }));
27619
- offsetRow.appendChild(this.createFormGroup('Y', this.offsetYInput, { inline: true }));
29726
+ this.addImmediateApplyListener(this.offsetXInput, () => this.applyChanges());
29727
+ this.addImmediateApplyListener(this.offsetYInput, () => this.applyChanges());
29728
+ offsetRow.appendChild(this.createFormGroup('X:', this.offsetXInput, { inline: true }));
29729
+ offsetRow.appendChild(this.createFormGroup('Y:', this.offsetYInput, { inline: true }));
27620
29730
  this.offsetGroup.appendChild(offsetRow);
27621
29731
  positionSection.appendChild(this.offsetGroup);
27622
29732
  container.appendChild(positionSection);
27623
- // Fit mode section
27624
- const fitSection = this.createSection('Display');
29733
+ container.appendChild(document.createElement('hr'));
29734
+ // Display section — Fit Mode and Resize Mode with aligned labels
29735
+ const displaySection = document.createElement('div');
29736
+ displaySection.className = 'pc-pane-image-display';
27625
29737
  this.fitModeSelect = this.createSelect([
27626
29738
  { value: 'contain', label: 'Contain' },
27627
29739
  { value: 'cover', label: 'Cover' },
@@ -27629,34 +29741,31 @@ class ImagePane extends BasePane {
27629
29741
  { value: 'none', label: 'None (original size)' },
27630
29742
  { value: 'tile', label: 'Tile' }
27631
29743
  ], 'contain');
27632
- fitSection.appendChild(this.createFormGroup('Fit Mode', this.fitModeSelect));
29744
+ this.addImmediateApplyListener(this.fitModeSelect, () => this.applyChanges());
29745
+ displaySection.appendChild(this.createFormGroup('Fit Mode:', this.fitModeSelect, { inline: true }));
27633
29746
  this.resizeModeSelect = this.createSelect([
27634
29747
  { value: 'locked-aspect-ratio', label: 'Lock Aspect Ratio' },
27635
29748
  { value: 'free', label: 'Free Resize' }
27636
29749
  ], 'locked-aspect-ratio');
27637
- fitSection.appendChild(this.createFormGroup('Resize Mode', this.resizeModeSelect));
27638
- container.appendChild(fitSection);
27639
- // Alt text section
27640
- const altSection = this.createSection('Accessibility');
29750
+ this.addImmediateApplyListener(this.resizeModeSelect, () => this.applyChanges());
29751
+ displaySection.appendChild(this.createFormGroup('Resize Mode:', this.resizeModeSelect, { inline: true }));
29752
+ container.appendChild(displaySection);
29753
+ container.appendChild(document.createElement('hr'));
29754
+ // Alt Text — inline row
27641
29755
  this.altTextInput = this.createTextInput({ placeholder: 'Description of the image' });
27642
- altSection.appendChild(this.createFormGroup('Alt Text', this.altTextInput));
27643
- container.appendChild(altSection);
27644
- // Source section
27645
- const sourceSection = this.createSection('Source');
29756
+ this.addImmediateApplyListener(this.altTextInput, () => this.applyChanges());
29757
+ container.appendChild(this.createFormGroup('Alt Text:', this.altTextInput, { inline: true }));
29758
+ container.appendChild(document.createElement('hr'));
29759
+ // Source change image button
27646
29760
  this.fileInput = document.createElement('input');
27647
29761
  this.fileInput.type = 'file';
27648
29762
  this.fileInput.accept = 'image/*';
27649
29763
  this.fileInput.style.display = 'none';
27650
29764
  this.fileInput.addEventListener('change', (e) => this.handleFileChange(e));
27651
- sourceSection.appendChild(this.fileInput);
29765
+ container.appendChild(this.fileInput);
27652
29766
  const changeSourceBtn = this.createButton('Change Image...');
27653
29767
  this.addButtonListener(changeSourceBtn, () => this.fileInput?.click());
27654
- sourceSection.appendChild(changeSourceBtn);
27655
- container.appendChild(sourceSection);
27656
- // Apply button
27657
- const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
27658
- this.addButtonListener(applyBtn, () => this.applyChanges());
27659
- container.appendChild(applyBtn);
29768
+ container.appendChild(changeSourceBtn);
27660
29769
  return container;
27661
29770
  }
27662
29771
  updateFromSelection() {
@@ -27838,8 +29947,9 @@ class TablePane extends BasePane {
27838
29947
  // Default controls
27839
29948
  this.defaultPaddingInput = null;
27840
29949
  this.defaultBorderColorInput = null;
27841
- // Row loop controls
27842
- this.loopFieldInput = null;
29950
+ // Merge/split buttons
29951
+ this.mergeCellsBtn = null;
29952
+ this.splitCellBtn = null;
27843
29953
  // Cell formatting controls
27844
29954
  this.cellBgColorInput = null;
27845
29955
  this.borderTopCheck = null;
@@ -27859,12 +29969,12 @@ class TablePane extends BasePane {
27859
29969
  // Listen for selection/focus changes
27860
29970
  const updateHandler = () => this.updateFromFocusedTable();
27861
29971
  this.editor.on('selection-change', updateHandler);
27862
- this.editor.on('table-cell-focus', updateHandler);
27863
- this.editor.on('table-cell-selection', updateHandler);
29972
+ this.editor.on('tablecell-cursor-changed', updateHandler);
29973
+ this.editor.on('table-cell-selection-changed', updateHandler);
27864
29974
  this.eventCleanup.push(() => {
27865
29975
  this.editor?.off('selection-change', updateHandler);
27866
- this.editor?.off('table-cell-focus', updateHandler);
27867
- this.editor?.off('table-cell-selection', updateHandler);
29976
+ this.editor?.off('tablecell-cursor-changed', updateHandler);
29977
+ this.editor?.off('table-cell-selection-changed', updateHandler);
27868
29978
  });
27869
29979
  // Initial update
27870
29980
  this.updateFromFocusedTable();
@@ -27874,78 +29984,78 @@ class TablePane extends BasePane {
27874
29984
  const container = document.createElement('div');
27875
29985
  // Structure section
27876
29986
  const structureSection = this.createSection('Structure');
29987
+ // Rows/Columns info with aligned labels
27877
29988
  const structureInfo = document.createElement('div');
27878
- structureInfo.className = 'pc-pane-info-list';
29989
+ structureInfo.className = 'pc-pane-table-structure-info';
27879
29990
  this.rowCountDisplay = document.createElement('span');
29991
+ this.rowCountDisplay.className = 'pc-pane-info-value';
27880
29992
  this.colCountDisplay = document.createElement('span');
27881
- const rowInfo = document.createElement('div');
27882
- rowInfo.className = 'pc-pane-info';
27883
- rowInfo.innerHTML = '<span class="pc-pane-info-label">Rows</span>';
27884
- rowInfo.appendChild(this.rowCountDisplay);
27885
- const colInfo = document.createElement('div');
27886
- colInfo.className = 'pc-pane-info';
27887
- colInfo.innerHTML = '<span class="pc-pane-info-label">Columns</span>';
27888
- colInfo.appendChild(this.colCountDisplay);
27889
- structureInfo.appendChild(rowInfo);
27890
- structureInfo.appendChild(colInfo);
29993
+ this.colCountDisplay.className = 'pc-pane-info-value';
29994
+ structureInfo.appendChild(this.createFormGroup('Rows:', this.rowCountDisplay, { inline: true }));
29995
+ structureInfo.appendChild(this.createFormGroup('Columns:', this.colCountDisplay, { inline: true }));
27891
29996
  structureSection.appendChild(structureInfo);
27892
- // Row/column buttons
27893
- const structureBtns = this.createButtonGroup();
29997
+ // Row buttons
29998
+ const rowBtns = this.createButtonGroup();
27894
29999
  const addRowBtn = this.createButton('+ Row');
27895
30000
  this.addButtonListener(addRowBtn, () => this.insertRow());
27896
30001
  const removeRowBtn = this.createButton('- Row');
27897
30002
  this.addButtonListener(removeRowBtn, () => this.removeRow());
30003
+ rowBtns.appendChild(addRowBtn);
30004
+ rowBtns.appendChild(removeRowBtn);
30005
+ structureSection.appendChild(rowBtns);
30006
+ // Column buttons (separate row)
30007
+ const colBtns = this.createButtonGroup();
27898
30008
  const addColBtn = this.createButton('+ Column');
27899
30009
  this.addButtonListener(addColBtn, () => this.insertColumn());
27900
30010
  const removeColBtn = this.createButton('- Column');
27901
30011
  this.addButtonListener(removeColBtn, () => this.removeColumn());
27902
- structureBtns.appendChild(addRowBtn);
27903
- structureBtns.appendChild(removeRowBtn);
27904
- structureBtns.appendChild(addColBtn);
27905
- structureBtns.appendChild(removeColBtn);
27906
- structureSection.appendChild(structureBtns);
27907
- container.appendChild(structureSection);
27908
- // Headers section
27909
- const headersSection = this.createSection('Headers');
27910
- const headerRow = this.createRow();
30012
+ colBtns.appendChild(addColBtn);
30013
+ colBtns.appendChild(removeColBtn);
30014
+ structureSection.appendChild(colBtns);
30015
+ // Header rows/cols (with separator and aligned labels)
30016
+ structureSection.appendChild(document.createElement('hr'));
30017
+ const headersGroup = document.createElement('div');
30018
+ headersGroup.className = 'pc-pane-table-headers';
27911
30019
  this.headerRowInput = this.createNumberInput({ min: 0, max: 10, value: 0 });
30020
+ this.addImmediateApplyListener(this.headerRowInput, () => this.applyHeaders());
30021
+ headersGroup.appendChild(this.createFormGroup('Header Rows:', this.headerRowInput, { inline: true }));
27912
30022
  this.headerColInput = this.createNumberInput({ min: 0, max: 10, value: 0 });
27913
- headerRow.appendChild(this.createFormGroup('Header Rows', this.headerRowInput, { inline: true }));
27914
- headerRow.appendChild(this.createFormGroup('Header Cols', this.headerColInput, { inline: true }));
27915
- headersSection.appendChild(headerRow);
27916
- const applyHeadersBtn = this.createButton('Apply Headers');
27917
- this.addButtonListener(applyHeadersBtn, () => this.applyHeaders());
27918
- headersSection.appendChild(applyHeadersBtn);
27919
- container.appendChild(headersSection);
27920
- // Row Loop section
27921
- const loopSection = this.createSection('Row Loop');
27922
- this.loopFieldInput = this.createTextInput({ placeholder: 'items' });
27923
- loopSection.appendChild(this.createFormGroup('Array Field', this.loopFieldInput, {
27924
- hint: 'Creates a loop on the currently focused row'
27925
- }));
27926
- const createLoopBtn = this.createButton('Create Row Loop');
27927
- this.addButtonListener(createLoopBtn, () => this.createRowLoop());
27928
- loopSection.appendChild(createLoopBtn);
27929
- container.appendChild(loopSection);
27930
- // Defaults section
30023
+ this.addImmediateApplyListener(this.headerColInput, () => this.applyHeaders());
30024
+ headersGroup.appendChild(this.createFormGroup('Header Cols:', this.headerColInput, { inline: true }));
30025
+ structureSection.appendChild(headersGroup);
30026
+ container.appendChild(structureSection);
30027
+ // Defaults section (aligned labels)
27931
30028
  const defaultsSection = this.createSection('Defaults');
27932
- const defaultsRow = this.createRow();
30029
+ const defaultsGroup = document.createElement('div');
30030
+ defaultsGroup.className = 'pc-pane-table-defaults';
27933
30031
  this.defaultPaddingInput = this.createNumberInput({ min: 0, max: 20, value: 8 });
30032
+ this.addImmediateApplyListener(this.defaultPaddingInput, () => this.applyDefaults());
30033
+ defaultsGroup.appendChild(this.createFormGroup('Padding:', this.defaultPaddingInput, { inline: true }));
27934
30034
  this.defaultBorderColorInput = this.createColorInput('#cccccc');
27935
- defaultsRow.appendChild(this.createFormGroup('Padding', this.defaultPaddingInput, { inline: true }));
27936
- defaultsRow.appendChild(this.createFormGroup('Border', this.defaultBorderColorInput, { inline: true }));
27937
- defaultsSection.appendChild(defaultsRow);
27938
- const applyDefaultsBtn = this.createButton('Apply Defaults');
27939
- this.addButtonListener(applyDefaultsBtn, () => this.applyDefaults());
27940
- defaultsSection.appendChild(applyDefaultsBtn);
30035
+ this.addImmediateApplyListener(this.defaultBorderColorInput, () => this.applyDefaults());
30036
+ defaultsGroup.appendChild(this.createFormGroup('Border:', this.defaultBorderColorInput, { inline: true }));
30037
+ defaultsSection.appendChild(defaultsGroup);
27941
30038
  container.appendChild(defaultsSection);
27942
30039
  // Cell formatting section
27943
30040
  const cellSection = this.createSection('Cell Formatting');
27944
30041
  this.cellSelectionDisplay = this.createHint('No cell selected');
27945
30042
  cellSection.appendChild(this.cellSelectionDisplay);
27946
- // Background
30043
+ // Merge/Split buttons
30044
+ const mergeBtnGroup = this.createButtonGroup();
30045
+ this.mergeCellsBtn = this.createButton('Merge Cells');
30046
+ this.mergeCellsBtn.disabled = true;
30047
+ this.splitCellBtn = this.createButton('Split Cell');
30048
+ this.splitCellBtn.disabled = true;
30049
+ this.addButtonListener(this.mergeCellsBtn, () => this.doMergeCells());
30050
+ this.addButtonListener(this.splitCellBtn, () => this.doSplitCell());
30051
+ mergeBtnGroup.appendChild(this.mergeCellsBtn);
30052
+ mergeBtnGroup.appendChild(this.splitCellBtn);
30053
+ cellSection.appendChild(mergeBtnGroup);
30054
+ cellSection.appendChild(document.createElement('hr'));
30055
+ // Background — inline
27947
30056
  this.cellBgColorInput = this.createColorInput('#ffffff');
27948
- cellSection.appendChild(this.createFormGroup('Background', this.cellBgColorInput));
30057
+ this.addImmediateApplyListener(this.cellBgColorInput, () => this.applyCellFormatting());
30058
+ cellSection.appendChild(this.createFormGroup('Background:', this.cellBgColorInput, { inline: true }));
27949
30059
  // Border checkboxes
27950
30060
  const borderChecks = document.createElement('div');
27951
30061
  borderChecks.className = 'pc-pane-row';
@@ -27977,24 +30087,29 @@ class TablePane extends BasePane {
27977
30087
  checkLabels[2].replaceChild(this.borderBottomCheck, checkLabels[2].querySelector('input'));
27978
30088
  if (checkLabels[3])
27979
30089
  checkLabels[3].replaceChild(this.borderLeftCheck, checkLabels[3].querySelector('input'));
27980
- cellSection.appendChild(this.createFormGroup('Borders', borderChecks));
30090
+ // Add change listeners for immediate apply on checkboxes
30091
+ for (const check of [this.borderTopCheck, this.borderRightCheck, this.borderBottomCheck, this.borderLeftCheck]) {
30092
+ check.addEventListener('change', () => this.applyCellFormatting());
30093
+ }
30094
+ cellSection.appendChild(this.createFormGroup('Borders:', borderChecks));
27981
30095
  // Border properties
27982
30096
  const borderPropsRow = this.createRow();
27983
30097
  this.borderWidthInput = this.createNumberInput({ min: 0, max: 5, value: 1 });
27984
30098
  this.borderColorInput = this.createColorInput('#cccccc');
27985
- borderPropsRow.appendChild(this.createFormGroup('Width', this.borderWidthInput, { inline: true }));
27986
- borderPropsRow.appendChild(this.createFormGroup('Color', this.borderColorInput, { inline: true }));
30099
+ this.addImmediateApplyListener(this.borderWidthInput, () => this.applyCellFormatting());
30100
+ this.addImmediateApplyListener(this.borderColorInput, () => this.applyCellFormatting());
30101
+ borderPropsRow.appendChild(this.createFormGroup('Width:', this.borderWidthInput, { inline: true }));
30102
+ borderPropsRow.appendChild(this.createFormGroup('Color:', this.borderColorInput, { inline: true }));
27987
30103
  cellSection.appendChild(borderPropsRow);
30104
+ // Style — inline
27988
30105
  this.borderStyleSelect = this.createSelect([
27989
30106
  { value: 'solid', label: 'Solid' },
27990
30107
  { value: 'dashed', label: 'Dashed' },
27991
30108
  { value: 'dotted', label: 'Dotted' },
27992
30109
  { value: 'none', label: 'None' }
27993
30110
  ], 'solid');
27994
- cellSection.appendChild(this.createFormGroup('Style', this.borderStyleSelect));
27995
- const applyCellBtn = this.createButton('Apply to Cell(s)', { variant: 'primary' });
27996
- this.addButtonListener(applyCellBtn, () => this.applyCellFormatting());
27997
- cellSection.appendChild(applyCellBtn);
30111
+ this.addImmediateApplyListener(this.borderStyleSelect, () => this.applyCellFormatting());
30112
+ cellSection.appendChild(this.createFormGroup('Style:', this.borderStyleSelect, { inline: true }));
27998
30113
  container.appendChild(cellSection);
27999
30114
  return container;
28000
30115
  }
@@ -28059,6 +30174,15 @@ class TablePane extends BasePane {
28059
30174
  return;
28060
30175
  const focusedCell = table.focusedCell;
28061
30176
  const selectedRange = table.selectedRange;
30177
+ // Update merge/split button states
30178
+ if (this.mergeCellsBtn) {
30179
+ const canMerge = selectedRange ? table.canMergeRange(selectedRange).canMerge : false;
30180
+ this.mergeCellsBtn.disabled = !canMerge;
30181
+ }
30182
+ if (this.splitCellBtn) {
30183
+ const canSplit = focusedCell ? table.canSplitCell(focusedCell.row, focusedCell.col).canSplit : false;
30184
+ this.splitCellBtn.disabled = !canSplit;
30185
+ }
28062
30186
  if (selectedRange) {
28063
30187
  const count = (selectedRange.end.row - selectedRange.start.row + 1) *
28064
30188
  (selectedRange.end.col - selectedRange.start.col + 1);
@@ -28216,20 +30340,20 @@ class TablePane extends BasePane {
28216
30340
  hasTable() {
28217
30341
  return this.currentTable !== null;
28218
30342
  }
28219
- createRowLoop() {
28220
- if (!this.editor || !this.currentTable) {
28221
- this.onApplyCallback?.(false, new Error('No table focused'));
30343
+ doMergeCells() {
30344
+ if (!this.editor || !this.currentTable)
28222
30345
  return;
28223
- }
28224
- const fieldPath = this.loopFieldInput?.value.trim() || '';
28225
- if (!fieldPath) {
28226
- this.onApplyCallback?.(false, new Error('Array field path is required'));
30346
+ this.editor.tableMergeCells(this.currentTable);
30347
+ this.updateFromFocusedTable();
30348
+ }
30349
+ doSplitCell() {
30350
+ if (!this.editor || !this.currentTable)
28227
30351
  return;
28228
- }
28229
- // Uses the unified createRepeatingSection API which detects
28230
- // that a table is focused and creates a row loop on the focused row
28231
- this.editor.createRepeatingSection(0, 0, fieldPath);
28232
- this.onApplyCallback?.(true);
30352
+ const focused = this.currentTable.focusedCell;
30353
+ if (!focused)
30354
+ return;
30355
+ this.editor.tableSplitCell(this.currentTable, focused.row, focused.col);
30356
+ this.updateFromFocusedTable();
28233
30357
  }
28234
30358
  /**
28235
30359
  * Update the pane from current editor state.
@@ -28245,6 +30369,8 @@ exports.BasePane = BasePane;
28245
30369
  exports.BaseTextRegion = BaseTextRegion;
28246
30370
  exports.BodyTextRegion = BodyTextRegion;
28247
30371
  exports.ClipboardManager = ClipboardManager;
30372
+ exports.ConditionalSectionManager = ConditionalSectionManager;
30373
+ exports.ConditionalSectionPane = ConditionalSectionPane;
28248
30374
  exports.ContentAnalyzer = ContentAnalyzer;
28249
30375
  exports.DEFAULT_IMPORT_OPTIONS = DEFAULT_IMPORT_OPTIONS;
28250
30376
  exports.Document = Document;
@@ -28255,6 +30381,7 @@ exports.EmbeddedObjectFactory = EmbeddedObjectFactory;
28255
30381
  exports.EmbeddedObjectManager = EmbeddedObjectManager;
28256
30382
  exports.EventEmitter = EventEmitter;
28257
30383
  exports.FlowingTextContent = FlowingTextContent;
30384
+ exports.FontManager = FontManager;
28258
30385
  exports.FooterTextRegion = FooterTextRegion;
28259
30386
  exports.FormattingPane = FormattingPane;
28260
30387
  exports.HeaderTextRegion = HeaderTextRegion;
@@ -28270,6 +30397,7 @@ exports.PDFImportError = PDFImportError;
28270
30397
  exports.PDFImporter = PDFImporter;
28271
30398
  exports.PDFParser = PDFParser;
28272
30399
  exports.Page = Page;
30400
+ exports.PredicateEvaluator = PredicateEvaluator;
28273
30401
  exports.RegionManager = RegionManager;
28274
30402
  exports.RepeatingSectionManager = RepeatingSectionManager;
28275
30403
  exports.RepeatingSectionPane = RepeatingSectionPane;