pptx-angular-viewer 1.1.41 → 1.1.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import { NgStyle, NgClass, NgTemplateOutlet } from '@angular/common';
2
2
  import * as i0 from '@angular/core';
3
- import { InjectionToken, input, output, computed, ChangeDetectionStrategy, Component, signal, Injectable, HostListener, effect, inject, DestroyRef, ElementRef, viewChild, afterNextRender, Injector } from '@angular/core';
3
+ import { InjectionToken, input, output, computed, ChangeDetectionStrategy, Component, signal, Injectable, inject, DestroyRef, HostListener, effect, ElementRef, viewChild, afterNextRender, Injector } from '@angular/core';
4
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, svgPathToPolygons, deobfuscateFont, detectFontFormat, getSubstituteFontFamily, checkMissingAltText, checkMissingSlideTitle, checkLowContrast, checkComplexTables, checkBlankSlide, checkDuplicateTitles, createChartElement, formatCommentTimestamp, setChartTitle, setChartLegend, setChartDataLabels, setChartAxis, setChartSeriesTrendline, setChartSeriesErrorBars, setChartDataPointLabel, 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';
@@ -26142,6 +26142,298 @@ function issueTrackKey(issue, index) {
26142
26142
  return `${issue.slideIndex}-${issue.type}-${issue.elementId ?? 'slide'}-${index}`;
26143
26143
  }
26144
26144
 
26145
+ /**
26146
+ * Accessibility utilities for the PowerPoint viewer.
26147
+ *
26148
+ * Provides functions for computing reading order, generating ARIA labels,
26149
+ * determining ARIA roles, and detecting reduced-motion preferences.
26150
+ *
26151
+ * Pure (framework-agnostic): consumed by every binding's element renderer.
26152
+ *
26153
+ * @module render/accessibility
26154
+ */
26155
+ // ---------------------------------------------------------------------------
26156
+ // Reading order
26157
+ // ---------------------------------------------------------------------------
26158
+ /**
26159
+ * Computes a reading-order index for each element on a slide.
26160
+ *
26161
+ * Elements are sorted top-to-bottom first, then left-to-right for elements
26162
+ * at roughly the same vertical position (within a tolerance band).
26163
+ * Returns a `Map<elementId, tabIndex>` with 1-based indices.
26164
+ *
26165
+ * @param elements - The flat list of elements on the current slide.
26166
+ * @param tolerancePx - Vertical tolerance for grouping elements into the
26167
+ * same "row" (default 20px).
26168
+ * @returns Map from element ID to 1-based reading order index.
26169
+ */
26170
+ function computeReadingOrder(elements, tolerancePx = 20) {
26171
+ if (elements.length === 0) {
26172
+ return new Map();
26173
+ }
26174
+ const sorted = [...elements]
26175
+ .filter((el) => !el.hidden)
26176
+ .sort((a, b) => {
26177
+ const dy = a.y - b.y;
26178
+ if (Math.abs(dy) > tolerancePx) {
26179
+ return dy;
26180
+ }
26181
+ return a.x - b.x;
26182
+ });
26183
+ const result = new Map();
26184
+ sorted.forEach((el, idx) => {
26185
+ result.set(el.id, idx + 1);
26186
+ });
26187
+ return result;
26188
+ }
26189
+ // ---------------------------------------------------------------------------
26190
+ // ARIA roles
26191
+ // ---------------------------------------------------------------------------
26192
+ /**
26193
+ * Maps a PptxElement to its appropriate ARIA role.
26194
+ *
26195
+ * - image / picture => "img"
26196
+ * - table => "table"
26197
+ * - group => "group"
26198
+ * - chart => "img" (complex visualisation treated as image)
26199
+ * - text / shape with text => "text" is not valid; use undefined and rely on
26200
+ * aria-label alone. However, shapes without text act as decorative images.
26201
+ * - connector => "img"
26202
+ * - media => "application"
26203
+ *
26204
+ * @param element - The element to determine a role for.
26205
+ * @returns The ARIA role string, or undefined when none is needed.
26206
+ */
26207
+ function getAriaRole(element) {
26208
+ switch (element.type) {
26209
+ case 'image':
26210
+ case 'picture':
26211
+ return 'img';
26212
+ case 'table':
26213
+ return 'table';
26214
+ case 'group':
26215
+ return 'group';
26216
+ case 'chart':
26217
+ return 'img';
26218
+ case 'smartArt':
26219
+ return 'img';
26220
+ case 'connector':
26221
+ return 'img';
26222
+ case 'media':
26223
+ return 'application';
26224
+ case 'ink':
26225
+ return 'img';
26226
+ case 'model3d':
26227
+ return 'img';
26228
+ case 'text':
26229
+ return undefined;
26230
+ case 'shape': {
26231
+ // Shapes with text act as text containers; shapes without text are decorative
26232
+ if (hasTextProperties(element) && element.text) {
26233
+ return undefined;
26234
+ }
26235
+ return 'img';
26236
+ }
26237
+ default:
26238
+ return undefined;
26239
+ }
26240
+ }
26241
+ // ---------------------------------------------------------------------------
26242
+ // ARIA labels
26243
+ // ---------------------------------------------------------------------------
26244
+ /**
26245
+ * Generates a human-readable ARIA label for an element.
26246
+ *
26247
+ * Priority:
26248
+ * 1. Image altText (for image/picture elements)
26249
+ * 2. Text content (for text-bearing elements)
26250
+ * 3. Chart title (from chartData)
26251
+ * 4. Element type fallback label
26252
+ *
26253
+ * @param element - The element to generate a label for.
26254
+ * @returns A descriptive string for the `aria-label` attribute.
26255
+ */
26256
+ function getAriaLabel(element) {
26257
+ // Image alt text
26258
+ if ((element.type === 'image' || element.type === 'picture') &&
26259
+ 'altText' in element &&
26260
+ typeof element.altText === 'string' &&
26261
+ element.altText.trim()) {
26262
+ return element.altText.trim();
26263
+ }
26264
+ // Text content
26265
+ if (hasTextProperties(element) && element.text) {
26266
+ const text = element.text.trim();
26267
+ if (text) {
26268
+ // Truncate long text for the label
26269
+ return text.length > 120 ? `${text.slice(0, 117)}...` : text;
26270
+ }
26271
+ }
26272
+ // Chart title
26273
+ if (element.type === 'chart' && 'chartData' in element && element.chartData) {
26274
+ const cd = element.chartData;
26275
+ if (cd.title) {
26276
+ return `Chart: ${cd.title}`;
26277
+ }
26278
+ return 'Chart';
26279
+ }
26280
+ // SmartArt
26281
+ if (element.type === 'smartArt') {
26282
+ return 'SmartArt diagram';
26283
+ }
26284
+ // Table
26285
+ if (element.type === 'table') {
26286
+ return 'Table';
26287
+ }
26288
+ // Media
26289
+ if (element.type === 'media') {
26290
+ return 'Media element';
26291
+ }
26292
+ // Group
26293
+ if (element.type === 'group') {
26294
+ return 'Group of elements';
26295
+ }
26296
+ // Connector
26297
+ if (element.type === 'connector') {
26298
+ return 'Connector line';
26299
+ }
26300
+ // Ink
26301
+ if (element.type === 'ink') {
26302
+ return 'Ink drawing';
26303
+ }
26304
+ // 3D Model
26305
+ if (element.type === 'model3d') {
26306
+ return '3D Model';
26307
+ }
26308
+ // Shape fallback
26309
+ if (element.type === 'shape') {
26310
+ if ('shapeType' in element && element.shapeType) {
26311
+ return `Shape: ${element.shapeType}`;
26312
+ }
26313
+ return 'Shape';
26314
+ }
26315
+ // Generic fallback
26316
+ return getTypeFallbackLabel(element.type);
26317
+ }
26318
+ /**
26319
+ * Returns a user-friendly fallback label based on element type.
26320
+ */
26321
+ function getTypeFallbackLabel(type) {
26322
+ switch (type) {
26323
+ case 'text':
26324
+ return 'Text box';
26325
+ case 'shape':
26326
+ return 'Shape';
26327
+ case 'image':
26328
+ case 'picture':
26329
+ return 'Image';
26330
+ case 'table':
26331
+ return 'Table';
26332
+ case 'chart':
26333
+ return 'Chart';
26334
+ case 'connector':
26335
+ return 'Connector';
26336
+ case 'group':
26337
+ return 'Group';
26338
+ case 'smartArt':
26339
+ return 'SmartArt';
26340
+ case 'media':
26341
+ return 'Media';
26342
+ case 'ink':
26343
+ return 'Drawing';
26344
+ case 'ole':
26345
+ return 'Embedded object';
26346
+ case 'contentPart':
26347
+ return 'Content part';
26348
+ case 'model3d':
26349
+ return '3D Model';
26350
+ default:
26351
+ return 'Element';
26352
+ }
26353
+ }
26354
+ // ---------------------------------------------------------------------------
26355
+ // ARIA role descriptions
26356
+ // ---------------------------------------------------------------------------
26357
+ /**
26358
+ * Returns an `aria-roledescription` string for an element, providing
26359
+ * assistive-technology users with a more specific description of the
26360
+ * element's purpose than the generic ARIA role alone.
26361
+ *
26362
+ * @param element - The element to describe.
26363
+ * @returns A human-readable role description, or undefined when none is needed.
26364
+ */
26365
+ function getAriaRoleDescription(element) {
26366
+ switch (element.type) {
26367
+ case 'shape': {
26368
+ const shapeType = 'shapeType' in element && typeof element.shapeType === 'string'
26369
+ ? element.shapeType
26370
+ : undefined;
26371
+ if (shapeType) {
26372
+ return `shape: ${shapeType}`;
26373
+ }
26374
+ return 'shape';
26375
+ }
26376
+ case 'chart':
26377
+ return 'chart';
26378
+ case 'connector':
26379
+ return 'connector line';
26380
+ case 'smartArt':
26381
+ return 'diagram';
26382
+ case 'image':
26383
+ case 'picture':
26384
+ return 'image';
26385
+ case 'table':
26386
+ return 'data table';
26387
+ case 'group':
26388
+ return 'grouped elements';
26389
+ case 'media':
26390
+ return 'media player';
26391
+ case 'ink':
26392
+ return 'ink drawing';
26393
+ case 'model3d':
26394
+ return '3D model';
26395
+ default:
26396
+ return undefined;
26397
+ }
26398
+ }
26399
+ // ---------------------------------------------------------------------------
26400
+ // Reduced motion detection
26401
+ // ---------------------------------------------------------------------------
26402
+ /**
26403
+ * Queries the `prefers-reduced-motion: reduce` media query.
26404
+ *
26405
+ * @returns `true` if the user prefers reduced motion, `false` otherwise.
26406
+ */
26407
+ function prefersReducedMotion() {
26408
+ if (typeof window === 'undefined') {
26409
+ return false;
26410
+ }
26411
+ return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
26412
+ }
26413
+ /**
26414
+ * Returns a CSS-property object (framework-agnostic; spreadable into any
26415
+ * binding's style prop) that disables or tones
26416
+ * down animations and transitions when the user's OS-level
26417
+ * `prefers-reduced-motion: reduce` setting is active.
26418
+ *
26419
+ * When reduced motion is **not** preferred the function returns an empty
26420
+ * object so it can always be spread into a style prop safely.
26421
+ *
26422
+ * @returns CSS properties that suppress animations when appropriate.
26423
+ */
26424
+ function getReducedMotionStyles() {
26425
+ if (!prefersReducedMotion()) {
26426
+ return {};
26427
+ }
26428
+ return {
26429
+ animationDuration: '0.001ms',
26430
+ animationIterationCount: 1,
26431
+ transitionDuration: '0.001ms',
26432
+ animationDelay: '0ms',
26433
+ transitionDelay: '0ms',
26434
+ };
26435
+ }
26436
+
26145
26437
  /**
26146
26438
  * Convert an array of points into an SVG path `d` attribute string.
26147
26439
  * - 0 points -> `''`
@@ -29380,6 +29672,229 @@ function canUseClipboard(nav) {
29380
29672
  typeof nav.clipboard.writeText === 'function');
29381
29673
  }
29382
29674
 
29675
+ /**
29676
+ * is-mobile.ts: Injectable signal-based mobile-detection service plus a
29677
+ * pure `computeIsMobile` helper for unit-testing.
29678
+ *
29679
+ * Ported from: packages/react/src/viewer/hooks/useIsMobile.ts
29680
+ *
29681
+ * The service tracks two independent media conditions:
29682
+ * - `(pointer: coarse)`: primary pointer is a touch screen / stylus
29683
+ * - viewport width below {@link MOBILE_BREAKPOINT}
29684
+ *
29685
+ * Both conditions are evaluated once at construction time and then kept live
29686
+ * via `MediaQueryList.addEventListener`. The service is SSR-safe: when
29687
+ * `matchMedia` is not available (server / test environment without a DOM) all
29688
+ * signals default to `false` and no listeners are registered.
29689
+ */
29690
+ // ---------------------------------------------------------------------------
29691
+ // Constants
29692
+ // ---------------------------------------------------------------------------
29693
+ /** Viewport width (px) below which the UI switches to mobile layout. */
29694
+ const MOBILE_BREAKPOINT = 768;
29695
+ /** Tablet breakpoint: below this width (but >= MOBILE) is tablet. */
29696
+ const TABLET_BREAKPOINT = 1024;
29697
+ /**
29698
+ * Max viewport height (px) at which a *touch* device is treated as mobile
29699
+ * regardless of width. Catches landscape phones (e.g. 915×412), which are wide
29700
+ * enough to fall in the "tablet" width band but far too short for the desktop
29701
+ * ribbon + side panels, so they need the mobile chrome. Tablets in landscape are
29702
+ * taller (~760px+) so they stay on the desktop layout. Mirrors React's
29703
+ * `MOBILE_LANDSCAPE_MAX_HEIGHT` in useIsMobile.ts.
29704
+ */
29705
+ const MOBILE_LANDSCAPE_MAX_HEIGHT = 500;
29706
+ // ---------------------------------------------------------------------------
29707
+ // Pure helpers (no Angular deps, safe in vitest without a DOM)
29708
+ // ---------------------------------------------------------------------------
29709
+ /**
29710
+ * Decide whether the current environment should use the mobile layout:
29711
+ * - a narrow viewport (`width < MOBILE_BREAKPOINT`), OR
29712
+ * - a short *touch* viewport below the tablet width: a landscape phone, which
29713
+ * is wide enough to look like a tablet but far too short for the desktop
29714
+ * ribbon + side panels.
29715
+ *
29716
+ * This mirrors React's `isMobileViewport(width, height, isTouch)` so the three
29717
+ * frameworks switch chrome at the same breakpoints (and the shared mobile e2e
29718
+ * specs pass identically). A tall touch tablet (e.g. 820×1180) is NOT mobile.
29719
+ *
29720
+ * @pure: no side effects, fully testable without a DOM.
29721
+ */
29722
+ function computeIsMobile(width, height, isTouch) {
29723
+ if (width < MOBILE_BREAKPOINT) {
29724
+ return true;
29725
+ }
29726
+ return isTouch && height > 0 && height < MOBILE_LANDSCAPE_MAX_HEIGHT && width < TABLET_BREAKPOINT;
29727
+ }
29728
+ /**
29729
+ * Decide whether the current environment is "tablet" (desktop chrome, but in
29730
+ * the 768–1023px width band). A short landscape-phone touch viewport is mobile,
29731
+ * not tablet (handled by {@link computeIsMobile}).
29732
+ *
29733
+ * @pure
29734
+ */
29735
+ function computeIsTablet(width, height, isTouch) {
29736
+ if (computeIsMobile(width, height, isTouch)) {
29737
+ return false;
29738
+ }
29739
+ return width >= MOBILE_BREAKPOINT && width < TABLET_BREAKPOINT;
29740
+ }
29741
+ // ---------------------------------------------------------------------------
29742
+ // IsMobileService
29743
+ // ---------------------------------------------------------------------------
29744
+ /**
29745
+ * `IsMobileService`: provides reactive signals for the current viewport /
29746
+ * pointer kind so components can switch between mobile and desktop chrome
29747
+ * without subscribing to resize events themselves.
29748
+ *
29749
+ * Inject at the component level (or provide at root); the service cleans up
29750
+ * its `MediaQueryList` listeners automatically via `DestroyRef`.
29751
+ *
29752
+ * ```ts
29753
+ * providers: [IsMobileService]
29754
+ * // or globally:
29755
+ * // provideIsMobile() (see factory below)
29756
+ * ```
29757
+ *
29758
+ * ```ts
29759
+ * protected readonly mobile = inject(IsMobileService);
29760
+ * // in template: @if (mobile.isMobile()) { … }
29761
+ * ```
29762
+ */
29763
+ class IsMobileService {
29764
+ /** True when the primary pointer is coarse (touch / stylus). */
29765
+ isCoarsePointer = signal(false, /* @ts-ignore */
29766
+ ...(ngDevMode ? [{ debugName: "isCoarsePointer" }] : /* istanbul ignore next */ []));
29767
+ /** True when the viewport width is below {@link MOBILE_BREAKPOINT}. */
29768
+ isNarrowViewport = signal(false, /* @ts-ignore */
29769
+ ...(ngDevMode ? [{ debugName: "isNarrowViewport" }] : /* istanbul ignore next */ []));
29770
+ /**
29771
+ * True when either the pointer is coarse OR the viewport is narrow.
29772
+ * Use this as the single gate for showing mobile chrome.
29773
+ */
29774
+ isMobile = signal(false, /* @ts-ignore */
29775
+ ...(ngDevMode ? [{ debugName: "isMobile" }] : /* istanbul ignore next */ []));
29776
+ /** True when the viewport is in the tablet range (desktop pointer only). */
29777
+ isTablet = signal(false, /* @ts-ignore */
29778
+ ...(ngDevMode ? [{ debugName: "isTablet" }] : /* istanbul ignore next */ []));
29779
+ /**
29780
+ * CSS pixels the on-screen keyboard currently covers at the bottom of the
29781
+ * layout viewport (0 when no keyboard is open). The orchestrator lifts the
29782
+ * fixed mobile bottom bar by this amount so it stays above the keyboard.
29783
+ */
29784
+ keyboardInset = signal(0, /* @ts-ignore */
29785
+ ...(ngDevMode ? [{ debugName: "keyboardInset" }] : /* istanbul ignore next */ []));
29786
+ /** True while {@link keyboardInset} is large enough to count as "open". */
29787
+ isKeyboardOpen = signal(false, /* @ts-ignore */
29788
+ ...(ngDevMode ? [{ debugName: "isKeyboardOpen" }] : /* istanbul ignore next */ []));
29789
+ constructor() {
29790
+ const destroyRef = inject(DestroyRef);
29791
+ // Guard: matchMedia / window may not be available in SSR / test envs.
29792
+ if (typeof matchMedia !== 'function' || typeof window === 'undefined') {
29793
+ return;
29794
+ }
29795
+ this._wireKeyboardInset(destroyRef);
29796
+ // ── Coarse-pointer media query (drives the touch flag) ───────────────────
29797
+ const coarseMql = matchMedia('(pointer: coarse)');
29798
+ this.isCoarsePointer.set(coarseMql.matches);
29799
+ const onCoarseChange = (evt) => {
29800
+ this.isCoarsePointer.set(evt.matches);
29801
+ this._recompute();
29802
+ };
29803
+ coarseMql.addEventListener('change', onCoarseChange);
29804
+ // ── Viewport size tracking ───────────────────────────────────────────────
29805
+ // Width-only media queries cannot express the "short landscape phone"
29806
+ // rule (which depends on height + touch), so track the live viewport size
29807
+ // on resize and recompute the derived flags from width/height/touch.
29808
+ const onResize = () => this._recompute();
29809
+ window.addEventListener('resize', onResize);
29810
+ if (typeof screen !== 'undefined' && screen.orientation) {
29811
+ screen.orientation.addEventListener('change', onResize);
29812
+ }
29813
+ this._recompute();
29814
+ // ── Cleanup on destroy ───────────────────────────────────────────────────
29815
+ destroyRef.onDestroy(() => {
29816
+ coarseMql.removeEventListener('change', onCoarseChange);
29817
+ window.removeEventListener('resize', onResize);
29818
+ if (typeof screen !== 'undefined' && screen.orientation) {
29819
+ screen.orientation.removeEventListener('change', onResize);
29820
+ }
29821
+ });
29822
+ }
29823
+ /** Recompute all derived flags from the live viewport size + pointer kind. */
29824
+ _recompute() {
29825
+ const width = window.innerWidth;
29826
+ const height = window.innerHeight;
29827
+ const touch = this.isCoarsePointer();
29828
+ this.isNarrowViewport.set(width < MOBILE_BREAKPOINT);
29829
+ this.isMobile.set(computeIsMobile(width, height, touch));
29830
+ this.isTablet.set(computeIsTablet(width, height, touch));
29831
+ }
29832
+ /**
29833
+ * Track the on-screen-keyboard inset via the `VisualViewport` API and keep the
29834
+ * focused editable visible: when the keyboard shrinks the visual viewport,
29835
+ * update {@link keyboardInset} / {@link isKeyboardOpen} and scroll the active
29836
+ * input / textarea / contenteditable into the area above the keyboard. No-op
29837
+ * when `visualViewport` is unavailable (desktop / SSR), so desktop is unchanged.
29838
+ */
29839
+ _wireKeyboardInset(destroyRef) {
29840
+ const vv = window.visualViewport;
29841
+ if (!vv) {
29842
+ return;
29843
+ }
29844
+ const update = () => {
29845
+ const metrics = readViewportMetrics(window);
29846
+ const inset = metrics ? computeKeyboardInset(metrics) : 0;
29847
+ this.keyboardInset.set(inset);
29848
+ this.isKeyboardOpen.set(isKeyboardOpen(inset));
29849
+ if (inset > 0) {
29850
+ window.requestAnimationFrame(() => this._scrollFocusedIntoView(inset));
29851
+ }
29852
+ };
29853
+ const onFocusIn = () => {
29854
+ window.requestAnimationFrame(() => {
29855
+ const metrics = readViewportMetrics(window);
29856
+ const inset = metrics ? computeKeyboardInset(metrics) : 0;
29857
+ if (inset > 0) {
29858
+ this._scrollFocusedIntoView(inset);
29859
+ }
29860
+ });
29861
+ };
29862
+ update();
29863
+ vv.addEventListener('resize', update);
29864
+ vv.addEventListener('scroll', update);
29865
+ document.addEventListener('focusin', onFocusIn);
29866
+ destroyRef.onDestroy(() => {
29867
+ vv.removeEventListener('resize', update);
29868
+ vv.removeEventListener('scroll', update);
29869
+ document.removeEventListener('focusin', onFocusIn);
29870
+ });
29871
+ }
29872
+ /** Scroll the focused editable into the area above the keyboard, if needed. */
29873
+ _scrollFocusedIntoView(keyboardInset) {
29874
+ if (keyboardInset <= 0 || typeof document === 'undefined') {
29875
+ return;
29876
+ }
29877
+ const active = document.activeElement;
29878
+ if (!(active instanceof HTMLElement)) {
29879
+ return;
29880
+ }
29881
+ const tag = active.tagName;
29882
+ if (tag !== 'INPUT' && tag !== 'TEXTAREA' && !active.isContentEditable) {
29883
+ return;
29884
+ }
29885
+ const rect = active.getBoundingClientRect();
29886
+ const delta = computeScrollDelta({ top: rect.top, bottom: rect.bottom }, window.innerHeight, keyboardInset);
29887
+ if (delta !== 0) {
29888
+ window.scrollBy({ top: delta, behavior: 'smooth' });
29889
+ }
29890
+ }
29891
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: IsMobileService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
29892
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: IsMobileService });
29893
+ }
29894
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: IsMobileService, decorators: [{
29895
+ type: Injectable
29896
+ }], ctorParameters: () => [] });
29897
+
29383
29898
  /**
29384
29899
  * modal-dialog.component.ts: Reusable, accessible modal dialog shell.
29385
29900
  *
@@ -29406,6 +29921,16 @@ function canUseClipboard(nav) {
29406
29921
  * </pptx-modal-dialog>
29407
29922
  * ```
29408
29923
  */
29924
+ /**
29925
+ * Map the mobile flag to the modal-panel class list. Pure (no Angular / DOM) so
29926
+ * it can be unit-tested without TestBed: on mobile the panel gets the
29927
+ * `is-mobile` modifier that turns it into a bottom sheet (matching the CSS media
29928
+ * query, but driven by the {@link IsMobileService} 768px breakpoint so the three
29929
+ * frameworks switch chrome at the same width).
29930
+ */
29931
+ function modalPanelClass(isMobile) {
29932
+ return isMobile ? 'pptx-ng-modal-panel is-mobile' : 'pptx-ng-modal-panel';
29933
+ }
29409
29934
  class ModalDialogComponent {
29410
29935
  /** Whether the dialog is visible. */
29411
29936
  open = input(false, /* @ts-ignore */
@@ -29415,6 +29940,14 @@ class ModalDialogComponent {
29415
29940
  ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
29416
29941
  /** Fired on backdrop click, the `×` button, and `Escape`. */
29417
29942
  close = output();
29943
+ /** Reactive viewport / pointer flags (drives the bottom-sheet layout). */
29944
+ mobile = inject(IsMobileService);
29945
+ /**
29946
+ * Panel class list: gains the `is-mobile` bottom-sheet modifier under the
29947
+ * mobile breakpoint. See {@link modalPanelClass}.
29948
+ */
29949
+ panelClass = computed(() => modalPanelClass(this.mobile.isMobile()), /* @ts-ignore */
29950
+ ...(ngDevMode ? [{ debugName: "panelClass" }] : /* istanbul ignore next */ []));
29418
29951
  /** Close on `Escape`, regardless of where focus currently sits. */
29419
29952
  onDocumentKeydown(event) {
29420
29953
  if (!this.open()) {
@@ -29477,11 +30010,15 @@ class ModalDialogComponent {
29477
30010
  this.dragY.set(0);
29478
30011
  }
29479
30012
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ModalDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
29480
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: ModalDialogComponent, isStandalone: true, selector: "pptx-modal-dialog", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { close: "close" }, host: { listeners: { "document:keydown": "onDocumentKeydown($event)" } }, ngImport: i0, template: `
30013
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: ModalDialogComponent, isStandalone: true, selector: "pptx-modal-dialog", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { close: "close" }, host: { listeners: { "document:keydown": "onDocumentKeydown($event)" } }, providers: [IsMobileService], ngImport: i0, template: `
29481
30014
  @if (open()) {
29482
- <div class="pptx-ng-modal-backdrop" (click)="onBackdropClick()">
30015
+ <div
30016
+ class="pptx-ng-modal-backdrop"
30017
+ [class.is-mobile]="mobile.isMobile()"
30018
+ (click)="onBackdropClick()"
30019
+ >
29483
30020
  <div
29484
- class="pptx-ng-modal-panel"
30021
+ [class]="panelClass()"
29485
30022
  role="dialog"
29486
30023
  aria-modal="true"
29487
30024
  [attr.aria-label]="title() || null"
@@ -29521,15 +30058,19 @@ class ModalDialogComponent {
29521
30058
  </div>
29522
30059
  </div>
29523
30060
  }
29524
- `, isInline: true, styles: [".pptx-ng-modal-backdrop{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;background:#00000073}.pptx-ng-modal-panel{display:flex;flex-direction:column;min-width:320px;max-width:min(92vw,480px);max-height:88vh;overflow:hidden;background:var(--pptx-popover, #ffffff);color:var(--pptx-foreground, #111827);border:1px solid var(--pptx-border, #e5e7eb);border-radius:var(--pptx-radius, 8px);box-shadow:0 10px 40px #00000059}.pptx-ng-modal-header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 16px;border-bottom:1px solid var(--pptx-border, #e5e7eb);touch-action:none}.pptx-ng-modal-title{margin:0;font-size:14px;font-weight:600;line-height:1.4}.pptx-ng-modal-close{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;font-size:18px;line-height:1;color:var(--pptx-muted-foreground, #6b7280);background:transparent;border:none;border-radius:4px;cursor:pointer}.pptx-ng-modal-close:hover{color:var(--pptx-foreground, #111827);background:var(--pptx-muted, #f3f4f6)}.pptx-ng-modal-body{padding:16px;overflow-y:auto}.pptx-ng-modal-footer{display:flex;justify-content:flex-end;gap:8px;padding:12px 16px;border-top:1px solid var(--pptx-border, #e5e7eb)}.pptx-ng-modal-footer:empty{display:none}@media(max-width:640px),(pointer:coarse){.pptx-ng-modal-backdrop{align-items:flex-end;justify-content:stretch}.pptx-ng-modal-panel{min-width:0;max-width:none;width:100%;max-height:88dvh;border-left:none;border-right:none;border-bottom:none;border-radius:16px 16px 0 0;padding-bottom:max(env(safe-area-inset-bottom),0px)}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
30061
+ `, isInline: true, styles: [".pptx-ng-modal-backdrop{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;background:#00000073}.pptx-ng-modal-panel{display:flex;flex-direction:column;min-width:320px;max-width:min(92vw,480px);max-height:88vh;overflow:hidden;background:var(--pptx-popover, #ffffff);color:var(--pptx-foreground, #111827);border:1px solid var(--pptx-border, #e5e7eb);border-radius:var(--pptx-radius, 8px);box-shadow:0 10px 40px #00000059}.pptx-ng-modal-header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 16px;border-bottom:1px solid var(--pptx-border, #e5e7eb);touch-action:none}.pptx-ng-modal-title{margin:0;font-size:14px;font-weight:600;line-height:1.4}.pptx-ng-modal-close{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;font-size:18px;line-height:1;color:var(--pptx-muted-foreground, #6b7280);background:transparent;border:none;border-radius:4px;cursor:pointer}.pptx-ng-modal-close:hover{color:var(--pptx-foreground, #111827);background:var(--pptx-muted, #f3f4f6)}.pptx-ng-modal-body{padding:16px;overflow-y:auto}.pptx-ng-modal-footer{display:flex;justify-content:flex-end;gap:8px;padding:12px 16px;border-top:1px solid var(--pptx-border, #e5e7eb)}.pptx-ng-modal-footer:empty{display:none}.pptx-ng-modal-backdrop.is-mobile{align-items:flex-end;justify-content:stretch}.pptx-ng-modal-panel.is-mobile{min-width:0;max-width:none;width:100%;max-height:88dvh;border-left:none;border-right:none;border-bottom:none;border-radius:16px 16px 0 0;padding-bottom:max(env(safe-area-inset-bottom),0px)}@media(max-width:767px),(pointer:coarse){.pptx-ng-modal-backdrop{align-items:flex-end;justify-content:stretch}.pptx-ng-modal-panel{min-width:0;max-width:none;width:100%;max-height:88dvh;border-left:none;border-right:none;border-bottom:none;border-radius:16px 16px 0 0;padding-bottom:max(env(safe-area-inset-bottom),0px)}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
29525
30062
  }
29526
30063
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ModalDialogComponent, decorators: [{
29527
30064
  type: Component,
29528
- args: [{ selector: 'pptx-modal-dialog', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
30065
+ args: [{ selector: 'pptx-modal-dialog', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, providers: [IsMobileService], template: `
29529
30066
  @if (open()) {
29530
- <div class="pptx-ng-modal-backdrop" (click)="onBackdropClick()">
30067
+ <div
30068
+ class="pptx-ng-modal-backdrop"
30069
+ [class.is-mobile]="mobile.isMobile()"
30070
+ (click)="onBackdropClick()"
30071
+ >
29531
30072
  <div
29532
- class="pptx-ng-modal-panel"
30073
+ [class]="panelClass()"
29533
30074
  role="dialog"
29534
30075
  aria-modal="true"
29535
30076
  [attr.aria-label]="title() || null"
@@ -29569,7 +30110,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
29569
30110
  </div>
29570
30111
  </div>
29571
30112
  }
29572
- `, styles: [".pptx-ng-modal-backdrop{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;background:#00000073}.pptx-ng-modal-panel{display:flex;flex-direction:column;min-width:320px;max-width:min(92vw,480px);max-height:88vh;overflow:hidden;background:var(--pptx-popover, #ffffff);color:var(--pptx-foreground, #111827);border:1px solid var(--pptx-border, #e5e7eb);border-radius:var(--pptx-radius, 8px);box-shadow:0 10px 40px #00000059}.pptx-ng-modal-header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 16px;border-bottom:1px solid var(--pptx-border, #e5e7eb);touch-action:none}.pptx-ng-modal-title{margin:0;font-size:14px;font-weight:600;line-height:1.4}.pptx-ng-modal-close{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;font-size:18px;line-height:1;color:var(--pptx-muted-foreground, #6b7280);background:transparent;border:none;border-radius:4px;cursor:pointer}.pptx-ng-modal-close:hover{color:var(--pptx-foreground, #111827);background:var(--pptx-muted, #f3f4f6)}.pptx-ng-modal-body{padding:16px;overflow-y:auto}.pptx-ng-modal-footer{display:flex;justify-content:flex-end;gap:8px;padding:12px 16px;border-top:1px solid var(--pptx-border, #e5e7eb)}.pptx-ng-modal-footer:empty{display:none}@media(max-width:640px),(pointer:coarse){.pptx-ng-modal-backdrop{align-items:flex-end;justify-content:stretch}.pptx-ng-modal-panel{min-width:0;max-width:none;width:100%;max-height:88dvh;border-left:none;border-right:none;border-bottom:none;border-radius:16px 16px 0 0;padding-bottom:max(env(safe-area-inset-bottom),0px)}}\n"] }]
30113
+ `, styles: [".pptx-ng-modal-backdrop{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;background:#00000073}.pptx-ng-modal-panel{display:flex;flex-direction:column;min-width:320px;max-width:min(92vw,480px);max-height:88vh;overflow:hidden;background:var(--pptx-popover, #ffffff);color:var(--pptx-foreground, #111827);border:1px solid var(--pptx-border, #e5e7eb);border-radius:var(--pptx-radius, 8px);box-shadow:0 10px 40px #00000059}.pptx-ng-modal-header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 16px;border-bottom:1px solid var(--pptx-border, #e5e7eb);touch-action:none}.pptx-ng-modal-title{margin:0;font-size:14px;font-weight:600;line-height:1.4}.pptx-ng-modal-close{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;font-size:18px;line-height:1;color:var(--pptx-muted-foreground, #6b7280);background:transparent;border:none;border-radius:4px;cursor:pointer}.pptx-ng-modal-close:hover{color:var(--pptx-foreground, #111827);background:var(--pptx-muted, #f3f4f6)}.pptx-ng-modal-body{padding:16px;overflow-y:auto}.pptx-ng-modal-footer{display:flex;justify-content:flex-end;gap:8px;padding:12px 16px;border-top:1px solid var(--pptx-border, #e5e7eb)}.pptx-ng-modal-footer:empty{display:none}.pptx-ng-modal-backdrop.is-mobile{align-items:flex-end;justify-content:stretch}.pptx-ng-modal-panel.is-mobile{min-width:0;max-width:none;width:100%;max-height:88dvh;border-left:none;border-right:none;border-bottom:none;border-radius:16px 16px 0 0;padding-bottom:max(env(safe-area-inset-bottom),0px)}@media(max-width:767px),(pointer:coarse){.pptx-ng-modal-backdrop{align-items:flex-end;justify-content:stretch}.pptx-ng-modal-panel{min-width:0;max-width:none;width:100%;max-height:88dvh;border-left:none;border-right:none;border-bottom:none;border-radius:16px 16px 0 0;padding-bottom:max(env(safe-area-inset-bottom),0px)}}\n"] }]
29573
30114
  }], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], close: [{ type: i0.Output, args: ["close"] }], onDocumentKeydown: [{
29574
30115
  type: HostListener,
29575
30116
  args: ['document:keydown', ['$event']]
@@ -40197,6 +40738,15 @@ class InspectorPanelComponent {
40197
40738
  slideIndex = input.required(/* @ts-ignore */
40198
40739
  ...(ngDevMode ? [{ debugName: "slideIndex" }] : /* istanbul ignore next */ []));
40199
40740
  editor = inject(EditorStateService);
40741
+ /** Reactive viewport / pointer flags (drives the bottom-sheet layout). */
40742
+ mobile = inject(IsMobileService);
40743
+ /**
40744
+ * Root class list: gains the `is-mobile` modifier under the mobile
40745
+ * breakpoint so the panel becomes a full-width, touch-sized bottom sheet.
40746
+ * See {@link inspectorRootClass}.
40747
+ */
40748
+ inspectorClass = computed(() => inspectorRootClass(this.mobile.isMobile()), /* @ts-ignore */
40749
+ ...(ngDevMode ? [{ debugName: "inspectorClass" }] : /* istanbul ignore next */ []));
40200
40750
  /** Alias so the template can call el() without conflicting with Angular internals. */
40201
40751
  el = computed(() => this.element(), /* @ts-ignore */
40202
40752
  ...(ngDevMode ? [{ debugName: "el" }] : /* istanbul ignore next */ []));
@@ -40372,7 +40922,7 @@ class InspectorPanelComponent {
40372
40922
  this.editor.updateElement(this.slideIndex(), cur.id, textStylePatch(cur, { underline: !this.currentUnderline() }));
40373
40923
  }
40374
40924
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: InspectorPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
40375
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: InspectorPanelComponent, isStandalone: true, selector: "pptx-inspector-panel", inputs: { element: { classPropertyName: "element", publicName: "element", isSignal: true, isRequired: true, transformFunction: null }, slideIndex: { classPropertyName: "slideIndex", publicName: "slideIndex", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
40925
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: InspectorPanelComponent, isStandalone: true, selector: "pptx-inspector-panel", inputs: { element: { classPropertyName: "element", publicName: "element", isSignal: true, isRequired: true, transformFunction: null }, slideIndex: { classPropertyName: "slideIndex", publicName: "slideIndex", isSignal: true, isRequired: true, transformFunction: null } }, providers: [IsMobileService], ngImport: i0, template: `
40376
40926
  <!--
40377
40927
  NOTE (mobile-safe inputs): every numeric / colour input is keyed on the
40378
40928
  selected element's id via @if blocks. Angular destroys and recreates the
@@ -40382,7 +40932,7 @@ class InspectorPanelComponent {
40382
40932
  mid-edit: the caret stays put and the on-screen keyboard does not dismiss.
40383
40933
  All commits happen on (change) (blur), reading event.target.value.
40384
40934
  -->
40385
- <aside class="pptx-ng-inspector" aria-label="Element properties">
40935
+ <aside [class]="inspectorClass()" aria-label="Element properties">
40386
40936
  <!-- ── Transform: Position & Size ─────────────────────────────────── -->
40387
40937
  <section class="pptx-ng-inspector__section">
40388
40938
  <h3 class="pptx-ng-inspector__heading">Transform</h3>
@@ -40665,7 +41215,7 @@ class InspectorPanelComponent {
40665
41215
  </div>
40666
41216
  </section>
40667
41217
  </aside>
40668
- `, 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 });
41218
+ `, 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}.pptx-ng-inspector.is-mobile{width:100%;min-width:0;box-sizing:border-box;font-size:14px}.pptx-ng-inspector.is-mobile .pptx-ng-inspector__row{flex-wrap:wrap;gap:.5rem}.pptx-ng-inspector.is-mobile .pptx-ng-inspector__label{min-width:28px}.pptx-ng-inspector.is-mobile .pptx-ng-inspector__input{flex:1 1 auto;min-height:40px;font-size:16px;padding:6px 8px}.pptx-ng-inspector.is-mobile .pptx-ng-inspector__input--number{width:auto;min-width:72px}.pptx-ng-inspector.is-mobile .pptx-ng-inspector__color{width:44px;height:40px}.pptx-ng-inspector.is-mobile .pptx-ng-inspector__toggle{min-width:44px;width:auto;flex:1 1 auto;height:40px;font-size:15px}.pptx-ng-inspector.is-mobile .pptx-ng-inspector__btn{min-height:40px;padding:8px 10px;font-size:13px}@media(pointer:coarse),(max-width:767px){.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 });
40669
41219
  }
40670
41220
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: InspectorPanelComponent, decorators: [{
40671
41221
  type: Component,
@@ -40677,7 +41227,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
40677
41227
  ChartDataEditorComponent,
40678
41228
  SmartArtPropertiesComponent,
40679
41229
  AnimationAuthorPanelComponent,
40680
- ], template: `
41230
+ ], providers: [IsMobileService], template: `
40681
41231
  <!--
40682
41232
  NOTE (mobile-safe inputs): every numeric / colour input is keyed on the
40683
41233
  selected element's id via @if blocks. Angular destroys and recreates the
@@ -40687,7 +41237,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
40687
41237
  mid-edit: the caret stays put and the on-screen keyboard does not dismiss.
40688
41238
  All commits happen on (change) (blur), reading event.target.value.
40689
41239
  -->
40690
- <aside class="pptx-ng-inspector" aria-label="Element properties">
41240
+ <aside [class]="inspectorClass()" aria-label="Element properties">
40691
41241
  <!-- ── Transform: Position & Size ─────────────────────────────────── -->
40692
41242
  <section class="pptx-ng-inspector__section">
40693
41243
  <h3 class="pptx-ng-inspector__heading">Transform</h3>
@@ -40970,8 +41520,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
40970
41520
  </div>
40971
41521
  </section>
40972
41522
  </aside>
40973
- `, 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"] }]
41523
+ `, 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}.pptx-ng-inspector.is-mobile{width:100%;min-width:0;box-sizing:border-box;font-size:14px}.pptx-ng-inspector.is-mobile .pptx-ng-inspector__row{flex-wrap:wrap;gap:.5rem}.pptx-ng-inspector.is-mobile .pptx-ng-inspector__label{min-width:28px}.pptx-ng-inspector.is-mobile .pptx-ng-inspector__input{flex:1 1 auto;min-height:40px;font-size:16px;padding:6px 8px}.pptx-ng-inspector.is-mobile .pptx-ng-inspector__input--number{width:auto;min-width:72px}.pptx-ng-inspector.is-mobile .pptx-ng-inspector__color{width:44px;height:40px}.pptx-ng-inspector.is-mobile .pptx-ng-inspector__toggle{min-width:44px;width:auto;flex:1 1 auto;height:40px;font-size:15px}.pptx-ng-inspector.is-mobile .pptx-ng-inspector__btn{min-height:40px;padding:8px 10px;font-size:13px}@media(pointer:coarse),(max-width:767px){.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"] }]
40974
41524
  }], propDecorators: { element: [{ type: i0.Input, args: [{ isSignal: true, alias: "element", required: true }] }], slideIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "slideIndex", required: true }] }] } });
41525
+ // ── Pure helpers (no Angular / DOM; unit-testable without TestBed) ────────────
41526
+ /**
41527
+ * Map the mobile flag to the inspector root class list. On mobile the panel
41528
+ * gains the `is-mobile` modifier that makes it a full-width, touch-sized
41529
+ * bottom-sheet body (the orchestrator host wraps it with the drawer chrome).
41530
+ */
41531
+ function inspectorRootClass(isMobile) {
41532
+ return isMobile ? 'pptx-ng-inspector is-mobile' : 'pptx-ng-inspector';
41533
+ }
40975
41534
  // ── Module-private helpers ───────────────────────────────────────────────────
40976
41535
  /** Extract a finite number from an input change event, or null if invalid. */
40977
41536
  function numberFromEvent(event) {
@@ -40992,229 +41551,6 @@ function stringFromEvent(event) {
40992
41551
  return val.length > 0 ? val : null;
40993
41552
  }
40994
41553
 
40995
- /**
40996
- * is-mobile.ts: Injectable signal-based mobile-detection service plus a
40997
- * pure `computeIsMobile` helper for unit-testing.
40998
- *
40999
- * Ported from: packages/react/src/viewer/hooks/useIsMobile.ts
41000
- *
41001
- * The service tracks two independent media conditions:
41002
- * - `(pointer: coarse)`: primary pointer is a touch screen / stylus
41003
- * - viewport width below {@link MOBILE_BREAKPOINT}
41004
- *
41005
- * Both conditions are evaluated once at construction time and then kept live
41006
- * via `MediaQueryList.addEventListener`. The service is SSR-safe: when
41007
- * `matchMedia` is not available (server / test environment without a DOM) all
41008
- * signals default to `false` and no listeners are registered.
41009
- */
41010
- // ---------------------------------------------------------------------------
41011
- // Constants
41012
- // ---------------------------------------------------------------------------
41013
- /** Viewport width (px) below which the UI switches to mobile layout. */
41014
- const MOBILE_BREAKPOINT = 768;
41015
- /** Tablet breakpoint: below this width (but >= MOBILE) is tablet. */
41016
- const TABLET_BREAKPOINT = 1024;
41017
- /**
41018
- * Max viewport height (px) at which a *touch* device is treated as mobile
41019
- * regardless of width. Catches landscape phones (e.g. 915×412), which are wide
41020
- * enough to fall in the "tablet" width band but far too short for the desktop
41021
- * ribbon + side panels, so they need the mobile chrome. Tablets in landscape are
41022
- * taller (~760px+) so they stay on the desktop layout. Mirrors React's
41023
- * `MOBILE_LANDSCAPE_MAX_HEIGHT` in useIsMobile.ts.
41024
- */
41025
- const MOBILE_LANDSCAPE_MAX_HEIGHT = 500;
41026
- // ---------------------------------------------------------------------------
41027
- // Pure helpers (no Angular deps, safe in vitest without a DOM)
41028
- // ---------------------------------------------------------------------------
41029
- /**
41030
- * Decide whether the current environment should use the mobile layout:
41031
- * - a narrow viewport (`width < MOBILE_BREAKPOINT`), OR
41032
- * - a short *touch* viewport below the tablet width: a landscape phone, which
41033
- * is wide enough to look like a tablet but far too short for the desktop
41034
- * ribbon + side panels.
41035
- *
41036
- * This mirrors React's `isMobileViewport(width, height, isTouch)` so the three
41037
- * frameworks switch chrome at the same breakpoints (and the shared mobile e2e
41038
- * specs pass identically). A tall touch tablet (e.g. 820×1180) is NOT mobile.
41039
- *
41040
- * @pure: no side effects, fully testable without a DOM.
41041
- */
41042
- function computeIsMobile(width, height, isTouch) {
41043
- if (width < MOBILE_BREAKPOINT) {
41044
- return true;
41045
- }
41046
- return isTouch && height > 0 && height < MOBILE_LANDSCAPE_MAX_HEIGHT && width < TABLET_BREAKPOINT;
41047
- }
41048
- /**
41049
- * Decide whether the current environment is "tablet" (desktop chrome, but in
41050
- * the 768–1023px width band). A short landscape-phone touch viewport is mobile,
41051
- * not tablet (handled by {@link computeIsMobile}).
41052
- *
41053
- * @pure
41054
- */
41055
- function computeIsTablet(width, height, isTouch) {
41056
- if (computeIsMobile(width, height, isTouch)) {
41057
- return false;
41058
- }
41059
- return width >= MOBILE_BREAKPOINT && width < TABLET_BREAKPOINT;
41060
- }
41061
- // ---------------------------------------------------------------------------
41062
- // IsMobileService
41063
- // ---------------------------------------------------------------------------
41064
- /**
41065
- * `IsMobileService`: provides reactive signals for the current viewport /
41066
- * pointer kind so components can switch between mobile and desktop chrome
41067
- * without subscribing to resize events themselves.
41068
- *
41069
- * Inject at the component level (or provide at root); the service cleans up
41070
- * its `MediaQueryList` listeners automatically via `DestroyRef`.
41071
- *
41072
- * ```ts
41073
- * providers: [IsMobileService]
41074
- * // or globally:
41075
- * // provideIsMobile() (see factory below)
41076
- * ```
41077
- *
41078
- * ```ts
41079
- * protected readonly mobile = inject(IsMobileService);
41080
- * // in template: @if (mobile.isMobile()) { … }
41081
- * ```
41082
- */
41083
- class IsMobileService {
41084
- /** True when the primary pointer is coarse (touch / stylus). */
41085
- isCoarsePointer = signal(false, /* @ts-ignore */
41086
- ...(ngDevMode ? [{ debugName: "isCoarsePointer" }] : /* istanbul ignore next */ []));
41087
- /** True when the viewport width is below {@link MOBILE_BREAKPOINT}. */
41088
- isNarrowViewport = signal(false, /* @ts-ignore */
41089
- ...(ngDevMode ? [{ debugName: "isNarrowViewport" }] : /* istanbul ignore next */ []));
41090
- /**
41091
- * True when either the pointer is coarse OR the viewport is narrow.
41092
- * Use this as the single gate for showing mobile chrome.
41093
- */
41094
- isMobile = signal(false, /* @ts-ignore */
41095
- ...(ngDevMode ? [{ debugName: "isMobile" }] : /* istanbul ignore next */ []));
41096
- /** True when the viewport is in the tablet range (desktop pointer only). */
41097
- isTablet = signal(false, /* @ts-ignore */
41098
- ...(ngDevMode ? [{ debugName: "isTablet" }] : /* istanbul ignore next */ []));
41099
- /**
41100
- * CSS pixels the on-screen keyboard currently covers at the bottom of the
41101
- * layout viewport (0 when no keyboard is open). The orchestrator lifts the
41102
- * fixed mobile bottom bar by this amount so it stays above the keyboard.
41103
- */
41104
- keyboardInset = signal(0, /* @ts-ignore */
41105
- ...(ngDevMode ? [{ debugName: "keyboardInset" }] : /* istanbul ignore next */ []));
41106
- /** True while {@link keyboardInset} is large enough to count as "open". */
41107
- isKeyboardOpen = signal(false, /* @ts-ignore */
41108
- ...(ngDevMode ? [{ debugName: "isKeyboardOpen" }] : /* istanbul ignore next */ []));
41109
- constructor() {
41110
- const destroyRef = inject(DestroyRef);
41111
- // Guard: matchMedia / window may not be available in SSR / test envs.
41112
- if (typeof matchMedia !== 'function' || typeof window === 'undefined') {
41113
- return;
41114
- }
41115
- this._wireKeyboardInset(destroyRef);
41116
- // ── Coarse-pointer media query (drives the touch flag) ───────────────────
41117
- const coarseMql = matchMedia('(pointer: coarse)');
41118
- this.isCoarsePointer.set(coarseMql.matches);
41119
- const onCoarseChange = (evt) => {
41120
- this.isCoarsePointer.set(evt.matches);
41121
- this._recompute();
41122
- };
41123
- coarseMql.addEventListener('change', onCoarseChange);
41124
- // ── Viewport size tracking ───────────────────────────────────────────────
41125
- // Width-only media queries cannot express the "short landscape phone"
41126
- // rule (which depends on height + touch), so track the live viewport size
41127
- // on resize and recompute the derived flags from width/height/touch.
41128
- const onResize = () => this._recompute();
41129
- window.addEventListener('resize', onResize);
41130
- if (typeof screen !== 'undefined' && screen.orientation) {
41131
- screen.orientation.addEventListener('change', onResize);
41132
- }
41133
- this._recompute();
41134
- // ── Cleanup on destroy ───────────────────────────────────────────────────
41135
- destroyRef.onDestroy(() => {
41136
- coarseMql.removeEventListener('change', onCoarseChange);
41137
- window.removeEventListener('resize', onResize);
41138
- if (typeof screen !== 'undefined' && screen.orientation) {
41139
- screen.orientation.removeEventListener('change', onResize);
41140
- }
41141
- });
41142
- }
41143
- /** Recompute all derived flags from the live viewport size + pointer kind. */
41144
- _recompute() {
41145
- const width = window.innerWidth;
41146
- const height = window.innerHeight;
41147
- const touch = this.isCoarsePointer();
41148
- this.isNarrowViewport.set(width < MOBILE_BREAKPOINT);
41149
- this.isMobile.set(computeIsMobile(width, height, touch));
41150
- this.isTablet.set(computeIsTablet(width, height, touch));
41151
- }
41152
- /**
41153
- * Track the on-screen-keyboard inset via the `VisualViewport` API and keep the
41154
- * focused editable visible: when the keyboard shrinks the visual viewport,
41155
- * update {@link keyboardInset} / {@link isKeyboardOpen} and scroll the active
41156
- * input / textarea / contenteditable into the area above the keyboard. No-op
41157
- * when `visualViewport` is unavailable (desktop / SSR), so desktop is unchanged.
41158
- */
41159
- _wireKeyboardInset(destroyRef) {
41160
- const vv = window.visualViewport;
41161
- if (!vv) {
41162
- return;
41163
- }
41164
- const update = () => {
41165
- const metrics = readViewportMetrics(window);
41166
- const inset = metrics ? computeKeyboardInset(metrics) : 0;
41167
- this.keyboardInset.set(inset);
41168
- this.isKeyboardOpen.set(isKeyboardOpen(inset));
41169
- if (inset > 0) {
41170
- window.requestAnimationFrame(() => this._scrollFocusedIntoView(inset));
41171
- }
41172
- };
41173
- const onFocusIn = () => {
41174
- window.requestAnimationFrame(() => {
41175
- const metrics = readViewportMetrics(window);
41176
- const inset = metrics ? computeKeyboardInset(metrics) : 0;
41177
- if (inset > 0) {
41178
- this._scrollFocusedIntoView(inset);
41179
- }
41180
- });
41181
- };
41182
- update();
41183
- vv.addEventListener('resize', update);
41184
- vv.addEventListener('scroll', update);
41185
- document.addEventListener('focusin', onFocusIn);
41186
- destroyRef.onDestroy(() => {
41187
- vv.removeEventListener('resize', update);
41188
- vv.removeEventListener('scroll', update);
41189
- document.removeEventListener('focusin', onFocusIn);
41190
- });
41191
- }
41192
- /** Scroll the focused editable into the area above the keyboard, if needed. */
41193
- _scrollFocusedIntoView(keyboardInset) {
41194
- if (keyboardInset <= 0 || typeof document === 'undefined') {
41195
- return;
41196
- }
41197
- const active = document.activeElement;
41198
- if (!(active instanceof HTMLElement)) {
41199
- return;
41200
- }
41201
- const tag = active.tagName;
41202
- if (tag !== 'INPUT' && tag !== 'TEXTAREA' && !active.isContentEditable) {
41203
- return;
41204
- }
41205
- const rect = active.getBoundingClientRect();
41206
- const delta = computeScrollDelta({ top: rect.top, bottom: rect.bottom }, window.innerHeight, keyboardInset);
41207
- if (delta !== 0) {
41208
- window.scrollBy({ top: delta, behavior: 'smooth' });
41209
- }
41210
- }
41211
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: IsMobileService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
41212
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: IsMobileService });
41213
- }
41214
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: IsMobileService, decorators: [{
41215
- type: Injectable
41216
- }], ctorParameters: () => [] });
41217
-
41218
41554
  /**
41219
41555
  * `LoadContentService`: Angular port of the React `useLoadContent` hook and
41220
41556
  * the Vue `useLoadContent` composable.
@@ -53172,6 +53508,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
53172
53508
  * }
53173
53509
  * ```
53174
53510
  */
53511
+ /**
53512
+ * Map the mobile flag to the print-dialog panel class list. Pure (no Angular /
53513
+ * DOM) so it can be unit-tested without TestBed. On mobile the dialog gains the
53514
+ * `is-mobile` modifier that turns it into a full-width bottom sheet.
53515
+ */
53516
+ function printDialogClass(isMobile) {
53517
+ return isMobile ? 'pptx-ng-print-dialog is-mobile' : 'pptx-ng-print-dialog';
53518
+ }
53175
53519
  class PrintDialogComponent {
53176
53520
  // -------------------------------------------------------------------------
53177
53521
  // Inputs / outputs
@@ -53192,6 +53536,14 @@ class PrintDialogComponent {
53192
53536
  print = output();
53193
53537
  /** Emits when the dialog is dismissed (Cancel / Escape / backdrop). */
53194
53538
  cancel = output();
53539
+ /** Reactive viewport / pointer flags (drives the bottom-sheet layout). */
53540
+ mobile = inject(IsMobileService);
53541
+ /**
53542
+ * Dialog class list: gains the `is-mobile` bottom-sheet modifier under the
53543
+ * mobile breakpoint. See {@link printDialogClass}.
53544
+ */
53545
+ dialogClass = computed(() => printDialogClass(this.mobile.isMobile()), /* @ts-ignore */
53546
+ ...(ngDevMode ? [{ debugName: "dialogClass" }] : /* istanbul ignore next */ []));
53195
53547
  // -------------------------------------------------------------------------
53196
53548
  // State
53197
53549
  // -------------------------------------------------------------------------
@@ -53259,15 +53611,16 @@ class PrintDialogComponent {
53259
53611
  }
53260
53612
  }
53261
53613
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: PrintDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
53262
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.2", type: PrintDialogComponent, isStandalone: true, selector: "pptx-print-dialog", inputs: { slides: { classPropertyName: "slides", publicName: "slides", isSignal: true, isRequired: true, transformFunction: null }, activeSlideIndex: { classPropertyName: "activeSlideIndex", publicName: "activeSlideIndex", isSignal: true, isRequired: true, transformFunction: null }, defaultSlidesPerPage: { classPropertyName: "defaultSlidesPerPage", publicName: "defaultSlidesPerPage", isSignal: true, isRequired: false, transformFunction: null }, defaultFrameSlides: { classPropertyName: "defaultFrameSlides", publicName: "defaultFrameSlides", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { print: "print", cancel: "cancel" }, host: { listeners: { "document:keydown": "onKeydown($event)" } }, ngImport: i0, template: `
53614
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.2", type: PrintDialogComponent, isStandalone: true, selector: "pptx-print-dialog", inputs: { slides: { classPropertyName: "slides", publicName: "slides", isSignal: true, isRequired: true, transformFunction: null }, activeSlideIndex: { classPropertyName: "activeSlideIndex", publicName: "activeSlideIndex", isSignal: true, isRequired: true, transformFunction: null }, defaultSlidesPerPage: { classPropertyName: "defaultSlidesPerPage", publicName: "defaultSlidesPerPage", isSignal: true, isRequired: false, transformFunction: null }, defaultFrameSlides: { classPropertyName: "defaultFrameSlides", publicName: "defaultFrameSlides", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { print: "print", cancel: "cancel" }, host: { listeners: { "document:keydown": "onKeydown($event)" } }, providers: [IsMobileService], ngImport: i0, template: `
53263
53615
  <div
53264
53616
  class="pptx-ng-print-dialog__backdrop"
53617
+ [class.is-mobile]="mobile.isMobile()"
53265
53618
  role="dialog"
53266
53619
  aria-modal="true"
53267
53620
  aria-label="Print"
53268
53621
  (click)="onBackdropClick($event)"
53269
53622
  >
53270
- <div class="pptx-ng-print-dialog">
53623
+ <div [class]="dialogClass()">
53271
53624
  <!-- Header -->
53272
53625
  <div class="pptx-ng-print-dialog__header">
53273
53626
  <h2 class="pptx-ng-print-dialog__title">Print</h2>
@@ -53313,19 +53666,20 @@ class PrintDialogComponent {
53313
53666
  </div>
53314
53667
  </div>
53315
53668
  </div>
53316
- `, isInline: true, styles: [".pptx-ng-print-dialog__backdrop{position:fixed;inset:0;z-index:1200;display:flex;align-items:center;justify-content:center;background:#0009;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.pptx-ng-print-dialog{display:flex;flex-direction:column;width:780px;max-width:calc(100vw - 2rem);max-height:90vh;border:1px solid rgba(255,255,255,.12);border-radius:.75rem;background:#1e1e1e;color:#e5e5e5;box-shadow:0 20px 60px #0009}.pptx-ng-print-dialog__header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1.25rem;border-bottom:1px solid rgba(255,255,255,.1)}.pptx-ng-print-dialog__title{margin:0;font-size:.875rem;font-weight:600;color:#fff}.pptx-ng-print-dialog__icon-btn{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;padding:0;border:0;border-radius:.25rem;background:transparent;color:#fff9;font-size:.75rem;cursor:pointer;transition:background .12s,color .12s}.pptx-ng-print-dialog__icon-btn:hover{background:#ffffff1a;color:#fff}.pptx-ng-print-dialog__body{flex:1;overflow-y:auto;padding:1rem 1.25rem}.pptx-ng-print-dialog__footer{display:flex;align-items:center;justify-content:space-between;padding:.75rem 1.25rem;border-top:1px solid rgba(255,255,255,.1)}.pptx-ng-print-dialog__estimate{font-size:.75rem;color:#ffffff80}.pptx-ng-print-dialog__actions{display:flex;gap:.5rem}.pptx-ng-print-dialog__btn{padding:.5rem 1rem;border:1px solid rgba(255,255,255,.15);border-radius:.5rem;background:#ffffff0a;color:#ffffffb3;font-size:.8125rem;cursor:pointer;transition:background .12s,color .12s,border-color .12s}.pptx-ng-print-dialog__btn:hover{background:#ffffff1a;color:#fff}.pptx-ng-print-dialog__btn--primary{border-color:#3b82f6;background:#3b82f6;color:#fff}.pptx-ng-print-dialog__btn--primary:hover{background:#2f6fd6}\n"], dependencies: [{ kind: "component", type: PrintSettingsPanelComponent, selector: "pptx-print-settings-panel", inputs: ["settings", "totalSlides", "activeSlideIndex"], outputs: ["settingsChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
53669
+ `, isInline: true, styles: [".pptx-ng-print-dialog__backdrop{position:fixed;inset:0;z-index:1200;display:flex;align-items:center;justify-content:center;background:#0009;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.pptx-ng-print-dialog{display:flex;flex-direction:column;width:780px;max-width:calc(100vw - 2rem);max-height:90vh;border:1px solid rgba(255,255,255,.12);border-radius:.75rem;background:#1e1e1e;color:#e5e5e5;box-shadow:0 20px 60px #0009}.pptx-ng-print-dialog__header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1.25rem;border-bottom:1px solid rgba(255,255,255,.1)}.pptx-ng-print-dialog__title{margin:0;font-size:.875rem;font-weight:600;color:#fff}.pptx-ng-print-dialog__icon-btn{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;padding:0;border:0;border-radius:.25rem;background:transparent;color:#fff9;font-size:.75rem;cursor:pointer;transition:background .12s,color .12s}.pptx-ng-print-dialog__icon-btn:hover{background:#ffffff1a;color:#fff}.pptx-ng-print-dialog__body{flex:1;overflow-y:auto;padding:1rem 1.25rem}.pptx-ng-print-dialog__footer{display:flex;align-items:center;justify-content:space-between;padding:.75rem 1.25rem;border-top:1px solid rgba(255,255,255,.1)}.pptx-ng-print-dialog__estimate{font-size:.75rem;color:#ffffff80}.pptx-ng-print-dialog__actions{display:flex;gap:.5rem}.pptx-ng-print-dialog__btn{padding:.5rem 1rem;border:1px solid rgba(255,255,255,.15);border-radius:.5rem;background:#ffffff0a;color:#ffffffb3;font-size:.8125rem;cursor:pointer;transition:background .12s,color .12s,border-color .12s}.pptx-ng-print-dialog__btn:hover{background:#ffffff1a;color:#fff}.pptx-ng-print-dialog__btn--primary{border-color:#3b82f6;background:#3b82f6;color:#fff}.pptx-ng-print-dialog__btn--primary:hover{background:#2f6fd6}.pptx-ng-print-dialog__backdrop.is-mobile{align-items:flex-end}.pptx-ng-print-dialog.is-mobile{width:100%;max-width:none;max-height:88dvh;border-left:none;border-right:none;border-bottom:none;border-top-left-radius:1rem;border-top-right-radius:1rem;border-bottom-left-radius:0;border-bottom-right-radius:0;padding-bottom:max(env(safe-area-inset-bottom),0px)}@media(max-width:767px),(pointer:coarse){.pptx-ng-print-dialog__backdrop{align-items:flex-end}.pptx-ng-print-dialog{width:100%;max-width:none;max-height:88dvh;border-left:none;border-right:none;border-bottom:none;border-top-left-radius:1rem;border-top-right-radius:1rem;border-bottom-left-radius:0;border-bottom-right-radius:0;padding-bottom:max(env(safe-area-inset-bottom),0px)}}\n"], dependencies: [{ kind: "component", type: PrintSettingsPanelComponent, selector: "pptx-print-settings-panel", inputs: ["settings", "totalSlides", "activeSlideIndex"], outputs: ["settingsChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
53317
53670
  }
53318
53671
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: PrintDialogComponent, decorators: [{
53319
53672
  type: Component,
53320
- args: [{ selector: 'pptx-print-dialog', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [PrintSettingsPanelComponent], template: `
53673
+ args: [{ selector: 'pptx-print-dialog', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [PrintSettingsPanelComponent], providers: [IsMobileService], template: `
53321
53674
  <div
53322
53675
  class="pptx-ng-print-dialog__backdrop"
53676
+ [class.is-mobile]="mobile.isMobile()"
53323
53677
  role="dialog"
53324
53678
  aria-modal="true"
53325
53679
  aria-label="Print"
53326
53680
  (click)="onBackdropClick($event)"
53327
53681
  >
53328
- <div class="pptx-ng-print-dialog">
53682
+ <div [class]="dialogClass()">
53329
53683
  <!-- Header -->
53330
53684
  <div class="pptx-ng-print-dialog__header">
53331
53685
  <h2 class="pptx-ng-print-dialog__title">Print</h2>
@@ -53371,7 +53725,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
53371
53725
  </div>
53372
53726
  </div>
53373
53727
  </div>
53374
- `, styles: [".pptx-ng-print-dialog__backdrop{position:fixed;inset:0;z-index:1200;display:flex;align-items:center;justify-content:center;background:#0009;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.pptx-ng-print-dialog{display:flex;flex-direction:column;width:780px;max-width:calc(100vw - 2rem);max-height:90vh;border:1px solid rgba(255,255,255,.12);border-radius:.75rem;background:#1e1e1e;color:#e5e5e5;box-shadow:0 20px 60px #0009}.pptx-ng-print-dialog__header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1.25rem;border-bottom:1px solid rgba(255,255,255,.1)}.pptx-ng-print-dialog__title{margin:0;font-size:.875rem;font-weight:600;color:#fff}.pptx-ng-print-dialog__icon-btn{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;padding:0;border:0;border-radius:.25rem;background:transparent;color:#fff9;font-size:.75rem;cursor:pointer;transition:background .12s,color .12s}.pptx-ng-print-dialog__icon-btn:hover{background:#ffffff1a;color:#fff}.pptx-ng-print-dialog__body{flex:1;overflow-y:auto;padding:1rem 1.25rem}.pptx-ng-print-dialog__footer{display:flex;align-items:center;justify-content:space-between;padding:.75rem 1.25rem;border-top:1px solid rgba(255,255,255,.1)}.pptx-ng-print-dialog__estimate{font-size:.75rem;color:#ffffff80}.pptx-ng-print-dialog__actions{display:flex;gap:.5rem}.pptx-ng-print-dialog__btn{padding:.5rem 1rem;border:1px solid rgba(255,255,255,.15);border-radius:.5rem;background:#ffffff0a;color:#ffffffb3;font-size:.8125rem;cursor:pointer;transition:background .12s,color .12s,border-color .12s}.pptx-ng-print-dialog__btn:hover{background:#ffffff1a;color:#fff}.pptx-ng-print-dialog__btn--primary{border-color:#3b82f6;background:#3b82f6;color:#fff}.pptx-ng-print-dialog__btn--primary:hover{background:#2f6fd6}\n"] }]
53728
+ `, styles: [".pptx-ng-print-dialog__backdrop{position:fixed;inset:0;z-index:1200;display:flex;align-items:center;justify-content:center;background:#0009;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.pptx-ng-print-dialog{display:flex;flex-direction:column;width:780px;max-width:calc(100vw - 2rem);max-height:90vh;border:1px solid rgba(255,255,255,.12);border-radius:.75rem;background:#1e1e1e;color:#e5e5e5;box-shadow:0 20px 60px #0009}.pptx-ng-print-dialog__header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1.25rem;border-bottom:1px solid rgba(255,255,255,.1)}.pptx-ng-print-dialog__title{margin:0;font-size:.875rem;font-weight:600;color:#fff}.pptx-ng-print-dialog__icon-btn{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;padding:0;border:0;border-radius:.25rem;background:transparent;color:#fff9;font-size:.75rem;cursor:pointer;transition:background .12s,color .12s}.pptx-ng-print-dialog__icon-btn:hover{background:#ffffff1a;color:#fff}.pptx-ng-print-dialog__body{flex:1;overflow-y:auto;padding:1rem 1.25rem}.pptx-ng-print-dialog__footer{display:flex;align-items:center;justify-content:space-between;padding:.75rem 1.25rem;border-top:1px solid rgba(255,255,255,.1)}.pptx-ng-print-dialog__estimate{font-size:.75rem;color:#ffffff80}.pptx-ng-print-dialog__actions{display:flex;gap:.5rem}.pptx-ng-print-dialog__btn{padding:.5rem 1rem;border:1px solid rgba(255,255,255,.15);border-radius:.5rem;background:#ffffff0a;color:#ffffffb3;font-size:.8125rem;cursor:pointer;transition:background .12s,color .12s,border-color .12s}.pptx-ng-print-dialog__btn:hover{background:#ffffff1a;color:#fff}.pptx-ng-print-dialog__btn--primary{border-color:#3b82f6;background:#3b82f6;color:#fff}.pptx-ng-print-dialog__btn--primary:hover{background:#2f6fd6}.pptx-ng-print-dialog__backdrop.is-mobile{align-items:flex-end}.pptx-ng-print-dialog.is-mobile{width:100%;max-width:none;max-height:88dvh;border-left:none;border-right:none;border-bottom:none;border-top-left-radius:1rem;border-top-right-radius:1rem;border-bottom-left-radius:0;border-bottom-right-radius:0;padding-bottom:max(env(safe-area-inset-bottom),0px)}@media(max-width:767px),(pointer:coarse){.pptx-ng-print-dialog__backdrop{align-items:flex-end}.pptx-ng-print-dialog{width:100%;max-width:none;max-height:88dvh;border-left:none;border-right:none;border-bottom:none;border-top-left-radius:1rem;border-top-right-radius:1rem;border-bottom-left-radius:0;border-bottom-right-radius:0;padding-bottom:max(env(safe-area-inset-bottom),0px)}}\n"] }]
53375
53729
  }], ctorParameters: () => [], propDecorators: { slides: [{ type: i0.Input, args: [{ isSignal: true, alias: "slides", required: true }] }], activeSlideIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeSlideIndex", required: true }] }], defaultSlidesPerPage: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultSlidesPerPage", required: false }] }], defaultFrameSlides: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultFrameSlides", required: false }] }], print: [{ type: i0.Output, args: ["print"] }], cancel: [{ type: i0.Output, args: ["cancel"] }], onKeydown: [{
53376
53730
  type: HostListener,
53377
53731
  args: ['document:keydown', ['$event']]