pptx-angular-viewer 1.1.34 → 1.1.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  import { NgStyle, NgClass, NgTemplateOutlet } from '@angular/common';
2
2
  import * as i0 from '@angular/core';
3
3
  import { InjectionToken, input, output, computed, ChangeDetectionStrategy, Component, signal, Injectable, HostListener, effect, inject, DestroyRef, ElementRef, viewChild, afterNextRender, Injector } from '@angular/core';
4
- import { guideEmuToPx, getShapeClipPath, getAdjustmentAwareShapeClipPath, getShapeClipPathFromPreset, getCloudPathForRendering, hasShapeProperties, applyDrawingColorTransforms as applyDrawingColorTransforms$1, hslToRgb as hslToRgb$1, PRESET_COLOR_MAP, isImageLikeElement, hasTextProperties, MIN_ELEMENT_SIZE as MIN_ELEMENT_SIZE$2, THEME_COLOR_SCHEME_KEYS, getLinkedTextBoxSegments, deobfuscateFont, detectFontFormat, getSubstituteFontFamily, checkMissingAltText, checkMissingSlideTitle, checkLowContrast, checkComplexTables, checkBlankSlide, checkDuplicateTitles, createChartElement, formatCommentTimestamp, setChartAxisLogScale, setChartAxisTitleStyle, setChartAxisGridlineStyle, setChartSeriesMarker, setChartSeriesChartType, setChartDataPointFill, setChartDataPointExplosion, chartDataAddSeries, chartDataRemoveSeries, chartDataAddCategory, chartDataRemoveCategory, chartDataUpdatePoint, chartDataChangeType, PptxHandler, EncryptedFileError, parseSignatureXml, isInkElement, isZoomElement, THEME_PRESETS, applyThemeToData, getAnimationPresetInfo } from 'pptx-viewer-core';
4
+ import { guideEmuToPx, getShapeClipPath, getAdjustmentAwareShapeClipPath, getShapeClipPathFromPreset, getCloudPathForRendering, hasShapeProperties, applyDrawingColorTransforms as applyDrawingColorTransforms$1, hslToRgb as hslToRgb$1, PRESET_COLOR_MAP, isImageLikeElement, hasTextProperties, MIN_ELEMENT_SIZE as MIN_ELEMENT_SIZE$2, THEME_COLOR_SCHEME_KEYS, getLinkedTextBoxSegments, deobfuscateFont, detectFontFormat, getSubstituteFontFamily, checkMissingAltText, checkMissingSlideTitle, checkLowContrast, checkComplexTables, checkBlankSlide, checkDuplicateTitles, createChartElement, formatCommentTimestamp, setChartAxisLogScale, setChartAxisTitleStyle, setChartAxisGridlineStyle, setChartSeriesMarker, setChartSeriesChartType, setChartDataPointFill, setChartDataPointExplosion, chartDataAddSeries, chartDataRemoveSeries, chartDataAddCategory, chartDataRemoveCategory, chartDataUpdatePoint, chartDataChangeType, updateSmartArtNodeText, addSmartArtNodeAsChild, removeSmartArtNode, promoteSmartArtNode, demoteSmartArtNode, reorderSmartArtNode, switchSmartArtLayout, SWITCHABLE_LAYOUT_TYPES, PptxHandler, EncryptedFileError, parseSignatureXml, isInkElement, isZoomElement, THEME_PRESETS, applyThemeToData, getAnimationPresetInfo } from 'pptx-viewer-core';
5
5
  import { jsPDF } from 'jspdf';
6
6
  import html2canvasPro from 'html2canvas-pro';
7
7
  import { DomSanitizer } from '@angular/platform-browser';
@@ -24354,6 +24354,57 @@ const PRESETS = [
24354
24354
  category: 'process',
24355
24355
  defaultItems: ['Phase 1', 'Phase 2', 'Phase 3'],
24356
24356
  },
24357
+ {
24358
+ layout: 'basicTimeline',
24359
+ label: 'Basic Timeline',
24360
+ category: 'process',
24361
+ defaultItems: ['2021', '2022', '2023'],
24362
+ },
24363
+ {
24364
+ layout: 'bendingProcess',
24365
+ label: 'Bending Process',
24366
+ category: 'process',
24367
+ defaultItems: ['Step 1', 'Step 2', 'Step 3', 'Step 4'],
24368
+ },
24369
+ // Relationship: target / matrix / pyramid / funnel / gear families.
24370
+ // These share the dialog's "relationship" sidebar tab (PowerPoint groups
24371
+ // matrix/pyramid under separate tabs, which this gallery folds into one).
24372
+ {
24373
+ layout: 'basicTarget',
24374
+ label: 'Basic Target',
24375
+ category: 'relationship',
24376
+ defaultItems: ['Outer', 'Middle', 'Inner'],
24377
+ },
24378
+ {
24379
+ layout: 'interlockingGears',
24380
+ label: 'Interlocking Gears',
24381
+ category: 'relationship',
24382
+ defaultItems: ['Gear 1', 'Gear 2', 'Gear 3'],
24383
+ },
24384
+ {
24385
+ layout: 'basicMatrix',
24386
+ label: 'Basic Matrix',
24387
+ category: 'relationship',
24388
+ defaultItems: ['Quadrant 1', 'Quadrant 2', 'Quadrant 3', 'Quadrant 4'],
24389
+ },
24390
+ {
24391
+ layout: 'basicPyramid',
24392
+ label: 'Basic Pyramid',
24393
+ category: 'relationship',
24394
+ defaultItems: ['Top', 'Middle', 'Base'],
24395
+ },
24396
+ {
24397
+ layout: 'invertedPyramid',
24398
+ label: 'Inverted Pyramid',
24399
+ category: 'relationship',
24400
+ defaultItems: ['Wide', 'Narrower', 'Tip'],
24401
+ },
24402
+ {
24403
+ layout: 'basicFunnel',
24404
+ label: 'Basic Funnel',
24405
+ category: 'relationship',
24406
+ defaultItems: ['Input 1', 'Input 2', 'Input 3', 'Result'],
24407
+ },
24357
24408
  ];
24358
24409
  /** Sidebar tabs, in display order. `label` is a (best-effort) i18n key. */
24359
24410
  const CATEGORIES = [
@@ -25373,6 +25424,149 @@ function toggleCommentResolvedInList(comments, id) {
25373
25424
  return next;
25374
25425
  }
25375
25426
 
25427
+ /**
25428
+ * Framework-agnostic touch-gesture state machine for the viewer canvas.
25429
+ *
25430
+ * Recognises three gestures from raw DOM `TouchEvent`s and emits callbacks:
25431
+ * - Pinch-to-zoom: two-finger spread/pinch, scaled by the distance ratio.
25432
+ * - Swipe: single-finger horizontal swipe (slide navigation).
25433
+ * - Long-press: single-finger hold (context-menu trigger).
25434
+ *
25435
+ * The recogniser holds all mutable pinch/swipe/long-press state internally and
25436
+ * exposes four event handlers plus a `cancel()`. The listener attach/detach
25437
+ * lifecycle (and any framework reactivity) stays in each binding; only this
25438
+ * pure state machine is shared.
25439
+ *
25440
+ * @module touch-gestures
25441
+ */
25442
+ // ---------------------------------------------------------------------------
25443
+ // Constants
25444
+ // ---------------------------------------------------------------------------
25445
+ /** The minimum horizontal distance (px) to recognise a swipe. */
25446
+ const SWIPE_THRESHOLD_PX = 50;
25447
+ /** Maximum vertical deviation (px) for a swipe to still count. */
25448
+ const SWIPE_MAX_VERTICAL_PX = 100;
25449
+ /** The hold duration (ms) for a long-press. */
25450
+ const LONG_PRESS_DURATION_MS = 500;
25451
+ /** If the finger moves more than this (px) during a hold, cancel the long-press. */
25452
+ const LONG_PRESS_MOVE_TOLERANCE_PX = 10;
25453
+ // ---------------------------------------------------------------------------
25454
+ // Pure helpers
25455
+ // ---------------------------------------------------------------------------
25456
+ /** Compute the Euclidean distance between two touch points. */
25457
+ function getTouchDistance(t1, t2) {
25458
+ const dx = t1.clientX - t2.clientX;
25459
+ const dy = t1.clientY - t2.clientY;
25460
+ return Math.sqrt(dx * dx + dy * dy);
25461
+ }
25462
+ /**
25463
+ * Clamp a scale value into the allowed zoom range. The bounds are parameters
25464
+ * (not imported constants) to keep this module framework- and binding-agnostic.
25465
+ */
25466
+ function clampScale$1(value, minScale, maxScale) {
25467
+ return Math.min(Math.max(value, minScale), maxScale);
25468
+ }
25469
+ // ---------------------------------------------------------------------------
25470
+ // Recogniser factory
25471
+ // ---------------------------------------------------------------------------
25472
+ /**
25473
+ * Create a stateful touch-gesture recogniser. The returned handlers mirror the
25474
+ * React viewer's native `touch*` listeners exactly, including `preventDefault()`
25475
+ * on the pinch path and the `setTimeout`-based long-press timer.
25476
+ */
25477
+ function createTouchGestureRecognizer(config) {
25478
+ const { getScale, minScale, maxScale, callbacks } = config;
25479
+ // Pinch state.
25480
+ let initialPinchDistance = 0;
25481
+ let pinchBaseScale = 1;
25482
+ let isPinching = false;
25483
+ // Swipe state.
25484
+ let swipeStartX = 0;
25485
+ let swipeStartY = 0;
25486
+ // Long-press state.
25487
+ let longPressTimer = null;
25488
+ let longPressStartX = 0;
25489
+ let longPressStartY = 0;
25490
+ const cancelLongPress = () => {
25491
+ if (longPressTimer !== null) {
25492
+ clearTimeout(longPressTimer);
25493
+ longPressTimer = null;
25494
+ }
25495
+ };
25496
+ const onTouchStart = (e) => {
25497
+ if (e.touches.length === 2) {
25498
+ // Start pinch.
25499
+ isPinching = true;
25500
+ initialPinchDistance = getTouchDistance(e.touches[0], e.touches[1]);
25501
+ pinchBaseScale = getScale();
25502
+ cancelLongPress();
25503
+ e.preventDefault();
25504
+ }
25505
+ else if (e.touches.length === 1) {
25506
+ // Potential swipe or long-press.
25507
+ swipeStartX = e.touches[0].clientX;
25508
+ swipeStartY = e.touches[0].clientY;
25509
+ longPressStartX = e.touches[0].clientX;
25510
+ longPressStartY = e.touches[0].clientY;
25511
+ cancelLongPress();
25512
+ longPressTimer = setTimeout(() => {
25513
+ longPressTimer = null;
25514
+ callbacks.onLongPress?.(longPressStartX, longPressStartY);
25515
+ }, LONG_PRESS_DURATION_MS);
25516
+ }
25517
+ };
25518
+ const onTouchMove = (e) => {
25519
+ if (e.touches.length === 2 && isPinching) {
25520
+ e.preventDefault();
25521
+ const currentDistance = getTouchDistance(e.touches[0], e.touches[1]);
25522
+ if (initialPinchDistance > 0) {
25523
+ const ratio = currentDistance / initialPinchDistance;
25524
+ const newScale = clampScale$1(pinchBaseScale * ratio, minScale, maxScale);
25525
+ callbacks.onPinchZoom?.(newScale);
25526
+ }
25527
+ }
25528
+ else if (e.touches.length === 1) {
25529
+ // Cancel the long-press if the finger moved too far.
25530
+ const dx = e.touches[0].clientX - longPressStartX;
25531
+ const dy = e.touches[0].clientY - longPressStartY;
25532
+ if (Math.abs(dx) > LONG_PRESS_MOVE_TOLERANCE_PX ||
25533
+ Math.abs(dy) > LONG_PRESS_MOVE_TOLERANCE_PX) {
25534
+ cancelLongPress();
25535
+ }
25536
+ }
25537
+ };
25538
+ const onTouchEnd = (e) => {
25539
+ if (isPinching) {
25540
+ isPinching = false;
25541
+ initialPinchDistance = 0;
25542
+ return;
25543
+ }
25544
+ cancelLongPress();
25545
+ // Detect a swipe from the touch that just ended.
25546
+ if (e.changedTouches.length === 1 && e.touches.length === 0) {
25547
+ const endX = e.changedTouches[0].clientX;
25548
+ const endY = e.changedTouches[0].clientY;
25549
+ const deltaX = endX - swipeStartX;
25550
+ const deltaY = endY - swipeStartY;
25551
+ if (Math.abs(deltaX) >= SWIPE_THRESHOLD_PX && Math.abs(deltaY) < SWIPE_MAX_VERTICAL_PX) {
25552
+ callbacks.onSwipe?.(deltaX > 0 ? 1 : -1);
25553
+ }
25554
+ }
25555
+ };
25556
+ const onTouchCancel = () => {
25557
+ isPinching = false;
25558
+ initialPinchDistance = 0;
25559
+ cancelLongPress();
25560
+ };
25561
+ return {
25562
+ onTouchStart,
25563
+ onTouchMove,
25564
+ onTouchEnd,
25565
+ onTouchCancel,
25566
+ cancel: onTouchCancel,
25567
+ };
25568
+ }
25569
+
25376
25570
  /**
25377
25571
  * insert-chart.ts - framework-agnostic factory for a sensible DEFAULT new
25378
25572
  * chart element, the single source of truth every binding (React, Vue,
@@ -35190,6 +35384,555 @@ function stringFromEvent$2(event) {
35190
35384
  * preserves the historical Angular import surface.
35191
35385
  */
35192
35386
 
35387
+ /**
35388
+ * smart-art-properties-helpers.ts: pure logic for the Angular SmartArt inspector.
35389
+ *
35390
+ * The presentational `SmartArtPropertiesComponent` is intentionally thin: every
35391
+ * mutation it performs is delegated to one of the pure functions below, which in
35392
+ * turn wrap the framework-agnostic SmartArt editing operations re-exported from
35393
+ * `editor-insert.ts` (sourced from `pptx-viewer-core`). Keeping this logic out of
35394
+ * the component lets it be unit-tested in plain vitest (the Angular package's
35395
+ * vitest setup has no Angular compiler, so TestBed component tests are not
35396
+ * available - see PORTING.md).
35397
+ *
35398
+ * Mirrors the React inspector:
35399
+ * packages/react/src/viewer/components/inspector/SmartArtPropertiesPanel.tsx
35400
+ * packages/react/src/viewer/components/inspector/SmartArtLayoutSwitcher.tsx
35401
+ *
35402
+ * @module angular-viewer/smart-art-properties-helpers
35403
+ */
35404
+ // ── Option constants (mirror the React panel) ───────────────────────────────
35405
+ /** Selectable colour schemes, in display order. */
35406
+ const SMART_ART_COLOR_SCHEMES = [
35407
+ 'colorful1',
35408
+ 'colorful2',
35409
+ 'colorful3',
35410
+ 'monochromatic1',
35411
+ 'monochromatic2',
35412
+ ];
35413
+ /** Selectable style intensities, in display order. */
35414
+ const SMART_ART_STYLE_OPTIONS = [
35415
+ 'flat',
35416
+ 'moderate',
35417
+ 'intense',
35418
+ ];
35419
+ /** Default colour scheme used when the data has none set. */
35420
+ const DEFAULT_COLOR_SCHEME = 'colorful1';
35421
+ /** Default style used when the data has none set. */
35422
+ const DEFAULT_STYLE = 'flat';
35423
+ /** Default resolved layout used when the data has none set. */
35424
+ const DEFAULT_LAYOUT = 'list';
35425
+ /** Default label for a newly added sub-item (matches the React panel). */
35426
+ const SUB_ITEM_LABEL = 'Sub-item';
35427
+ // ── Read helpers ─────────────────────────────────────────────────────────────
35428
+ /** The diagram's nodes, or an empty array when none are present. */
35429
+ function smartArtNodes(data) {
35430
+ return data.nodes ?? [];
35431
+ }
35432
+ /** Whether a node has a parent (is a child / sub-item). */
35433
+ function isChildNode(node) {
35434
+ return Boolean(node.parentId);
35435
+ }
35436
+ /** Current colour scheme, falling back to the default. */
35437
+ function currentColorScheme(data) {
35438
+ return data.colorScheme ?? DEFAULT_COLOR_SCHEME;
35439
+ }
35440
+ /** Current style intensity, falling back to the default. */
35441
+ function currentStyle(data) {
35442
+ return data.style ?? DEFAULT_STYLE;
35443
+ }
35444
+ /** Current resolved layout, falling back to the default. */
35445
+ function currentLayout(data) {
35446
+ return data.resolvedLayoutType ?? DEFAULT_LAYOUT;
35447
+ }
35448
+ // ── Mutation helpers (all return new immutable PptxSmartArtData) ──────────────
35449
+ /** Update a single node's text. */
35450
+ function setNodeText(data, nodeId, text) {
35451
+ return updateSmartArtNodeText(data, nodeId, text);
35452
+ }
35453
+ /** Append a new top-level item. */
35454
+ function addItem(data) {
35455
+ return addSmartArtNodeAsChild(data);
35456
+ }
35457
+ /** Add a sub-item beneath the given parent node. */
35458
+ function addSubItem(data, parentId) {
35459
+ return addSmartArtNodeAsChild(data, parentId, SUB_ITEM_LABEL);
35460
+ }
35461
+ /** Remove a node by id (no-op when it would empty the diagram). */
35462
+ function removeNode(data, nodeId) {
35463
+ if (smartArtNodes(data).length <= 1) {
35464
+ return data;
35465
+ }
35466
+ return removeSmartArtNode(data, nodeId);
35467
+ }
35468
+ /** Promote a node up one level in the hierarchy. */
35469
+ function promoteNode(data, nodeId) {
35470
+ return promoteSmartArtNode(data, nodeId);
35471
+ }
35472
+ /** Demote a node beneath its preceding sibling. */
35473
+ function demoteNode(data, nodeId) {
35474
+ return demoteSmartArtNode(data, nodeId);
35475
+ }
35476
+ /** Move a node up among its siblings. */
35477
+ function moveNodeUp(data, nodeId) {
35478
+ return reorderSmartArtNode(data, nodeId, -1);
35479
+ }
35480
+ /** Move a node down among its siblings. */
35481
+ function moveNodeDown(data, nodeId) {
35482
+ return reorderSmartArtNode(data, nodeId, 1);
35483
+ }
35484
+ /** Apply a new colour scheme; clears stale drawing shapes for reflow. */
35485
+ function setColorScheme(data, scheme) {
35486
+ return { ...data, colorScheme: scheme, drawingShapes: undefined };
35487
+ }
35488
+ /** Apply a new style intensity; clears stale drawing shapes for reflow. */
35489
+ function setStyle(data, style) {
35490
+ return { ...data, style, drawingShapes: undefined };
35491
+ }
35492
+ /**
35493
+ * Switch the diagram to a new layout, preserving node data and connections.
35494
+ * Returns the original data unchanged when the target equals the current layout.
35495
+ */
35496
+ function setLayout(data, layout) {
35497
+ if (currentLayout(data) === layout) {
35498
+ return data;
35499
+ }
35500
+ return switchSmartArtLayout(data, layout);
35501
+ }
35502
+
35503
+ /**
35504
+ * smart-art-properties.component.ts: SmartArt editing inspector (Angular).
35505
+ *
35506
+ * Selector: `pptx-smart-art-properties`
35507
+ *
35508
+ * Presentational panel for editing a selected SmartArt element's data model:
35509
+ * per-node text editing, add item / add sub-item, remove, promote / demote,
35510
+ * reorder up / down, colour-scheme select, style toggle (flat / moderate /
35511
+ * intense), and a layout switcher. All mutations are delegated to the pure
35512
+ * helpers in `smart-art-properties-helpers.ts` (which wrap the core editing ops
35513
+ * re-exported via `editor-insert.ts`); no editing logic lives here. Every edit
35514
+ * is emitted as a complete new `PptxSmartArtData` via `smartArtDataChange`, so
35515
+ * the parent (`InspectorPanelComponent`) can commit it through
35516
+ * `EditorStateService.updateElement()` as a single undo/redo history entry.
35517
+ *
35518
+ * Ported from the React inspector:
35519
+ * packages/react/src/viewer/components/inspector/SmartArtPropertiesPanel.tsx
35520
+ * packages/react/src/viewer/components/inspector/SmartArtLayoutSwitcher.tsx
35521
+ *
35522
+ * Usage:
35523
+ * ```html
35524
+ * <pptx-smart-art-properties
35525
+ * [smartArtData]="data"
35526
+ * [canEdit]="canEdit"
35527
+ * (smartArtDataChange)="onSmartArtChange($event)"
35528
+ * />
35529
+ * ```
35530
+ *
35531
+ * @module angular-viewer/smart-art-properties
35532
+ */
35533
+ class SmartArtPropertiesComponent {
35534
+ /** The SmartArt data model being edited. */
35535
+ smartArtData = input.required(/* @ts-ignore */
35536
+ ...(ngDevMode ? [{ debugName: "smartArtData" }] : /* istanbul ignore next */ []));
35537
+ /** Whether editing is enabled (read-only when false). */
35538
+ canEdit = input(true, /* @ts-ignore */
35539
+ ...(ngDevMode ? [{ debugName: "canEdit" }] : /* istanbul ignore next */ []));
35540
+ /** Emits a complete new data model after any edit. */
35541
+ smartArtDataChange = output();
35542
+ // ── Static option lists (template-bound) ─────────────────────────────────
35543
+ layoutTypes = SWITCHABLE_LAYOUT_TYPES;
35544
+ colorSchemes = SMART_ART_COLOR_SCHEMES;
35545
+ styleOptions = SMART_ART_STYLE_OPTIONS;
35546
+ // ── Derived state ─────────────────────────────────────────────────────────
35547
+ nodes = computed(() => smartArtNodes(this.smartArtData()), /* @ts-ignore */
35548
+ ...(ngDevMode ? [{ debugName: "nodes" }] : /* istanbul ignore next */ []));
35549
+ activeLayout = computed(() => currentLayout(this.smartArtData()), /* @ts-ignore */
35550
+ ...(ngDevMode ? [{ debugName: "activeLayout" }] : /* istanbul ignore next */ []));
35551
+ activeColorScheme = computed(() => currentColorScheme(this.smartArtData()), /* @ts-ignore */
35552
+ ...(ngDevMode ? [{ debugName: "activeColorScheme" }] : /* istanbul ignore next */ []));
35553
+ activeStyle = computed(() => currentStyle(this.smartArtData()), /* @ts-ignore */
35554
+ ...(ngDevMode ? [{ debugName: "activeStyle" }] : /* istanbul ignore next */ []));
35555
+ isChild = isChildNode;
35556
+ // ── Event handlers (each emits a fresh data model) ───────────────────────
35557
+ onLayout(layout) {
35558
+ this.commit(setLayout(this.smartArtData(), layout));
35559
+ }
35560
+ onColorScheme(event) {
35561
+ const value = selectValue(event);
35562
+ if (value === null) {
35563
+ return;
35564
+ }
35565
+ this.commit(setColorScheme(this.smartArtData(), value));
35566
+ }
35567
+ onStyle(style) {
35568
+ this.commit(setStyle(this.smartArtData(), style));
35569
+ }
35570
+ onAddItem() {
35571
+ this.commit(addItem(this.smartArtData()));
35572
+ }
35573
+ onAddSubItem(parentId) {
35574
+ this.commit(addSubItem(this.smartArtData(), parentId));
35575
+ }
35576
+ onRemove(nodeId) {
35577
+ this.commit(removeNode(this.smartArtData(), nodeId));
35578
+ }
35579
+ onPromote(nodeId) {
35580
+ this.commit(promoteNode(this.smartArtData(), nodeId));
35581
+ }
35582
+ onDemote(nodeId) {
35583
+ this.commit(demoteNode(this.smartArtData(), nodeId));
35584
+ }
35585
+ onMoveUp(nodeId) {
35586
+ this.commit(moveNodeUp(this.smartArtData(), nodeId));
35587
+ }
35588
+ onMoveDown(nodeId) {
35589
+ this.commit(moveNodeDown(this.smartArtData(), nodeId));
35590
+ }
35591
+ onNodeText(event, nodeId) {
35592
+ const value = inputValue(event);
35593
+ if (value === null) {
35594
+ return;
35595
+ }
35596
+ this.commit(setNodeText(this.smartArtData(), nodeId, value));
35597
+ }
35598
+ onNodeKeydown(event, nodeId) {
35599
+ if (event.key !== 'Tab') {
35600
+ return;
35601
+ }
35602
+ event.preventDefault();
35603
+ const next = event.shiftKey
35604
+ ? promoteNode(this.smartArtData(), nodeId)
35605
+ : demoteNode(this.smartArtData(), nodeId);
35606
+ this.commit(next);
35607
+ }
35608
+ /** Emit a new data model only when it differs from the current one. */
35609
+ commit(next) {
35610
+ if (!this.canEdit() || next === this.smartArtData()) {
35611
+ return;
35612
+ }
35613
+ this.smartArtDataChange.emit(next);
35614
+ }
35615
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: SmartArtPropertiesComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
35616
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: SmartArtPropertiesComponent, isStandalone: true, selector: "pptx-smart-art-properties", inputs: { smartArtData: { classPropertyName: "smartArtData", publicName: "smartArtData", isSignal: true, isRequired: true, transformFunction: null }, canEdit: { classPropertyName: "canEdit", publicName: "canEdit", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { smartArtDataChange: "smartArtDataChange" }, ngImport: i0, template: `
35617
+ <section class="pptx-sa-props" aria-label="SmartArt properties">
35618
+ <h3 class="pptx-sa-props__heading">SmartArt</h3>
35619
+
35620
+ <!-- ── Layout switcher ──────────────────────────────────────────── -->
35621
+ <div class="pptx-sa-props__field">
35622
+ <span class="pptx-sa-props__label">Layout</span>
35623
+ <div class="pptx-sa-props__layouts" role="group" aria-label="Switch layout">
35624
+ @for (layout of layoutTypes; track layout) {
35625
+ <button
35626
+ type="button"
35627
+ class="pptx-sa-props__layout"
35628
+ [class.is-active]="activeLayout() === layout"
35629
+ [disabled]="!canEdit()"
35630
+ [attr.aria-pressed]="activeLayout() === layout"
35631
+ (click)="onLayout(layout)"
35632
+ >
35633
+ {{ layout }}
35634
+ </button>
35635
+ }
35636
+ </div>
35637
+ </div>
35638
+
35639
+ <!-- ── Colour scheme ────────────────────────────────────────────── -->
35640
+ <label class="pptx-sa-props__field">
35641
+ <span class="pptx-sa-props__label">Color scheme</span>
35642
+ <select
35643
+ class="pptx-sa-props__select"
35644
+ [disabled]="!canEdit()"
35645
+ [value]="activeColorScheme()"
35646
+ (change)="onColorScheme($event)"
35647
+ >
35648
+ @for (scheme of colorSchemes; track scheme) {
35649
+ <option [value]="scheme">{{ scheme }}</option>
35650
+ }
35651
+ </select>
35652
+ </label>
35653
+
35654
+ <!-- ── Style intensity ──────────────────────────────────────────── -->
35655
+ <div class="pptx-sa-props__field">
35656
+ <span class="pptx-sa-props__label">Style</span>
35657
+ <div class="pptx-sa-props__styles" role="group" aria-label="Style intensity">
35658
+ @for (styleOpt of styleOptions; track styleOpt) {
35659
+ <button
35660
+ type="button"
35661
+ class="pptx-sa-props__style"
35662
+ [class.is-active]="activeStyle() === styleOpt"
35663
+ [disabled]="!canEdit()"
35664
+ [attr.aria-pressed]="activeStyle() === styleOpt"
35665
+ (click)="onStyle(styleOpt)"
35666
+ >
35667
+ {{ styleOpt }}
35668
+ </button>
35669
+ }
35670
+ </div>
35671
+ </div>
35672
+
35673
+ <!-- ── Text pane / node list ────────────────────────────────────── -->
35674
+ <div class="pptx-sa-props__pane-header">
35675
+ <span class="pptx-sa-props__label">Text pane ({{ nodes().length }})</span>
35676
+ <button
35677
+ type="button"
35678
+ class="pptx-sa-props__btn"
35679
+ [disabled]="!canEdit()"
35680
+ (click)="onAddItem()"
35681
+ >
35682
+ + Item
35683
+ </button>
35684
+ </div>
35685
+
35686
+ <ul class="pptx-sa-props__nodes" role="list">
35687
+ @for (node of nodes(); track node.id; let i = $index) {
35688
+ <li
35689
+ class="pptx-sa-props__node"
35690
+ [class.pptx-sa-props__node--child]="isChild(node)"
35691
+ [attr.data-node-id]="node.id"
35692
+ >
35693
+ <span class="pptx-sa-props__bullet">{{ isChild(node) ? '•' : i + 1 }}</span>
35694
+ <input
35695
+ type="text"
35696
+ class="pptx-sa-props__node-input"
35697
+ [disabled]="!canEdit()"
35698
+ [value]="node.text"
35699
+ placeholder="Type here"
35700
+ (change)="onNodeText($event, node.id)"
35701
+ (keydown)="onNodeKeydown($event, node.id)"
35702
+ />
35703
+ <div class="pptx-sa-props__node-actions">
35704
+ @if (!isChild(node)) {
35705
+ <button
35706
+ type="button"
35707
+ class="pptx-sa-props__icon"
35708
+ title="Add sub-item"
35709
+ [disabled]="!canEdit()"
35710
+ (click)="onAddSubItem(node.id)"
35711
+ >
35712
+ +Sub
35713
+ </button>
35714
+ }
35715
+ <button
35716
+ type="button"
35717
+ class="pptx-sa-props__icon"
35718
+ title="Promote"
35719
+ [disabled]="!canEdit() || isChild(node) === false"
35720
+ (click)="onPromote(node.id)"
35721
+ >
35722
+ &#8592;
35723
+ </button>
35724
+ <button
35725
+ type="button"
35726
+ class="pptx-sa-props__icon"
35727
+ title="Demote"
35728
+ [disabled]="!canEdit()"
35729
+ (click)="onDemote(node.id)"
35730
+ >
35731
+ &#8594;
35732
+ </button>
35733
+ <button
35734
+ type="button"
35735
+ class="pptx-sa-props__icon"
35736
+ title="Move up"
35737
+ [disabled]="!canEdit()"
35738
+ (click)="onMoveUp(node.id)"
35739
+ >
35740
+ &#8593;
35741
+ </button>
35742
+ <button
35743
+ type="button"
35744
+ class="pptx-sa-props__icon"
35745
+ title="Move down"
35746
+ [disabled]="!canEdit()"
35747
+ (click)="onMoveDown(node.id)"
35748
+ >
35749
+ &#8595;
35750
+ </button>
35751
+ <button
35752
+ type="button"
35753
+ class="pptx-sa-props__icon pptx-sa-props__icon--danger"
35754
+ title="Remove"
35755
+ [disabled]="!canEdit() || nodes().length <= 1"
35756
+ (click)="onRemove(node.id)"
35757
+ >
35758
+ &times;
35759
+ </button>
35760
+ </div>
35761
+ </li>
35762
+ }
35763
+ </ul>
35764
+
35765
+ <p class="pptx-sa-props__hint">Tab to demote, Shift+Tab to promote.</p>
35766
+ </section>
35767
+ `, isInline: true, styles: [".pptx-sa-props{display:flex;flex-direction:column;gap:.4rem;padding:.5rem 0}.pptx-sa-props__heading{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--pptx-inspector-muted, #888);margin:0}.pptx-sa-props__field{display:flex;flex-direction:column;gap:.2rem}.pptx-sa-props__label{font-size:10px;color:var(--pptx-inspector-muted, #888)}.pptx-sa-props__layouts,.pptx-sa-props__styles{display:flex;flex-wrap:wrap;gap:.25rem}.pptx-sa-props__layout,.pptx-sa-props__style,.pptx-sa-props__btn,.pptx-sa-props__icon{background:var(--pptx-inspector-input-bg, #2d2d2d);border:1px solid var(--pptx-inspector-border, #444);color:inherit;border-radius:3px;cursor:pointer;font-size:10px;padding:2px 6px;white-space:nowrap}.pptx-sa-props__layout,.pptx-sa-props__style{flex:1 0 auto;text-transform:capitalize}.pptx-sa-props__layout.is-active,.pptx-sa-props__style.is-active{background:var(--pptx-inspector-active, #0078d4);border-color:var(--pptx-inspector-active, #0078d4);color:#fff}.pptx-sa-props__select{background:var(--pptx-inspector-input-bg, #2d2d2d);border:1px solid var(--pptx-inspector-border, #444);color:inherit;border-radius:3px;padding:2px 4px;font-size:11px}.pptx-sa-props__pane-header{display:flex;align-items:center;justify-content:space-between;gap:.35rem;margin-top:.2rem}.pptx-sa-props__nodes{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:.2rem;max-height:14rem;overflow-y:auto}.pptx-sa-props__node{display:flex;align-items:center;gap:.25rem;padding:2px;border:1px solid var(--pptx-inspector-border, #333);border-radius:3px}.pptx-sa-props__node--child{margin-left:1rem;border-color:var(--pptx-inspector-border, #2a2a2a)}.pptx-sa-props__bullet{font-size:9px;color:var(--pptx-inspector-muted, #888);min-width:12px;text-align:center;flex-shrink:0}.pptx-sa-props__node-input{flex:1;min-width:0;background:var(--pptx-inspector-input-bg, #2d2d2d);border:1px solid var(--pptx-inspector-border, #444);color:inherit;border-radius:3px;padding:2px 4px;font-size:11px}.pptx-sa-props__node-actions{display:flex;gap:1px;flex-shrink:0}.pptx-sa-props__icon{padding:1px 3px;font-size:10px;line-height:1.2}.pptx-sa-props__icon--danger{color:var(--pptx-inspector-danger, #f47c7c)}.pptx-sa-props__layout:disabled,.pptx-sa-props__style:disabled,.pptx-sa-props__btn:disabled,.pptx-sa-props__icon:disabled,.pptx-sa-props__select:disabled,.pptx-sa-props__node-input:disabled{opacity:.45;cursor:not-allowed}.pptx-sa-props__hint{font-size:9px;color:var(--pptx-inspector-muted, #888);margin:.1rem 0 0}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
35768
+ }
35769
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: SmartArtPropertiesComponent, decorators: [{
35770
+ type: Component,
35771
+ args: [{ selector: 'pptx-smart-art-properties', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
35772
+ <section class="pptx-sa-props" aria-label="SmartArt properties">
35773
+ <h3 class="pptx-sa-props__heading">SmartArt</h3>
35774
+
35775
+ <!-- ── Layout switcher ──────────────────────────────────────────── -->
35776
+ <div class="pptx-sa-props__field">
35777
+ <span class="pptx-sa-props__label">Layout</span>
35778
+ <div class="pptx-sa-props__layouts" role="group" aria-label="Switch layout">
35779
+ @for (layout of layoutTypes; track layout) {
35780
+ <button
35781
+ type="button"
35782
+ class="pptx-sa-props__layout"
35783
+ [class.is-active]="activeLayout() === layout"
35784
+ [disabled]="!canEdit()"
35785
+ [attr.aria-pressed]="activeLayout() === layout"
35786
+ (click)="onLayout(layout)"
35787
+ >
35788
+ {{ layout }}
35789
+ </button>
35790
+ }
35791
+ </div>
35792
+ </div>
35793
+
35794
+ <!-- ── Colour scheme ────────────────────────────────────────────── -->
35795
+ <label class="pptx-sa-props__field">
35796
+ <span class="pptx-sa-props__label">Color scheme</span>
35797
+ <select
35798
+ class="pptx-sa-props__select"
35799
+ [disabled]="!canEdit()"
35800
+ [value]="activeColorScheme()"
35801
+ (change)="onColorScheme($event)"
35802
+ >
35803
+ @for (scheme of colorSchemes; track scheme) {
35804
+ <option [value]="scheme">{{ scheme }}</option>
35805
+ }
35806
+ </select>
35807
+ </label>
35808
+
35809
+ <!-- ── Style intensity ──────────────────────────────────────────── -->
35810
+ <div class="pptx-sa-props__field">
35811
+ <span class="pptx-sa-props__label">Style</span>
35812
+ <div class="pptx-sa-props__styles" role="group" aria-label="Style intensity">
35813
+ @for (styleOpt of styleOptions; track styleOpt) {
35814
+ <button
35815
+ type="button"
35816
+ class="pptx-sa-props__style"
35817
+ [class.is-active]="activeStyle() === styleOpt"
35818
+ [disabled]="!canEdit()"
35819
+ [attr.aria-pressed]="activeStyle() === styleOpt"
35820
+ (click)="onStyle(styleOpt)"
35821
+ >
35822
+ {{ styleOpt }}
35823
+ </button>
35824
+ }
35825
+ </div>
35826
+ </div>
35827
+
35828
+ <!-- ── Text pane / node list ────────────────────────────────────── -->
35829
+ <div class="pptx-sa-props__pane-header">
35830
+ <span class="pptx-sa-props__label">Text pane ({{ nodes().length }})</span>
35831
+ <button
35832
+ type="button"
35833
+ class="pptx-sa-props__btn"
35834
+ [disabled]="!canEdit()"
35835
+ (click)="onAddItem()"
35836
+ >
35837
+ + Item
35838
+ </button>
35839
+ </div>
35840
+
35841
+ <ul class="pptx-sa-props__nodes" role="list">
35842
+ @for (node of nodes(); track node.id; let i = $index) {
35843
+ <li
35844
+ class="pptx-sa-props__node"
35845
+ [class.pptx-sa-props__node--child]="isChild(node)"
35846
+ [attr.data-node-id]="node.id"
35847
+ >
35848
+ <span class="pptx-sa-props__bullet">{{ isChild(node) ? '•' : i + 1 }}</span>
35849
+ <input
35850
+ type="text"
35851
+ class="pptx-sa-props__node-input"
35852
+ [disabled]="!canEdit()"
35853
+ [value]="node.text"
35854
+ placeholder="Type here"
35855
+ (change)="onNodeText($event, node.id)"
35856
+ (keydown)="onNodeKeydown($event, node.id)"
35857
+ />
35858
+ <div class="pptx-sa-props__node-actions">
35859
+ @if (!isChild(node)) {
35860
+ <button
35861
+ type="button"
35862
+ class="pptx-sa-props__icon"
35863
+ title="Add sub-item"
35864
+ [disabled]="!canEdit()"
35865
+ (click)="onAddSubItem(node.id)"
35866
+ >
35867
+ +Sub
35868
+ </button>
35869
+ }
35870
+ <button
35871
+ type="button"
35872
+ class="pptx-sa-props__icon"
35873
+ title="Promote"
35874
+ [disabled]="!canEdit() || isChild(node) === false"
35875
+ (click)="onPromote(node.id)"
35876
+ >
35877
+ &#8592;
35878
+ </button>
35879
+ <button
35880
+ type="button"
35881
+ class="pptx-sa-props__icon"
35882
+ title="Demote"
35883
+ [disabled]="!canEdit()"
35884
+ (click)="onDemote(node.id)"
35885
+ >
35886
+ &#8594;
35887
+ </button>
35888
+ <button
35889
+ type="button"
35890
+ class="pptx-sa-props__icon"
35891
+ title="Move up"
35892
+ [disabled]="!canEdit()"
35893
+ (click)="onMoveUp(node.id)"
35894
+ >
35895
+ &#8593;
35896
+ </button>
35897
+ <button
35898
+ type="button"
35899
+ class="pptx-sa-props__icon"
35900
+ title="Move down"
35901
+ [disabled]="!canEdit()"
35902
+ (click)="onMoveDown(node.id)"
35903
+ >
35904
+ &#8595;
35905
+ </button>
35906
+ <button
35907
+ type="button"
35908
+ class="pptx-sa-props__icon pptx-sa-props__icon--danger"
35909
+ title="Remove"
35910
+ [disabled]="!canEdit() || nodes().length <= 1"
35911
+ (click)="onRemove(node.id)"
35912
+ >
35913
+ &times;
35914
+ </button>
35915
+ </div>
35916
+ </li>
35917
+ }
35918
+ </ul>
35919
+
35920
+ <p class="pptx-sa-props__hint">Tab to demote, Shift+Tab to promote.</p>
35921
+ </section>
35922
+ `, styles: [".pptx-sa-props{display:flex;flex-direction:column;gap:.4rem;padding:.5rem 0}.pptx-sa-props__heading{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--pptx-inspector-muted, #888);margin:0}.pptx-sa-props__field{display:flex;flex-direction:column;gap:.2rem}.pptx-sa-props__label{font-size:10px;color:var(--pptx-inspector-muted, #888)}.pptx-sa-props__layouts,.pptx-sa-props__styles{display:flex;flex-wrap:wrap;gap:.25rem}.pptx-sa-props__layout,.pptx-sa-props__style,.pptx-sa-props__btn,.pptx-sa-props__icon{background:var(--pptx-inspector-input-bg, #2d2d2d);border:1px solid var(--pptx-inspector-border, #444);color:inherit;border-radius:3px;cursor:pointer;font-size:10px;padding:2px 6px;white-space:nowrap}.pptx-sa-props__layout,.pptx-sa-props__style{flex:1 0 auto;text-transform:capitalize}.pptx-sa-props__layout.is-active,.pptx-sa-props__style.is-active{background:var(--pptx-inspector-active, #0078d4);border-color:var(--pptx-inspector-active, #0078d4);color:#fff}.pptx-sa-props__select{background:var(--pptx-inspector-input-bg, #2d2d2d);border:1px solid var(--pptx-inspector-border, #444);color:inherit;border-radius:3px;padding:2px 4px;font-size:11px}.pptx-sa-props__pane-header{display:flex;align-items:center;justify-content:space-between;gap:.35rem;margin-top:.2rem}.pptx-sa-props__nodes{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:.2rem;max-height:14rem;overflow-y:auto}.pptx-sa-props__node{display:flex;align-items:center;gap:.25rem;padding:2px;border:1px solid var(--pptx-inspector-border, #333);border-radius:3px}.pptx-sa-props__node--child{margin-left:1rem;border-color:var(--pptx-inspector-border, #2a2a2a)}.pptx-sa-props__bullet{font-size:9px;color:var(--pptx-inspector-muted, #888);min-width:12px;text-align:center;flex-shrink:0}.pptx-sa-props__node-input{flex:1;min-width:0;background:var(--pptx-inspector-input-bg, #2d2d2d);border:1px solid var(--pptx-inspector-border, #444);color:inherit;border-radius:3px;padding:2px 4px;font-size:11px}.pptx-sa-props__node-actions{display:flex;gap:1px;flex-shrink:0}.pptx-sa-props__icon{padding:1px 3px;font-size:10px;line-height:1.2}.pptx-sa-props__icon--danger{color:var(--pptx-inspector-danger, #f47c7c)}.pptx-sa-props__layout:disabled,.pptx-sa-props__style:disabled,.pptx-sa-props__btn:disabled,.pptx-sa-props__icon:disabled,.pptx-sa-props__select:disabled,.pptx-sa-props__node-input:disabled{opacity:.45;cursor:not-allowed}.pptx-sa-props__hint{font-size:9px;color:var(--pptx-inspector-muted, #888);margin:.1rem 0 0}\n"] }]
35923
+ }], propDecorators: { smartArtData: [{ type: i0.Input, args: [{ isSignal: true, alias: "smartArtData", required: true }] }], canEdit: [{ type: i0.Input, args: [{ isSignal: true, alias: "canEdit", required: false }] }], smartArtDataChange: [{ type: i0.Output, args: ["smartArtDataChange"] }] } });
35924
+ // ── Module-private helpers ───────────────────────────────────────────────────
35925
+ /** Read the value of a `<select>` change event. */
35926
+ function selectValue(event) {
35927
+ const target = event.target;
35928
+ return target instanceof HTMLSelectElement ? target.value : null;
35929
+ }
35930
+ /** Read the value of an `<input>` change event. */
35931
+ function inputValue(event) {
35932
+ const target = event.target;
35933
+ return target instanceof HTMLInputElement ? target.value : null;
35934
+ }
35935
+
35193
35936
  /**
35194
35937
  * table-data-helpers.ts: Pure immutable helpers for table data editing.
35195
35938
  *
@@ -36385,6 +37128,23 @@ class InspectorPanelComponent {
36385
37128
  /** The selected element narrowed to a chart, or undefined. */
36386
37129
  chartEl = computed(() => this.el().type === 'chart' ? this.el() : undefined, /* @ts-ignore */
36387
37130
  ...(ngDevMode ? [{ debugName: "chartEl" }] : /* istanbul ignore next */ []));
37131
+ /** The selected element narrowed to SmartArt, or undefined. */
37132
+ smartArtEl = computed(() => this.el().type === 'smartArt' ? this.el() : undefined, /* @ts-ignore */
37133
+ ...(ngDevMode ? [{ debugName: "smartArtEl" }] : /* istanbul ignore next */ []));
37134
+ /** The selected SmartArt element's data model, or undefined. */
37135
+ smartArtData = computed(() => this.smartArtEl()?.smartArtData, /* @ts-ignore */
37136
+ ...(ngDevMode ? [{ debugName: "smartArtData" }] : /* istanbul ignore next */ []));
37137
+ /**
37138
+ * Commit an updated SmartArt data model as one history entry. Patching only
37139
+ * `smartArtData` routes through the same `EditorStateService.updateElement`
37140
+ * path as every other inspector section, so undo/redo and persistence work
37141
+ * identically.
37142
+ */
37143
+ onSmartArtDataChange(smartArtData) {
37144
+ this.editor.updateElement(this.slideIndex(), this.el().id, {
37145
+ smartArtData,
37146
+ });
37147
+ }
36388
37148
  /** Commit a partial-element patch from an advanced sub-panel as one history entry. */
36389
37149
  onPatch(patch) {
36390
37150
  this.editor.updateElement(this.slideIndex(), this.el().id, patch);
@@ -36740,6 +37500,16 @@ class InspectorPanelComponent {
36740
37500
  </details>
36741
37501
  }
36742
37502
 
37503
+ <!-- ── SmartArt editing ───────────────────────────────────────────── -->
37504
+ @if (smartArtData(); as sa) {
37505
+ <section class="pptx-ng-inspector__section">
37506
+ <pptx-smart-art-properties
37507
+ [smartArtData]="sa"
37508
+ (smartArtDataChange)="onSmartArtDataChange($event)"
37509
+ />
37510
+ </section>
37511
+ }
37512
+
36743
37513
  <!-- ── Animation authoring ────────────────────────────────────────── -->
36744
37514
  <details class="pptx-ng-inspector__details">
36745
37515
  <summary class="pptx-ng-inspector__summary">Animation</summary>
@@ -36771,7 +37541,7 @@ class InspectorPanelComponent {
36771
37541
  </div>
36772
37542
  </section>
36773
37543
  </aside>
36774
- `, isInline: true, styles: [".pptx-ng-inspector{display:flex;flex-direction:column;gap:0;padding:.5rem;background:var(--pptx-inspector-bg, #1e1e1e);color:var(--pptx-inspector-fg, #e0e0e0);font-size:12px;min-width:220px;overflow-y:auto}.pptx-ng-inspector__section{padding:.5rem 0;border-bottom:1px solid var(--pptx-inspector-border, #333)}.pptx-ng-inspector__section:last-child{border-bottom:none}.pptx-ng-inspector__heading{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--pptx-inspector-muted, #888);margin:0 0 .35rem}.pptx-ng-inspector__details{border-bottom:1px solid var(--pptx-inspector-border, #333)}.pptx-ng-inspector__summary{padding:.5rem 0;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--pptx-inspector-muted, #888);cursor:pointer;-webkit-user-select:none;user-select:none}.pptx-ng-inspector__row{display:flex;align-items:center;gap:.35rem;margin-bottom:.35rem}.pptx-ng-inspector__row:last-child{margin-bottom:0}.pptx-ng-inspector__row--toggles{gap:.25rem}.pptx-ng-inspector__label{font-size:10px;color:var(--pptx-inspector-muted, #888);min-width:32px;text-align:right;flex-shrink:0}.pptx-ng-inspector__input{background:var(--pptx-inspector-input-bg, #2d2d2d);border:1px solid var(--pptx-inspector-border, #444);color:inherit;border-radius:3px;padding:2px 4px;font-size:12px}.pptx-ng-inspector__input--number{width:62px;text-align:right}.pptx-ng-inspector__color{width:32px;height:22px;border:1px solid var(--pptx-inspector-border, #444);border-radius:3px;padding:1px;cursor:pointer;background:transparent;flex-shrink:0}.pptx-ng-inspector__toggle{width:26px;height:22px;background:var(--pptx-inspector-input-bg, #2d2d2d);border:1px solid var(--pptx-inspector-border, #444);color:inherit;border-radius:3px;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:12px}@media(pointer:coarse),(max-width:640px){.pptx-ng-inspector{width:100%;min-width:0;box-sizing:border-box;font-size:14px}.pptx-ng-inspector__row{flex-wrap:wrap;gap:.5rem}.pptx-ng-inspector__label{min-width:28px}.pptx-ng-inspector__input{flex:1 1 auto;min-height:40px;font-size:16px;padding:6px 8px}.pptx-ng-inspector__input--number{width:auto;min-width:72px}.pptx-ng-inspector__color{width:44px;height:40px}.pptx-ng-inspector__toggle{min-width:44px;width:auto;flex:1 1 auto;height:40px;font-size:15px}.pptx-ng-inspector__btn{min-height:40px;padding:8px 10px;font-size:13px}}.pptx-ng-inspector__toggle.is-active{background:var(--pptx-inspector-active, #0078d4);border-color:var(--pptx-inspector-active, #0078d4);color:#fff}.pptx-ng-inspector__btn{flex:1;padding:3px 6px;background:var(--pptx-inspector-input-bg, #2d2d2d);border:1px solid var(--pptx-inspector-border, #444);color:inherit;border-radius:3px;cursor:pointer;font-size:11px;white-space:nowrap}.pptx-ng-inspector__btn:hover{background:var(--pptx-inspector-hover, #3a3a3a)}.pptx-ng-inspector__btn--danger{color:var(--pptx-inspector-danger, #f47c7c);border-color:var(--pptx-inspector-danger-border, #6b2a2a)}.pptx-ng-inspector__btn--danger:hover{background:var(--pptx-inspector-danger-hover, #4a1a1a)}\n"], dependencies: [{ kind: "component", type: GradientPickerComponent, selector: "pptx-gradient-picker", inputs: ["element"], outputs: ["patch"] }, { kind: "component", type: EffectsPanelComponent, selector: "pptx-effects-panel", inputs: ["element"], outputs: ["patch"] }, { kind: "component", type: TextAdvancedPanelComponent, selector: "pptx-text-advanced-panel", inputs: ["element"], outputs: ["patch"] }, { kind: "component", type: TableDataEditorComponent, selector: "pptx-table-data-editor", inputs: ["element", "canEdit"], outputs: ["elementChange"] }, { kind: "component", type: ChartDataEditorComponent, selector: "pptx-chart-data-editor", inputs: ["element", "canEdit"], outputs: ["elementChange"] }, { kind: "component", type: AnimationAuthorPanelComponent, selector: "pptx-animation-author-panel", inputs: ["element", "slideIndex", "animations", "canEdit"], outputs: ["animationsChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
37544
+ `, isInline: true, styles: [".pptx-ng-inspector{display:flex;flex-direction:column;gap:0;padding:.5rem;background:var(--pptx-inspector-bg, #1e1e1e);color:var(--pptx-inspector-fg, #e0e0e0);font-size:12px;min-width:220px;overflow-y:auto}.pptx-ng-inspector__section{padding:.5rem 0;border-bottom:1px solid var(--pptx-inspector-border, #333)}.pptx-ng-inspector__section:last-child{border-bottom:none}.pptx-ng-inspector__heading{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--pptx-inspector-muted, #888);margin:0 0 .35rem}.pptx-ng-inspector__details{border-bottom:1px solid var(--pptx-inspector-border, #333)}.pptx-ng-inspector__summary{padding:.5rem 0;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--pptx-inspector-muted, #888);cursor:pointer;-webkit-user-select:none;user-select:none}.pptx-ng-inspector__row{display:flex;align-items:center;gap:.35rem;margin-bottom:.35rem}.pptx-ng-inspector__row:last-child{margin-bottom:0}.pptx-ng-inspector__row--toggles{gap:.25rem}.pptx-ng-inspector__label{font-size:10px;color:var(--pptx-inspector-muted, #888);min-width:32px;text-align:right;flex-shrink:0}.pptx-ng-inspector__input{background:var(--pptx-inspector-input-bg, #2d2d2d);border:1px solid var(--pptx-inspector-border, #444);color:inherit;border-radius:3px;padding:2px 4px;font-size:12px}.pptx-ng-inspector__input--number{width:62px;text-align:right}.pptx-ng-inspector__color{width:32px;height:22px;border:1px solid var(--pptx-inspector-border, #444);border-radius:3px;padding:1px;cursor:pointer;background:transparent;flex-shrink:0}.pptx-ng-inspector__toggle{width:26px;height:22px;background:var(--pptx-inspector-input-bg, #2d2d2d);border:1px solid var(--pptx-inspector-border, #444);color:inherit;border-radius:3px;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:12px}@media(pointer:coarse),(max-width:640px){.pptx-ng-inspector{width:100%;min-width:0;box-sizing:border-box;font-size:14px}.pptx-ng-inspector__row{flex-wrap:wrap;gap:.5rem}.pptx-ng-inspector__label{min-width:28px}.pptx-ng-inspector__input{flex:1 1 auto;min-height:40px;font-size:16px;padding:6px 8px}.pptx-ng-inspector__input--number{width:auto;min-width:72px}.pptx-ng-inspector__color{width:44px;height:40px}.pptx-ng-inspector__toggle{min-width:44px;width:auto;flex:1 1 auto;height:40px;font-size:15px}.pptx-ng-inspector__btn{min-height:40px;padding:8px 10px;font-size:13px}}.pptx-ng-inspector__toggle.is-active{background:var(--pptx-inspector-active, #0078d4);border-color:var(--pptx-inspector-active, #0078d4);color:#fff}.pptx-ng-inspector__btn{flex:1;padding:3px 6px;background:var(--pptx-inspector-input-bg, #2d2d2d);border:1px solid var(--pptx-inspector-border, #444);color:inherit;border-radius:3px;cursor:pointer;font-size:11px;white-space:nowrap}.pptx-ng-inspector__btn:hover{background:var(--pptx-inspector-hover, #3a3a3a)}.pptx-ng-inspector__btn--danger{color:var(--pptx-inspector-danger, #f47c7c);border-color:var(--pptx-inspector-danger-border, #6b2a2a)}.pptx-ng-inspector__btn--danger:hover{background:var(--pptx-inspector-danger-hover, #4a1a1a)}\n"], dependencies: [{ kind: "component", type: GradientPickerComponent, selector: "pptx-gradient-picker", inputs: ["element"], outputs: ["patch"] }, { kind: "component", type: EffectsPanelComponent, selector: "pptx-effects-panel", inputs: ["element"], outputs: ["patch"] }, { kind: "component", type: TextAdvancedPanelComponent, selector: "pptx-text-advanced-panel", inputs: ["element"], outputs: ["patch"] }, { kind: "component", type: TableDataEditorComponent, selector: "pptx-table-data-editor", inputs: ["element", "canEdit"], outputs: ["elementChange"] }, { kind: "component", type: ChartDataEditorComponent, selector: "pptx-chart-data-editor", inputs: ["element", "canEdit"], outputs: ["elementChange"] }, { kind: "component", type: SmartArtPropertiesComponent, selector: "pptx-smart-art-properties", inputs: ["smartArtData", "canEdit"], outputs: ["smartArtDataChange"] }, { kind: "component", type: AnimationAuthorPanelComponent, selector: "pptx-animation-author-panel", inputs: ["element", "slideIndex", "animations", "canEdit"], outputs: ["animationsChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
36775
37545
  }
36776
37546
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: InspectorPanelComponent, decorators: [{
36777
37547
  type: Component,
@@ -36781,6 +37551,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
36781
37551
  TextAdvancedPanelComponent,
36782
37552
  TableDataEditorComponent,
36783
37553
  ChartDataEditorComponent,
37554
+ SmartArtPropertiesComponent,
36784
37555
  AnimationAuthorPanelComponent,
36785
37556
  ], template: `
36786
37557
  <!--
@@ -37034,6 +37805,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
37034
37805
  </details>
37035
37806
  }
37036
37807
 
37808
+ <!-- ── SmartArt editing ───────────────────────────────────────────── -->
37809
+ @if (smartArtData(); as sa) {
37810
+ <section class="pptx-ng-inspector__section">
37811
+ <pptx-smart-art-properties
37812
+ [smartArtData]="sa"
37813
+ (smartArtDataChange)="onSmartArtDataChange($event)"
37814
+ />
37815
+ </section>
37816
+ }
37817
+
37037
37818
  <!-- ── Animation authoring ────────────────────────────────────────── -->
37038
37819
  <details class="pptx-ng-inspector__details">
37039
37820
  <summary class="pptx-ng-inspector__summary">Animation</summary>
@@ -53739,6 +54520,67 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
53739
54520
  }]
53740
54521
  }], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }], activeName: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeName", required: false }] }], applyTheme: [{ type: i0.Output, args: ["applyTheme"] }], close: [{ type: i0.Output, args: ["close"] }] } });
53741
54522
 
54523
+ /**
54524
+ * touch-gestures.ts: thin Angular adapter over the framework-agnostic
54525
+ * `createTouchGestureRecognizer` from `pptx-viewer-shared`.
54526
+ *
54527
+ * The gesture state machine itself (pinch-to-zoom, swipe, long-press) lives in
54528
+ * shared and is identical across React / Vue / Angular. This adapter only owns
54529
+ * the DOM listener attach/detach lifecycle, mirroring the React hook
54530
+ * `packages/react/src/viewer/hooks/useTouchGestures.ts`:
54531
+ * - `touchstart` / `touchmove` are registered with `{ passive: false }` so the
54532
+ * recogniser can call `preventDefault()` to suppress the browser's native
54533
+ * pinch-zoom on the canvas;
54534
+ * - `touchend` / `touchcancel` are passive (they never call preventDefault).
54535
+ *
54536
+ * `attachTouchGestures(element, config)` returns a teardown function that
54537
+ * removes every listener and cancels any pending long-press timer. Components
54538
+ * call it from `afterNextRender` (when the target node is live) and run the
54539
+ * teardown via `DestroyRef.onDestroy`.
54540
+ *
54541
+ * @module touch-gestures (angular)
54542
+ */
54543
+ /**
54544
+ * Viewer zoom bounds. The Angular viewer clamps zoom to [0.2, 3] (see
54545
+ * `ZOOM_MIN` / `ZOOM_MAX` in `power-point-viewer.component.ts`); the pinch path
54546
+ * must clamp to the same range so a pinch can never exceed the buttons.
54547
+ */
54548
+ const MIN_ZOOM_SCALE = 0.2;
54549
+ const MAX_ZOOM_SCALE = 3;
54550
+ /** Clamp a scale value to the Angular viewer's allowed zoom range. */
54551
+ function clampScale(value) {
54552
+ return clampScale$1(value, MIN_ZOOM_SCALE, MAX_ZOOM_SCALE);
54553
+ }
54554
+ /**
54555
+ * Attach the shared touch-gesture recogniser to `element` and return a teardown
54556
+ * function. Mirrors the React hook's listener lifecycle exactly:
54557
+ * `touchstart`/`touchmove` are non-passive (so pinch can `preventDefault`),
54558
+ * `touchend`/`touchcancel` are passive.
54559
+ */
54560
+ function attachTouchGestures(element, config) {
54561
+ const recognizer = createTouchGestureRecognizer({
54562
+ getScale: config.getScale,
54563
+ minScale: MIN_ZOOM_SCALE,
54564
+ maxScale: MAX_ZOOM_SCALE,
54565
+ callbacks: config.callbacks,
54566
+ });
54567
+ const onStart = (e) => recognizer.onTouchStart(e);
54568
+ const onMove = (e) => recognizer.onTouchMove(e);
54569
+ const onEnd = (e) => recognizer.onTouchEnd(e);
54570
+ const onCancel = () => recognizer.onTouchCancel();
54571
+ element.addEventListener('touchstart', onStart, { passive: false });
54572
+ element.addEventListener('touchmove', onMove, { passive: false });
54573
+ element.addEventListener('touchend', onEnd, { passive: true });
54574
+ element.addEventListener('touchcancel', onCancel, { passive: true });
54575
+ return () => {
54576
+ element.removeEventListener('touchstart', onStart);
54577
+ element.removeEventListener('touchmove', onMove);
54578
+ element.removeEventListener('touchend', onEnd);
54579
+ element.removeEventListener('touchcancel', onCancel);
54580
+ recognizer.cancel();
54581
+ };
54582
+ }
54583
+
53742
54584
  const ZOOM_STEP = 0.1;
53743
54585
  const ZOOM_MIN = 0.2;
53744
54586
  const ZOOM_MAX = 3;
@@ -54172,6 +55014,9 @@ class PowerPointViewerComponent {
54172
55014
  this.activeSession.set(null);
54173
55015
  }
54174
55016
  });
55017
+ // Attach multi-touch gestures (pinch-zoom / swipe-nav / long-press menu)
55018
+ // to the canvas host once it is rendered. See setupTouchGestures().
55019
+ this.setupTouchGestures();
54175
55020
  }
54176
55021
  /**
54177
55022
  * Serialise the current presentation to `.pptx` bytes (imperative handle).
@@ -54308,54 +55153,60 @@ class PowerPointViewerComponent {
54308
55153
  this.collab.disconnect();
54309
55154
  this.activeSession.set(null);
54310
55155
  }
54311
- /** Horizontal-swipe tracking start coordinates (touch begins on the canvas). */
54312
- swipeStartX = null;
54313
- swipeStartY = null;
54314
55156
  /**
54315
- * Begin tracking a horizontal swipe for slide navigation.
55157
+ * Wire the framework-agnostic touch-gesture recogniser to the `<main>` canvas
55158
+ * host once it is in the DOM. Mirrors React's `useTouchGestures` wiring:
55159
+ * - pinch-to-zoom always updates the zoom signal (clamped to the viewer
55160
+ * range), with `preventDefault()` on the pinch path to suppress the
55161
+ * browser's native pinch-zoom;
55162
+ * - horizontal swipe navigates slides, but only when editing is off
55163
+ * (`!canEdit()`): in edit mode single-finger gestures belong to element
55164
+ * manipulation (move/resize/rotate), so we never hijack them. The large
55165
+ * ‹ › buttons remain available for explicit navigation in all modes;
55166
+ * - long-press in edit mode opens the editor context menu at the press
55167
+ * point for the current selection (mirrors React's onLongPress path).
54316
55168
  *
54317
- * To disambiguate a navigation swipe from an element drag, swipe-nav is only
54318
- * armed when editing is off (`!canEdit()`). When `canEdit()` is true,
54319
- * pointer/touch gestures belong to element manipulation (move/resize/rotate),
54320
- * so we never hijack them. The large ‹ › buttons remain available in all
54321
- * modes for explicit navigation.
55169
+ * The recogniser's swipe/long-press callbacks check the live `canEdit()` /
55170
+ * selection state, so a single attach handles every mode without re-binding.
54322
55171
  */
54323
- onMainTouchStart(event) {
54324
- if (this.canEdit() || event.changedTouches.length !== 1) {
54325
- this.swipeStartX = null;
54326
- this.swipeStartY = null;
54327
- return;
54328
- }
54329
- const touch = event.changedTouches[0];
54330
- this.swipeStartX = touch.clientX;
54331
- this.swipeStartY = touch.clientY;
54332
- }
54333
- /**
54334
- * Complete a swipe: a predominantly horizontal drag of at least the threshold
54335
- * navigates to the previous (swipe right) or next (swipe left) slide.
54336
- */
54337
- onMainTouchEnd(event) {
54338
- const startX = this.swipeStartX;
54339
- const startY = this.swipeStartY;
54340
- this.swipeStartX = null;
54341
- this.swipeStartY = null;
54342
- if (startX === null || startY === null || event.changedTouches.length !== 1) {
54343
- return;
54344
- }
54345
- const touch = event.changedTouches[0];
54346
- const dx = touch.clientX - startX;
54347
- const dy = touch.clientY - startY;
54348
- const SWIPE_THRESHOLD = 50;
54349
- // Ignore vertical-dominant gestures (scrolling) and short drags.
54350
- if (Math.abs(dx) < SWIPE_THRESHOLD || Math.abs(dx) <= Math.abs(dy)) {
54351
- return;
54352
- }
54353
- if (dx < 0) {
54354
- this.goNext();
54355
- }
54356
- else {
54357
- this.goPrev();
54358
- }
55172
+ setupTouchGestures() {
55173
+ const destroyRef = inject(DestroyRef);
55174
+ afterNextRender(() => {
55175
+ const el = this.mainEl()?.nativeElement;
55176
+ if (!el) {
55177
+ return;
55178
+ }
55179
+ const teardown = attachTouchGestures(el, {
55180
+ getScale: () => this.zoom(),
55181
+ callbacks: {
55182
+ onPinchZoom: (newScale) => this.zoom.set(newScale),
55183
+ onSwipe: (direction) => {
55184
+ // Edit mode: leave single-finger gestures to element manipulation.
55185
+ if (this.canEdit()) {
55186
+ return;
55187
+ }
55188
+ // direction 1 = swipe right (previous), -1 = swipe left (next).
55189
+ if (direction === 1) {
55190
+ this.goPrev();
55191
+ }
55192
+ else {
55193
+ this.goNext();
55194
+ }
55195
+ },
55196
+ onLongPress: (x, y) => {
55197
+ if (!this.canEdit() || this.presenting()) {
55198
+ return;
55199
+ }
55200
+ const selected = this.selectedElement();
55201
+ if (!selected) {
55202
+ return;
55203
+ }
55204
+ this.contextMenuPos.set({ x, y });
55205
+ },
55206
+ },
55207
+ });
55208
+ destroyRef.onDestroy(teardown);
55209
+ });
54359
55210
  }
54360
55211
  zoomIn() {
54361
55212
  this.zoom.set(Math.min(ZOOM_MAX, Number((this.zoom() + ZOOM_STEP).toFixed(2))));
@@ -55153,12 +56004,7 @@ class PowerPointViewerComponent {
55153
56004
  </nav>
55154
56005
  }
55155
56006
 
55156
- <main
55157
- class="pptx-ng-main"
55158
- #mainEl
55159
- (touchstart)="onMainTouchStart($event)"
55160
- (touchend)="onMainTouchEnd($event)"
55161
- >
56007
+ <main class="pptx-ng-main" #mainEl>
55162
56008
  <pptx-slide-canvas
55163
56009
  [slide]="activeSlide()"
55164
56010
  [canvasSize]="loader.canvasSize()"
@@ -55723,12 +56569,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
55723
56569
  </nav>
55724
56570
  }
55725
56571
 
55726
- <main
55727
- class="pptx-ng-main"
55728
- #mainEl
55729
- (touchstart)="onMainTouchStart($event)"
55730
- (touchend)="onMainTouchEnd($event)"
55731
- >
56572
+ <main class="pptx-ng-main" #mainEl>
55732
56573
  <pptx-slide-canvas
55733
56574
  [slide]="activeSlide()"
55734
56575
  [canvasSize]="loader.canvasSize()"