pptx-react-viewer 1.0.11 → 1.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -4,7 +4,7 @@ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
4
  import * as ReactDOM from 'react-dom/client';
5
5
  import { clsx } from 'clsx';
6
6
  import { twMerge } from 'tailwind-merge';
7
- import { LuMessageSquare, LuEyeOff, LuSettings, LuX, LuShieldCheck, LuPlus, LuPanelLeftClose, LuStickyNote, LuMonitor, LuColumns2, LuPresentation, LuMinus, LuClock, LuDownload, LuTrash2, LuCheck, LuLock, LuEye, LuFileText, LuType, LuLoader, LuShieldAlert, LuSignature, LuInfo, LuTriangleAlert, LuPrinter, LuPenTool, LuWifi, LuWifiOff, LuUsers, LuCopy, LuMonitorOff, LuChevronLeft, LuChevronRight, LuPlay, LuPause, LuPanelLeft, LuUndo, LuRedo, LuSearch, LuShare2, LuPanelRight, LuFolderOpen, LuVideo, LuImage, LuClipboardPaste, LuScissors, LuPaintbrush, LuChevronDown, LuSquare, LuDatabase, LuLayers, LuAArrowUp, LuAArrowDown, LuRemoveFormatting, LuHighlighter, LuList, LuListOrdered, LuIndentDecrease, LuIndentIncrease, LuChevronUp, LuPalette, LuPencil, LuPaintBucket, LuSparkles, LuCast, LuCaptions, LuSpellCheck, LuGitCompare, LuPipette, LuCaseSensitive, LuReplace, LuTimer, LuMousePointer2, LuEraser, LuGripVertical, LuUpload, LuBold, LuItalic, LuUnderline, LuStrikethrough, LuLink, LuGrid2X2, LuCopyPlus, LuEllipsis, LuCircle, LuMoveRight, LuTriangle, LuDiamond, LuAlignLeft, LuAlignCenter, LuAlignRight, LuAlignJustify, LuSpline, LuSettings2, LuMove, LuRadio, LuArrowDown, LuArrowUp, LuArrowRight, LuArrowLeft, LuReply, LuRotateCw, LuBookmark } from 'react-icons/lu';
7
+ import { LuMessageSquare, LuEyeOff, LuSettings, LuX, LuCast, LuShieldCheck, LuPlus, LuPanelLeftClose, LuStickyNote, LuMonitor, LuColumns2, LuPresentation, LuMinus, LuClock, LuDownload, LuTrash2, LuCheck, LuLock, LuEye, LuFileText, LuType, LuLoader, LuShieldAlert, LuSignature, LuInfo, LuTriangleAlert, LuPrinter, LuPenTool, LuWifi, LuWifiOff, LuUsers, LuCopy, LuMonitorOff, LuChevronLeft, LuChevronRight, LuPlay, LuPause, LuPanelLeft, LuUndo, LuRedo, LuSearch, LuShare2, LuPanelRight, LuFolderOpen, LuVideo, LuImage, LuClipboardPaste, LuScissors, LuPaintbrush, LuChevronDown, LuSquare, LuDatabase, LuLayers, LuAArrowUp, LuAArrowDown, LuRemoveFormatting, LuHighlighter, LuList, LuListOrdered, LuIndentDecrease, LuIndentIncrease, LuChevronUp, LuPalette, LuPencil, LuPaintBucket, LuSparkles, LuCaptions, LuSpellCheck, LuGitCompare, LuPipette, LuCaseSensitive, LuReplace, LuTimer, LuMousePointer2, LuEraser, LuGripVertical, LuUpload, LuBold, LuItalic, LuUnderline, LuStrikethrough, LuLink, LuGrid2X2, LuCopyPlus, LuEllipsis, LuCircle, LuMoveRight, LuTriangle, LuDiamond, LuAlignLeft, LuAlignCenter, LuAlignRight, LuAlignJustify, LuSpline, LuSettings2, LuMove, LuRadio, LuArrowDown, LuArrowUp, LuArrowRight, LuArrowLeft, LuReply, LuRotateCw, LuBookmark } from 'react-icons/lu';
8
8
  import { hasShapeProperties, hasTextProperties, SWITCHABLE_LAYOUT_TYPES, isCalloutShape, getCalloutLeaderLineGeometry, buildCalloutLeaderLineSvgPath, getCalloutViewBoxBounds, isInkElement, getLinkedTextBoxSegments, isImageLikeElement, getSubstituteFontFamily, PptxHandler, EncryptedFileError, guidePxToEmu, guideEmuToPx, THEME_COLOR_SCHEME_KEYS, hslToRgb, PRESET_COLOR_MAP, elementActionToPptxAction, mergeShapes, SvgExporter, applyDrawingColorTransforms as applyDrawingColorTransforms$1, getPresetShapeClipPath, svgPathToPolygons, polygonsToSvgPath, EMU_PER_PX as EMU_PER_PX$1, chartDataChangeType, chartDataUpdatePoint, chartDataAddCategory, chartDataRemoveCategory, chartDataAddSeries, chartDataRemoveSeries, getOleObjectTypeLabel, pptxActionToElementAction, hasNonTrivialOverride, COLOR_MAP_ALIAS_KEYS, DEFAULT_COLOR_MAP, addSmartArtNodeAsChild, updateSmartArtNodeText, removeSmartArtNode, switchSmartArtLayout } from 'pptx-viewer-core';
9
9
  import { useTranslation } from 'react-i18next';
10
10
  import html2canvasPro from 'html2canvas-pro';
@@ -43302,7 +43302,7 @@ var require_use_sync_external_store_shim_development = __commonJS({
43302
43302
  return x2 === y && (0 !== x2 || 1 / x2 === 1 / y) || x2 !== x2 && y !== y;
43303
43303
  }
43304
43304
  function useSyncExternalStore$2(subscribe3, getSnapshot2) {
43305
- didWarnOld18Alpha || void 0 === React95.startTransition || (didWarnOld18Alpha = true, console.error(
43305
+ didWarnOld18Alpha || void 0 === React96.startTransition || (didWarnOld18Alpha = true, console.error(
43306
43306
  "You are using an outdated, pre-release alpha of React 18 that does not support useSyncExternalStore. The use-sync-external-store shim will not work correctly. Upgrade to a newer pre-release."
43307
43307
  ));
43308
43308
  var value = getSnapshot2();
@@ -43312,7 +43312,7 @@ var require_use_sync_external_store_shim_development = __commonJS({
43312
43312
  "The result of getSnapshot should be cached to avoid an infinite loop"
43313
43313
  ), didWarnUncachedGetSnapshot = true);
43314
43314
  }
43315
- cachedValue = useState84({
43315
+ cachedValue = useState85({
43316
43316
  inst: { value, getSnapshot: getSnapshot2 }
43317
43317
  });
43318
43318
  var inst = cachedValue[0].inst, forceUpdate = cachedValue[1];
@@ -43324,7 +43324,7 @@ var require_use_sync_external_store_shim_development = __commonJS({
43324
43324
  },
43325
43325
  [subscribe3, value, getSnapshot2]
43326
43326
  );
43327
- useEffect68(
43327
+ useEffect71(
43328
43328
  function() {
43329
43329
  checkIfSnapshotChanged(inst) && forceUpdate({ inst });
43330
43330
  return subscribe3(function() {
@@ -43350,8 +43350,8 @@ var require_use_sync_external_store_shim_development = __commonJS({
43350
43350
  return getSnapshot2();
43351
43351
  }
43352
43352
  "undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" === typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart && __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(Error());
43353
- var React95 = __require("react"), objectIs = "function" === typeof Object.is ? Object.is : is2, useState84 = React95.useState, useEffect68 = React95.useEffect, useLayoutEffect7 = React95.useLayoutEffect, useDebugValue = React95.useDebugValue, didWarnOld18Alpha = false, didWarnUncachedGetSnapshot = false, shim = "undefined" === typeof window || "undefined" === typeof window.document || "undefined" === typeof window.document.createElement ? useSyncExternalStore$1 : useSyncExternalStore$2;
43354
- exports$1.useSyncExternalStore = void 0 !== React95.useSyncExternalStore ? React95.useSyncExternalStore : shim;
43353
+ var React96 = __require("react"), objectIs = "function" === typeof Object.is ? Object.is : is2, useState85 = React96.useState, useEffect71 = React96.useEffect, useLayoutEffect7 = React96.useLayoutEffect, useDebugValue = React96.useDebugValue, didWarnOld18Alpha = false, didWarnUncachedGetSnapshot = false, shim = "undefined" === typeof window || "undefined" === typeof window.document || "undefined" === typeof window.document.createElement ? useSyncExternalStore$1 : useSyncExternalStore$2;
43354
+ exports$1.useSyncExternalStore = void 0 !== React96.useSyncExternalStore ? React96.useSyncExternalStore : shim;
43355
43355
  "undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" === typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop && __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(Error());
43356
43356
  })();
43357
43357
  }
@@ -43374,9 +43374,9 @@ var require_with_selector_development = __commonJS({
43374
43374
  return x2 === y && (0 !== x2 || 1 / x2 === 1 / y) || x2 !== x2 && y !== y;
43375
43375
  }
43376
43376
  "undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" === typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart && __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(Error());
43377
- var React95 = __require("react"), shim = require_shim(), objectIs = "function" === typeof Object.is ? Object.is : is2, useSyncExternalStore3 = shim.useSyncExternalStore, useRef69 = React95.useRef, useEffect68 = React95.useEffect, useMemo41 = React95.useMemo, useDebugValue = React95.useDebugValue;
43377
+ var React96 = __require("react"), shim = require_shim(), objectIs = "function" === typeof Object.is ? Object.is : is2, useSyncExternalStore3 = shim.useSyncExternalStore, useRef72 = React96.useRef, useEffect71 = React96.useEffect, useMemo41 = React96.useMemo, useDebugValue = React96.useDebugValue;
43378
43378
  exports$1.useSyncExternalStoreWithSelector = function(subscribe3, getSnapshot2, getServerSnapshot2, selector, isEqual) {
43379
- var instRef = useRef69(null);
43379
+ var instRef = useRef72(null);
43380
43380
  if (null === instRef.current) {
43381
43381
  var inst = { hasValue: false, value: null };
43382
43382
  instRef.current = inst;
@@ -43417,7 +43417,7 @@ var require_with_selector_development = __commonJS({
43417
43417
  [getSnapshot2, getServerSnapshot2, selector, isEqual]
43418
43418
  );
43419
43419
  var value = useSyncExternalStore3(subscribe3, instRef[0], instRef[1]);
43420
- useEffect68(
43420
+ useEffect71(
43421
43421
  function() {
43422
43422
  inst.hasValue = true;
43423
43423
  inst.value = value;
@@ -91102,6 +91102,542 @@ function useVirtualizedSlides({
91102
91102
  scrollToIndex
91103
91103
  };
91104
91104
  }
91105
+
91106
+ // src/viewer/hooks/collaboration/sanitize.ts
91107
+ var ROOM_ID_REGEX = /^[a-zA-Z0-9_-]{1,128}$/;
91108
+ function validateRoomId(roomId) {
91109
+ if (!ROOM_ID_REGEX.test(roomId)) {
91110
+ throw new Error(
91111
+ `Invalid collaboration room ID: "${roomId}". Must be 1-128 alphanumeric characters, hyphens, or underscores.`
91112
+ );
91113
+ }
91114
+ return roomId;
91115
+ }
91116
+ function sanitizeUserName(name) {
91117
+ if (typeof name !== "string") {
91118
+ return "Anonymous";
91119
+ }
91120
+ const stripped = name.replace(/<[^>]*>/g, "");
91121
+ const trimmed = stripped.trim().slice(0, 64);
91122
+ return trimmed || "Anonymous";
91123
+ }
91124
+ function clampCursorPosition(value, min2, max2) {
91125
+ if (typeof value !== "number" || !Number.isFinite(value)) {
91126
+ return 0;
91127
+ }
91128
+ const margin = 20;
91129
+ return Math.max(min2 - margin, Math.min(max2 + margin, value));
91130
+ }
91131
+ var HEX_COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
91132
+ function sanitizeColor(color, fallback = "#6366f1") {
91133
+ if (typeof color !== "string") {
91134
+ return fallback;
91135
+ }
91136
+ return HEX_COLOR_REGEX.test(color) ? color : fallback;
91137
+ }
91138
+ function sanitizeAvatarUrl(url) {
91139
+ if (typeof url !== "string") {
91140
+ return void 0;
91141
+ }
91142
+ try {
91143
+ const parsed = new URL(url);
91144
+ if (parsed.protocol === "https:" || parsed.protocol === "http:" || parsed.protocol === "data:") {
91145
+ return url;
91146
+ }
91147
+ } catch {
91148
+ }
91149
+ return void 0;
91150
+ }
91151
+ function sanitizeSlideIndex(value) {
91152
+ if (typeof value !== "number" || !Number.isFinite(value)) {
91153
+ return 0;
91154
+ }
91155
+ return Math.max(0, Math.floor(value));
91156
+ }
91157
+ function sanitizePresence(raw, canvasWidth, canvasHeight) {
91158
+ if (typeof raw.clientId !== "number") {
91159
+ return null;
91160
+ }
91161
+ return {
91162
+ clientId: raw.clientId,
91163
+ userName: sanitizeUserName(raw.userName),
91164
+ userAvatar: sanitizeAvatarUrl(raw.userAvatar),
91165
+ userColor: sanitizeColor(raw.userColor),
91166
+ activeSlideIndex: sanitizeSlideIndex(raw.activeSlideIndex),
91167
+ cursorX: clampCursorPosition(raw.cursorX, 0, canvasWidth),
91168
+ cursorY: clampCursorPosition(raw.cursorY, 0, canvasHeight),
91169
+ lastUpdated: typeof raw.lastUpdated === "string" ? raw.lastUpdated : (/* @__PURE__ */ new Date()).toISOString(),
91170
+ selectedElementId: typeof raw.selectedElementId === "string" ? raw.selectedElementId.slice(0, 128) : void 0,
91171
+ role: raw.role === "broadcaster" || raw.role === "viewer" || raw.role === "collaborator" ? raw.role : void 0
91172
+ };
91173
+ }
91174
+ var BROADCAST_THROTTLE_MS = 50;
91175
+ var STALE_PRESENCE_MS = 3e4;
91176
+ function usePresenceTracking({
91177
+ awareness,
91178
+ localClientId,
91179
+ userName,
91180
+ userColor,
91181
+ userAvatar,
91182
+ role,
91183
+ canvasWidth,
91184
+ canvasHeight
91185
+ }) {
91186
+ const [remoteUsers, setRemoteUsers] = useState([]);
91187
+ const lastBroadcastRef = useRef(0);
91188
+ const pendingBroadcastRef = useRef(null);
91189
+ const latestLocalState = useRef({});
91190
+ const broadcastPresence = useCallback(
91191
+ (update2) => {
91192
+ if (!awareness) {
91193
+ return;
91194
+ }
91195
+ Object.assign(latestLocalState.current, update2);
91196
+ const now = Date.now();
91197
+ const elapsed = now - lastBroadcastRef.current;
91198
+ const flush = () => {
91199
+ const state2 = {
91200
+ ...latestLocalState.current,
91201
+ userName,
91202
+ userColor,
91203
+ userAvatar,
91204
+ role,
91205
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
91206
+ };
91207
+ awareness.setLocalStateField("presence", state2);
91208
+ lastBroadcastRef.current = Date.now();
91209
+ };
91210
+ if (elapsed >= BROADCAST_THROTTLE_MS) {
91211
+ if (pendingBroadcastRef.current) {
91212
+ clearTimeout(pendingBroadcastRef.current);
91213
+ pendingBroadcastRef.current = null;
91214
+ }
91215
+ flush();
91216
+ } else if (!pendingBroadcastRef.current) {
91217
+ pendingBroadcastRef.current = setTimeout(() => {
91218
+ pendingBroadcastRef.current = null;
91219
+ flush();
91220
+ }, BROADCAST_THROTTLE_MS - elapsed);
91221
+ }
91222
+ },
91223
+ [awareness, userName, userColor, userAvatar, role]
91224
+ );
91225
+ useEffect(() => {
91226
+ if (!awareness) {
91227
+ return;
91228
+ }
91229
+ awareness.setLocalStateField("presence", {
91230
+ userName,
91231
+ userColor,
91232
+ userAvatar,
91233
+ role,
91234
+ activeSlideIndex: 0,
91235
+ cursorX: 0,
91236
+ cursorY: 0,
91237
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
91238
+ });
91239
+ }, [awareness, userName, userColor, userAvatar, role]);
91240
+ useEffect(() => {
91241
+ if (!awareness || localClientId === null) {
91242
+ return;
91243
+ }
91244
+ const handleChange = () => {
91245
+ const now = Date.now();
91246
+ const states = awareness.getStates();
91247
+ const users = [];
91248
+ states.forEach((state2, cid) => {
91249
+ if (cid === localClientId) {
91250
+ return;
91251
+ }
91252
+ const raw = state2?.presence;
91253
+ if (!raw || typeof raw !== "object") {
91254
+ return;
91255
+ }
91256
+ const sanitized = sanitizePresence({ ...raw, clientId: cid }, canvasWidth, canvasHeight);
91257
+ if (!sanitized) {
91258
+ return;
91259
+ }
91260
+ const updatedAt = new Date(sanitized.lastUpdated).getTime();
91261
+ if (Number.isNaN(updatedAt) || now - updatedAt > STALE_PRESENCE_MS) {
91262
+ return;
91263
+ }
91264
+ users.push(sanitized);
91265
+ });
91266
+ setRemoteUsers(users);
91267
+ };
91268
+ awareness.on("change", handleChange);
91269
+ awareness.on("update", handleChange);
91270
+ handleChange();
91271
+ return () => {
91272
+ awareness.off("change", handleChange);
91273
+ awareness.off("update", handleChange);
91274
+ };
91275
+ }, [awareness, localClientId, canvasWidth, canvasHeight]);
91276
+ useEffect(() => {
91277
+ if (!awareness) {
91278
+ return;
91279
+ }
91280
+ const interval = setInterval(() => {
91281
+ awareness.setLocalStateField("presence", {
91282
+ ...latestLocalState.current,
91283
+ userName,
91284
+ userColor,
91285
+ userAvatar,
91286
+ role,
91287
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
91288
+ });
91289
+ }, 1e4);
91290
+ return () => clearInterval(interval);
91291
+ }, [awareness, userName, userColor, userAvatar, role]);
91292
+ useEffect(() => {
91293
+ return () => {
91294
+ if (pendingBroadcastRef.current) {
91295
+ clearTimeout(pendingBroadcastRef.current);
91296
+ }
91297
+ };
91298
+ }, []);
91299
+ return { remoteUsers, broadcastPresence };
91300
+ }
91301
+ function useYjsProvider({ config }) {
91302
+ const [status, setStatus] = useState("disconnected");
91303
+ const [awareness, setAwareness] = useState(null);
91304
+ const [doc2, setDoc] = useState(null);
91305
+ const [clientId, setClientId] = useState(null);
91306
+ const cleanupRef = useRef(null);
91307
+ const init = useCallback(async () => {
91308
+ const roomId = validateRoomId(config.roomId);
91309
+ setStatus("connecting");
91310
+ try {
91311
+ const [Y, { WebsocketProvider: WebsocketProvider2 }] = await Promise.all([Promise.resolve().then(() => (init_yjs(), yjs_exports)), Promise.resolve().then(() => (init_y_websocket(), y_websocket_exports))]);
91312
+ const yDoc = new Y.Doc();
91313
+ const provider = new WebsocketProvider2(
91314
+ config.serverUrl,
91315
+ roomId,
91316
+ yDoc,
91317
+ // eslint-disable-line @typescript-eslint/no-explicit-any
91318
+ {
91319
+ params: config.authToken ? { token: config.authToken } : void 0
91320
+ }
91321
+ );
91322
+ const handleStatus = (event) => {
91323
+ if (event.status === "connected") {
91324
+ setStatus("connected");
91325
+ } else if (event.status === "disconnected") {
91326
+ setStatus("disconnected");
91327
+ }
91328
+ };
91329
+ provider.on("status", handleStatus);
91330
+ if (provider.wsconnected) {
91331
+ setStatus("connected");
91332
+ }
91333
+ setDoc(yDoc);
91334
+ setAwareness(provider.awareness);
91335
+ setClientId(provider.awareness.clientID);
91336
+ cleanupRef.current = () => {
91337
+ provider.off("status", handleStatus);
91338
+ provider.destroy();
91339
+ yDoc.destroy();
91340
+ setDoc(null);
91341
+ setAwareness(null);
91342
+ setClientId(null);
91343
+ setStatus("disconnected");
91344
+ };
91345
+ } catch (err) {
91346
+ console.warn(
91347
+ "[pptx-viewer] Collaboration packages not available:",
91348
+ err instanceof Error ? err.message : err
91349
+ );
91350
+ setStatus("error");
91351
+ }
91352
+ }, [config.roomId, config.serverUrl, config.authToken]);
91353
+ useEffect(() => {
91354
+ init();
91355
+ return () => {
91356
+ cleanupRef.current?.();
91357
+ cleanupRef.current = null;
91358
+ };
91359
+ }, [init]);
91360
+ return { status, awareness, doc: doc2, clientId };
91361
+ }
91362
+
91363
+ // src/viewer/hooks/collaboration/useCollaborativeState.ts
91364
+ function useCollaborativeState({
91365
+ config,
91366
+ canvasWidth,
91367
+ canvasHeight
91368
+ }) {
91369
+ const userColor = sanitizeColor(config.userColor, "#6366f1");
91370
+ const { status, awareness, doc: doc2, clientId } = useYjsProvider({ config });
91371
+ const { remoteUsers, broadcastPresence } = usePresenceTracking({
91372
+ awareness,
91373
+ localClientId: clientId,
91374
+ userName: config.userName,
91375
+ userColor,
91376
+ userAvatar: config.userAvatar,
91377
+ role: config.role,
91378
+ canvasWidth,
91379
+ canvasHeight
91380
+ });
91381
+ const connectedCount = status === "connected" ? remoteUsers.length + 1 : remoteUsers.length;
91382
+ return {
91383
+ status,
91384
+ remoteUsers,
91385
+ broadcastPresence,
91386
+ connectedCount,
91387
+ config,
91388
+ doc: doc2
91389
+ };
91390
+ }
91391
+ var CollaborationContext = createContext(null);
91392
+ function useCollaboration() {
91393
+ return useContext(CollaborationContext);
91394
+ }
91395
+ function CollaborationProvider({
91396
+ config,
91397
+ canvasWidth,
91398
+ canvasHeight,
91399
+ children
91400
+ }) {
91401
+ const value = useCollaborativeState({
91402
+ config,
91403
+ canvasWidth,
91404
+ canvasHeight
91405
+ });
91406
+ return /* @__PURE__ */ jsx(CollaborationContext.Provider, { value, children });
91407
+ }
91408
+ function RemoteUserCursors({
91409
+ remoteUsers,
91410
+ activeSlideIndex,
91411
+ canvasWidth,
91412
+ canvasHeight
91413
+ }) {
91414
+ const visibleUsers = remoteUsers.filter((u2) => u2.activeSlideIndex === activeSlideIndex);
91415
+ if (visibleUsers.length === 0) {
91416
+ return null;
91417
+ }
91418
+ return /* @__PURE__ */ jsx(
91419
+ "svg",
91420
+ {
91421
+ "data-testid": "remote-user-cursors",
91422
+ "data-export-ignore": "true",
91423
+ className: "absolute inset-0 pointer-events-none",
91424
+ style: { zIndex: 9999 },
91425
+ width: canvasWidth,
91426
+ height: canvasHeight,
91427
+ viewBox: `0 0 ${canvasWidth} ${canvasHeight}`,
91428
+ "aria-hidden": "true",
91429
+ children: visibleUsers.map((user) => /* @__PURE__ */ jsxs(
91430
+ "g",
91431
+ {
91432
+ transform: `translate(${user.cursorX}, ${user.cursorY})`,
91433
+ "data-testid": `remote-cursor-${user.clientId}`,
91434
+ children: [
91435
+ /* @__PURE__ */ jsx(
91436
+ "path",
91437
+ {
91438
+ d: "M0 0 L0 16 L4.5 12.5 L8 20 L10.5 19 L7 11.5 L12 11 Z",
91439
+ fill: user.userColor,
91440
+ stroke: "#fff",
91441
+ strokeWidth: 1,
91442
+ opacity: 0.9
91443
+ }
91444
+ ),
91445
+ /* @__PURE__ */ jsxs("g", { transform: "translate(14, 18)", children: [
91446
+ /* @__PURE__ */ jsx(
91447
+ "rect",
91448
+ {
91449
+ rx: 3,
91450
+ ry: 3,
91451
+ x: -2,
91452
+ y: -10,
91453
+ width: Math.min(user.userName.length * 7 + 8, 150),
91454
+ height: 16,
91455
+ fill: user.userColor,
91456
+ opacity: 0.85
91457
+ }
91458
+ ),
91459
+ /* @__PURE__ */ jsx(
91460
+ "text",
91461
+ {
91462
+ fill: "#fff",
91463
+ fontSize: 10,
91464
+ fontFamily: "system-ui, sans-serif",
91465
+ fontWeight: 500,
91466
+ dominantBaseline: "central",
91467
+ y: -2,
91468
+ x: 2,
91469
+ children: user.userName.length > 20 ? `${user.userName.slice(0, 18)}...` : user.userName
91470
+ }
91471
+ )
91472
+ ] })
91473
+ ]
91474
+ },
91475
+ user.clientId
91476
+ ))
91477
+ }
91478
+ );
91479
+ }
91480
+ var STATUS_STYLES = {
91481
+ connected: {
91482
+ dot: "bg-green-400",
91483
+ text: "text-green-400",
91484
+ label: "Connected"
91485
+ },
91486
+ connecting: {
91487
+ dot: "bg-yellow-400 animate-pulse",
91488
+ text: "text-yellow-400",
91489
+ label: "Connecting..."
91490
+ },
91491
+ disconnected: {
91492
+ dot: "bg-gray-500",
91493
+ text: "text-gray-500",
91494
+ label: "Disconnected"
91495
+ },
91496
+ error: {
91497
+ dot: "bg-red-400",
91498
+ text: "text-red-400",
91499
+ label: "Connection error"
91500
+ }
91501
+ };
91502
+ function CollaborationStatusIndicator({
91503
+ status,
91504
+ connectedCount
91505
+ }) {
91506
+ const { t: t2 } = useTranslation();
91507
+ const style = STATUS_STYLES[status];
91508
+ return /* @__PURE__ */ jsxs(
91509
+ "div",
91510
+ {
91511
+ "data-testid": "collaboration-status",
91512
+ className: "flex items-center gap-1.5",
91513
+ "aria-label": t2("pptx.collaboration.statusAriaLabel", {
91514
+ status: t2(`pptx.collaboration.status.${status}`),
91515
+ count: connectedCount
91516
+ }),
91517
+ children: [
91518
+ /* @__PURE__ */ jsx("span", { className: `inline-block w-2 h-2 rounded-full ${style.dot}`, "aria-hidden": "true" }),
91519
+ /* @__PURE__ */ jsx("span", { className: `text-[10px] ${style.text}`, children: status === "connected" ? t2("pptx.collaboration.userCount", { count: connectedCount }) : t2(`pptx.collaboration.status.${status}`) })
91520
+ ]
91521
+ }
91522
+ );
91523
+ }
91524
+ function CollaborationCursorOverlay({
91525
+ activeSlideIndex,
91526
+ canvasWidth,
91527
+ canvasHeight,
91528
+ selectedElementId
91529
+ }) {
91530
+ const collab = useCollaboration();
91531
+ const containerRef = useRef(null);
91532
+ const prevSelectionRef = useRef(selectedElementId);
91533
+ useEffect(() => {
91534
+ if (!collab || selectedElementId === prevSelectionRef.current) {
91535
+ return;
91536
+ }
91537
+ prevSelectionRef.current = selectedElementId;
91538
+ collab.broadcastPresence({
91539
+ selectedElementId: selectedElementId ?? void 0,
91540
+ activeSlideIndex
91541
+ });
91542
+ }, [collab, selectedElementId, activeSlideIndex]);
91543
+ useEffect(() => {
91544
+ if (!collab) {
91545
+ return;
91546
+ }
91547
+ const parent = containerRef.current?.parentElement;
91548
+ if (!parent) {
91549
+ return;
91550
+ }
91551
+ const handler = (e2) => {
91552
+ const rect = parent.getBoundingClientRect();
91553
+ const x2 = (e2.clientX - rect.left) / rect.width * canvasWidth;
91554
+ const y = (e2.clientY - rect.top) / rect.height * canvasHeight;
91555
+ collab.broadcastPresence({
91556
+ cursorX: x2,
91557
+ cursorY: y,
91558
+ activeSlideIndex
91559
+ });
91560
+ };
91561
+ parent.addEventListener("pointermove", handler);
91562
+ return () => parent.removeEventListener("pointermove", handler);
91563
+ }, [collab, canvasWidth, canvasHeight, activeSlideIndex]);
91564
+ if (!collab) {
91565
+ return null;
91566
+ }
91567
+ return /* @__PURE__ */ jsx(
91568
+ "div",
91569
+ {
91570
+ ref: containerRef,
91571
+ "data-testid": "collab-pointer-tracker",
91572
+ "data-export-ignore": "true",
91573
+ style: { display: "contents" },
91574
+ children: /* @__PURE__ */ jsx(
91575
+ RemoteUserCursors,
91576
+ {
91577
+ remoteUsers: collab.remoteUsers,
91578
+ activeSlideIndex,
91579
+ canvasWidth,
91580
+ canvasHeight
91581
+ }
91582
+ )
91583
+ }
91584
+ );
91585
+ }
91586
+ function RemoteSelectionOverlay({
91587
+ elements,
91588
+ activeSlideIndex
91589
+ }) {
91590
+ const collab = useCollaboration();
91591
+ if (!collab) {
91592
+ return null;
91593
+ }
91594
+ const elementMap = /* @__PURE__ */ new Map();
91595
+ for (const el of elements) {
91596
+ elementMap.set(el.id, el);
91597
+ }
91598
+ const selections = [];
91599
+ for (const user of collab.remoteUsers) {
91600
+ if (user.activeSlideIndex === activeSlideIndex && user.selectedElementId) {
91601
+ const el = elementMap.get(user.selectedElementId);
91602
+ if (el) {
91603
+ selections.push({
91604
+ userName: user.userName,
91605
+ userColor: user.userColor,
91606
+ element: el
91607
+ });
91608
+ }
91609
+ }
91610
+ }
91611
+ if (selections.length === 0) {
91612
+ return null;
91613
+ }
91614
+ return /* @__PURE__ */ jsx(Fragment, { children: selections.map((sel) => /* @__PURE__ */ jsx(
91615
+ "div",
91616
+ {
91617
+ "data-testid": `remote-selection-${sel.element.id}`,
91618
+ "data-export-ignore": "true",
91619
+ className: "absolute pointer-events-none",
91620
+ style: {
91621
+ left: sel.element.x,
91622
+ top: sel.element.y,
91623
+ width: sel.element.width,
91624
+ height: sel.element.height,
91625
+ zIndex: 9997,
91626
+ border: `2px solid ${sel.userColor}`,
91627
+ borderRadius: 2
91628
+ },
91629
+ children: /* @__PURE__ */ jsx(
91630
+ "span",
91631
+ {
91632
+ className: "absolute -top-5 left-0 px-1 py-0.5 text-[9px] font-medium text-white rounded-sm whitespace-nowrap leading-none",
91633
+ style: { backgroundColor: sel.userColor },
91634
+ children: sel.userName
91635
+ }
91636
+ )
91637
+ },
91638
+ `remote-sel-${sel.element.id}`
91639
+ )) });
91640
+ }
91105
91641
  function SectionContextMenu({
91106
91642
  state: state2,
91107
91643
  sectionGroups,
@@ -91383,6 +91919,7 @@ function SlideItemInner({
91383
91919
  canvasSize,
91384
91920
  canEdit,
91385
91921
  rehearsalTimings,
91922
+ presenceUsers,
91386
91923
  onSelectSlide,
91387
91924
  onSlideContextMenu,
91388
91925
  onAddSection,
@@ -91425,16 +91962,27 @@ function SlideItemInner({
91425
91962
  onDragOver,
91426
91963
  onDrop: (e2) => onDrop(e2, slideIndex),
91427
91964
  children: [
91428
- /* @__PURE__ */ jsx(
91429
- "span",
91430
- {
91431
- className: cn(
91432
- "text-[10px] tabular-nums w-5 text-right shrink-0 select-none",
91433
- isActive ? "text-primary font-medium" : "text-muted-foreground"
91434
- ),
91435
- children: slideIndex + 1
91436
- }
91437
- ),
91965
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-0.5 w-5 shrink-0", children: [
91966
+ /* @__PURE__ */ jsx(
91967
+ "span",
91968
+ {
91969
+ className: cn(
91970
+ "text-[10px] tabular-nums text-right select-none w-full",
91971
+ isActive ? "text-primary font-medium" : "text-muted-foreground"
91972
+ ),
91973
+ children: slideIndex + 1
91974
+ }
91975
+ ),
91976
+ presenceUsers && presenceUsers.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap justify-center gap-px", children: presenceUsers.slice(0, 4).map((u2, i3) => /* @__PURE__ */ jsx(
91977
+ "span",
91978
+ {
91979
+ className: "w-[6px] h-[6px] rounded-full",
91980
+ style: { backgroundColor: u2.userColor },
91981
+ title: u2.userName
91982
+ },
91983
+ i3
91984
+ )) })
91985
+ ] }),
91438
91986
  /* @__PURE__ */ jsxs(
91439
91987
  "div",
91440
91988
  {
@@ -91584,8 +92132,26 @@ function SlidesPaneSidebar({
91584
92132
  panelWidth
91585
92133
  }) {
91586
92134
  const { t: t2 } = useTranslation();
92135
+ const collab = useCollaboration();
91587
92136
  const slideRefs = useRef(/* @__PURE__ */ new Map());
91588
92137
  const renameInputRef = useRef(null);
92138
+ const slidePresenceMap = useMemo(() => {
92139
+ if (!collab || collab.remoteUsers.length === 0) {
92140
+ return void 0;
92141
+ }
92142
+ const map3 = /* @__PURE__ */ new Map();
92143
+ for (const user of collab.remoteUsers) {
92144
+ const idx = user.activeSlideIndex;
92145
+ const existing = map3.get(idx);
92146
+ const entry = { userName: user.userName, userColor: user.userColor };
92147
+ if (existing) {
92148
+ existing.push(entry);
92149
+ } else {
92150
+ map3.set(idx, [entry]);
92151
+ }
92152
+ }
92153
+ return map3;
92154
+ }, [collab]);
91589
92155
  const estimatedItemHeight = useMemo(
91590
92156
  () => estimateSlideItemHeight(canvasSize.width, canvasSize.height),
91591
92157
  [canvasSize.width, canvasSize.height]
@@ -91707,6 +92273,7 @@ function SlidesPaneSidebar({
91707
92273
  canvasSize,
91708
92274
  canEdit,
91709
92275
  rehearsalTimings,
92276
+ presenceUsers: slidePresenceMap?.get(item.slideIndex),
91710
92277
  onSelectSlide,
91711
92278
  onSlideContextMenu,
91712
92279
  onAddSection,
@@ -91760,6 +92327,7 @@ function SlidesPaneSidebar({
91760
92327
  canvasSize,
91761
92328
  canEdit,
91762
92329
  rehearsalTimings,
92330
+ presenceUsers: slidePresenceMap?.get(idx),
91763
92331
  onSelectSlide,
91764
92332
  onSlideContextMenu,
91765
92333
  onAddSection,
@@ -96526,16 +97094,31 @@ var ALIGN_BTNS = [
96526
97094
  { k: "bottom", el: /* @__PURE__ */ jsx(LuChevronDown, { className: ic2 }) }
96527
97095
  ];
96528
97096
  var DRAW_TOOLS = [
96529
- { id: "select", icon: /* @__PURE__ */ jsx(LuMoveRight, { className: ic2 }), t: "Select" },
96530
- { id: "pen", icon: /* @__PURE__ */ jsx(LuPencil, { className: ic2 }), t: "Pen" },
97097
+ {
97098
+ id: "select",
97099
+ icon: /* @__PURE__ */ jsx(LuMoveRight, { className: ic2 }),
97100
+ t: "Select",
97101
+ ac: "bg-primary text-primary-foreground"
97102
+ },
97103
+ {
97104
+ id: "pen",
97105
+ icon: /* @__PURE__ */ jsx(LuPencil, { className: ic2 }),
97106
+ t: "Pen",
97107
+ ac: "bg-primary text-primary-foreground"
97108
+ },
96531
97109
  {
96532
97110
  id: "highlighter",
96533
97111
  icon: /* @__PURE__ */ jsx(LuType, { className: ic2 }),
96534
97112
  t: "Highlighter",
96535
97113
  ac: "bg-yellow-600 text-white"
96536
97114
  },
96537
- { id: "eraser", icon: /* @__PURE__ */ jsx(LuMinus, { className: ic2 }), t: "Eraser" },
96538
- { id: "freeform", icon: /* @__PURE__ */ jsx(LuSpline, { className: ic2 }), t: "Freeform" }
97115
+ { id: "eraser", icon: /* @__PURE__ */ jsx(LuMinus, { className: ic2 }), t: "Eraser", ac: "bg-red-600 text-white" },
97116
+ {
97117
+ id: "freeform",
97118
+ icon: /* @__PURE__ */ jsx(LuSpline, { className: ic2 }),
97119
+ t: "Freeform",
97120
+ ac: "bg-primary text-primary-foreground"
97121
+ }
96539
97122
  ];
96540
97123
  var OV = [
96541
97124
  {
@@ -96741,7 +97324,7 @@ function AnimationsSection(p3) {
96741
97324
  "button",
96742
97325
  {
96743
97326
  type: "button",
96744
- onClick: p3.onToggleInspector,
97327
+ onClick: p3.onOpenAnimationPanel ?? p3.onToggleInspector,
96745
97328
  className: cn(
96746
97329
  pill,
96747
97330
  p3.isInspectorPaneOpen ? "bg-primary hover:bg-primary/80 text-primary-foreground" : ""
@@ -98268,347 +98851,6 @@ function TextSection(p3) {
98268
98851
  ] })
98269
98852
  ] });
98270
98853
  }
98271
-
98272
- // src/viewer/hooks/collaboration/sanitize.ts
98273
- var ROOM_ID_REGEX = /^[a-zA-Z0-9_-]{1,128}$/;
98274
- function validateRoomId(roomId) {
98275
- if (!ROOM_ID_REGEX.test(roomId)) {
98276
- throw new Error(
98277
- `Invalid collaboration room ID: "${roomId}". Must be 1-128 alphanumeric characters, hyphens, or underscores.`
98278
- );
98279
- }
98280
- return roomId;
98281
- }
98282
- function sanitizeUserName(name) {
98283
- if (typeof name !== "string") {
98284
- return "Anonymous";
98285
- }
98286
- const stripped = name.replace(/<[^>]*>/g, "");
98287
- const trimmed = stripped.trim().slice(0, 64);
98288
- return trimmed || "Anonymous";
98289
- }
98290
- function clampCursorPosition(value, min2, max2) {
98291
- if (typeof value !== "number" || !Number.isFinite(value)) {
98292
- return 0;
98293
- }
98294
- const margin = 20;
98295
- return Math.max(min2 - margin, Math.min(max2 + margin, value));
98296
- }
98297
- var HEX_COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
98298
- function sanitizeColor(color, fallback = "#6366f1") {
98299
- if (typeof color !== "string") {
98300
- return fallback;
98301
- }
98302
- return HEX_COLOR_REGEX.test(color) ? color : fallback;
98303
- }
98304
- function sanitizeAvatarUrl(url) {
98305
- if (typeof url !== "string") {
98306
- return void 0;
98307
- }
98308
- try {
98309
- const parsed = new URL(url);
98310
- if (parsed.protocol === "https:" || parsed.protocol === "http:" || parsed.protocol === "data:") {
98311
- return url;
98312
- }
98313
- } catch {
98314
- }
98315
- return void 0;
98316
- }
98317
- function sanitizeSlideIndex(value) {
98318
- if (typeof value !== "number" || !Number.isFinite(value)) {
98319
- return 0;
98320
- }
98321
- return Math.max(0, Math.floor(value));
98322
- }
98323
- function sanitizePresence(raw, canvasWidth, canvasHeight) {
98324
- if (typeof raw.clientId !== "number") {
98325
- return null;
98326
- }
98327
- return {
98328
- clientId: raw.clientId,
98329
- userName: sanitizeUserName(raw.userName),
98330
- userAvatar: sanitizeAvatarUrl(raw.userAvatar),
98331
- userColor: sanitizeColor(raw.userColor),
98332
- activeSlideIndex: sanitizeSlideIndex(raw.activeSlideIndex),
98333
- cursorX: clampCursorPosition(raw.cursorX, 0, canvasWidth),
98334
- cursorY: clampCursorPosition(raw.cursorY, 0, canvasHeight),
98335
- lastUpdated: typeof raw.lastUpdated === "string" ? raw.lastUpdated : (/* @__PURE__ */ new Date()).toISOString(),
98336
- selectedElementId: typeof raw.selectedElementId === "string" ? raw.selectedElementId.slice(0, 128) : void 0
98337
- };
98338
- }
98339
- var BROADCAST_THROTTLE_MS = 50;
98340
- var STALE_PRESENCE_MS = 3e4;
98341
- function usePresenceTracking({
98342
- awareness,
98343
- localClientId,
98344
- userName,
98345
- userColor,
98346
- userAvatar,
98347
- canvasWidth,
98348
- canvasHeight
98349
- }) {
98350
- const [remoteUsers, setRemoteUsers] = useState([]);
98351
- const lastBroadcastRef = useRef(0);
98352
- const pendingBroadcastRef = useRef(null);
98353
- const latestLocalState = useRef({});
98354
- const broadcastPresence = useCallback(
98355
- (update2) => {
98356
- if (!awareness) {
98357
- return;
98358
- }
98359
- Object.assign(latestLocalState.current, update2);
98360
- const now = Date.now();
98361
- const elapsed = now - lastBroadcastRef.current;
98362
- const flush = () => {
98363
- const state2 = {
98364
- ...latestLocalState.current,
98365
- userName,
98366
- userColor,
98367
- userAvatar,
98368
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
98369
- };
98370
- awareness.setLocalStateField("presence", state2);
98371
- lastBroadcastRef.current = Date.now();
98372
- };
98373
- if (elapsed >= BROADCAST_THROTTLE_MS) {
98374
- if (pendingBroadcastRef.current) {
98375
- clearTimeout(pendingBroadcastRef.current);
98376
- pendingBroadcastRef.current = null;
98377
- }
98378
- flush();
98379
- } else if (!pendingBroadcastRef.current) {
98380
- pendingBroadcastRef.current = setTimeout(() => {
98381
- pendingBroadcastRef.current = null;
98382
- flush();
98383
- }, BROADCAST_THROTTLE_MS - elapsed);
98384
- }
98385
- },
98386
- [awareness, userName, userColor, userAvatar]
98387
- );
98388
- useEffect(() => {
98389
- if (!awareness) {
98390
- return;
98391
- }
98392
- awareness.setLocalStateField("presence", {
98393
- userName,
98394
- userColor,
98395
- userAvatar,
98396
- activeSlideIndex: 0,
98397
- cursorX: 0,
98398
- cursorY: 0,
98399
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
98400
- });
98401
- }, [awareness, userName, userColor, userAvatar]);
98402
- useEffect(() => {
98403
- if (!awareness || localClientId === null) {
98404
- return;
98405
- }
98406
- const handleChange = () => {
98407
- const now = Date.now();
98408
- const states = awareness.getStates();
98409
- const users = [];
98410
- states.forEach((state2, cid) => {
98411
- if (cid === localClientId) {
98412
- return;
98413
- }
98414
- const raw = state2?.presence;
98415
- if (!raw || typeof raw !== "object") {
98416
- return;
98417
- }
98418
- const sanitized = sanitizePresence({ ...raw, clientId: cid }, canvasWidth, canvasHeight);
98419
- if (!sanitized) {
98420
- return;
98421
- }
98422
- const updatedAt = new Date(sanitized.lastUpdated).getTime();
98423
- if (Number.isNaN(updatedAt) || now - updatedAt > STALE_PRESENCE_MS) {
98424
- return;
98425
- }
98426
- users.push(sanitized);
98427
- });
98428
- setRemoteUsers(users);
98429
- };
98430
- awareness.on("change", handleChange);
98431
- awareness.on("update", handleChange);
98432
- handleChange();
98433
- return () => {
98434
- awareness.off("change", handleChange);
98435
- awareness.off("update", handleChange);
98436
- };
98437
- }, [awareness, localClientId, canvasWidth, canvasHeight]);
98438
- useEffect(() => {
98439
- if (!awareness) {
98440
- return;
98441
- }
98442
- const interval = setInterval(() => {
98443
- awareness.setLocalStateField("presence", {
98444
- ...latestLocalState.current,
98445
- userName,
98446
- userColor,
98447
- userAvatar,
98448
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
98449
- });
98450
- }, 1e4);
98451
- return () => clearInterval(interval);
98452
- }, [awareness, userName, userColor, userAvatar]);
98453
- useEffect(() => {
98454
- return () => {
98455
- if (pendingBroadcastRef.current) {
98456
- clearTimeout(pendingBroadcastRef.current);
98457
- }
98458
- };
98459
- }, []);
98460
- return { remoteUsers, broadcastPresence };
98461
- }
98462
- function useYjsProvider({ config }) {
98463
- const [status, setStatus] = useState("disconnected");
98464
- const [awareness, setAwareness] = useState(null);
98465
- const [doc2, setDoc] = useState(null);
98466
- const [clientId, setClientId] = useState(null);
98467
- const cleanupRef = useRef(null);
98468
- const init = useCallback(async () => {
98469
- const roomId = validateRoomId(config.roomId);
98470
- setStatus("connecting");
98471
- try {
98472
- const [Y, { WebsocketProvider: WebsocketProvider2 }] = await Promise.all([Promise.resolve().then(() => (init_yjs(), yjs_exports)), Promise.resolve().then(() => (init_y_websocket(), y_websocket_exports))]);
98473
- const yDoc = new Y.Doc();
98474
- const provider = new WebsocketProvider2(
98475
- config.serverUrl,
98476
- roomId,
98477
- yDoc,
98478
- // eslint-disable-line @typescript-eslint/no-explicit-any
98479
- {
98480
- params: config.authToken ? { token: config.authToken } : void 0
98481
- }
98482
- );
98483
- const handleStatus = (event) => {
98484
- if (event.status === "connected") {
98485
- setStatus("connected");
98486
- } else if (event.status === "disconnected") {
98487
- setStatus("disconnected");
98488
- }
98489
- };
98490
- provider.on("status", handleStatus);
98491
- if (provider.wsconnected) {
98492
- setStatus("connected");
98493
- }
98494
- setDoc(yDoc);
98495
- setAwareness(provider.awareness);
98496
- setClientId(provider.awareness.clientID);
98497
- cleanupRef.current = () => {
98498
- provider.off("status", handleStatus);
98499
- provider.destroy();
98500
- yDoc.destroy();
98501
- setDoc(null);
98502
- setAwareness(null);
98503
- setClientId(null);
98504
- setStatus("disconnected");
98505
- };
98506
- } catch (err) {
98507
- console.warn(
98508
- "[pptx-viewer] Collaboration packages not available:",
98509
- err instanceof Error ? err.message : err
98510
- );
98511
- setStatus("error");
98512
- }
98513
- }, [config.roomId, config.serverUrl, config.authToken]);
98514
- useEffect(() => {
98515
- init();
98516
- return () => {
98517
- cleanupRef.current?.();
98518
- cleanupRef.current = null;
98519
- };
98520
- }, [init]);
98521
- return { status, awareness, doc: doc2, clientId };
98522
- }
98523
-
98524
- // src/viewer/hooks/collaboration/useCollaborativeState.ts
98525
- function useCollaborativeState({
98526
- config,
98527
- canvasWidth,
98528
- canvasHeight
98529
- }) {
98530
- const userColor = sanitizeColor(config.userColor, "#6366f1");
98531
- const { status, awareness, doc: doc2, clientId } = useYjsProvider({ config });
98532
- const { remoteUsers, broadcastPresence } = usePresenceTracking({
98533
- awareness,
98534
- localClientId: clientId,
98535
- userName: config.userName,
98536
- userColor,
98537
- userAvatar: config.userAvatar,
98538
- canvasWidth,
98539
- canvasHeight
98540
- });
98541
- const connectedCount = status === "connected" ? remoteUsers.length + 1 : remoteUsers.length;
98542
- return {
98543
- status,
98544
- remoteUsers,
98545
- broadcastPresence,
98546
- connectedCount,
98547
- config,
98548
- doc: doc2
98549
- };
98550
- }
98551
- var CollaborationContext = createContext(null);
98552
- function useCollaboration() {
98553
- return useContext(CollaborationContext);
98554
- }
98555
- function CollaborationProvider({
98556
- config,
98557
- canvasWidth,
98558
- canvasHeight,
98559
- children
98560
- }) {
98561
- const value = useCollaborativeState({
98562
- config,
98563
- canvasWidth,
98564
- canvasHeight
98565
- });
98566
- return /* @__PURE__ */ jsx(CollaborationContext.Provider, { value, children });
98567
- }
98568
- var STATUS_STYLES = {
98569
- connected: {
98570
- dot: "bg-green-400",
98571
- text: "text-green-400",
98572
- label: "Connected"
98573
- },
98574
- connecting: {
98575
- dot: "bg-yellow-400 animate-pulse",
98576
- text: "text-yellow-400",
98577
- label: "Connecting..."
98578
- },
98579
- disconnected: {
98580
- dot: "bg-gray-500",
98581
- text: "text-gray-500",
98582
- label: "Disconnected"
98583
- },
98584
- error: {
98585
- dot: "bg-red-400",
98586
- text: "text-red-400",
98587
- label: "Connection error"
98588
- }
98589
- };
98590
- function CollaborationStatusIndicator({
98591
- status,
98592
- connectedCount
98593
- }) {
98594
- const { t: t2 } = useTranslation();
98595
- const style = STATUS_STYLES[status];
98596
- return /* @__PURE__ */ jsxs(
98597
- "div",
98598
- {
98599
- "data-testid": "collaboration-status",
98600
- className: "flex items-center gap-1.5",
98601
- "aria-label": t2("pptx.collaboration.statusAriaLabel", {
98602
- status: t2(`pptx.collaboration.status.${status}`),
98603
- count: connectedCount
98604
- }),
98605
- children: [
98606
- /* @__PURE__ */ jsx("span", { className: `inline-block w-2 h-2 rounded-full ${style.dot}`, "aria-hidden": "true" }),
98607
- /* @__PURE__ */ jsx("span", { className: `text-[10px] ${style.text}`, children: status === "connected" ? t2("pptx.collaboration.userCount", { count: connectedCount }) : t2(`pptx.collaboration.status.${status}`) })
98608
- ]
98609
- }
98610
- );
98611
- }
98612
98854
  function CustomShowsControls({
98613
98855
  customShows,
98614
98856
  activeCustomShowId,
@@ -99047,29 +99289,38 @@ function ToolbarPrimaryRow(p3) {
99047
99289
  ]
99048
99290
  }
99049
99291
  ),
99050
- collab && (collab.status === "connected" || collab.status === "connecting") && collab.remoteUsers.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center -space-x-1.5 mx-1", children: [
99051
- collab.remoteUsers.slice(0, 4).map((user) => /* @__PURE__ */ jsx(
99052
- "div",
99053
- {
99054
- className: "w-6 h-6 rounded-full border-2 border-background flex items-center justify-center text-[8px] font-semibold text-white shrink-0",
99055
- style: { backgroundColor: user.userColor },
99056
- title: user.userName,
99057
- children: user.userAvatar ? /* @__PURE__ */ jsx(
99058
- "img",
99292
+ collab && (collab.status === "connected" || collab.status === "connecting") && collab.remoteUsers.length > 0 && /* @__PURE__ */ jsxs(
99293
+ "button",
99294
+ {
99295
+ type: "button",
99296
+ onClick: p3.onOpenShareDialog,
99297
+ className: "flex items-center -space-x-1.5 mx-1 rounded-sm px-1 py-0.5 hover:bg-accent/60 transition-colors cursor-pointer",
99298
+ title: t2("pptx.toolbar.sharingUsers", { count: collab.connectedCount }),
99299
+ children: [
99300
+ collab.remoteUsers.slice(0, 4).map((user) => /* @__PURE__ */ jsx(
99301
+ "div",
99059
99302
  {
99060
- src: user.userAvatar,
99061
- alt: user.userName,
99062
- className: "w-full h-full rounded-full object-cover"
99063
- }
99064
- ) : user.userName.slice(0, 2).toUpperCase()
99065
- },
99066
- user.clientId
99067
- )),
99068
- collab.remoteUsers.length > 4 && /* @__PURE__ */ jsxs("div", { className: "w-6 h-6 rounded-full border-2 border-background bg-muted flex items-center justify-center text-[8px] text-muted-foreground shrink-0", children: [
99069
- "+",
99070
- collab.remoteUsers.length - 4
99071
- ] })
99072
- ] }),
99303
+ className: "w-6 h-6 rounded-full border-2 border-background flex items-center justify-center text-[8px] font-semibold text-white shrink-0",
99304
+ style: { backgroundColor: user.userColor },
99305
+ title: user.userName,
99306
+ children: user.userAvatar ? /* @__PURE__ */ jsx(
99307
+ "img",
99308
+ {
99309
+ src: user.userAvatar,
99310
+ alt: user.userName,
99311
+ className: "w-full h-full rounded-full object-cover"
99312
+ }
99313
+ ) : user.userName.slice(0, 2).toUpperCase()
99314
+ },
99315
+ user.clientId
99316
+ )),
99317
+ collab.remoteUsers.length > 4 && /* @__PURE__ */ jsxs("div", { className: "w-6 h-6 rounded-full border-2 border-background bg-muted flex items-center justify-center text-[8px] text-muted-foreground shrink-0", children: [
99318
+ "+",
99319
+ collab.remoteUsers.length - 4
99320
+ ] })
99321
+ ]
99322
+ }
99323
+ ),
99073
99324
  /* @__PURE__ */ jsx(
99074
99325
  ModeSwitcher,
99075
99326
  {
@@ -99446,7 +99697,8 @@ function Toolbar(p3) {
99446
99697
  canEdit: p3.canEdit,
99447
99698
  selectedElement: p3.selectedElement,
99448
99699
  isInspectorPaneOpen: p3.isInspectorPaneOpen,
99449
- onToggleInspector: p3.onToggleInspector
99700
+ onToggleInspector: p3.onToggleInspector,
99701
+ onOpenAnimationPanel: p3.onOpenAnimationPanel
99450
99702
  }
99451
99703
  ),
99452
99704
  sSlw && /* @__PURE__ */ jsx(
@@ -106891,9 +107143,75 @@ function SetUpSlideShowDialog({
106891
107143
  }
106892
107144
  function BroadcastDialog({
106893
107145
  open,
106894
- onClose
107146
+ onClose,
107147
+ onStartBroadcast,
107148
+ onStopBroadcast,
107149
+ onStartPresenting,
107150
+ defaultRoomId,
107151
+ defaultUserName,
107152
+ defaultServerUrl
106895
107153
  }) {
106896
107154
  const { t: t2 } = useTranslation();
107155
+ const collab = useCollaboration();
107156
+ const isBroadcasting = collab !== null && collab.status !== "disconnected" && collab.status !== "error" && collab.config.role === "broadcaster";
107157
+ const [roomId, setRoomId] = useState("");
107158
+ const [userName, setUserName] = useState("");
107159
+ const [serverUrl, setServerUrl] = useState("");
107160
+ const [copied, setCopied] = useState(false);
107161
+ const dialogRef = useRef(null);
107162
+ useEffect(() => {
107163
+ if (open && !isBroadcasting) {
107164
+ const broadcastRoom = defaultRoomId ? `broadcast-${defaultRoomId}` : `broadcast-${Math.random().toString(36).slice(2, 10)}`;
107165
+ setRoomId(broadcastRoom);
107166
+ setUserName(defaultUserName ?? "");
107167
+ setServerUrl(defaultServerUrl ?? "ws://localhost:1234");
107168
+ }
107169
+ }, [open, isBroadcasting, defaultRoomId, defaultUserName, defaultServerUrl]);
107170
+ useEffect(() => {
107171
+ if (!open) {
107172
+ return;
107173
+ }
107174
+ function handleKeyDown(e2) {
107175
+ if (e2.key === "Escape") {
107176
+ onClose();
107177
+ }
107178
+ }
107179
+ document.addEventListener("keydown", handleKeyDown);
107180
+ return () => document.removeEventListener("keydown", handleKeyDown);
107181
+ }, [open, onClose]);
107182
+ useEffect(() => {
107183
+ if (open && dialogRef.current) {
107184
+ dialogRef.current.focus();
107185
+ }
107186
+ }, [open]);
107187
+ const broadcastUrl = typeof window !== "undefined" ? `${window.location.origin}${window.location.pathname}?broadcast=${encodeURIComponent(isBroadcasting ? collab.config.roomId : roomId)}&server=${encodeURIComponent(isBroadcasting ? collab.config.serverUrl : serverUrl)}` : roomId;
107188
+ const handleCopyUrl = useCallback(() => {
107189
+ void navigator.clipboard.writeText(broadcastUrl).then(() => {
107190
+ setCopied(true);
107191
+ setTimeout(() => setCopied(false), 2e3);
107192
+ return void 0;
107193
+ });
107194
+ }, [broadcastUrl]);
107195
+ const handleStartBroadcast = useCallback(() => {
107196
+ if (!roomId.trim() || !userName.trim()) {
107197
+ return;
107198
+ }
107199
+ onStartBroadcast?.({
107200
+ roomId: roomId.trim(),
107201
+ serverUrl: serverUrl.trim(),
107202
+ userName: userName.trim(),
107203
+ role: "broadcaster"
107204
+ });
107205
+ setTimeout(() => {
107206
+ onStartPresenting?.();
107207
+ }, 100);
107208
+ onClose();
107209
+ }, [roomId, userName, serverUrl, onStartBroadcast, onStartPresenting, onClose]);
107210
+ const handleStopBroadcast = useCallback(() => {
107211
+ onStopBroadcast?.();
107212
+ onClose();
107213
+ }, [onStopBroadcast, onClose]);
107214
+ const canStart = roomId.trim().length > 0 && userName.trim().length > 0 && serverUrl.trim().length > 0;
106897
107215
  if (!open) {
106898
107216
  return null;
106899
107217
  }
@@ -106907,42 +107225,225 @@ function BroadcastDialog({
106907
107225
  onClick: onClose
106908
107226
  }
106909
107227
  ),
106910
- /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-[201] flex items-center justify-center pointer-events-none", children: /* @__PURE__ */ jsxs("div", { className: "pointer-events-auto w-[380px] rounded-xl border border-border bg-background shadow-2xl", children: [
106911
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-5 py-3 border-b border-border", children: [
106912
- /* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold text-foreground", children: t2("pptx.broadcast.title") }),
107228
+ /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-[201] flex items-center justify-center pointer-events-none", children: /* @__PURE__ */ jsxs(
107229
+ "div",
107230
+ {
107231
+ ref: dialogRef,
107232
+ role: "dialog",
107233
+ "aria-modal": "true",
107234
+ "aria-label": t2("pptx.broadcast.title"),
107235
+ tabIndex: -1,
107236
+ className: "pointer-events-auto w-full max-w-md rounded-xl border border-border bg-popover text-foreground shadow-2xl outline-none",
107237
+ children: [
107238
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-5 py-3 border-b border-border", children: [
107239
+ /* @__PURE__ */ jsxs("h2", { className: "text-sm font-semibold text-foreground flex items-center gap-2", children: [
107240
+ /* @__PURE__ */ jsx(LuCast, { className: "w-4 h-4" }),
107241
+ isBroadcasting ? t2("pptx.broadcast.broadcasting") : t2("pptx.broadcast.title")
107242
+ ] }),
107243
+ /* @__PURE__ */ jsx(
107244
+ "button",
107245
+ {
107246
+ type: "button",
107247
+ onClick: onClose,
107248
+ className: "text-muted-foreground hover:text-foreground text-lg leading-none",
107249
+ "aria-label": "Close",
107250
+ children: "\xD7"
107251
+ }
107252
+ )
107253
+ ] }),
107254
+ /* @__PURE__ */ jsx("div", { className: "px-5 py-4", children: isBroadcasting ? /* @__PURE__ */ jsx(
107255
+ ActiveBroadcastView,
107256
+ {
107257
+ collab,
107258
+ broadcastUrl,
107259
+ copied,
107260
+ onCopyUrl: handleCopyUrl,
107261
+ onStopBroadcast: handleStopBroadcast
107262
+ }
107263
+ ) : /* @__PURE__ */ jsx(
107264
+ StartBroadcastForm,
107265
+ {
107266
+ roomId,
107267
+ userName,
107268
+ serverUrl,
107269
+ onRoomIdChange: setRoomId,
107270
+ onUserNameChange: setUserName,
107271
+ onServerUrlChange: setServerUrl
107272
+ }
107273
+ ) }),
107274
+ !isBroadcasting && /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2 px-5 py-3 border-t border-border", children: [
107275
+ /* @__PURE__ */ jsx(
107276
+ "button",
107277
+ {
107278
+ type: "button",
107279
+ onClick: onClose,
107280
+ className: "px-3 py-1.5 rounded bg-muted hover:bg-accent text-[12px] text-foreground transition-colors",
107281
+ children: t2("common.close")
107282
+ }
107283
+ ),
107284
+ /* @__PURE__ */ jsx(
107285
+ "button",
107286
+ {
107287
+ type: "button",
107288
+ disabled: !canStart,
107289
+ onClick: handleStartBroadcast,
107290
+ className: "px-3 py-1.5 rounded bg-primary hover:bg-primary/90 text-[12px] text-primary-foreground transition-colors disabled:opacity-40 disabled:cursor-not-allowed",
107291
+ children: t2("pptx.broadcast.startBroadcast")
107292
+ }
107293
+ )
107294
+ ] })
107295
+ ]
107296
+ }
107297
+ ) })
107298
+ ] });
107299
+ }
107300
+ function StartBroadcastForm({
107301
+ roomId,
107302
+ userName,
107303
+ serverUrl,
107304
+ onRoomIdChange,
107305
+ onUserNameChange,
107306
+ onServerUrlChange
107307
+ }) {
107308
+ const { t: t2 } = useTranslation();
107309
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
107310
+ /* @__PURE__ */ jsx("p", { className: "text-[13px] text-muted-foreground leading-relaxed", children: t2("pptx.broadcast.description") }),
107311
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
107312
+ /* @__PURE__ */ jsx(
107313
+ "label",
107314
+ {
107315
+ htmlFor: "broadcast-room-id",
107316
+ className: "block text-[12px] font-medium text-foreground",
107317
+ children: t2("pptx.broadcast.sessionName")
107318
+ }
107319
+ ),
107320
+ /* @__PURE__ */ jsx(
107321
+ "input",
107322
+ {
107323
+ id: "broadcast-room-id",
107324
+ type: "text",
107325
+ value: roomId,
107326
+ onChange: (e2) => onRoomIdChange(e2.target.value),
107327
+ placeholder: "broadcast-abc123",
107328
+ className: "w-full px-3 py-1.5 rounded border border-border bg-background text-foreground text-[13px] placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary"
107329
+ }
107330
+ )
107331
+ ] }),
107332
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
107333
+ /* @__PURE__ */ jsx(
107334
+ "label",
107335
+ {
107336
+ htmlFor: "broadcast-user-name",
107337
+ className: "block text-[12px] font-medium text-foreground",
107338
+ children: t2("pptx.broadcast.displayName")
107339
+ }
107340
+ ),
107341
+ /* @__PURE__ */ jsx(
107342
+ "input",
107343
+ {
107344
+ id: "broadcast-user-name",
107345
+ type: "text",
107346
+ value: userName,
107347
+ onChange: (e2) => onUserNameChange(e2.target.value),
107348
+ placeholder: "Presenter",
107349
+ className: "w-full px-3 py-1.5 rounded border border-border bg-background text-foreground text-[13px] placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary"
107350
+ }
107351
+ )
107352
+ ] }),
107353
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
107354
+ /* @__PURE__ */ jsx(
107355
+ "label",
107356
+ {
107357
+ htmlFor: "broadcast-server-url",
107358
+ className: "block text-[12px] font-medium text-foreground",
107359
+ children: t2("pptx.broadcast.serverLabel")
107360
+ }
107361
+ ),
107362
+ /* @__PURE__ */ jsx(
107363
+ "input",
107364
+ {
107365
+ id: "broadcast-server-url",
107366
+ type: "text",
107367
+ value: serverUrl,
107368
+ onChange: (e2) => onServerUrlChange(e2.target.value),
107369
+ placeholder: "ws://localhost:1234",
107370
+ className: "w-full px-3 py-1.5 rounded border border-border bg-background text-foreground text-[13px] placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary"
107371
+ }
107372
+ )
107373
+ ] }),
107374
+ /* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground/70 leading-relaxed", children: t2("pptx.broadcast.hint") })
107375
+ ] });
107376
+ }
107377
+ function ActiveBroadcastView({
107378
+ collab,
107379
+ broadcastUrl,
107380
+ copied,
107381
+ onCopyUrl,
107382
+ onStopBroadcast
107383
+ }) {
107384
+ const { t: t2 } = useTranslation();
107385
+ const statusColor = collab.status === "connected" ? "text-green-400" : collab.status === "connecting" ? "text-yellow-400" : "text-red-400";
107386
+ const statusIcon = collab.status === "connected" || collab.status === "connecting" ? /* @__PURE__ */ jsx(LuWifi, { className: "w-4 h-4" }) : /* @__PURE__ */ jsx(LuWifiOff, { className: "w-4 h-4" });
107387
+ const viewerCount = collab.remoteUsers.filter((u2) => u2.role === "viewer").length;
107388
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
107389
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
107390
+ /* @__PURE__ */ jsx("span", { className: statusColor, children: statusIcon }),
107391
+ /* @__PURE__ */ jsx("span", { className: "text-[13px] font-medium text-foreground capitalize", children: collab.status }),
107392
+ /* @__PURE__ */ jsxs("span", { className: "text-[12px] text-muted-foreground ml-auto flex items-center gap-1", children: [
107393
+ /* @__PURE__ */ jsx(LuUsers, { className: "w-3.5 h-3.5" }),
107394
+ t2("pptx.broadcast.viewerCount", { count: viewerCount })
107395
+ ] })
107396
+ ] }),
107397
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
107398
+ /* @__PURE__ */ jsx("label", { className: "block text-[12px] font-medium text-foreground", children: t2("pptx.broadcast.viewerLink") }),
107399
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
107400
+ /* @__PURE__ */ jsx("div", { className: "flex-1 px-3 py-1.5 rounded border border-border bg-background text-[11px] text-foreground select-all font-mono truncate", children: broadcastUrl }),
106913
107401
  /* @__PURE__ */ jsx(
106914
107402
  "button",
106915
107403
  {
106916
107404
  type: "button",
106917
- onClick: onClose,
106918
- className: "text-muted-foreground hover:text-foreground text-lg leading-none",
106919
- "aria-label": "Close",
106920
- children: "\xD7"
107405
+ onClick: onCopyUrl,
107406
+ className: "flex items-center gap-1 px-2.5 py-1.5 rounded border border-border bg-muted hover:bg-accent text-[12px] text-foreground transition-colors shrink-0",
107407
+ title: t2("pptx.broadcast.copyLink"),
107408
+ children: copied ? /* @__PURE__ */ jsxs(Fragment, { children: [
107409
+ /* @__PURE__ */ jsx(LuCheck, { className: "w-3.5 h-3.5 text-green-400" }),
107410
+ /* @__PURE__ */ jsx("span", { children: t2("pptx.share.copied") })
107411
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
107412
+ /* @__PURE__ */ jsx(LuCopy, { className: "w-3.5 h-3.5" }),
107413
+ /* @__PURE__ */ jsx("span", { children: t2("pptx.share.copyUrl") })
107414
+ ] })
106921
107415
  }
106922
107416
  )
106923
107417
  ] }),
106924
- /* @__PURE__ */ jsx("div", { className: "px-5 py-6 text-center", children: /* @__PURE__ */ jsx("p", { className: "text-[13px] text-muted-foreground leading-relaxed", children: t2("pptx.broadcast.description") }) }),
106925
- /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2 px-5 py-3 border-t border-border", children: [
107418
+ /* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground", children: t2("pptx.broadcast.shareHint") })
107419
+ ] }),
107420
+ collab.remoteUsers.length > 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
107421
+ /* @__PURE__ */ jsx("label", { className: "block text-[12px] font-medium text-foreground", children: t2("pptx.broadcast.viewers") }),
107422
+ /* @__PURE__ */ jsx("div", { className: "rounded border border-border bg-background divide-y divide-border max-h-[120px] overflow-y-auto", children: collab.remoteUsers.map((user) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-3 py-2", children: [
106926
107423
  /* @__PURE__ */ jsx(
106927
- "button",
107424
+ "div",
106928
107425
  {
106929
- type: "button",
106930
- onClick: onClose,
106931
- className: "px-3 py-1.5 rounded bg-muted hover:bg-accent text-[12px] text-foreground transition-colors",
106932
- children: t2("common.close")
107426
+ className: "w-5 h-5 rounded-full flex items-center justify-center text-[8px] font-semibold text-white shrink-0",
107427
+ style: { backgroundColor: user.userColor },
107428
+ children: user.userName.slice(0, 2).toUpperCase()
106933
107429
  }
106934
107430
  ),
106935
- /* @__PURE__ */ jsx(
106936
- "button",
106937
- {
106938
- type: "button",
106939
- disabled: true,
106940
- className: "px-3 py-1.5 rounded bg-primary/40 text-[12px] text-white/50 cursor-not-allowed",
106941
- children: t2("pptx.broadcast.startBroadcast")
106942
- }
106943
- )
106944
- ] })
106945
- ] }) })
107431
+ /* @__PURE__ */ jsx("span", { className: "text-[12px] text-foreground truncate", children: user.userName }),
107432
+ /* @__PURE__ */ jsxs("span", { className: "text-[10px] text-muted-foreground ml-auto", children: [
107433
+ "Slide ",
107434
+ user.activeSlideIndex + 1
107435
+ ] })
107436
+ ] }, user.clientId)) })
107437
+ ] }),
107438
+ /* @__PURE__ */ jsx(
107439
+ "button",
107440
+ {
107441
+ type: "button",
107442
+ onClick: onStopBroadcast,
107443
+ className: "w-full px-3 py-2 rounded border border-red-500/30 bg-red-500/10 hover:bg-red-500/20 text-[12px] text-red-400 font-medium transition-colors",
107444
+ children: t2("pptx.broadcast.stopBroadcast")
107445
+ }
107446
+ )
106946
107447
  ] });
106947
107448
  }
106948
107449
  function getInitials(name) {
@@ -108526,7 +109027,8 @@ function useDrawingOverlay({
108526
109027
  drawingWidth,
108527
109028
  isDrawingRef,
108528
109029
  onAddInkElement,
108529
- onAddFreeformShape
109030
+ onAddFreeformShape,
109031
+ onEraseInkElement
108530
109032
  }) {
108531
109033
  const isDrawing = activeTool !== "select";
108532
109034
  const [currentStrokePoints, setCurrentStrokePoints] = useState(
@@ -108575,6 +109077,7 @@ function useDrawingOverlay({
108575
109077
  continue;
108576
109078
  }
108577
109079
  if (pt2.x >= el.x - HIT_RADIUS && pt2.x <= el.x + el.width + HIT_RADIUS && pt2.y >= el.y - HIT_RADIUS && pt2.y <= el.y + el.height + HIT_RADIUS) {
109080
+ onEraseInkElement?.(el.id);
108578
109081
  break;
108579
109082
  }
108580
109083
  }
@@ -108593,7 +109096,7 @@ function useDrawingOverlay({
108593
109096
  isDrawingRef.current = true;
108594
109097
  }
108595
109098
  },
108596
- [activeTool, activeSlide, pointerToCanvasCoords, isDrawingRef]
109099
+ [activeTool, activeSlide, pointerToCanvasCoords, isDrawingRef, onEraseInkElement]
108597
109100
  );
108598
109101
  const handleDrawPointerMove = useCallback(
108599
109102
  (e2) => {
@@ -108663,6 +109166,9 @@ function useDrawingOverlay({
108663
109166
  i3 === 0 ? { type: "moveTo", pt: scaledPt } : { type: "lineTo", pt: scaledPt }
108664
109167
  );
108665
109168
  }
109169
+ if (segments.length > 2) {
109170
+ segments.push({ type: "close" });
109171
+ }
108666
109172
  const freeformShape = {
108667
109173
  id: `shape-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
108668
109174
  type: "shape",
@@ -108672,7 +109178,7 @@ function useDrawingOverlay({
108672
109178
  height: h2,
108673
109179
  shapeType: "custom",
108674
109180
  shapeStyle: {
108675
- fillColor: drawingColor,
109181
+ fillColor: "transparent",
108676
109182
  strokeColor: drawingColor,
108677
109183
  strokeWidth: drawingWidth
108678
109184
  },
@@ -108822,6 +109328,7 @@ function SlideCanvas({
108822
109328
  isDrawingRef,
108823
109329
  onAddInkElement,
108824
109330
  onAddFreeformShape,
109331
+ onEraseInkElement,
108825
109332
  onActionClick,
108826
109333
  onHyperlinkClick,
108827
109334
  comments,
@@ -108907,7 +109414,8 @@ function SlideCanvas({
108907
109414
  drawingWidth,
108908
109415
  isDrawingRef,
108909
109416
  onAddInkElement,
108910
- onAddFreeformShape
109417
+ onAddFreeformShape,
109418
+ onEraseInkElement
108911
109419
  });
108912
109420
  const rulerOffset = showRulers ? RULER_THICKNESS : 0;
108913
109421
  return /* @__PURE__ */ jsx(
@@ -109403,6 +109911,10 @@ function ViewerToolbarSection(props) {
109403
109911
  onSetMode,
109404
109912
  onToggleSidebar: () => s.setIsSlidesPaneOpen((p3) => !p3),
109405
109913
  onToggleInspector: () => s.setIsInspectorPaneOpen((p3) => !p3),
109914
+ onOpenAnimationPanel: () => {
109915
+ s.setIsInspectorPaneOpen(true);
109916
+ s.setSidebarPanelMode("properties");
109917
+ },
109406
109918
  onToggleCompactToolbar: () => s.setIsCompactToolbarOpen((p3) => !p3),
109407
109919
  onSetToolbarSection: s.setToolbarSection,
109408
109920
  onZoomIn: zoom.handleZoomIn,
@@ -111074,13 +111586,6 @@ function ViewerDialogGroup(props) {
111074
111586
  slideCount: slides.length
111075
111587
  }
111076
111588
  ),
111077
- /* @__PURE__ */ jsx(
111078
- BroadcastDialog,
111079
- {
111080
- open: dialogs.isBroadcastDialogOpen,
111081
- onClose: () => dialogs.setIsBroadcastDialogOpen(false)
111082
- }
111083
- ),
111084
111589
  /* @__PURE__ */ jsx(
111085
111590
  PrintDialog,
111086
111591
  {
@@ -111323,6 +111828,7 @@ function ViewerCanvasArea(props) {
111323
111828
  isDrawingRef: s.isDrawingRef,
111324
111829
  onAddInkElement: insertHandlers.handleAddInkElement,
111325
111830
  onAddFreeformShape: insertHandlers.handleAddFreeformShape,
111831
+ onEraseInkElement: insertHandlers.handleEraseInkElement,
111326
111832
  onActionClick: handleActionClick,
111327
111833
  onHyperlinkClick: handleHyperlinkClick,
111328
111834
  allSlides: mode === "present" ? slides : void 0,
@@ -111330,6 +111836,24 @@ function ViewerCanvasArea(props) {
111330
111836
  sourceSlideIndex: mode === "present" ? activeSlideIndex : void 0,
111331
111837
  fieldContext,
111332
111838
  tableStyleContext,
111839
+ collaborationOverlay: /* @__PURE__ */ jsxs(Fragment, { children: [
111840
+ /* @__PURE__ */ jsx(
111841
+ RemoteSelectionOverlay,
111842
+ {
111843
+ elements: effectiveSlide?.elements ?? [],
111844
+ activeSlideIndex
111845
+ }
111846
+ ),
111847
+ /* @__PURE__ */ jsx(
111848
+ CollaborationCursorOverlay,
111849
+ {
111850
+ activeSlideIndex,
111851
+ canvasWidth: canvasSize.width,
111852
+ canvasHeight: canvasSize.height,
111853
+ selectedElementId: s.selectedElementId
111854
+ }
111855
+ )
111856
+ ] }),
111333
111857
  comments: activeSlide?.comments,
111334
111858
  showCommentMarkers: s.sidebarPanelMode === "comments",
111335
111859
  onCommentMarkerClick: () => s.setSidebarPanelMode("comments"),
@@ -112911,6 +113435,37 @@ function useYjsDocumentSync({
112911
113435
  };
112912
113436
  }, [doc2, isConnected, getDocMap, setSlides]);
112913
113437
  }
113438
+ function useBroadcastFollower({
113439
+ collab,
113440
+ activeSlideIndex,
113441
+ setActiveSlideIndex,
113442
+ slideCount
113443
+ }) {
113444
+ const lastBroadcasterSlide = useRef(-1);
113445
+ useEffect(() => {
113446
+ if (!collab) {
113447
+ return;
113448
+ }
113449
+ if (collab.config.role !== "viewer") {
113450
+ return;
113451
+ }
113452
+ const broadcaster = collab.remoteUsers.find((u2) => u2.role === "broadcaster");
113453
+ if (!broadcaster) {
113454
+ return;
113455
+ }
113456
+ const targetSlide = broadcaster.activeSlideIndex;
113457
+ if (targetSlide < 0 || targetSlide >= slideCount) {
113458
+ return;
113459
+ }
113460
+ if (targetSlide === lastBroadcasterSlide.current) {
113461
+ return;
113462
+ }
113463
+ lastBroadcasterSlide.current = targetSlide;
113464
+ if (targetSlide !== activeSlideIndex) {
113465
+ setActiveSlideIndex(targetSlide);
113466
+ }
113467
+ }, [collab, activeSlideIndex, setActiveSlideIndex, slideCount]);
113468
+ }
112914
113469
  function computeGridSpacingPx(presentationGridSpacing) {
112915
113470
  if (presentationGridSpacing) {
112916
113471
  const px2 = Math.round(presentationGridSpacing.cx / EMU_PER_PX);
@@ -115251,6 +115806,17 @@ function useInsertElements(input) {
115251
115806
  }
115252
115807
  addElement(shape);
115253
115808
  };
115809
+ const handleEraseInkElement = (elementId) => {
115810
+ if (!activeSlide) {
115811
+ return;
115812
+ }
115813
+ ops.updateSlides(
115814
+ (prev) => prev.map(
115815
+ (s, i3) => i3 === activeSlideIndex ? { ...s, elements: s.elements.filter((el) => el.id !== elementId) } : s
115816
+ )
115817
+ );
115818
+ history.markDirty();
115819
+ };
115254
115820
  return {
115255
115821
  handleAddTextBox,
115256
115822
  handleAddShape,
@@ -115258,6 +115824,7 @@ function useInsertElements(input) {
115258
115824
  ...structured,
115259
115825
  handleAddInkElement,
115260
115826
  handleAddFreeformShape,
115827
+ handleEraseInkElement,
115261
115828
  ...fileHandlers
115262
115829
  };
115263
115830
  }
@@ -116087,7 +116654,8 @@ function useEditorOperations(input) {
116087
116654
  selectedElementIds,
116088
116655
  canvasSize,
116089
116656
  dialogs,
116090
- presentation
116657
+ presentation,
116658
+ userName
116091
116659
  } = input;
116092
116660
  const ops = useElementOperations({
116093
116661
  activeSlide,
@@ -116121,6 +116689,7 @@ function useEditorOperations(input) {
116121
116689
  const comments = useComments({
116122
116690
  slides,
116123
116691
  canEdit,
116692
+ userName,
116124
116693
  selectedElementId: state2.selectedElementId,
116125
116694
  onUpdateSlides: ops.updateSlides,
116126
116695
  onMarkDirty: history.markDirty
@@ -122867,6 +123436,7 @@ var PowerPointViewer = forwardRef(
122867
123436
  onDirtyChange,
122868
123437
  onActiveSlideChange,
122869
123438
  theme,
123439
+ authorName,
122870
123440
  collaboration,
122871
123441
  onStartCollaboration,
122872
123442
  onStopCollaboration,
@@ -123024,7 +123594,8 @@ var PowerPointViewer = forwardRef(
123024
123594
  selectedElementIds,
123025
123595
  canvasSize,
123026
123596
  dialogs,
123027
- presentation
123597
+ presentation,
123598
+ userName: authorName ?? collaboration?.userName
123028
123599
  });
123029
123600
  const {
123030
123601
  exportHandlers,
@@ -123227,6 +123798,19 @@ var PowerPointViewer = forwardRef(
123227
123798
  defaultServerUrl: shareDefaults?.serverUrl
123228
123799
  }
123229
123800
  ),
123801
+ /* @__PURE__ */ jsx(
123802
+ BroadcastDialog,
123803
+ {
123804
+ open: dialogs.isBroadcastDialogOpen,
123805
+ onClose: () => dialogs.setIsBroadcastDialogOpen(false),
123806
+ onStartBroadcast: onStartCollaboration,
123807
+ onStopBroadcast: onStopCollaboration,
123808
+ onStartPresenting: () => handleSetMode("present"),
123809
+ defaultRoomId: shareDefaults?.roomId,
123810
+ defaultUserName: shareDefaults?.userName,
123811
+ defaultServerUrl: shareDefaults?.serverUrl
123812
+ }
123813
+ ),
123230
123814
  /* @__PURE__ */ jsx(
123231
123815
  ViewerOverlays,
123232
123816
  {
@@ -123276,6 +123860,14 @@ var PowerPointViewer = forwardRef(
123276
123860
  canvasHeight: canvasSize.height,
123277
123861
  children: [
123278
123862
  /* @__PURE__ */ jsx(CollaborationDocumentSync, { slides, setSlides: state2.setSlides }),
123863
+ /* @__PURE__ */ jsx(
123864
+ BroadcastFollowerSync,
123865
+ {
123866
+ activeSlideIndex,
123867
+ setActiveSlideIndex: state2.setActiveSlideIndex,
123868
+ slideCount: slides.length
123869
+ }
123870
+ ),
123279
123871
  viewerContent
123280
123872
  ]
123281
123873
  }
@@ -123303,6 +123895,20 @@ function CollaborationDocumentSync({
123303
123895
  });
123304
123896
  return null;
123305
123897
  }
123898
+ function BroadcastFollowerSync({
123899
+ activeSlideIndex,
123900
+ setActiveSlideIndex,
123901
+ slideCount
123902
+ }) {
123903
+ const collab = useCollaboration();
123904
+ useBroadcastFollower({
123905
+ collab,
123906
+ activeSlideIndex,
123907
+ setActiveSlideIndex,
123908
+ slideCount
123909
+ });
123910
+ return null;
123911
+ }
123306
123912
  /*! Bundled license information:
123307
123913
 
123308
123914
  three/build/three.core.js: