pptx-angular-viewer 1.1.41 → 1.1.43

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';
@@ -23966,6 +23966,516 @@ function buildSmartArt3DModel(layout, options = {}) {
23966
23966
  return options.spatial ? applySpatialLayout(model) : model;
23967
23967
  }
23968
23968
 
23969
+ /**
23970
+ * Vanilla three.js GLTF/GLB model scene controller (framework-agnostic).
23971
+ *
23972
+ * Mounts an interactive 3D model into a caller-provided container element:
23973
+ * dynamically imports `three` plus its `GLTFLoader` and `OrbitControls` addons,
23974
+ * builds a scene (perspective camera, lights), auto-centres and fits the model
23975
+ * to the view via a `Box3`, renders with a RAF loop, and exposes `resize()` /
23976
+ * `dispose()` for deterministic teardown of GPU resources.
23977
+ *
23978
+ * `three` is an OPTIONAL peer dependency: every import is dynamic and guarded.
23979
+ * When `three` is not installed, {@link mountModel3D} resolves to a sentinel
23980
+ * handle ({@link THREE_UNAVAILABLE}) so the caller can fall back to a poster.
23981
+ * No framework imports - the React, Vue, and Angular bindings can all mount it.
23982
+ */
23983
+ /**
23984
+ * Sentinel handle returned when `three` is unavailable or the model fails to
23985
+ * load. All methods are no-ops; `ok` is `false` so callers can show a poster.
23986
+ */
23987
+ const THREE_UNAVAILABLE = {
23988
+ ok: false,
23989
+ resize: () => { },
23990
+ setInteractive: () => { },
23991
+ dispose: () => { },
23992
+ };
23993
+ const FOV$1 = 50;
23994
+ /** Dynamically load `three`; returns `null` when the package is not installed. */
23995
+ async function loadThree$1() {
23996
+ try {
23997
+ return (await import('three'));
23998
+ }
23999
+ catch {
24000
+ return null;
24001
+ }
24002
+ }
24003
+ /** Dynamically load the GLTFLoader addon; returns `null` when unavailable. */
24004
+ async function loadGltfLoaderCtor() {
24005
+ try {
24006
+ const mod = await import('three/examples/jsm/loaders/GLTFLoader.js');
24007
+ return mod.GLTFLoader;
24008
+ }
24009
+ catch {
24010
+ return null;
24011
+ }
24012
+ }
24013
+ /** Dynamically load the OrbitControls addon; returns `null` when unavailable. */
24014
+ async function loadOrbitControlsCtor$1() {
24015
+ try {
24016
+ const mod = await import('three/examples/jsm/controls/OrbitControls.js');
24017
+ return mod.OrbitControls;
24018
+ }
24019
+ catch {
24020
+ return null;
24021
+ }
24022
+ }
24023
+ /** Promisified `GLTFLoader.load`. */
24024
+ function loadGltf(loader, url) {
24025
+ return new Promise((resolve, reject) => {
24026
+ loader.load(url, resolve, undefined, reject);
24027
+ });
24028
+ }
24029
+ /**
24030
+ * Auto-centre the model at the origin and uniformly scale it so its largest
24031
+ * dimension fills a 2-unit cube, mirroring the previous react-three-fiber fit.
24032
+ */
24033
+ function centerAndFit(three, root) {
24034
+ const box = new three.Box3().setFromObject(root);
24035
+ const size = new three.Vector3();
24036
+ box.getSize(size);
24037
+ const maxDim = Math.max(size.x, size.y, size.z);
24038
+ if (maxDim > 0) {
24039
+ root.scale.setScalar(2 / maxDim);
24040
+ }
24041
+ const center = new three.Vector3();
24042
+ box.getCenter(center);
24043
+ root.position.sub(center.multiplyScalar(root.scale.x));
24044
+ }
24045
+ /**
24046
+ * Mount an interactive GLTF/GLB model into `container` and start rendering.
24047
+ *
24048
+ * Resolves to {@link THREE_UNAVAILABLE} (a no-op handle with `ok === false`)
24049
+ * when `three` or its addons cannot be loaded, or when the model fails to parse.
24050
+ */
24051
+ async function mountModel3D(container, modelUrl, options) {
24052
+ const three = await loadThree$1();
24053
+ const GltfLoaderCtor = three ? await loadGltfLoaderCtor() : null;
24054
+ if (!three || !GltfLoaderCtor) {
24055
+ return THREE_UNAVAILABLE;
24056
+ }
24057
+ const width = Math.max(1, options.width);
24058
+ const height = Math.max(1, options.height);
24059
+ const renderer = new three.WebGLRenderer({ antialias: true, alpha: !options.background });
24060
+ renderer.setPixelRatio(Math.min(typeof window === 'undefined' ? 1 : window.devicePixelRatio || 1, options.maxPixelRatio ?? 2));
24061
+ renderer.setSize(width, height, false);
24062
+ const canvas = renderer.domElement;
24063
+ canvas.style.width = `${width}px`;
24064
+ canvas.style.height = `${height}px`;
24065
+ canvas.style.display = 'block';
24066
+ canvas.style.willChange = 'transform';
24067
+ container.appendChild(canvas);
24068
+ const scene = new three.Scene();
24069
+ if (options.background) {
24070
+ scene.background = new three.Color(options.background);
24071
+ }
24072
+ const camera = new three.PerspectiveCamera(FOV$1, width / height, 0.1, 1000);
24073
+ camera.position.set(0, 0, 5);
24074
+ camera.lookAt(0, 0, 0);
24075
+ scene.add(new three.AmbientLight(0xffffff, 0.5));
24076
+ const key = new three.DirectionalLight(0xffffff, 1);
24077
+ key.position.set(5, 5, 5);
24078
+ scene.add(key);
24079
+ const fill = new three.DirectionalLight(0xffffff, 0.3);
24080
+ fill.position.set(-3, -3, 2);
24081
+ scene.add(fill);
24082
+ let gltf;
24083
+ try {
24084
+ gltf = await loadGltf(new GltfLoaderCtor(), modelUrl);
24085
+ }
24086
+ catch {
24087
+ renderer.dispose();
24088
+ canvas.remove();
24089
+ return THREE_UNAVAILABLE;
24090
+ }
24091
+ const model = gltf.scene;
24092
+ centerAndFit(three, model);
24093
+ scene.add(model);
24094
+ let controls = null;
24095
+ const OrbitCtrlCtor = await loadOrbitControlsCtor$1();
24096
+ const enableControls = (on) => {
24097
+ if (on && !controls && OrbitCtrlCtor) {
24098
+ controls = new OrbitCtrlCtor(camera, canvas);
24099
+ controls.enablePan = false;
24100
+ controls.enableZoom = true;
24101
+ controls.enableRotate = true;
24102
+ controls.minDistance = 2;
24103
+ controls.maxDistance = 20;
24104
+ controls.update();
24105
+ }
24106
+ else if (!on && controls) {
24107
+ controls.dispose();
24108
+ controls = null;
24109
+ }
24110
+ };
24111
+ enableControls(options.interactive ?? true);
24112
+ let frame = 0;
24113
+ let disposed = false;
24114
+ const renderLoop = () => {
24115
+ if (disposed) {
24116
+ return;
24117
+ }
24118
+ frame = requestAnimationFrame(renderLoop);
24119
+ controls?.update();
24120
+ renderer.render(scene, camera);
24121
+ };
24122
+ frame = requestAnimationFrame(renderLoop);
24123
+ /** Recursively dispose every geometry/material/texture under the model. */
24124
+ const disposeModel = () => {
24125
+ model.traverse((obj) => {
24126
+ const mesh = obj;
24127
+ mesh.geometry?.dispose();
24128
+ const material = mesh.material;
24129
+ if (Array.isArray(material)) {
24130
+ for (const m of material) {
24131
+ m.dispose();
24132
+ }
24133
+ }
24134
+ else {
24135
+ material?.dispose();
24136
+ }
24137
+ });
24138
+ };
24139
+ return {
24140
+ ok: true,
24141
+ resize(w, h) {
24142
+ const nextW = Math.max(1, w);
24143
+ const nextH = Math.max(1, h);
24144
+ camera.aspect = nextW / nextH;
24145
+ camera.updateProjectionMatrix();
24146
+ renderer.setSize(nextW, nextH, false);
24147
+ canvas.style.width = `${nextW}px`;
24148
+ canvas.style.height = `${nextH}px`;
24149
+ },
24150
+ setInteractive(on) {
24151
+ enableControls(on);
24152
+ },
24153
+ dispose() {
24154
+ if (disposed) {
24155
+ return;
24156
+ }
24157
+ disposed = true;
24158
+ cancelAnimationFrame(frame);
24159
+ controls?.dispose();
24160
+ disposeModel();
24161
+ scene.clear();
24162
+ renderer.dispose();
24163
+ canvas.remove();
24164
+ },
24165
+ };
24166
+ }
24167
+
24168
+ /**
24169
+ * Pure surface-chart 3D geometry helpers (framework- and three-agnostic).
24170
+ *
24171
+ * The vanilla three scene controller ({@link mountSurfaceChart3D}) feeds these
24172
+ * the loaded `three` module so the heavy library stays a dynamic, optional
24173
+ * import while the grid maths (plane subdivision -> height/colour displacement,
24174
+ * world-space label anchors) lives here as small, separately testable units.
24175
+ */
24176
+ /** Half-unit spacing between adjacent grid points, in world units. */
24177
+ const GRID_SPACING = 0.5;
24178
+ /** Maximum height displacement (world units) for a normalised value of 1. */
24179
+ const MAX_HEIGHT = 1.5;
24180
+ /** Compute the world-space width/depth of the data grid. */
24181
+ function computeGridExtent(cols, rows) {
24182
+ return {
24183
+ gridWidth: Math.max(cols - 1, 1) * GRID_SPACING,
24184
+ gridDepth: Math.max(rows - 1, 1) * GRID_SPACING,
24185
+ };
24186
+ }
24187
+ /**
24188
+ * Build the surface mesh + wireframe geometries from the normalised height and
24189
+ * colour maps. The plane is subdivided to match the data grid, rotated into the
24190
+ * XZ plane, then each vertex is displaced in Y by its height and tinted by its
24191
+ * colour. Returns disposable geometries the caller adds to the scene.
24192
+ */
24193
+ function buildSurfaceGeometry(three, cols, rows, heightMap, colorMap) {
24194
+ const widthSegs = Math.max(cols - 1, 0);
24195
+ const depthSegs = Math.max(rows - 1, 0);
24196
+ const { gridWidth, gridDepth } = computeGridExtent(cols, rows);
24197
+ const geo = new three.PlaneGeometry(gridWidth, gridDepth, widthSegs, depthSegs);
24198
+ // PlaneGeometry lies in the XY plane by default; rotate it flat into XZ.
24199
+ geo.rotateX(-Math.PI / 2);
24200
+ const pos = geo.attributes.position;
24201
+ const vertexCount = pos.count;
24202
+ const colors = new Float32Array(vertexCount * 3);
24203
+ for (let i = 0; i < vertexCount; i++) {
24204
+ // After the rotation, the (widthSegs+1) x (depthSegs+1) vertices stay in
24205
+ // row-major order, so col = i % cols and row = floor(i / cols) map onto the
24206
+ // data grid directly.
24207
+ const row = Math.floor(i / cols);
24208
+ const col = i % cols;
24209
+ const idx = row * cols + col;
24210
+ const h = idx < heightMap.length ? heightMap[idx] : 0;
24211
+ pos.setY(i, h * MAX_HEIGHT);
24212
+ const ci = idx * 3;
24213
+ colors[i * 3] = ci < colorMap.length ? colorMap[ci] : 0.5;
24214
+ colors[i * 3 + 1] = ci + 1 < colorMap.length ? colorMap[ci + 1] : 0.5;
24215
+ colors[i * 3 + 2] = ci + 2 < colorMap.length ? colorMap[ci + 2] : 0.5;
24216
+ }
24217
+ geo.setAttribute('color', new three.BufferAttribute(colors, 3));
24218
+ geo.computeVertexNormals();
24219
+ pos.needsUpdate = true;
24220
+ const wireGeometry = new three.WireframeGeometry(geo);
24221
+ return { geometry: geo, wireGeometry };
24222
+ }
24223
+ /**
24224
+ * Build the axis labels (category along the front edge, series along the right
24225
+ * edge, plus a single "Value" Y-axis label) as world-anchored {@link
24226
+ * SurfaceLabel}s. Category/series labels are thinned to at most `maxCat`/`maxSer`
24227
+ * entries to avoid clutter, mirroring the prior drei `Html` layout.
24228
+ */
24229
+ function buildSurfaceLabels(cols, rows, categoryLabels, seriesNames, maxCat = 8, maxSer = 6) {
24230
+ const { gridWidth, gridDepth } = computeGridExtent(cols, rows);
24231
+ const labels = [];
24232
+ const catStep = Math.max(1, Math.ceil(categoryLabels.length / maxCat));
24233
+ for (let i = 0; i < categoryLabels.length; i += catStep) {
24234
+ const x = -gridWidth / 2 + (i / Math.max(cols - 1, 1)) * gridWidth;
24235
+ labels.push({
24236
+ key: `cat-${i}`,
24237
+ text: categoryLabels[i],
24238
+ anchor: [x, -0.15, gridDepth / 2 + 0.25],
24239
+ axis: 'category',
24240
+ });
24241
+ }
24242
+ const serStep = Math.max(1, Math.ceil(seriesNames.length / maxSer));
24243
+ for (let i = 0; i < seriesNames.length; i += serStep) {
24244
+ const z = -gridDepth / 2 + (i / Math.max(rows - 1, 1)) * gridDepth;
24245
+ labels.push({
24246
+ key: `ser-${i}`,
24247
+ text: seriesNames[i],
24248
+ anchor: [gridWidth / 2 + 0.3, -0.15, z],
24249
+ axis: 'series',
24250
+ });
24251
+ }
24252
+ labels.push({
24253
+ key: 'value-axis',
24254
+ text: 'Value',
24255
+ anchor: [-gridWidth / 2 - 0.35, 0.75, -gridDepth / 2],
24256
+ axis: 'value',
24257
+ });
24258
+ return labels;
24259
+ }
24260
+ /** Isometric-like camera placement that frames the whole grid. */
24261
+ function computeCameraPlacement(cols, rows) {
24262
+ const { gridWidth, gridDepth } = computeGridExtent(cols, rows);
24263
+ const maxExtent = Math.max(gridWidth, gridDepth, MAX_HEIGHT);
24264
+ const dist = maxExtent * 1.8;
24265
+ return {
24266
+ position: [dist * 0.8, dist * 0.7, dist * 0.8],
24267
+ target: [0, 0.3, 0],
24268
+ };
24269
+ }
24270
+
24271
+ /**
24272
+ * Vanilla three.js 3D surface-chart scene controller (framework-agnostic).
24273
+ *
24274
+ * Mounts an interactive surface chart into a caller-provided container element:
24275
+ * dynamically imports `three` plus its `OrbitControls` addon, builds a colour-
24276
+ * displaced surface mesh (with optional wireframe), grid floor, lights, and an
24277
+ * isometric camera, renders with a RAF loop, and overlays axis labels as DOM
24278
+ * nodes that are re-projected to screen each frame. Exposes `dispose()` for
24279
+ * deterministic teardown of GPU resources, listeners, and overlay nodes.
24280
+ *
24281
+ * `three` is an OPTIONAL peer dependency: every import is dynamic and guarded.
24282
+ * When `three` (or its OrbitControls addon) is missing, {@link mountSurfaceChart3D}
24283
+ * resolves to a sentinel handle ({@link SURFACE_THREE_UNAVAILABLE}) so the caller
24284
+ * can fall back to the 2D renderer. No framework imports - React, Vue, and
24285
+ * Angular bindings can all mount it.
24286
+ */
24287
+ /** No-op sentinel returned when `three` or its OrbitControls addon is missing. */
24288
+ const SURFACE_THREE_UNAVAILABLE = {
24289
+ ok: false,
24290
+ resize: () => { },
24291
+ dispose: () => { },
24292
+ };
24293
+ const FOV = 45;
24294
+ /** Dynamically load `three`; returns `null` when the package is not installed. */
24295
+ async function loadThree() {
24296
+ try {
24297
+ return (await import('three'));
24298
+ }
24299
+ catch {
24300
+ return null;
24301
+ }
24302
+ }
24303
+ /** Dynamically load the OrbitControls addon; returns `null` when unavailable. */
24304
+ async function loadOrbitControlsCtor() {
24305
+ try {
24306
+ const mod = await import('three/examples/jsm/controls/OrbitControls.js');
24307
+ return mod.OrbitControls;
24308
+ }
24309
+ catch {
24310
+ return null;
24311
+ }
24312
+ }
24313
+ /** Create the DOM overlay nodes for the axis labels, returned with the layer. */
24314
+ function createLabelOverlay(doc, labels) {
24315
+ const layer = doc.createElement('div');
24316
+ Object.assign(layer.style, {
24317
+ position: 'absolute',
24318
+ inset: '0',
24319
+ pointerEvents: 'none',
24320
+ overflow: 'hidden',
24321
+ });
24322
+ const nodes = labels.map((label) => {
24323
+ const node = doc.createElement('div');
24324
+ node.textContent = label.text;
24325
+ const color = label.axis === 'value' ? '#999' : '#666';
24326
+ Object.assign(node.style, {
24327
+ position: 'absolute',
24328
+ fontSize: '9px',
24329
+ color,
24330
+ whiteSpace: 'nowrap',
24331
+ userSelect: 'none',
24332
+ transform: 'translate(-50%, -50%)',
24333
+ willChange: 'left, top',
24334
+ });
24335
+ if (label.axis === 'value') {
24336
+ node.style.writingMode = 'vertical-rl';
24337
+ }
24338
+ layer.appendChild(node);
24339
+ return node;
24340
+ });
24341
+ return { layer, nodes };
24342
+ }
24343
+ /**
24344
+ * Mount an interactive 3D surface chart into `container` and start rendering.
24345
+ *
24346
+ * Resolves to {@link SURFACE_THREE_UNAVAILABLE} when `three` or its OrbitControls
24347
+ * addon cannot be loaded, so the caller can fall back to a 2D surface renderer.
24348
+ */
24349
+ async function mountSurfaceChart3D(container, options) {
24350
+ const three = await loadThree();
24351
+ const OrbitCtrlCtor = three ? await loadOrbitControlsCtor() : null;
24352
+ if (!three || !OrbitCtrlCtor) {
24353
+ return SURFACE_THREE_UNAVAILABLE;
24354
+ }
24355
+ const { cols, rows } = options;
24356
+ let width = Math.max(1, options.width);
24357
+ let height = Math.max(1, options.height);
24358
+ const renderer = new three.WebGLRenderer({ antialias: true, alpha: true });
24359
+ renderer.setPixelRatio(Math.min(typeof window === 'undefined' ? 1 : window.devicePixelRatio || 1, options.maxPixelRatio ?? 2));
24360
+ renderer.setSize(width, height, false);
24361
+ const canvas = renderer.domElement;
24362
+ canvas.style.width = `${width}px`;
24363
+ canvas.style.height = `${height}px`;
24364
+ canvas.style.display = 'block';
24365
+ canvas.style.willChange = 'transform';
24366
+ container.appendChild(canvas);
24367
+ const scene = new three.Scene();
24368
+ scene.add(new three.AmbientLight(0xffffff, 0.6));
24369
+ const key = new three.DirectionalLight(0xffffff, 0.8);
24370
+ key.position.set(5, 8, 5);
24371
+ scene.add(key);
24372
+ const fill = new three.DirectionalLight(0xffffff, 0.3);
24373
+ fill.position.set(-3, 4, -2);
24374
+ scene.add(fill);
24375
+ const camera = new three.PerspectiveCamera(FOV, width / height, 0.1, 1000);
24376
+ const placement = computeCameraPlacement(cols, rows);
24377
+ camera.position.set(...placement.position);
24378
+ const target = new three.Vector3(...placement.target);
24379
+ camera.lookAt(target);
24380
+ // Grid floor under the surface.
24381
+ const { gridWidth, gridDepth } = computeGridExtent(cols, rows);
24382
+ const floorSize = Math.max(gridWidth, gridDepth) * 1.2;
24383
+ const gridFloor = new three.GridHelper(floorSize, Math.max(cols, rows), 0xcccccc, 0xe8e8e8);
24384
+ gridFloor.position.y = -0.02;
24385
+ scene.add(gridFloor);
24386
+ // Surface mesh + optional wireframe.
24387
+ const { geometry, wireGeometry } = buildSurfaceGeometry(three, cols, rows, options.heightMap, options.colorMap);
24388
+ const surfaceMaterial = new three.MeshPhongMaterial({
24389
+ vertexColors: true,
24390
+ side: three.DoubleSide,
24391
+ shininess: 30,
24392
+ transparent: true,
24393
+ opacity: 0.92,
24394
+ });
24395
+ const surfaceMesh = new three.Mesh(geometry, surfaceMaterial);
24396
+ scene.add(surfaceMesh);
24397
+ let wireMaterial = null;
24398
+ if (options.wireframe) {
24399
+ wireMaterial = new three.LineBasicMaterial({
24400
+ color: 0x333333,
24401
+ transparent: true,
24402
+ opacity: 0.25,
24403
+ });
24404
+ scene.add(new three.LineSegments(wireGeometry, wireMaterial));
24405
+ }
24406
+ const controls = new OrbitCtrlCtor(camera, canvas);
24407
+ controls.enablePan = true;
24408
+ controls.enableZoom = true;
24409
+ controls.enableRotate = true;
24410
+ controls.minDistance = 1;
24411
+ controls.maxDistance = 20;
24412
+ controls.maxPolarAngle = Math.PI / 2 + 0.3;
24413
+ controls.target.copy(target);
24414
+ controls.update();
24415
+ // Axis-label DOM overlay, re-projected to screen each frame.
24416
+ const doc = container.ownerDocument ?? document;
24417
+ const labels = buildSurfaceLabels(cols, rows, options.categoryLabels, options.seriesNames);
24418
+ const { layer, nodes } = createLabelOverlay(doc, labels);
24419
+ container.appendChild(layer);
24420
+ const anchors = labels.map((l) => new three.Vector3(...l.anchor));
24421
+ const projected = new three.Vector3();
24422
+ const updateLabels = () => {
24423
+ for (let i = 0; i < nodes.length; i++) {
24424
+ projected.copy(anchors[i]).project(camera);
24425
+ const node = nodes[i];
24426
+ // Hide labels behind the camera.
24427
+ if (projected.z > 1) {
24428
+ node.style.display = 'none';
24429
+ continue;
24430
+ }
24431
+ node.style.display = '';
24432
+ node.style.left = `${((projected.x + 1) / 2) * width}px`;
24433
+ node.style.top = `${((-projected.y + 1) / 2) * height}px`;
24434
+ }
24435
+ };
24436
+ let frame = 0;
24437
+ let disposed = false;
24438
+ const renderLoop = () => {
24439
+ if (disposed) {
24440
+ return;
24441
+ }
24442
+ frame = requestAnimationFrame(renderLoop);
24443
+ controls.update();
24444
+ renderer.render(scene, camera);
24445
+ updateLabels();
24446
+ };
24447
+ frame = requestAnimationFrame(renderLoop);
24448
+ return {
24449
+ ok: true,
24450
+ resize(w, h) {
24451
+ width = Math.max(1, w);
24452
+ height = Math.max(1, h);
24453
+ camera.aspect = width / height;
24454
+ camera.updateProjectionMatrix();
24455
+ renderer.setSize(width, height, false);
24456
+ canvas.style.width = `${width}px`;
24457
+ canvas.style.height = `${height}px`;
24458
+ },
24459
+ dispose() {
24460
+ if (disposed) {
24461
+ return;
24462
+ }
24463
+ disposed = true;
24464
+ cancelAnimationFrame(frame);
24465
+ controls.dispose();
24466
+ geometry.dispose();
24467
+ wireGeometry.dispose();
24468
+ surfaceMaterial.dispose();
24469
+ wireMaterial?.dispose();
24470
+ gridFloor.dispose();
24471
+ scene.clear();
24472
+ renderer.dispose();
24473
+ canvas.remove();
24474
+ layer.remove();
24475
+ },
24476
+ };
24477
+ }
24478
+
23969
24479
  /**
23970
24480
  * smartart-drawing.ts: Drawing-shape view-model helpers for the SmartArt
23971
24481
  * renderer, shared across the React, Vue, and Angular bindings.
@@ -26142,6 +26652,298 @@ function issueTrackKey(issue, index) {
26142
26652
  return `${issue.slideIndex}-${issue.type}-${issue.elementId ?? 'slide'}-${index}`;
26143
26653
  }
26144
26654
 
26655
+ /**
26656
+ * Accessibility utilities for the PowerPoint viewer.
26657
+ *
26658
+ * Provides functions for computing reading order, generating ARIA labels,
26659
+ * determining ARIA roles, and detecting reduced-motion preferences.
26660
+ *
26661
+ * Pure (framework-agnostic): consumed by every binding's element renderer.
26662
+ *
26663
+ * @module render/accessibility
26664
+ */
26665
+ // ---------------------------------------------------------------------------
26666
+ // Reading order
26667
+ // ---------------------------------------------------------------------------
26668
+ /**
26669
+ * Computes a reading-order index for each element on a slide.
26670
+ *
26671
+ * Elements are sorted top-to-bottom first, then left-to-right for elements
26672
+ * at roughly the same vertical position (within a tolerance band).
26673
+ * Returns a `Map<elementId, tabIndex>` with 1-based indices.
26674
+ *
26675
+ * @param elements - The flat list of elements on the current slide.
26676
+ * @param tolerancePx - Vertical tolerance for grouping elements into the
26677
+ * same "row" (default 20px).
26678
+ * @returns Map from element ID to 1-based reading order index.
26679
+ */
26680
+ function computeReadingOrder(elements, tolerancePx = 20) {
26681
+ if (elements.length === 0) {
26682
+ return new Map();
26683
+ }
26684
+ const sorted = [...elements]
26685
+ .filter((el) => !el.hidden)
26686
+ .sort((a, b) => {
26687
+ const dy = a.y - b.y;
26688
+ if (Math.abs(dy) > tolerancePx) {
26689
+ return dy;
26690
+ }
26691
+ return a.x - b.x;
26692
+ });
26693
+ const result = new Map();
26694
+ sorted.forEach((el, idx) => {
26695
+ result.set(el.id, idx + 1);
26696
+ });
26697
+ return result;
26698
+ }
26699
+ // ---------------------------------------------------------------------------
26700
+ // ARIA roles
26701
+ // ---------------------------------------------------------------------------
26702
+ /**
26703
+ * Maps a PptxElement to its appropriate ARIA role.
26704
+ *
26705
+ * - image / picture => "img"
26706
+ * - table => "table"
26707
+ * - group => "group"
26708
+ * - chart => "img" (complex visualisation treated as image)
26709
+ * - text / shape with text => "text" is not valid; use undefined and rely on
26710
+ * aria-label alone. However, shapes without text act as decorative images.
26711
+ * - connector => "img"
26712
+ * - media => "application"
26713
+ *
26714
+ * @param element - The element to determine a role for.
26715
+ * @returns The ARIA role string, or undefined when none is needed.
26716
+ */
26717
+ function getAriaRole(element) {
26718
+ switch (element.type) {
26719
+ case 'image':
26720
+ case 'picture':
26721
+ return 'img';
26722
+ case 'table':
26723
+ return 'table';
26724
+ case 'group':
26725
+ return 'group';
26726
+ case 'chart':
26727
+ return 'img';
26728
+ case 'smartArt':
26729
+ return 'img';
26730
+ case 'connector':
26731
+ return 'img';
26732
+ case 'media':
26733
+ return 'application';
26734
+ case 'ink':
26735
+ return 'img';
26736
+ case 'model3d':
26737
+ return 'img';
26738
+ case 'text':
26739
+ return undefined;
26740
+ case 'shape': {
26741
+ // Shapes with text act as text containers; shapes without text are decorative
26742
+ if (hasTextProperties(element) && element.text) {
26743
+ return undefined;
26744
+ }
26745
+ return 'img';
26746
+ }
26747
+ default:
26748
+ return undefined;
26749
+ }
26750
+ }
26751
+ // ---------------------------------------------------------------------------
26752
+ // ARIA labels
26753
+ // ---------------------------------------------------------------------------
26754
+ /**
26755
+ * Generates a human-readable ARIA label for an element.
26756
+ *
26757
+ * Priority:
26758
+ * 1. Image altText (for image/picture elements)
26759
+ * 2. Text content (for text-bearing elements)
26760
+ * 3. Chart title (from chartData)
26761
+ * 4. Element type fallback label
26762
+ *
26763
+ * @param element - The element to generate a label for.
26764
+ * @returns A descriptive string for the `aria-label` attribute.
26765
+ */
26766
+ function getAriaLabel(element) {
26767
+ // Image alt text
26768
+ if ((element.type === 'image' || element.type === 'picture') &&
26769
+ 'altText' in element &&
26770
+ typeof element.altText === 'string' &&
26771
+ element.altText.trim()) {
26772
+ return element.altText.trim();
26773
+ }
26774
+ // Text content
26775
+ if (hasTextProperties(element) && element.text) {
26776
+ const text = element.text.trim();
26777
+ if (text) {
26778
+ // Truncate long text for the label
26779
+ return text.length > 120 ? `${text.slice(0, 117)}...` : text;
26780
+ }
26781
+ }
26782
+ // Chart title
26783
+ if (element.type === 'chart' && 'chartData' in element && element.chartData) {
26784
+ const cd = element.chartData;
26785
+ if (cd.title) {
26786
+ return `Chart: ${cd.title}`;
26787
+ }
26788
+ return 'Chart';
26789
+ }
26790
+ // SmartArt
26791
+ if (element.type === 'smartArt') {
26792
+ return 'SmartArt diagram';
26793
+ }
26794
+ // Table
26795
+ if (element.type === 'table') {
26796
+ return 'Table';
26797
+ }
26798
+ // Media
26799
+ if (element.type === 'media') {
26800
+ return 'Media element';
26801
+ }
26802
+ // Group
26803
+ if (element.type === 'group') {
26804
+ return 'Group of elements';
26805
+ }
26806
+ // Connector
26807
+ if (element.type === 'connector') {
26808
+ return 'Connector line';
26809
+ }
26810
+ // Ink
26811
+ if (element.type === 'ink') {
26812
+ return 'Ink drawing';
26813
+ }
26814
+ // 3D Model
26815
+ if (element.type === 'model3d') {
26816
+ return '3D Model';
26817
+ }
26818
+ // Shape fallback
26819
+ if (element.type === 'shape') {
26820
+ if ('shapeType' in element && element.shapeType) {
26821
+ return `Shape: ${element.shapeType}`;
26822
+ }
26823
+ return 'Shape';
26824
+ }
26825
+ // Generic fallback
26826
+ return getTypeFallbackLabel(element.type);
26827
+ }
26828
+ /**
26829
+ * Returns a user-friendly fallback label based on element type.
26830
+ */
26831
+ function getTypeFallbackLabel(type) {
26832
+ switch (type) {
26833
+ case 'text':
26834
+ return 'Text box';
26835
+ case 'shape':
26836
+ return 'Shape';
26837
+ case 'image':
26838
+ case 'picture':
26839
+ return 'Image';
26840
+ case 'table':
26841
+ return 'Table';
26842
+ case 'chart':
26843
+ return 'Chart';
26844
+ case 'connector':
26845
+ return 'Connector';
26846
+ case 'group':
26847
+ return 'Group';
26848
+ case 'smartArt':
26849
+ return 'SmartArt';
26850
+ case 'media':
26851
+ return 'Media';
26852
+ case 'ink':
26853
+ return 'Drawing';
26854
+ case 'ole':
26855
+ return 'Embedded object';
26856
+ case 'contentPart':
26857
+ return 'Content part';
26858
+ case 'model3d':
26859
+ return '3D Model';
26860
+ default:
26861
+ return 'Element';
26862
+ }
26863
+ }
26864
+ // ---------------------------------------------------------------------------
26865
+ // ARIA role descriptions
26866
+ // ---------------------------------------------------------------------------
26867
+ /**
26868
+ * Returns an `aria-roledescription` string for an element, providing
26869
+ * assistive-technology users with a more specific description of the
26870
+ * element's purpose than the generic ARIA role alone.
26871
+ *
26872
+ * @param element - The element to describe.
26873
+ * @returns A human-readable role description, or undefined when none is needed.
26874
+ */
26875
+ function getAriaRoleDescription(element) {
26876
+ switch (element.type) {
26877
+ case 'shape': {
26878
+ const shapeType = 'shapeType' in element && typeof element.shapeType === 'string'
26879
+ ? element.shapeType
26880
+ : undefined;
26881
+ if (shapeType) {
26882
+ return `shape: ${shapeType}`;
26883
+ }
26884
+ return 'shape';
26885
+ }
26886
+ case 'chart':
26887
+ return 'chart';
26888
+ case 'connector':
26889
+ return 'connector line';
26890
+ case 'smartArt':
26891
+ return 'diagram';
26892
+ case 'image':
26893
+ case 'picture':
26894
+ return 'image';
26895
+ case 'table':
26896
+ return 'data table';
26897
+ case 'group':
26898
+ return 'grouped elements';
26899
+ case 'media':
26900
+ return 'media player';
26901
+ case 'ink':
26902
+ return 'ink drawing';
26903
+ case 'model3d':
26904
+ return '3D model';
26905
+ default:
26906
+ return undefined;
26907
+ }
26908
+ }
26909
+ // ---------------------------------------------------------------------------
26910
+ // Reduced motion detection
26911
+ // ---------------------------------------------------------------------------
26912
+ /**
26913
+ * Queries the `prefers-reduced-motion: reduce` media query.
26914
+ *
26915
+ * @returns `true` if the user prefers reduced motion, `false` otherwise.
26916
+ */
26917
+ function prefersReducedMotion() {
26918
+ if (typeof window === 'undefined') {
26919
+ return false;
26920
+ }
26921
+ return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
26922
+ }
26923
+ /**
26924
+ * Returns a CSS-property object (framework-agnostic; spreadable into any
26925
+ * binding's style prop) that disables or tones
26926
+ * down animations and transitions when the user's OS-level
26927
+ * `prefers-reduced-motion: reduce` setting is active.
26928
+ *
26929
+ * When reduced motion is **not** preferred the function returns an empty
26930
+ * object so it can always be spread into a style prop safely.
26931
+ *
26932
+ * @returns CSS properties that suppress animations when appropriate.
26933
+ */
26934
+ function getReducedMotionStyles() {
26935
+ if (!prefersReducedMotion()) {
26936
+ return {};
26937
+ }
26938
+ return {
26939
+ animationDuration: '0.001ms',
26940
+ animationIterationCount: 1,
26941
+ transitionDuration: '0.001ms',
26942
+ animationDelay: '0ms',
26943
+ transitionDelay: '0ms',
26944
+ };
26945
+ }
26946
+
26145
26947
  /**
26146
26948
  * Convert an array of points into an SVG path `d` attribute string.
26147
26949
  * - 0 points -> `''`
@@ -29380,6 +30182,229 @@ function canUseClipboard(nav) {
29380
30182
  typeof nav.clipboard.writeText === 'function');
29381
30183
  }
29382
30184
 
30185
+ /**
30186
+ * is-mobile.ts: Injectable signal-based mobile-detection service plus a
30187
+ * pure `computeIsMobile` helper for unit-testing.
30188
+ *
30189
+ * Ported from: packages/react/src/viewer/hooks/useIsMobile.ts
30190
+ *
30191
+ * The service tracks two independent media conditions:
30192
+ * - `(pointer: coarse)`: primary pointer is a touch screen / stylus
30193
+ * - viewport width below {@link MOBILE_BREAKPOINT}
30194
+ *
30195
+ * Both conditions are evaluated once at construction time and then kept live
30196
+ * via `MediaQueryList.addEventListener`. The service is SSR-safe: when
30197
+ * `matchMedia` is not available (server / test environment without a DOM) all
30198
+ * signals default to `false` and no listeners are registered.
30199
+ */
30200
+ // ---------------------------------------------------------------------------
30201
+ // Constants
30202
+ // ---------------------------------------------------------------------------
30203
+ /** Viewport width (px) below which the UI switches to mobile layout. */
30204
+ const MOBILE_BREAKPOINT = 768;
30205
+ /** Tablet breakpoint: below this width (but >= MOBILE) is tablet. */
30206
+ const TABLET_BREAKPOINT = 1024;
30207
+ /**
30208
+ * Max viewport height (px) at which a *touch* device is treated as mobile
30209
+ * regardless of width. Catches landscape phones (e.g. 915×412), which are wide
30210
+ * enough to fall in the "tablet" width band but far too short for the desktop
30211
+ * ribbon + side panels, so they need the mobile chrome. Tablets in landscape are
30212
+ * taller (~760px+) so they stay on the desktop layout. Mirrors React's
30213
+ * `MOBILE_LANDSCAPE_MAX_HEIGHT` in useIsMobile.ts.
30214
+ */
30215
+ const MOBILE_LANDSCAPE_MAX_HEIGHT = 500;
30216
+ // ---------------------------------------------------------------------------
30217
+ // Pure helpers (no Angular deps, safe in vitest without a DOM)
30218
+ // ---------------------------------------------------------------------------
30219
+ /**
30220
+ * Decide whether the current environment should use the mobile layout:
30221
+ * - a narrow viewport (`width < MOBILE_BREAKPOINT`), OR
30222
+ * - a short *touch* viewport below the tablet width: a landscape phone, which
30223
+ * is wide enough to look like a tablet but far too short for the desktop
30224
+ * ribbon + side panels.
30225
+ *
30226
+ * This mirrors React's `isMobileViewport(width, height, isTouch)` so the three
30227
+ * frameworks switch chrome at the same breakpoints (and the shared mobile e2e
30228
+ * specs pass identically). A tall touch tablet (e.g. 820×1180) is NOT mobile.
30229
+ *
30230
+ * @pure: no side effects, fully testable without a DOM.
30231
+ */
30232
+ function computeIsMobile(width, height, isTouch) {
30233
+ if (width < MOBILE_BREAKPOINT) {
30234
+ return true;
30235
+ }
30236
+ return isTouch && height > 0 && height < MOBILE_LANDSCAPE_MAX_HEIGHT && width < TABLET_BREAKPOINT;
30237
+ }
30238
+ /**
30239
+ * Decide whether the current environment is "tablet" (desktop chrome, but in
30240
+ * the 768–1023px width band). A short landscape-phone touch viewport is mobile,
30241
+ * not tablet (handled by {@link computeIsMobile}).
30242
+ *
30243
+ * @pure
30244
+ */
30245
+ function computeIsTablet(width, height, isTouch) {
30246
+ if (computeIsMobile(width, height, isTouch)) {
30247
+ return false;
30248
+ }
30249
+ return width >= MOBILE_BREAKPOINT && width < TABLET_BREAKPOINT;
30250
+ }
30251
+ // ---------------------------------------------------------------------------
30252
+ // IsMobileService
30253
+ // ---------------------------------------------------------------------------
30254
+ /**
30255
+ * `IsMobileService`: provides reactive signals for the current viewport /
30256
+ * pointer kind so components can switch between mobile and desktop chrome
30257
+ * without subscribing to resize events themselves.
30258
+ *
30259
+ * Inject at the component level (or provide at root); the service cleans up
30260
+ * its `MediaQueryList` listeners automatically via `DestroyRef`.
30261
+ *
30262
+ * ```ts
30263
+ * providers: [IsMobileService]
30264
+ * // or globally:
30265
+ * // provideIsMobile() (see factory below)
30266
+ * ```
30267
+ *
30268
+ * ```ts
30269
+ * protected readonly mobile = inject(IsMobileService);
30270
+ * // in template: @if (mobile.isMobile()) { … }
30271
+ * ```
30272
+ */
30273
+ class IsMobileService {
30274
+ /** True when the primary pointer is coarse (touch / stylus). */
30275
+ isCoarsePointer = signal(false, /* @ts-ignore */
30276
+ ...(ngDevMode ? [{ debugName: "isCoarsePointer" }] : /* istanbul ignore next */ []));
30277
+ /** True when the viewport width is below {@link MOBILE_BREAKPOINT}. */
30278
+ isNarrowViewport = signal(false, /* @ts-ignore */
30279
+ ...(ngDevMode ? [{ debugName: "isNarrowViewport" }] : /* istanbul ignore next */ []));
30280
+ /**
30281
+ * True when either the pointer is coarse OR the viewport is narrow.
30282
+ * Use this as the single gate for showing mobile chrome.
30283
+ */
30284
+ isMobile = signal(false, /* @ts-ignore */
30285
+ ...(ngDevMode ? [{ debugName: "isMobile" }] : /* istanbul ignore next */ []));
30286
+ /** True when the viewport is in the tablet range (desktop pointer only). */
30287
+ isTablet = signal(false, /* @ts-ignore */
30288
+ ...(ngDevMode ? [{ debugName: "isTablet" }] : /* istanbul ignore next */ []));
30289
+ /**
30290
+ * CSS pixels the on-screen keyboard currently covers at the bottom of the
30291
+ * layout viewport (0 when no keyboard is open). The orchestrator lifts the
30292
+ * fixed mobile bottom bar by this amount so it stays above the keyboard.
30293
+ */
30294
+ keyboardInset = signal(0, /* @ts-ignore */
30295
+ ...(ngDevMode ? [{ debugName: "keyboardInset" }] : /* istanbul ignore next */ []));
30296
+ /** True while {@link keyboardInset} is large enough to count as "open". */
30297
+ isKeyboardOpen = signal(false, /* @ts-ignore */
30298
+ ...(ngDevMode ? [{ debugName: "isKeyboardOpen" }] : /* istanbul ignore next */ []));
30299
+ constructor() {
30300
+ const destroyRef = inject(DestroyRef);
30301
+ // Guard: matchMedia / window may not be available in SSR / test envs.
30302
+ if (typeof matchMedia !== 'function' || typeof window === 'undefined') {
30303
+ return;
30304
+ }
30305
+ this._wireKeyboardInset(destroyRef);
30306
+ // ── Coarse-pointer media query (drives the touch flag) ───────────────────
30307
+ const coarseMql = matchMedia('(pointer: coarse)');
30308
+ this.isCoarsePointer.set(coarseMql.matches);
30309
+ const onCoarseChange = (evt) => {
30310
+ this.isCoarsePointer.set(evt.matches);
30311
+ this._recompute();
30312
+ };
30313
+ coarseMql.addEventListener('change', onCoarseChange);
30314
+ // ── Viewport size tracking ───────────────────────────────────────────────
30315
+ // Width-only media queries cannot express the "short landscape phone"
30316
+ // rule (which depends on height + touch), so track the live viewport size
30317
+ // on resize and recompute the derived flags from width/height/touch.
30318
+ const onResize = () => this._recompute();
30319
+ window.addEventListener('resize', onResize);
30320
+ if (typeof screen !== 'undefined' && screen.orientation) {
30321
+ screen.orientation.addEventListener('change', onResize);
30322
+ }
30323
+ this._recompute();
30324
+ // ── Cleanup on destroy ───────────────────────────────────────────────────
30325
+ destroyRef.onDestroy(() => {
30326
+ coarseMql.removeEventListener('change', onCoarseChange);
30327
+ window.removeEventListener('resize', onResize);
30328
+ if (typeof screen !== 'undefined' && screen.orientation) {
30329
+ screen.orientation.removeEventListener('change', onResize);
30330
+ }
30331
+ });
30332
+ }
30333
+ /** Recompute all derived flags from the live viewport size + pointer kind. */
30334
+ _recompute() {
30335
+ const width = window.innerWidth;
30336
+ const height = window.innerHeight;
30337
+ const touch = this.isCoarsePointer();
30338
+ this.isNarrowViewport.set(width < MOBILE_BREAKPOINT);
30339
+ this.isMobile.set(computeIsMobile(width, height, touch));
30340
+ this.isTablet.set(computeIsTablet(width, height, touch));
30341
+ }
30342
+ /**
30343
+ * Track the on-screen-keyboard inset via the `VisualViewport` API and keep the
30344
+ * focused editable visible: when the keyboard shrinks the visual viewport,
30345
+ * update {@link keyboardInset} / {@link isKeyboardOpen} and scroll the active
30346
+ * input / textarea / contenteditable into the area above the keyboard. No-op
30347
+ * when `visualViewport` is unavailable (desktop / SSR), so desktop is unchanged.
30348
+ */
30349
+ _wireKeyboardInset(destroyRef) {
30350
+ const vv = window.visualViewport;
30351
+ if (!vv) {
30352
+ return;
30353
+ }
30354
+ const update = () => {
30355
+ const metrics = readViewportMetrics(window);
30356
+ const inset = metrics ? computeKeyboardInset(metrics) : 0;
30357
+ this.keyboardInset.set(inset);
30358
+ this.isKeyboardOpen.set(isKeyboardOpen(inset));
30359
+ if (inset > 0) {
30360
+ window.requestAnimationFrame(() => this._scrollFocusedIntoView(inset));
30361
+ }
30362
+ };
30363
+ const onFocusIn = () => {
30364
+ window.requestAnimationFrame(() => {
30365
+ const metrics = readViewportMetrics(window);
30366
+ const inset = metrics ? computeKeyboardInset(metrics) : 0;
30367
+ if (inset > 0) {
30368
+ this._scrollFocusedIntoView(inset);
30369
+ }
30370
+ });
30371
+ };
30372
+ update();
30373
+ vv.addEventListener('resize', update);
30374
+ vv.addEventListener('scroll', update);
30375
+ document.addEventListener('focusin', onFocusIn);
30376
+ destroyRef.onDestroy(() => {
30377
+ vv.removeEventListener('resize', update);
30378
+ vv.removeEventListener('scroll', update);
30379
+ document.removeEventListener('focusin', onFocusIn);
30380
+ });
30381
+ }
30382
+ /** Scroll the focused editable into the area above the keyboard, if needed. */
30383
+ _scrollFocusedIntoView(keyboardInset) {
30384
+ if (keyboardInset <= 0 || typeof document === 'undefined') {
30385
+ return;
30386
+ }
30387
+ const active = document.activeElement;
30388
+ if (!(active instanceof HTMLElement)) {
30389
+ return;
30390
+ }
30391
+ const tag = active.tagName;
30392
+ if (tag !== 'INPUT' && tag !== 'TEXTAREA' && !active.isContentEditable) {
30393
+ return;
30394
+ }
30395
+ const rect = active.getBoundingClientRect();
30396
+ const delta = computeScrollDelta({ top: rect.top, bottom: rect.bottom }, window.innerHeight, keyboardInset);
30397
+ if (delta !== 0) {
30398
+ window.scrollBy({ top: delta, behavior: 'smooth' });
30399
+ }
30400
+ }
30401
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: IsMobileService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
30402
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: IsMobileService });
30403
+ }
30404
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: IsMobileService, decorators: [{
30405
+ type: Injectable
30406
+ }], ctorParameters: () => [] });
30407
+
29383
30408
  /**
29384
30409
  * modal-dialog.component.ts: Reusable, accessible modal dialog shell.
29385
30410
  *
@@ -29406,6 +30431,16 @@ function canUseClipboard(nav) {
29406
30431
  * </pptx-modal-dialog>
29407
30432
  * ```
29408
30433
  */
30434
+ /**
30435
+ * Map the mobile flag to the modal-panel class list. Pure (no Angular / DOM) so
30436
+ * it can be unit-tested without TestBed: on mobile the panel gets the
30437
+ * `is-mobile` modifier that turns it into a bottom sheet (matching the CSS media
30438
+ * query, but driven by the {@link IsMobileService} 768px breakpoint so the three
30439
+ * frameworks switch chrome at the same width).
30440
+ */
30441
+ function modalPanelClass(isMobile) {
30442
+ return isMobile ? 'pptx-ng-modal-panel is-mobile' : 'pptx-ng-modal-panel';
30443
+ }
29409
30444
  class ModalDialogComponent {
29410
30445
  /** Whether the dialog is visible. */
29411
30446
  open = input(false, /* @ts-ignore */
@@ -29415,6 +30450,14 @@ class ModalDialogComponent {
29415
30450
  ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
29416
30451
  /** Fired on backdrop click, the `×` button, and `Escape`. */
29417
30452
  close = output();
30453
+ /** Reactive viewport / pointer flags (drives the bottom-sheet layout). */
30454
+ mobile = inject(IsMobileService);
30455
+ /**
30456
+ * Panel class list: gains the `is-mobile` bottom-sheet modifier under the
30457
+ * mobile breakpoint. See {@link modalPanelClass}.
30458
+ */
30459
+ panelClass = computed(() => modalPanelClass(this.mobile.isMobile()), /* @ts-ignore */
30460
+ ...(ngDevMode ? [{ debugName: "panelClass" }] : /* istanbul ignore next */ []));
29418
30461
  /** Close on `Escape`, regardless of where focus currently sits. */
29419
30462
  onDocumentKeydown(event) {
29420
30463
  if (!this.open()) {
@@ -29477,11 +30520,15 @@ class ModalDialogComponent {
29477
30520
  this.dragY.set(0);
29478
30521
  }
29479
30522
  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: `
30523
+ 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
30524
  @if (open()) {
29482
- <div class="pptx-ng-modal-backdrop" (click)="onBackdropClick()">
30525
+ <div
30526
+ class="pptx-ng-modal-backdrop"
30527
+ [class.is-mobile]="mobile.isMobile()"
30528
+ (click)="onBackdropClick()"
30529
+ >
29483
30530
  <div
29484
- class="pptx-ng-modal-panel"
30531
+ [class]="panelClass()"
29485
30532
  role="dialog"
29486
30533
  aria-modal="true"
29487
30534
  [attr.aria-label]="title() || null"
@@ -29521,15 +30568,19 @@ class ModalDialogComponent {
29521
30568
  </div>
29522
30569
  </div>
29523
30570
  }
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 });
30571
+ `, 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
30572
  }
29526
30573
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ModalDialogComponent, decorators: [{
29527
30574
  type: Component,
29528
- args: [{ selector: 'pptx-modal-dialog', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
30575
+ args: [{ selector: 'pptx-modal-dialog', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, providers: [IsMobileService], template: `
29529
30576
  @if (open()) {
29530
- <div class="pptx-ng-modal-backdrop" (click)="onBackdropClick()">
30577
+ <div
30578
+ class="pptx-ng-modal-backdrop"
30579
+ [class.is-mobile]="mobile.isMobile()"
30580
+ (click)="onBackdropClick()"
30581
+ >
29531
30582
  <div
29532
- class="pptx-ng-modal-panel"
30583
+ [class]="panelClass()"
29533
30584
  role="dialog"
29534
30585
  aria-modal="true"
29535
30586
  [attr.aria-label]="title() || null"
@@ -29569,7 +30620,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
29569
30620
  </div>
29570
30621
  </div>
29571
30622
  }
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"] }]
30623
+ `, 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
30624
  }], 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
30625
  type: HostListener,
29575
30626
  args: ['document:keydown', ['$event']]
@@ -40197,6 +41248,15 @@ class InspectorPanelComponent {
40197
41248
  slideIndex = input.required(/* @ts-ignore */
40198
41249
  ...(ngDevMode ? [{ debugName: "slideIndex" }] : /* istanbul ignore next */ []));
40199
41250
  editor = inject(EditorStateService);
41251
+ /** Reactive viewport / pointer flags (drives the bottom-sheet layout). */
41252
+ mobile = inject(IsMobileService);
41253
+ /**
41254
+ * Root class list: gains the `is-mobile` modifier under the mobile
41255
+ * breakpoint so the panel becomes a full-width, touch-sized bottom sheet.
41256
+ * See {@link inspectorRootClass}.
41257
+ */
41258
+ inspectorClass = computed(() => inspectorRootClass(this.mobile.isMobile()), /* @ts-ignore */
41259
+ ...(ngDevMode ? [{ debugName: "inspectorClass" }] : /* istanbul ignore next */ []));
40200
41260
  /** Alias so the template can call el() without conflicting with Angular internals. */
40201
41261
  el = computed(() => this.element(), /* @ts-ignore */
40202
41262
  ...(ngDevMode ? [{ debugName: "el" }] : /* istanbul ignore next */ []));
@@ -40372,7 +41432,7 @@ class InspectorPanelComponent {
40372
41432
  this.editor.updateElement(this.slideIndex(), cur.id, textStylePatch(cur, { underline: !this.currentUnderline() }));
40373
41433
  }
40374
41434
  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: `
41435
+ 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
41436
  <!--
40377
41437
  NOTE (mobile-safe inputs): every numeric / colour input is keyed on the
40378
41438
  selected element's id via @if blocks. Angular destroys and recreates the
@@ -40382,7 +41442,7 @@ class InspectorPanelComponent {
40382
41442
  mid-edit: the caret stays put and the on-screen keyboard does not dismiss.
40383
41443
  All commits happen on (change) (blur), reading event.target.value.
40384
41444
  -->
40385
- <aside class="pptx-ng-inspector" aria-label="Element properties">
41445
+ <aside [class]="inspectorClass()" aria-label="Element properties">
40386
41446
  <!-- ── Transform: Position & Size ─────────────────────────────────── -->
40387
41447
  <section class="pptx-ng-inspector__section">
40388
41448
  <h3 class="pptx-ng-inspector__heading">Transform</h3>
@@ -40665,7 +41725,7 @@ class InspectorPanelComponent {
40665
41725
  </div>
40666
41726
  </section>
40667
41727
  </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 });
41728
+ `, 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
41729
  }
40670
41730
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: InspectorPanelComponent, decorators: [{
40671
41731
  type: Component,
@@ -40677,7 +41737,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
40677
41737
  ChartDataEditorComponent,
40678
41738
  SmartArtPropertiesComponent,
40679
41739
  AnimationAuthorPanelComponent,
40680
- ], template: `
41740
+ ], providers: [IsMobileService], template: `
40681
41741
  <!--
40682
41742
  NOTE (mobile-safe inputs): every numeric / colour input is keyed on the
40683
41743
  selected element's id via @if blocks. Angular destroys and recreates the
@@ -40687,7 +41747,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
40687
41747
  mid-edit: the caret stays put and the on-screen keyboard does not dismiss.
40688
41748
  All commits happen on (change) (blur), reading event.target.value.
40689
41749
  -->
40690
- <aside class="pptx-ng-inspector" aria-label="Element properties">
41750
+ <aside [class]="inspectorClass()" aria-label="Element properties">
40691
41751
  <!-- ── Transform: Position & Size ─────────────────────────────────── -->
40692
41752
  <section class="pptx-ng-inspector__section">
40693
41753
  <h3 class="pptx-ng-inspector__heading">Transform</h3>
@@ -40970,8 +42030,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
40970
42030
  </div>
40971
42031
  </section>
40972
42032
  </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"] }]
42033
+ `, 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
42034
  }], propDecorators: { element: [{ type: i0.Input, args: [{ isSignal: true, alias: "element", required: true }] }], slideIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "slideIndex", required: true }] }] } });
42035
+ // ── Pure helpers (no Angular / DOM; unit-testable without TestBed) ────────────
42036
+ /**
42037
+ * Map the mobile flag to the inspector root class list. On mobile the panel
42038
+ * gains the `is-mobile` modifier that makes it a full-width, touch-sized
42039
+ * bottom-sheet body (the orchestrator host wraps it with the drawer chrome).
42040
+ */
42041
+ function inspectorRootClass(isMobile) {
42042
+ return isMobile ? 'pptx-ng-inspector is-mobile' : 'pptx-ng-inspector';
42043
+ }
40975
42044
  // ── Module-private helpers ───────────────────────────────────────────────────
40976
42045
  /** Extract a finite number from an input change event, or null if invalid. */
40977
42046
  function numberFromEvent(event) {
@@ -40992,229 +42061,6 @@ function stringFromEvent(event) {
40992
42061
  return val.length > 0 ? val : null;
40993
42062
  }
40994
42063
 
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
42064
  /**
41219
42065
  * `LoadContentService`: Angular port of the React `useLoadContent` hook and
41220
42066
  * the Vue `useLoadContent` composable.
@@ -53172,6 +54018,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
53172
54018
  * }
53173
54019
  * ```
53174
54020
  */
54021
+ /**
54022
+ * Map the mobile flag to the print-dialog panel class list. Pure (no Angular /
54023
+ * DOM) so it can be unit-tested without TestBed. On mobile the dialog gains the
54024
+ * `is-mobile` modifier that turns it into a full-width bottom sheet.
54025
+ */
54026
+ function printDialogClass(isMobile) {
54027
+ return isMobile ? 'pptx-ng-print-dialog is-mobile' : 'pptx-ng-print-dialog';
54028
+ }
53175
54029
  class PrintDialogComponent {
53176
54030
  // -------------------------------------------------------------------------
53177
54031
  // Inputs / outputs
@@ -53192,6 +54046,14 @@ class PrintDialogComponent {
53192
54046
  print = output();
53193
54047
  /** Emits when the dialog is dismissed (Cancel / Escape / backdrop). */
53194
54048
  cancel = output();
54049
+ /** Reactive viewport / pointer flags (drives the bottom-sheet layout). */
54050
+ mobile = inject(IsMobileService);
54051
+ /**
54052
+ * Dialog class list: gains the `is-mobile` bottom-sheet modifier under the
54053
+ * mobile breakpoint. See {@link printDialogClass}.
54054
+ */
54055
+ dialogClass = computed(() => printDialogClass(this.mobile.isMobile()), /* @ts-ignore */
54056
+ ...(ngDevMode ? [{ debugName: "dialogClass" }] : /* istanbul ignore next */ []));
53195
54057
  // -------------------------------------------------------------------------
53196
54058
  // State
53197
54059
  // -------------------------------------------------------------------------
@@ -53259,15 +54121,16 @@ class PrintDialogComponent {
53259
54121
  }
53260
54122
  }
53261
54123
  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: `
54124
+ 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
54125
  <div
53264
54126
  class="pptx-ng-print-dialog__backdrop"
54127
+ [class.is-mobile]="mobile.isMobile()"
53265
54128
  role="dialog"
53266
54129
  aria-modal="true"
53267
54130
  aria-label="Print"
53268
54131
  (click)="onBackdropClick($event)"
53269
54132
  >
53270
- <div class="pptx-ng-print-dialog">
54133
+ <div [class]="dialogClass()">
53271
54134
  <!-- Header -->
53272
54135
  <div class="pptx-ng-print-dialog__header">
53273
54136
  <h2 class="pptx-ng-print-dialog__title">Print</h2>
@@ -53313,19 +54176,20 @@ class PrintDialogComponent {
53313
54176
  </div>
53314
54177
  </div>
53315
54178
  </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 });
54179
+ `, 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
54180
  }
53318
54181
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: PrintDialogComponent, decorators: [{
53319
54182
  type: Component,
53320
- args: [{ selector: 'pptx-print-dialog', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [PrintSettingsPanelComponent], template: `
54183
+ args: [{ selector: 'pptx-print-dialog', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [PrintSettingsPanelComponent], providers: [IsMobileService], template: `
53321
54184
  <div
53322
54185
  class="pptx-ng-print-dialog__backdrop"
54186
+ [class.is-mobile]="mobile.isMobile()"
53323
54187
  role="dialog"
53324
54188
  aria-modal="true"
53325
54189
  aria-label="Print"
53326
54190
  (click)="onBackdropClick($event)"
53327
54191
  >
53328
- <div class="pptx-ng-print-dialog">
54192
+ <div [class]="dialogClass()">
53329
54193
  <!-- Header -->
53330
54194
  <div class="pptx-ng-print-dialog__header">
53331
54195
  <h2 class="pptx-ng-print-dialog__title">Print</h2>
@@ -53371,7 +54235,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
53371
54235
  </div>
53372
54236
  </div>
53373
54237
  </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"] }]
54238
+ `, 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
54239
  }], 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
54240
  type: HostListener,
53377
54241
  args: ['document:keydown', ['$event']]