pptx-react-viewer 1.0.11 → 1.1.1

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.
@@ -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, applyThemeToData, THEME_PRESETS } 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, useMemo42 = 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, useMemo42 = 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;
@@ -91091,6 +91091,637 @@ function useVirtualizedSlides({
91091
91091
  scrollToIndex
91092
91092
  };
91093
91093
  }
91094
+
91095
+ // src/viewer/hooks/collaboration/sanitize.ts
91096
+ var ROOM_ID_REGEX = /^[a-zA-Z0-9_-]{1,128}$/;
91097
+ function validateRoomId(roomId) {
91098
+ if (!ROOM_ID_REGEX.test(roomId)) {
91099
+ throw new Error(
91100
+ `Invalid collaboration room ID: "${roomId}". Must be 1-128 alphanumeric characters, hyphens, or underscores.`
91101
+ );
91102
+ }
91103
+ return roomId;
91104
+ }
91105
+ function sanitizeUserName(name) {
91106
+ if (typeof name !== "string") {
91107
+ return "Anonymous";
91108
+ }
91109
+ const stripped = name.replace(/<[^>]*>/g, "");
91110
+ const trimmed = stripped.trim().slice(0, 64);
91111
+ return trimmed || "Anonymous";
91112
+ }
91113
+ function clampCursorPosition(value, min2, max2) {
91114
+ if (typeof value !== "number" || !Number.isFinite(value)) {
91115
+ return 0;
91116
+ }
91117
+ const margin = 20;
91118
+ return Math.max(min2 - margin, Math.min(max2 + margin, value));
91119
+ }
91120
+ var HEX_COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
91121
+ function sanitizeColor(color, fallback = "#6366f1") {
91122
+ if (typeof color !== "string") {
91123
+ return fallback;
91124
+ }
91125
+ return HEX_COLOR_REGEX.test(color) ? color : fallback;
91126
+ }
91127
+ function sanitizeAvatarUrl(url) {
91128
+ if (typeof url !== "string") {
91129
+ return void 0;
91130
+ }
91131
+ try {
91132
+ const parsed = new URL(url);
91133
+ if (parsed.protocol === "https:" || parsed.protocol === "http:" || parsed.protocol === "data:") {
91134
+ return url;
91135
+ }
91136
+ } catch {
91137
+ }
91138
+ return void 0;
91139
+ }
91140
+ function sanitizeSlideIndex(value) {
91141
+ if (typeof value !== "number" || !Number.isFinite(value)) {
91142
+ return 0;
91143
+ }
91144
+ return Math.max(0, Math.floor(value));
91145
+ }
91146
+ function sanitizePresence(raw, canvasWidth, canvasHeight) {
91147
+ if (typeof raw.clientId !== "number") {
91148
+ return null;
91149
+ }
91150
+ return {
91151
+ clientId: raw.clientId,
91152
+ userName: sanitizeUserName(raw.userName),
91153
+ userAvatar: sanitizeAvatarUrl(raw.userAvatar),
91154
+ userColor: sanitizeColor(raw.userColor),
91155
+ activeSlideIndex: sanitizeSlideIndex(raw.activeSlideIndex),
91156
+ cursorX: clampCursorPosition(raw.cursorX, 0, canvasWidth),
91157
+ cursorY: clampCursorPosition(raw.cursorY, 0, canvasHeight),
91158
+ lastUpdated: typeof raw.lastUpdated === "string" ? raw.lastUpdated : (/* @__PURE__ */ new Date()).toISOString(),
91159
+ selectedElementId: typeof raw.selectedElementId === "string" ? raw.selectedElementId.slice(0, 128) : void 0,
91160
+ role: raw.role === "broadcaster" || raw.role === "viewer" || raw.role === "collaborator" ? raw.role : void 0
91161
+ };
91162
+ }
91163
+ var BROADCAST_THROTTLE_MS = 50;
91164
+ var STALE_PRESENCE_MS = 3e4;
91165
+ function usePresenceTracking({
91166
+ awareness,
91167
+ localClientId,
91168
+ userName,
91169
+ userColor,
91170
+ userAvatar,
91171
+ role,
91172
+ canvasWidth,
91173
+ canvasHeight
91174
+ }) {
91175
+ const [remoteUsers, setRemoteUsers] = useState([]);
91176
+ const lastBroadcastRef = useRef(0);
91177
+ const pendingBroadcastRef = useRef(null);
91178
+ const latestLocalState = useRef({});
91179
+ const broadcastPresence = useCallback(
91180
+ (update2) => {
91181
+ if (!awareness) {
91182
+ return;
91183
+ }
91184
+ Object.assign(latestLocalState.current, update2);
91185
+ const now = Date.now();
91186
+ const elapsed = now - lastBroadcastRef.current;
91187
+ const flush = () => {
91188
+ const state2 = {
91189
+ ...latestLocalState.current,
91190
+ userName,
91191
+ userColor,
91192
+ userAvatar,
91193
+ role,
91194
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
91195
+ };
91196
+ awareness.setLocalStateField("presence", state2);
91197
+ lastBroadcastRef.current = Date.now();
91198
+ };
91199
+ if (elapsed >= BROADCAST_THROTTLE_MS) {
91200
+ if (pendingBroadcastRef.current) {
91201
+ clearTimeout(pendingBroadcastRef.current);
91202
+ pendingBroadcastRef.current = null;
91203
+ }
91204
+ flush();
91205
+ } else if (!pendingBroadcastRef.current) {
91206
+ pendingBroadcastRef.current = setTimeout(() => {
91207
+ pendingBroadcastRef.current = null;
91208
+ flush();
91209
+ }, BROADCAST_THROTTLE_MS - elapsed);
91210
+ }
91211
+ },
91212
+ [awareness, userName, userColor, userAvatar, role]
91213
+ );
91214
+ useEffect(() => {
91215
+ if (!awareness) {
91216
+ return;
91217
+ }
91218
+ awareness.setLocalStateField("presence", {
91219
+ userName,
91220
+ userColor,
91221
+ userAvatar,
91222
+ role,
91223
+ activeSlideIndex: 0,
91224
+ cursorX: 0,
91225
+ cursorY: 0,
91226
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
91227
+ });
91228
+ }, [awareness, userName, userColor, userAvatar, role]);
91229
+ useEffect(() => {
91230
+ if (!awareness || localClientId === null) {
91231
+ return;
91232
+ }
91233
+ const handleChange = () => {
91234
+ const now = Date.now();
91235
+ const states = awareness.getStates();
91236
+ const users = [];
91237
+ states.forEach((state2, cid) => {
91238
+ if (cid === localClientId) {
91239
+ return;
91240
+ }
91241
+ const raw = state2?.presence;
91242
+ if (!raw || typeof raw !== "object") {
91243
+ return;
91244
+ }
91245
+ const sanitized = sanitizePresence({ ...raw, clientId: cid }, canvasWidth, canvasHeight);
91246
+ if (!sanitized) {
91247
+ return;
91248
+ }
91249
+ const updatedAt = new Date(sanitized.lastUpdated).getTime();
91250
+ if (Number.isNaN(updatedAt) || now - updatedAt > STALE_PRESENCE_MS) {
91251
+ return;
91252
+ }
91253
+ users.push(sanitized);
91254
+ });
91255
+ setRemoteUsers(users);
91256
+ };
91257
+ awareness.on("change", handleChange);
91258
+ awareness.on("update", handleChange);
91259
+ handleChange();
91260
+ return () => {
91261
+ awareness.off("change", handleChange);
91262
+ awareness.off("update", handleChange);
91263
+ };
91264
+ }, [awareness, localClientId, canvasWidth, canvasHeight]);
91265
+ useEffect(() => {
91266
+ if (!awareness) {
91267
+ return;
91268
+ }
91269
+ const interval = setInterval(() => {
91270
+ awareness.setLocalStateField("presence", {
91271
+ ...latestLocalState.current,
91272
+ userName,
91273
+ userColor,
91274
+ userAvatar,
91275
+ role,
91276
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
91277
+ });
91278
+ }, 1e4);
91279
+ return () => clearInterval(interval);
91280
+ }, [awareness, userName, userColor, userAvatar, role]);
91281
+ useEffect(() => {
91282
+ return () => {
91283
+ if (pendingBroadcastRef.current) {
91284
+ clearTimeout(pendingBroadcastRef.current);
91285
+ }
91286
+ };
91287
+ }, []);
91288
+ return { remoteUsers, broadcastPresence };
91289
+ }
91290
+ function useYjsProvider({ config }) {
91291
+ const [status, setStatus] = useState("disconnected");
91292
+ const [awareness, setAwareness] = useState(null);
91293
+ const [doc2, setDoc] = useState(null);
91294
+ const [clientId, setClientId] = useState(null);
91295
+ const cleanupRef = useRef(null);
91296
+ const init = useCallback(async () => {
91297
+ const roomId = validateRoomId(config.roomId);
91298
+ setStatus("connecting");
91299
+ try {
91300
+ const [Y, { WebsocketProvider: WebsocketProvider2 }] = await Promise.all([Promise.resolve().then(() => (init_yjs(), yjs_exports)), Promise.resolve().then(() => (init_y_websocket(), y_websocket_exports))]);
91301
+ const yDoc = new Y.Doc();
91302
+ const provider = new WebsocketProvider2(
91303
+ config.serverUrl,
91304
+ roomId,
91305
+ yDoc,
91306
+ // eslint-disable-line @typescript-eslint/no-explicit-any
91307
+ {
91308
+ params: config.authToken ? { token: config.authToken } : void 0
91309
+ }
91310
+ );
91311
+ const handleStatus = (event) => {
91312
+ if (event.status === "connected") {
91313
+ setStatus("connected");
91314
+ } else if (event.status === "disconnected") {
91315
+ setStatus("disconnected");
91316
+ }
91317
+ };
91318
+ provider.on("status", handleStatus);
91319
+ if (provider.wsconnected) {
91320
+ setStatus("connected");
91321
+ }
91322
+ setDoc(yDoc);
91323
+ setAwareness(provider.awareness);
91324
+ setClientId(provider.awareness.clientID);
91325
+ cleanupRef.current = () => {
91326
+ provider.off("status", handleStatus);
91327
+ provider.destroy();
91328
+ yDoc.destroy();
91329
+ setDoc(null);
91330
+ setAwareness(null);
91331
+ setClientId(null);
91332
+ setStatus("disconnected");
91333
+ };
91334
+ } catch (err) {
91335
+ console.warn(
91336
+ "[pptx-viewer] Collaboration packages not available:",
91337
+ err instanceof Error ? err.message : err
91338
+ );
91339
+ setStatus("error");
91340
+ }
91341
+ }, [config.roomId, config.serverUrl, config.authToken]);
91342
+ useEffect(() => {
91343
+ init();
91344
+ return () => {
91345
+ cleanupRef.current?.();
91346
+ cleanupRef.current = null;
91347
+ };
91348
+ }, [init]);
91349
+ return { status, awareness, doc: doc2, clientId };
91350
+ }
91351
+
91352
+ // src/viewer/hooks/collaboration/useCollaborativeState.ts
91353
+ function useCollaborativeState({
91354
+ config,
91355
+ canvasWidth,
91356
+ canvasHeight
91357
+ }) {
91358
+ const userColor = sanitizeColor(config.userColor, "#6366f1");
91359
+ const { status, awareness, doc: doc2, clientId } = useYjsProvider({ config });
91360
+ const { remoteUsers, broadcastPresence } = usePresenceTracking({
91361
+ awareness,
91362
+ localClientId: clientId,
91363
+ userName: config.userName,
91364
+ userColor,
91365
+ userAvatar: config.userAvatar,
91366
+ role: config.role,
91367
+ canvasWidth,
91368
+ canvasHeight
91369
+ });
91370
+ const connectedCount = status === "connected" ? remoteUsers.length + 1 : remoteUsers.length;
91371
+ return {
91372
+ status,
91373
+ remoteUsers,
91374
+ broadcastPresence,
91375
+ connectedCount,
91376
+ config,
91377
+ doc: doc2
91378
+ };
91379
+ }
91380
+ var CollaborationContext = createContext(null);
91381
+ function useCollaboration() {
91382
+ return useContext(CollaborationContext);
91383
+ }
91384
+ function CollaborationProvider({
91385
+ config,
91386
+ canvasWidth,
91387
+ canvasHeight,
91388
+ children
91389
+ }) {
91390
+ const value = useCollaborativeState({
91391
+ config,
91392
+ canvasWidth,
91393
+ canvasHeight
91394
+ });
91395
+ return /* @__PURE__ */ jsx(CollaborationContext.Provider, { value, children });
91396
+ }
91397
+ function RemoteUserCursors({
91398
+ remoteUsers,
91399
+ activeSlideIndex,
91400
+ canvasWidth,
91401
+ canvasHeight
91402
+ }) {
91403
+ const visibleUsers = remoteUsers.filter((u2) => u2.activeSlideIndex === activeSlideIndex);
91404
+ if (visibleUsers.length === 0) {
91405
+ return null;
91406
+ }
91407
+ return /* @__PURE__ */ jsx(
91408
+ "svg",
91409
+ {
91410
+ "data-testid": "remote-user-cursors",
91411
+ "data-export-ignore": "true",
91412
+ className: "absolute inset-0 pointer-events-none",
91413
+ style: { zIndex: 9999 },
91414
+ width: canvasWidth,
91415
+ height: canvasHeight,
91416
+ viewBox: `0 0 ${canvasWidth} ${canvasHeight}`,
91417
+ "aria-hidden": "true",
91418
+ children: visibleUsers.map((user) => /* @__PURE__ */ jsxs(
91419
+ "g",
91420
+ {
91421
+ transform: `translate(${user.cursorX}, ${user.cursorY})`,
91422
+ "data-testid": `remote-cursor-${user.clientId}`,
91423
+ children: [
91424
+ /* @__PURE__ */ jsx(
91425
+ "path",
91426
+ {
91427
+ d: "M0 0 L0 16 L4.5 12.5 L8 20 L10.5 19 L7 11.5 L12 11 Z",
91428
+ fill: user.userColor,
91429
+ stroke: "#fff",
91430
+ strokeWidth: 1,
91431
+ opacity: 0.9
91432
+ }
91433
+ ),
91434
+ /* @__PURE__ */ jsxs("g", { transform: "translate(14, 18)", children: [
91435
+ /* @__PURE__ */ jsx(
91436
+ "rect",
91437
+ {
91438
+ rx: 3,
91439
+ ry: 3,
91440
+ x: -2,
91441
+ y: -10,
91442
+ width: Math.min(user.userName.length * 7 + 8, 150),
91443
+ height: 16,
91444
+ fill: user.userColor,
91445
+ opacity: 0.85
91446
+ }
91447
+ ),
91448
+ /* @__PURE__ */ jsx(
91449
+ "text",
91450
+ {
91451
+ fill: "#fff",
91452
+ fontSize: 10,
91453
+ fontFamily: "system-ui, sans-serif",
91454
+ fontWeight: 500,
91455
+ dominantBaseline: "central",
91456
+ y: -2,
91457
+ x: 2,
91458
+ children: user.userName.length > 20 ? `${user.userName.slice(0, 18)}...` : user.userName
91459
+ }
91460
+ )
91461
+ ] })
91462
+ ]
91463
+ },
91464
+ user.clientId
91465
+ ))
91466
+ }
91467
+ );
91468
+ }
91469
+ function getInitials(name) {
91470
+ const parts = name.trim().split(/\s+/);
91471
+ if (parts.length >= 2) {
91472
+ return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
91473
+ }
91474
+ return name.slice(0, 2).toUpperCase();
91475
+ }
91476
+ function AvatarCircle({
91477
+ name,
91478
+ color,
91479
+ avatar,
91480
+ isLocal
91481
+ }) {
91482
+ const { t: t2 } = useTranslation();
91483
+ const initials = getInitials(name);
91484
+ const title = isLocal ? t2("pptx.collaboration.youLabel", { name }) : name;
91485
+ return /* @__PURE__ */ jsx(
91486
+ "div",
91487
+ {
91488
+ className: "relative w-7 h-7 rounded-full flex items-center justify-center text-[10px] font-semibold text-white border-2 -ml-1 first:ml-0",
91489
+ style: {
91490
+ backgroundColor: color,
91491
+ borderColor: isLocal ? "#fff" : color
91492
+ },
91493
+ title,
91494
+ "aria-label": title,
91495
+ children: avatar ? /* @__PURE__ */ jsx(
91496
+ "img",
91497
+ {
91498
+ src: avatar,
91499
+ alt: "",
91500
+ className: "w-full h-full rounded-full object-cover",
91501
+ onError: (e2) => {
91502
+ e2.target.style.display = "none";
91503
+ }
91504
+ }
91505
+ ) : initials
91506
+ }
91507
+ );
91508
+ }
91509
+ function UserAvatarBar({
91510
+ remoteUsers,
91511
+ localUserName,
91512
+ localUserColor,
91513
+ localUserAvatar,
91514
+ status,
91515
+ maxVisible = 5
91516
+ }) {
91517
+ const { t: t2 } = useTranslation();
91518
+ if (status === "disconnected" || status === "error") {
91519
+ return null;
91520
+ }
91521
+ const allUsers = [
91522
+ { name: localUserName, color: localUserColor, avatar: localUserAvatar, isLocal: true },
91523
+ ...remoteUsers.map((u2) => ({
91524
+ name: u2.userName,
91525
+ color: u2.userColor,
91526
+ avatar: u2.userAvatar,
91527
+ isLocal: false
91528
+ }))
91529
+ ];
91530
+ const visible = allUsers.slice(0, maxVisible);
91531
+ const overflow = allUsers.length - maxVisible;
91532
+ return /* @__PURE__ */ jsxs(
91533
+ "div",
91534
+ {
91535
+ "data-testid": "user-avatar-bar",
91536
+ className: "flex items-center px-2",
91537
+ "aria-label": t2("pptx.collaboration.usersConnected", { count: allUsers.length }),
91538
+ children: [
91539
+ visible.map((user, i3) => /* @__PURE__ */ jsx(
91540
+ AvatarCircle,
91541
+ {
91542
+ name: user.name,
91543
+ color: user.color,
91544
+ avatar: user.avatar,
91545
+ isLocal: user.isLocal
91546
+ },
91547
+ user.isLocal ? "local" : `remote-${i3}`
91548
+ )),
91549
+ overflow > 0 && /* @__PURE__ */ jsxs(
91550
+ "div",
91551
+ {
91552
+ className: "w-7 h-7 rounded-full flex items-center justify-center text-[10px] font-semibold text-gray-300 bg-gray-700 border-2 border-gray-600 -ml-1",
91553
+ title: t2("pptx.collaboration.moreUsers", { count: overflow }),
91554
+ children: [
91555
+ "+",
91556
+ overflow
91557
+ ]
91558
+ }
91559
+ )
91560
+ ]
91561
+ }
91562
+ );
91563
+ }
91564
+ var STATUS_STYLES = {
91565
+ connected: {
91566
+ dot: "bg-green-400",
91567
+ text: "text-green-400",
91568
+ label: "Connected"
91569
+ },
91570
+ connecting: {
91571
+ dot: "bg-yellow-400 animate-pulse",
91572
+ text: "text-yellow-400",
91573
+ label: "Connecting..."
91574
+ },
91575
+ disconnected: {
91576
+ dot: "bg-gray-500",
91577
+ text: "text-gray-500",
91578
+ label: "Disconnected"
91579
+ },
91580
+ error: {
91581
+ dot: "bg-red-400",
91582
+ text: "text-red-400",
91583
+ label: "Connection error"
91584
+ }
91585
+ };
91586
+ function CollaborationStatusIndicator({
91587
+ status,
91588
+ connectedCount
91589
+ }) {
91590
+ const { t: t2 } = useTranslation();
91591
+ const style = STATUS_STYLES[status];
91592
+ return /* @__PURE__ */ jsxs(
91593
+ "div",
91594
+ {
91595
+ "data-testid": "collaboration-status",
91596
+ className: "flex items-center gap-1.5",
91597
+ "aria-label": t2("pptx.collaboration.statusAriaLabel", {
91598
+ status: t2(`pptx.collaboration.status.${status}`),
91599
+ count: connectedCount
91600
+ }),
91601
+ children: [
91602
+ /* @__PURE__ */ jsx("span", { className: `inline-block w-2 h-2 rounded-full ${style.dot}`, "aria-hidden": "true" }),
91603
+ /* @__PURE__ */ jsx("span", { className: `text-[10px] ${style.text}`, children: status === "connected" ? t2("pptx.collaboration.userCount", { count: connectedCount }) : t2(`pptx.collaboration.status.${status}`) })
91604
+ ]
91605
+ }
91606
+ );
91607
+ }
91608
+ function CollaborationCursorOverlay({
91609
+ activeSlideIndex,
91610
+ canvasWidth,
91611
+ canvasHeight,
91612
+ selectedElementId
91613
+ }) {
91614
+ const collab = useCollaboration();
91615
+ const containerRef = useRef(null);
91616
+ const prevSelectionRef = useRef(selectedElementId);
91617
+ useEffect(() => {
91618
+ if (!collab || selectedElementId === prevSelectionRef.current) {
91619
+ return;
91620
+ }
91621
+ prevSelectionRef.current = selectedElementId;
91622
+ collab.broadcastPresence({
91623
+ selectedElementId: selectedElementId ?? void 0,
91624
+ activeSlideIndex
91625
+ });
91626
+ }, [collab, selectedElementId, activeSlideIndex]);
91627
+ useEffect(() => {
91628
+ if (!collab) {
91629
+ return;
91630
+ }
91631
+ const parent = containerRef.current?.parentElement;
91632
+ if (!parent) {
91633
+ return;
91634
+ }
91635
+ const handler = (e2) => {
91636
+ const rect = parent.getBoundingClientRect();
91637
+ const x2 = (e2.clientX - rect.left) / rect.width * canvasWidth;
91638
+ const y = (e2.clientY - rect.top) / rect.height * canvasHeight;
91639
+ collab.broadcastPresence({
91640
+ cursorX: x2,
91641
+ cursorY: y,
91642
+ activeSlideIndex
91643
+ });
91644
+ };
91645
+ parent.addEventListener("pointermove", handler);
91646
+ return () => parent.removeEventListener("pointermove", handler);
91647
+ }, [collab, canvasWidth, canvasHeight, activeSlideIndex]);
91648
+ if (!collab) {
91649
+ return null;
91650
+ }
91651
+ return /* @__PURE__ */ jsx(
91652
+ "div",
91653
+ {
91654
+ ref: containerRef,
91655
+ "data-testid": "collab-pointer-tracker",
91656
+ "data-export-ignore": "true",
91657
+ style: { display: "contents" },
91658
+ children: /* @__PURE__ */ jsx(
91659
+ RemoteUserCursors,
91660
+ {
91661
+ remoteUsers: collab.remoteUsers,
91662
+ activeSlideIndex,
91663
+ canvasWidth,
91664
+ canvasHeight
91665
+ }
91666
+ )
91667
+ }
91668
+ );
91669
+ }
91670
+ function RemoteSelectionOverlay({
91671
+ elements,
91672
+ activeSlideIndex
91673
+ }) {
91674
+ const collab = useCollaboration();
91675
+ if (!collab) {
91676
+ return null;
91677
+ }
91678
+ const elementMap = /* @__PURE__ */ new Map();
91679
+ for (const el of elements) {
91680
+ elementMap.set(el.id, el);
91681
+ }
91682
+ const selections = [];
91683
+ for (const user of collab.remoteUsers) {
91684
+ if (user.activeSlideIndex === activeSlideIndex && user.selectedElementId) {
91685
+ const el = elementMap.get(user.selectedElementId);
91686
+ if (el) {
91687
+ selections.push({
91688
+ userName: user.userName,
91689
+ userColor: user.userColor,
91690
+ element: el
91691
+ });
91692
+ }
91693
+ }
91694
+ }
91695
+ if (selections.length === 0) {
91696
+ return null;
91697
+ }
91698
+ return /* @__PURE__ */ jsx(Fragment, { children: selections.map((sel) => /* @__PURE__ */ jsx(
91699
+ "div",
91700
+ {
91701
+ "data-testid": `remote-selection-${sel.element.id}`,
91702
+ "data-export-ignore": "true",
91703
+ className: "absolute pointer-events-none",
91704
+ style: {
91705
+ left: sel.element.x,
91706
+ top: sel.element.y,
91707
+ width: sel.element.width,
91708
+ height: sel.element.height,
91709
+ zIndex: 9997,
91710
+ border: `2px solid ${sel.userColor}`,
91711
+ borderRadius: 2
91712
+ },
91713
+ children: /* @__PURE__ */ jsx(
91714
+ "span",
91715
+ {
91716
+ className: "absolute -top-5 left-0 px-1 py-0.5 text-[9px] font-medium text-white rounded-sm whitespace-nowrap leading-none",
91717
+ style: { backgroundColor: sel.userColor },
91718
+ children: sel.userName
91719
+ }
91720
+ )
91721
+ },
91722
+ `remote-sel-${sel.element.id}`
91723
+ )) });
91724
+ }
91094
91725
  function SectionContextMenu({
91095
91726
  state: state2,
91096
91727
  sectionGroups,
@@ -91372,6 +92003,7 @@ function SlideItemInner({
91372
92003
  canvasSize,
91373
92004
  canEdit,
91374
92005
  rehearsalTimings,
92006
+ presenceUsers,
91375
92007
  onSelectSlide,
91376
92008
  onSlideContextMenu,
91377
92009
  onAddSection,
@@ -91414,16 +92046,27 @@ function SlideItemInner({
91414
92046
  onDragOver,
91415
92047
  onDrop: (e2) => onDrop(e2, slideIndex),
91416
92048
  children: [
91417
- /* @__PURE__ */ jsx(
91418
- "span",
91419
- {
91420
- className: cn(
91421
- "text-[10px] tabular-nums w-5 text-right shrink-0 select-none",
91422
- isActive ? "text-primary font-medium" : "text-muted-foreground"
91423
- ),
91424
- children: slideIndex + 1
91425
- }
91426
- ),
92049
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-0.5 w-5 shrink-0", children: [
92050
+ /* @__PURE__ */ jsx(
92051
+ "span",
92052
+ {
92053
+ className: cn(
92054
+ "text-[10px] tabular-nums text-right select-none w-full",
92055
+ isActive ? "text-primary font-medium" : "text-muted-foreground"
92056
+ ),
92057
+ children: slideIndex + 1
92058
+ }
92059
+ ),
92060
+ 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(
92061
+ "span",
92062
+ {
92063
+ className: "w-[6px] h-[6px] rounded-full",
92064
+ style: { backgroundColor: u2.userColor },
92065
+ title: u2.userName
92066
+ },
92067
+ i3
92068
+ )) })
92069
+ ] }),
91427
92070
  /* @__PURE__ */ jsxs(
91428
92071
  "div",
91429
92072
  {
@@ -91573,8 +92216,26 @@ function SlidesPaneSidebar({
91573
92216
  panelWidth
91574
92217
  }) {
91575
92218
  const { t: t2 } = useTranslation();
92219
+ const collab = useCollaboration();
91576
92220
  const slideRefs = useRef(/* @__PURE__ */ new Map());
91577
92221
  const renameInputRef = useRef(null);
92222
+ const slidePresenceMap = useMemo(() => {
92223
+ if (!collab || collab.remoteUsers.length === 0) {
92224
+ return void 0;
92225
+ }
92226
+ const map3 = /* @__PURE__ */ new Map();
92227
+ for (const user of collab.remoteUsers) {
92228
+ const idx = user.activeSlideIndex;
92229
+ const existing = map3.get(idx);
92230
+ const entry = { userName: user.userName, userColor: user.userColor };
92231
+ if (existing) {
92232
+ existing.push(entry);
92233
+ } else {
92234
+ map3.set(idx, [entry]);
92235
+ }
92236
+ }
92237
+ return map3;
92238
+ }, [collab]);
91578
92239
  const estimatedItemHeight = useMemo(
91579
92240
  () => estimateSlideItemHeight(canvasSize.width, canvasSize.height),
91580
92241
  [canvasSize.width, canvasSize.height]
@@ -91696,6 +92357,7 @@ function SlidesPaneSidebar({
91696
92357
  canvasSize,
91697
92358
  canEdit,
91698
92359
  rehearsalTimings,
92360
+ presenceUsers: slidePresenceMap?.get(item.slideIndex),
91699
92361
  onSelectSlide,
91700
92362
  onSlideContextMenu,
91701
92363
  onAddSection,
@@ -91749,6 +92411,7 @@ function SlidesPaneSidebar({
91749
92411
  canvasSize,
91750
92412
  canEdit,
91751
92413
  rehearsalTimings,
92414
+ presenceUsers: slidePresenceMap?.get(idx),
91752
92415
  onSelectSlide,
91753
92416
  onSlideContextMenu,
91754
92417
  onAddSection,
@@ -96515,16 +97178,31 @@ var ALIGN_BTNS = [
96515
97178
  { k: "bottom", el: /* @__PURE__ */ jsx(LuChevronDown, { className: ic2 }) }
96516
97179
  ];
96517
97180
  var DRAW_TOOLS = [
96518
- { id: "select", icon: /* @__PURE__ */ jsx(LuMoveRight, { className: ic2 }), t: "Select" },
96519
- { id: "pen", icon: /* @__PURE__ */ jsx(LuPencil, { className: ic2 }), t: "Pen" },
97181
+ {
97182
+ id: "select",
97183
+ icon: /* @__PURE__ */ jsx(LuMoveRight, { className: ic2 }),
97184
+ t: "Select",
97185
+ ac: "bg-primary text-primary-foreground"
97186
+ },
97187
+ {
97188
+ id: "pen",
97189
+ icon: /* @__PURE__ */ jsx(LuPencil, { className: ic2 }),
97190
+ t: "Pen",
97191
+ ac: "bg-primary text-primary-foreground"
97192
+ },
96520
97193
  {
96521
97194
  id: "highlighter",
96522
97195
  icon: /* @__PURE__ */ jsx(LuType, { className: ic2 }),
96523
97196
  t: "Highlighter",
96524
97197
  ac: "bg-yellow-600 text-white"
96525
97198
  },
96526
- { id: "eraser", icon: /* @__PURE__ */ jsx(LuMinus, { className: ic2 }), t: "Eraser" },
96527
- { id: "freeform", icon: /* @__PURE__ */ jsx(LuSpline, { className: ic2 }), t: "Freeform" }
97199
+ { id: "eraser", icon: /* @__PURE__ */ jsx(LuMinus, { className: ic2 }), t: "Eraser", ac: "bg-red-600 text-white" },
97200
+ {
97201
+ id: "freeform",
97202
+ icon: /* @__PURE__ */ jsx(LuSpline, { className: ic2 }),
97203
+ t: "Freeform",
97204
+ ac: "bg-primary text-primary-foreground"
97205
+ }
96528
97206
  ];
96529
97207
  var OV = [
96530
97208
  {
@@ -96730,7 +97408,7 @@ function AnimationsSection(p3) {
96730
97408
  "button",
96731
97409
  {
96732
97410
  type: "button",
96733
- onClick: p3.onToggleInspector,
97411
+ onClick: p3.onOpenAnimationPanel ?? p3.onToggleInspector,
96734
97412
  className: cn(
96735
97413
  pill,
96736
97414
  p3.isInspectorPaneOpen ? "bg-primary hover:bg-primary/80 text-primary-foreground" : ""
@@ -97829,413 +98507,106 @@ function SlideShowSection(p3) {
97829
98507
  /* @__PURE__ */ jsx(LuSettings, { className: ic2 }),
97830
98508
  "Set Up Slide Show"
97831
98509
  ] }),
97832
- /* @__PURE__ */ jsxs("button", { onClick: p3.onOpenBroadcastDialog, className: pill, title: "Broadcast slide show", children: [
97833
- /* @__PURE__ */ jsx(LuCast, { className: ic2 }),
97834
- "Broadcast"
97835
- ] }),
97836
- sep,
97837
- /* @__PURE__ */ jsxs(
97838
- "button",
97839
- {
97840
- onClick: p3.onToggleSubtitles,
97841
- className: cn(
97842
- pill,
97843
- p3.showSubtitles ? "bg-primary hover:bg-primary/80 text-primary-foreground" : ""
97844
- ),
97845
- title: "Toggle subtitles",
97846
- children: [
97847
- /* @__PURE__ */ jsx(LuCaptions, { className: ic2 }),
97848
- "Subtitles"
97849
- ]
97850
- }
97851
- )
97852
- ] });
97853
- }
97854
- var FONT_COLOR_PRESETS = [
97855
- "#000000",
97856
- "#ffffff",
97857
- "#ff0000",
97858
- "#00aa00",
97859
- "#0000ff",
97860
- "#ff8800",
97861
- "#8800cc",
97862
- "#00cccc",
97863
- "#ff69b4",
97864
- "#808080"
97865
- ];
97866
- var HIGHLIGHT_COLOR_PRESETS = [
97867
- "#ffff00",
97868
- "#00ff00",
97869
- "#00ffff",
97870
- "#ff00ff",
97871
- "#0000ff",
97872
- "#ff0000",
97873
- "#000080",
97874
- "#008080",
97875
- "#008000",
97876
- "#800080"
97877
- ];
97878
- function TextSection(p3) {
97879
- const hasSel = Boolean(p3.selectedElement);
97880
- const canMut = hasSel && p3.canEdit;
97881
- const isTextEl = hasSel && p3.selectedElement !== null && hasTextProperties(p3.selectedElement);
97882
- const isTable = hasSel && p3.selectedElement?.type === "table";
97883
- const canFormat = isTextEl || isTable;
97884
- const currentColor = isTextEl && p3.selectedElement && hasTextProperties(p3.selectedElement) ? p3.selectedElement.textSegments?.[0]?.style?.color ?? p3.selectedElement.textStyle?.color ?? "#000000" : "#000000";
97885
- const currentHighlight = isTextEl && p3.selectedElement && hasTextProperties(p3.selectedElement) ? p3.selectedElement.textSegments?.[0]?.style?.highlightColor ?? p3.selectedElement.textStyle?.highlightColor ?? "#ffff00" : "#ffff00";
97886
- const colorInputRef = useRef(null);
97887
- const highlightInputRef = useRef(null);
97888
- const handleColorChange = useCallback(
97889
- (color) => {
97890
- if (!canFormat) {
97891
- return;
97892
- }
97893
- p3.onUpdateTextStyle({ color });
97894
- },
97895
- [canFormat, p3]
97896
- );
97897
- const handleHighlightChange = useCallback(
97898
- (highlightColor) => {
97899
- if (!canFormat) {
97900
- return;
97901
- }
97902
- p3.onUpdateTextStyle({ highlightColor });
97903
- },
97904
- [canFormat, p3]
97905
- );
97906
- return /* @__PURE__ */ jsxs(Fragment, { children: [
97907
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-0.5", children: [
97908
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
97909
- /* @__PURE__ */ jsx("div", { className: grp, children: FMT.map((b2, i3, a2) => {
97910
- const handleClick = () => {
97911
- if (!canFormat || !p3.selectedElement) {
97912
- return;
97913
- }
97914
- const ts = hasTextProperties(p3.selectedElement) ? p3.selectedElement.textStyle : void 0;
97915
- switch (b2.t) {
97916
- case "Bold":
97917
- p3.onUpdateTextStyle({ bold: !ts?.bold });
97918
- break;
97919
- case "Italic":
97920
- p3.onUpdateTextStyle({ italic: !ts?.italic });
97921
- break;
97922
- case "Underline":
97923
- p3.onUpdateTextStyle({
97924
- underline: !ts?.underline
97925
- });
97926
- break;
97927
- case "Strikethrough":
97928
- p3.onUpdateTextStyle({
97929
- strikethrough: !ts?.strikethrough
97930
- });
97931
- break;
97932
- }
97933
- };
97934
- return /* @__PURE__ */ jsx(
97935
- "button",
97936
- {
97937
- type: "button",
97938
- disabled: !canMut,
97939
- onMouseDown: (e2) => e2.preventDefault(),
97940
- onClick: handleClick,
97941
- className: i3 < a2.length - 1 ? gB : gL,
97942
- title: b2.t,
97943
- children: b2.i
97944
- },
97945
- b2.t
97946
- );
97947
- }) }),
97948
- /* @__PURE__ */ jsxs("div", { className: grp, children: [
97949
- /* @__PURE__ */ jsx(
97950
- "button",
97951
- {
97952
- type: "button",
97953
- disabled: !canMut,
97954
- onMouseDown: (e2) => e2.preventDefault(),
97955
- onClick: () => {
97956
- if (!canFormat || !p3.selectedElement) {
97957
- return;
97958
- }
97959
- const ts = hasTextProperties(p3.selectedElement) ? p3.selectedElement.textStyle : void 0;
97960
- const current = ts?.fontSize ?? 18;
97961
- p3.onUpdateTextStyle({ fontSize: current + 2 });
97962
- },
97963
- className: gB,
97964
- title: "Increase Font Size",
97965
- children: /* @__PURE__ */ jsx(LuAArrowUp, { className: ic2 })
97966
- }
97967
- ),
97968
- /* @__PURE__ */ jsx(
97969
- "button",
97970
- {
97971
- type: "button",
97972
- disabled: !canMut,
97973
- onMouseDown: (e2) => e2.preventDefault(),
97974
- onClick: () => {
97975
- if (!canFormat || !p3.selectedElement) {
97976
- return;
97977
- }
97978
- const ts = hasTextProperties(p3.selectedElement) ? p3.selectedElement.textStyle : void 0;
97979
- const current = ts?.fontSize ?? 18;
97980
- p3.onUpdateTextStyle({ fontSize: Math.max(1, current - 2) });
97981
- },
97982
- className: gB,
97983
- title: "Decrease Font Size",
97984
- children: /* @__PURE__ */ jsx(LuAArrowDown, { className: ic2 })
97985
- }
97986
- ),
97987
- /* @__PURE__ */ jsx(
97988
- "button",
97989
- {
97990
- type: "button",
97991
- disabled: !canMut,
97992
- onMouseDown: (e2) => e2.preventDefault(),
97993
- onClick: () => {
97994
- if (!canFormat) {
97995
- return;
97996
- }
97997
- p3.onUpdateTextStyle({
97998
- bold: false,
97999
- italic: false,
98000
- underline: false,
98001
- strikethrough: false,
98002
- highlightColor: void 0
98003
- });
98004
- },
98005
- className: gL,
98006
- title: "Clear Formatting",
98007
- children: /* @__PURE__ */ jsx(LuRemoveFormatting, { className: ic2 })
98008
- }
98009
- )
98010
- ] }),
98011
- /* @__PURE__ */ jsxs("div", { className: "relative group", children: [
98012
- /* @__PURE__ */ jsxs(
98013
- "button",
98014
- {
98015
- type: "button",
98016
- disabled: !canMut,
98017
- onMouseDown: (e2) => e2.preventDefault(),
98018
- className: pill,
98019
- title: "Font Color",
98020
- children: [
98021
- /* @__PURE__ */ jsx(
98022
- "svg",
98023
- {
98024
- className: ic2,
98025
- viewBox: "0 0 24 24",
98026
- fill: "none",
98027
- stroke: "currentColor",
98028
- strokeWidth: "2",
98029
- strokeLinecap: "round",
98030
- strokeLinejoin: "round",
98031
- children: /* @__PURE__ */ jsx("path", { d: "M6 20h12M9.5 4h5L18 16H6L9.5 4z" })
98032
- }
98033
- ),
98034
- /* @__PURE__ */ jsx(
98035
- "div",
98036
- {
98037
- className: "w-4 h-1 rounded-sm -mt-0.5",
98038
- style: { backgroundColor: currentColor }
98039
- }
98040
- )
98041
- ]
98042
- }
98043
- ),
98044
- /* @__PURE__ */ jsx("div", { className: "absolute left-0 top-full z-50 hidden group-hover:block pt-1", children: /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-border bg-popover backdrop-blur-lg shadow-2xl p-2 w-36", children: [
98045
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-5 gap-1.5 mb-2", children: FONT_COLOR_PRESETS.map((c2) => /* @__PURE__ */ jsx(
98046
- "button",
98047
- {
98048
- type: "button",
98049
- className: `w-5 h-5 rounded-full border transition-transform hover:scale-125 ${currentColor?.toLowerCase() === c2 ? "border-primary ring-1 ring-primary" : "border-border"}`,
98050
- style: { backgroundColor: c2 },
98051
- onMouseDown: (e2) => e2.preventDefault(),
98052
- onClick: () => handleColorChange(c2)
98053
- },
98054
- c2
98055
- )) }),
98056
- /* @__PURE__ */ jsx(
98057
- "button",
98058
- {
98059
- type: "button",
98060
- className: "w-full text-[10px] text-muted-foreground hover:text-foreground py-1 transition-colors",
98061
- onMouseDown: (e2) => e2.preventDefault(),
98062
- onClick: () => colorInputRef.current?.click(),
98063
- children: "Custom colour..."
98064
- }
98065
- ),
98066
- /* @__PURE__ */ jsx(
98067
- "input",
98068
- {
98069
- ref: colorInputRef,
98070
- type: "color",
98071
- className: "sr-only",
98072
- value: currentColor,
98073
- onChange: (e2) => handleColorChange(e2.target.value)
98074
- }
98075
- )
98076
- ] }) })
98077
- ] }),
98078
- /* @__PURE__ */ jsxs("div", { className: "relative group", children: [
98079
- /* @__PURE__ */ jsxs(
98080
- "button",
98081
- {
98082
- type: "button",
98083
- disabled: !canMut,
98084
- onMouseDown: (e2) => e2.preventDefault(),
98085
- className: pill,
98086
- title: "Text Highlight Color",
98087
- children: [
98088
- /* @__PURE__ */ jsx(LuHighlighter, { className: ic2 }),
98089
- /* @__PURE__ */ jsx(
98090
- "div",
98091
- {
98092
- className: "w-4 h-1 rounded-sm -mt-0.5",
98093
- style: { backgroundColor: currentHighlight }
98094
- }
98095
- )
98096
- ]
98097
- }
98098
- ),
98099
- /* @__PURE__ */ jsx("div", { className: "absolute left-0 top-full z-50 hidden group-hover:block pt-1", children: /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-border bg-popover backdrop-blur-lg shadow-2xl p-2 w-36", children: [
98100
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-5 gap-1.5 mb-2", children: HIGHLIGHT_COLOR_PRESETS.map((c2) => /* @__PURE__ */ jsx(
98101
- "button",
98102
- {
98103
- type: "button",
98104
- className: `w-5 h-5 rounded-full border transition-transform hover:scale-125 ${currentHighlight?.toLowerCase() === c2 ? "border-primary ring-1 ring-primary" : "border-border"}`,
98105
- style: { backgroundColor: c2 },
98106
- onMouseDown: (e2) => e2.preventDefault(),
98107
- onClick: () => handleHighlightChange(c2)
98108
- },
98109
- c2
98110
- )) }),
98111
- /* @__PURE__ */ jsx(
98112
- "button",
98113
- {
98114
- type: "button",
98115
- className: "w-full text-[10px] text-muted-foreground hover:text-foreground py-1 transition-colors",
98116
- onMouseDown: (e2) => e2.preventDefault(),
98117
- onClick: () => highlightInputRef.current?.click(),
98118
- children: "Custom colour..."
98119
- }
98120
- ),
98121
- /* @__PURE__ */ jsx(
98122
- "input",
98123
- {
98124
- ref: highlightInputRef,
98125
- type: "color",
98126
- className: "sr-only",
98127
- value: currentHighlight,
98128
- onChange: (e2) => handleHighlightChange(e2.target.value)
98129
- }
98130
- )
98131
- ] }) })
98132
- ] })
98133
- ] }),
98134
- /* @__PURE__ */ jsx("span", { className: "text-[9px] text-muted-foreground leading-none", children: "Font" })
98135
- ] }),
98510
+ /* @__PURE__ */ jsxs("button", { onClick: p3.onOpenBroadcastDialog, className: pill, title: "Broadcast slide show", children: [
98511
+ /* @__PURE__ */ jsx(LuCast, { className: ic2 }),
98512
+ "Broadcast"
98513
+ ] }),
98136
98514
  sep,
98515
+ /* @__PURE__ */ jsxs(
98516
+ "button",
98517
+ {
98518
+ onClick: p3.onToggleSubtitles,
98519
+ className: cn(
98520
+ pill,
98521
+ p3.showSubtitles ? "bg-primary hover:bg-primary/80 text-primary-foreground" : ""
98522
+ ),
98523
+ title: "Toggle subtitles",
98524
+ children: [
98525
+ /* @__PURE__ */ jsx(LuCaptions, { className: ic2 }),
98526
+ "Subtitles"
98527
+ ]
98528
+ }
98529
+ )
98530
+ ] });
98531
+ }
98532
+ var FONT_COLOR_PRESETS = [
98533
+ "#000000",
98534
+ "#ffffff",
98535
+ "#ff0000",
98536
+ "#00aa00",
98537
+ "#0000ff",
98538
+ "#ff8800",
98539
+ "#8800cc",
98540
+ "#00cccc",
98541
+ "#ff69b4",
98542
+ "#808080"
98543
+ ];
98544
+ var HIGHLIGHT_COLOR_PRESETS = [
98545
+ "#ffff00",
98546
+ "#00ff00",
98547
+ "#00ffff",
98548
+ "#ff00ff",
98549
+ "#0000ff",
98550
+ "#ff0000",
98551
+ "#000080",
98552
+ "#008080",
98553
+ "#008000",
98554
+ "#800080"
98555
+ ];
98556
+ function TextSection(p3) {
98557
+ const hasSel = Boolean(p3.selectedElement);
98558
+ const canMut = hasSel && p3.canEdit;
98559
+ const isTextEl = hasSel && p3.selectedElement !== null && hasTextProperties(p3.selectedElement);
98560
+ const isTable = hasSel && p3.selectedElement?.type === "table";
98561
+ const canFormat = isTextEl || isTable;
98562
+ const currentColor = isTextEl && p3.selectedElement && hasTextProperties(p3.selectedElement) ? p3.selectedElement.textSegments?.[0]?.style?.color ?? p3.selectedElement.textStyle?.color ?? "#000000" : "#000000";
98563
+ const currentHighlight = isTextEl && p3.selectedElement && hasTextProperties(p3.selectedElement) ? p3.selectedElement.textSegments?.[0]?.style?.highlightColor ?? p3.selectedElement.textStyle?.highlightColor ?? "#ffff00" : "#ffff00";
98564
+ const colorInputRef = useRef(null);
98565
+ const highlightInputRef = useRef(null);
98566
+ const handleColorChange = useCallback(
98567
+ (color) => {
98568
+ if (!canFormat) {
98569
+ return;
98570
+ }
98571
+ p3.onUpdateTextStyle({ color });
98572
+ },
98573
+ [canFormat, p3]
98574
+ );
98575
+ const handleHighlightChange = useCallback(
98576
+ (highlightColor) => {
98577
+ if (!canFormat) {
98578
+ return;
98579
+ }
98580
+ p3.onUpdateTextStyle({ highlightColor });
98581
+ },
98582
+ [canFormat, p3]
98583
+ );
98584
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
98137
98585
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-0.5", children: [
98138
98586
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
98139
- /* @__PURE__ */ jsxs("div", { className: grp, children: [
98140
- /* @__PURE__ */ jsx(
98141
- "button",
98142
- {
98143
- type: "button",
98144
- disabled: !canMut,
98145
- onMouseDown: (e2) => e2.preventDefault(),
98146
- onClick: () => {
98147
- if (!canFormat || !p3.selectedElement) {
98148
- return;
98149
- }
98150
- const ts = hasTextProperties(p3.selectedElement) ? p3.selectedElement.textStyle : void 0;
98151
- p3.onUpdateTextStyle({
98152
- listType: ts?.listType === "bullet" ? "none" : "bullet"
98153
- });
98154
- },
98155
- className: gB,
98156
- title: "Bullet List",
98157
- children: /* @__PURE__ */ jsx(LuList, { className: ic2 })
98158
- }
98159
- ),
98160
- /* @__PURE__ */ jsx(
98161
- "button",
98162
- {
98163
- type: "button",
98164
- disabled: !canMut,
98165
- onMouseDown: (e2) => e2.preventDefault(),
98166
- onClick: () => {
98167
- if (!canFormat || !p3.selectedElement) {
98168
- return;
98169
- }
98170
- const ts = hasTextProperties(p3.selectedElement) ? p3.selectedElement.textStyle : void 0;
98171
- p3.onUpdateTextStyle({
98172
- listType: ts?.listType === "numbered" ? "none" : "numbered"
98173
- });
98174
- },
98175
- className: gL,
98176
- title: "Numbered List",
98177
- children: /* @__PURE__ */ jsx(LuListOrdered, { className: ic2 })
98587
+ /* @__PURE__ */ jsx("div", { className: grp, children: FMT.map((b2, i3, a2) => {
98588
+ const handleClick = () => {
98589
+ if (!canFormat || !p3.selectedElement) {
98590
+ return;
98178
98591
  }
98179
- )
98180
- ] }),
98181
- /* @__PURE__ */ jsxs("div", { className: grp, children: [
98182
- /* @__PURE__ */ jsx(
98183
- "button",
98184
- {
98185
- type: "button",
98186
- disabled: !canMut,
98187
- onMouseDown: (e2) => e2.preventDefault(),
98188
- onClick: () => {
98189
- if (!canFormat || !p3.selectedElement) {
98190
- return;
98191
- }
98192
- const ts = hasTextProperties(p3.selectedElement) ? p3.selectedElement.textStyle : void 0;
98193
- const current = ts?.paragraphMarginLeft ?? 0;
98592
+ const ts = hasTextProperties(p3.selectedElement) ? p3.selectedElement.textStyle : void 0;
98593
+ switch (b2.t) {
98594
+ case "Bold":
98595
+ p3.onUpdateTextStyle({ bold: !ts?.bold });
98596
+ break;
98597
+ case "Italic":
98598
+ p3.onUpdateTextStyle({ italic: !ts?.italic });
98599
+ break;
98600
+ case "Underline":
98194
98601
  p3.onUpdateTextStyle({
98195
- paragraphMarginLeft: Math.max(0, current - 24)
98602
+ underline: !ts?.underline
98196
98603
  });
98197
- },
98198
- className: gB,
98199
- title: "Decrease Indent",
98200
- children: /* @__PURE__ */ jsx(LuIndentDecrease, { className: ic2 })
98201
- }
98202
- ),
98203
- /* @__PURE__ */ jsx(
98204
- "button",
98205
- {
98206
- type: "button",
98207
- disabled: !canMut,
98208
- onMouseDown: (e2) => e2.preventDefault(),
98209
- onClick: () => {
98210
- if (!canFormat || !p3.selectedElement) {
98211
- return;
98212
- }
98213
- const ts = hasTextProperties(p3.selectedElement) ? p3.selectedElement.textStyle : void 0;
98214
- const current = ts?.paragraphMarginLeft ?? 0;
98604
+ break;
98605
+ case "Strikethrough":
98215
98606
  p3.onUpdateTextStyle({
98216
- paragraphMarginLeft: current + 24
98607
+ strikethrough: !ts?.strikethrough
98217
98608
  });
98218
- },
98219
- className: gL,
98220
- title: "Increase Indent",
98221
- children: /* @__PURE__ */ jsx(LuIndentIncrease, { className: ic2 })
98222
- }
98223
- )
98224
- ] }),
98225
- /* @__PURE__ */ jsx("div", { className: grp, children: ATXT.map((b2, i3, a2) => {
98226
- const handleClick = () => {
98227
- if (!canFormat) {
98228
- return;
98229
- }
98230
- const alignMap = {
98231
- "Align left": "left",
98232
- "Align center": "center",
98233
- "Align right": "right",
98234
- Justify: "justify"
98235
- };
98236
- const align = alignMap[b2.t];
98237
- if (align) {
98238
- p3.onUpdateTextStyle({ align });
98609
+ break;
98239
98610
  }
98240
98611
  };
98241
98612
  return /* @__PURE__ */ jsx(
@@ -98251,518 +98622,318 @@ function TextSection(p3) {
98251
98622
  },
98252
98623
  b2.t
98253
98624
  );
98254
- }) })
98255
- ] }),
98256
- /* @__PURE__ */ jsx("span", { className: "text-[9px] text-muted-foreground leading-none", children: "Paragraph" })
98257
- ] })
98258
- ] });
98259
- }
98260
-
98261
- // src/viewer/hooks/collaboration/sanitize.ts
98262
- var ROOM_ID_REGEX = /^[a-zA-Z0-9_-]{1,128}$/;
98263
- function validateRoomId(roomId) {
98264
- if (!ROOM_ID_REGEX.test(roomId)) {
98265
- throw new Error(
98266
- `Invalid collaboration room ID: "${roomId}". Must be 1-128 alphanumeric characters, hyphens, or underscores.`
98267
- );
98268
- }
98269
- return roomId;
98270
- }
98271
- function sanitizeUserName(name) {
98272
- if (typeof name !== "string") {
98273
- return "Anonymous";
98274
- }
98275
- const stripped = name.replace(/<[^>]*>/g, "");
98276
- const trimmed = stripped.trim().slice(0, 64);
98277
- return trimmed || "Anonymous";
98278
- }
98279
- function clampCursorPosition(value, min2, max2) {
98280
- if (typeof value !== "number" || !Number.isFinite(value)) {
98281
- return 0;
98282
- }
98283
- const margin = 20;
98284
- return Math.max(min2 - margin, Math.min(max2 + margin, value));
98285
- }
98286
- var HEX_COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
98287
- function sanitizeColor(color, fallback = "#6366f1") {
98288
- if (typeof color !== "string") {
98289
- return fallback;
98290
- }
98291
- return HEX_COLOR_REGEX.test(color) ? color : fallback;
98292
- }
98293
- function sanitizeAvatarUrl(url) {
98294
- if (typeof url !== "string") {
98295
- return void 0;
98296
- }
98297
- try {
98298
- const parsed = new URL(url);
98299
- if (parsed.protocol === "https:" || parsed.protocol === "http:" || parsed.protocol === "data:") {
98300
- return url;
98301
- }
98302
- } catch {
98303
- }
98304
- return void 0;
98305
- }
98306
- function sanitizeSlideIndex(value) {
98307
- if (typeof value !== "number" || !Number.isFinite(value)) {
98308
- return 0;
98309
- }
98310
- return Math.max(0, Math.floor(value));
98311
- }
98312
- function sanitizePresence(raw, canvasWidth, canvasHeight) {
98313
- if (typeof raw.clientId !== "number") {
98314
- return null;
98315
- }
98316
- return {
98317
- clientId: raw.clientId,
98318
- userName: sanitizeUserName(raw.userName),
98319
- userAvatar: sanitizeAvatarUrl(raw.userAvatar),
98320
- userColor: sanitizeColor(raw.userColor),
98321
- activeSlideIndex: sanitizeSlideIndex(raw.activeSlideIndex),
98322
- cursorX: clampCursorPosition(raw.cursorX, 0, canvasWidth),
98323
- cursorY: clampCursorPosition(raw.cursorY, 0, canvasHeight),
98324
- lastUpdated: typeof raw.lastUpdated === "string" ? raw.lastUpdated : (/* @__PURE__ */ new Date()).toISOString(),
98325
- selectedElementId: typeof raw.selectedElementId === "string" ? raw.selectedElementId.slice(0, 128) : void 0
98326
- };
98327
- }
98328
- var BROADCAST_THROTTLE_MS = 50;
98329
- var STALE_PRESENCE_MS = 3e4;
98330
- function usePresenceTracking({
98331
- awareness,
98332
- localClientId,
98333
- userName,
98334
- userColor,
98335
- userAvatar,
98336
- canvasWidth,
98337
- canvasHeight
98338
- }) {
98339
- const [remoteUsers, setRemoteUsers] = useState([]);
98340
- const lastBroadcastRef = useRef(0);
98341
- const pendingBroadcastRef = useRef(null);
98342
- const latestLocalState = useRef({});
98343
- const broadcastPresence = useCallback(
98344
- (update2) => {
98345
- if (!awareness) {
98346
- return;
98347
- }
98348
- Object.assign(latestLocalState.current, update2);
98349
- const now = Date.now();
98350
- const elapsed = now - lastBroadcastRef.current;
98351
- const flush = () => {
98352
- const state2 = {
98353
- ...latestLocalState.current,
98354
- userName,
98355
- userColor,
98356
- userAvatar,
98357
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
98358
- };
98359
- awareness.setLocalStateField("presence", state2);
98360
- lastBroadcastRef.current = Date.now();
98361
- };
98362
- if (elapsed >= BROADCAST_THROTTLE_MS) {
98363
- if (pendingBroadcastRef.current) {
98364
- clearTimeout(pendingBroadcastRef.current);
98365
- pendingBroadcastRef.current = null;
98366
- }
98367
- flush();
98368
- } else if (!pendingBroadcastRef.current) {
98369
- pendingBroadcastRef.current = setTimeout(() => {
98370
- pendingBroadcastRef.current = null;
98371
- flush();
98372
- }, BROADCAST_THROTTLE_MS - elapsed);
98373
- }
98374
- },
98375
- [awareness, userName, userColor, userAvatar]
98376
- );
98377
- useEffect(() => {
98378
- if (!awareness) {
98379
- return;
98380
- }
98381
- awareness.setLocalStateField("presence", {
98382
- userName,
98383
- userColor,
98384
- userAvatar,
98385
- activeSlideIndex: 0,
98386
- cursorX: 0,
98387
- cursorY: 0,
98388
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
98389
- });
98390
- }, [awareness, userName, userColor, userAvatar]);
98391
- useEffect(() => {
98392
- if (!awareness || localClientId === null) {
98393
- return;
98394
- }
98395
- const handleChange = () => {
98396
- const now = Date.now();
98397
- const states = awareness.getStates();
98398
- const users = [];
98399
- states.forEach((state2, cid) => {
98400
- if (cid === localClientId) {
98401
- return;
98402
- }
98403
- const raw = state2?.presence;
98404
- if (!raw || typeof raw !== "object") {
98405
- return;
98406
- }
98407
- const sanitized = sanitizePresence({ ...raw, clientId: cid }, canvasWidth, canvasHeight);
98408
- if (!sanitized) {
98409
- return;
98410
- }
98411
- const updatedAt = new Date(sanitized.lastUpdated).getTime();
98412
- if (Number.isNaN(updatedAt) || now - updatedAt > STALE_PRESENCE_MS) {
98413
- return;
98414
- }
98415
- users.push(sanitized);
98416
- });
98417
- setRemoteUsers(users);
98418
- };
98419
- awareness.on("change", handleChange);
98420
- awareness.on("update", handleChange);
98421
- handleChange();
98422
- return () => {
98423
- awareness.off("change", handleChange);
98424
- awareness.off("update", handleChange);
98425
- };
98426
- }, [awareness, localClientId, canvasWidth, canvasHeight]);
98427
- useEffect(() => {
98428
- if (!awareness) {
98429
- return;
98430
- }
98431
- const interval = setInterval(() => {
98432
- awareness.setLocalStateField("presence", {
98433
- ...latestLocalState.current,
98434
- userName,
98435
- userColor,
98436
- userAvatar,
98437
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
98438
- });
98439
- }, 1e4);
98440
- return () => clearInterval(interval);
98441
- }, [awareness, userName, userColor, userAvatar]);
98442
- useEffect(() => {
98443
- return () => {
98444
- if (pendingBroadcastRef.current) {
98445
- clearTimeout(pendingBroadcastRef.current);
98446
- }
98447
- };
98448
- }, []);
98449
- return { remoteUsers, broadcastPresence };
98450
- }
98451
- function useYjsProvider({ config }) {
98452
- const [status, setStatus] = useState("disconnected");
98453
- const [awareness, setAwareness] = useState(null);
98454
- const [doc2, setDoc] = useState(null);
98455
- const [clientId, setClientId] = useState(null);
98456
- const cleanupRef = useRef(null);
98457
- const init = useCallback(async () => {
98458
- const roomId = validateRoomId(config.roomId);
98459
- setStatus("connecting");
98460
- try {
98461
- const [Y, { WebsocketProvider: WebsocketProvider2 }] = await Promise.all([Promise.resolve().then(() => (init_yjs(), yjs_exports)), Promise.resolve().then(() => (init_y_websocket(), y_websocket_exports))]);
98462
- const yDoc = new Y.Doc();
98463
- const provider = new WebsocketProvider2(
98464
- config.serverUrl,
98465
- roomId,
98466
- yDoc,
98467
- // eslint-disable-line @typescript-eslint/no-explicit-any
98468
- {
98469
- params: config.authToken ? { token: config.authToken } : void 0
98470
- }
98471
- );
98472
- const handleStatus = (event) => {
98473
- if (event.status === "connected") {
98474
- setStatus("connected");
98475
- } else if (event.status === "disconnected") {
98476
- setStatus("disconnected");
98477
- }
98478
- };
98479
- provider.on("status", handleStatus);
98480
- if (provider.wsconnected) {
98481
- setStatus("connected");
98482
- }
98483
- setDoc(yDoc);
98484
- setAwareness(provider.awareness);
98485
- setClientId(provider.awareness.clientID);
98486
- cleanupRef.current = () => {
98487
- provider.off("status", handleStatus);
98488
- provider.destroy();
98489
- yDoc.destroy();
98490
- setDoc(null);
98491
- setAwareness(null);
98492
- setClientId(null);
98493
- setStatus("disconnected");
98494
- };
98495
- } catch (err) {
98496
- console.warn(
98497
- "[pptx-viewer] Collaboration packages not available:",
98498
- err instanceof Error ? err.message : err
98499
- );
98500
- setStatus("error");
98501
- }
98502
- }, [config.roomId, config.serverUrl, config.authToken]);
98503
- useEffect(() => {
98504
- init();
98505
- return () => {
98506
- cleanupRef.current?.();
98507
- cleanupRef.current = null;
98508
- };
98509
- }, [init]);
98510
- return { status, awareness, doc: doc2, clientId };
98511
- }
98512
-
98513
- // src/viewer/hooks/collaboration/useCollaborativeState.ts
98514
- function useCollaborativeState({
98515
- config,
98516
- canvasWidth,
98517
- canvasHeight
98518
- }) {
98519
- const userColor = sanitizeColor(config.userColor, "#6366f1");
98520
- const { status, awareness, doc: doc2, clientId } = useYjsProvider({ config });
98521
- const { remoteUsers, broadcastPresence } = usePresenceTracking({
98522
- awareness,
98523
- localClientId: clientId,
98524
- userName: config.userName,
98525
- userColor,
98526
- userAvatar: config.userAvatar,
98527
- canvasWidth,
98528
- canvasHeight
98529
- });
98530
- const connectedCount = status === "connected" ? remoteUsers.length + 1 : remoteUsers.length;
98531
- return {
98532
- status,
98533
- remoteUsers,
98534
- broadcastPresence,
98535
- connectedCount,
98536
- config,
98537
- doc: doc2
98538
- };
98539
- }
98540
- var CollaborationContext = createContext(null);
98541
- function useCollaboration() {
98542
- return useContext(CollaborationContext);
98543
- }
98544
- function CollaborationProvider({
98545
- config,
98546
- canvasWidth,
98547
- canvasHeight,
98548
- children
98549
- }) {
98550
- const value = useCollaborativeState({
98551
- config,
98552
- canvasWidth,
98553
- canvasHeight
98554
- });
98555
- return /* @__PURE__ */ jsx(CollaborationContext.Provider, { value, children });
98556
- }
98557
- function RemoteUserCursors({
98558
- remoteUsers,
98559
- activeSlideIndex,
98560
- canvasWidth,
98561
- canvasHeight
98562
- }) {
98563
- const visibleUsers = remoteUsers.filter((u2) => u2.activeSlideIndex === activeSlideIndex);
98564
- if (visibleUsers.length === 0) {
98565
- return null;
98566
- }
98567
- return /* @__PURE__ */ jsx(
98568
- "svg",
98569
- {
98570
- "data-testid": "remote-user-cursors",
98571
- className: "absolute inset-0 pointer-events-none",
98572
- style: { zIndex: 9999 },
98573
- width: canvasWidth,
98574
- height: canvasHeight,
98575
- viewBox: `0 0 ${canvasWidth} ${canvasHeight}`,
98576
- "aria-hidden": "true",
98577
- children: visibleUsers.map((user) => /* @__PURE__ */ jsxs(
98578
- "g",
98579
- {
98580
- transform: `translate(${user.cursorX}, ${user.cursorY})`,
98581
- "data-testid": `remote-cursor-${user.clientId}`,
98582
- children: [
98625
+ }) }),
98626
+ /* @__PURE__ */ jsxs("div", { className: grp, children: [
98627
+ /* @__PURE__ */ jsx(
98628
+ "button",
98629
+ {
98630
+ type: "button",
98631
+ disabled: !canMut,
98632
+ onMouseDown: (e2) => e2.preventDefault(),
98633
+ onClick: () => {
98634
+ if (!canFormat || !p3.selectedElement) {
98635
+ return;
98636
+ }
98637
+ const ts = hasTextProperties(p3.selectedElement) ? p3.selectedElement.textStyle : void 0;
98638
+ const current = ts?.fontSize ?? 18;
98639
+ p3.onUpdateTextStyle({ fontSize: current + 2 });
98640
+ },
98641
+ className: gB,
98642
+ title: "Increase Font Size",
98643
+ children: /* @__PURE__ */ jsx(LuAArrowUp, { className: ic2 })
98644
+ }
98645
+ ),
98646
+ /* @__PURE__ */ jsx(
98647
+ "button",
98648
+ {
98649
+ type: "button",
98650
+ disabled: !canMut,
98651
+ onMouseDown: (e2) => e2.preventDefault(),
98652
+ onClick: () => {
98653
+ if (!canFormat || !p3.selectedElement) {
98654
+ return;
98655
+ }
98656
+ const ts = hasTextProperties(p3.selectedElement) ? p3.selectedElement.textStyle : void 0;
98657
+ const current = ts?.fontSize ?? 18;
98658
+ p3.onUpdateTextStyle({ fontSize: Math.max(1, current - 2) });
98659
+ },
98660
+ className: gB,
98661
+ title: "Decrease Font Size",
98662
+ children: /* @__PURE__ */ jsx(LuAArrowDown, { className: ic2 })
98663
+ }
98664
+ ),
98665
+ /* @__PURE__ */ jsx(
98666
+ "button",
98667
+ {
98668
+ type: "button",
98669
+ disabled: !canMut,
98670
+ onMouseDown: (e2) => e2.preventDefault(),
98671
+ onClick: () => {
98672
+ if (!canFormat) {
98673
+ return;
98674
+ }
98675
+ p3.onUpdateTextStyle({
98676
+ bold: false,
98677
+ italic: false,
98678
+ underline: false,
98679
+ strikethrough: false,
98680
+ highlightColor: void 0
98681
+ });
98682
+ },
98683
+ className: gL,
98684
+ title: "Clear Formatting",
98685
+ children: /* @__PURE__ */ jsx(LuRemoveFormatting, { className: ic2 })
98686
+ }
98687
+ )
98688
+ ] }),
98689
+ /* @__PURE__ */ jsxs("div", { className: "relative group", children: [
98690
+ /* @__PURE__ */ jsxs(
98691
+ "button",
98692
+ {
98693
+ type: "button",
98694
+ disabled: !canMut,
98695
+ onMouseDown: (e2) => e2.preventDefault(),
98696
+ className: pill,
98697
+ title: "Font Color",
98698
+ children: [
98699
+ /* @__PURE__ */ jsx(
98700
+ "svg",
98701
+ {
98702
+ className: ic2,
98703
+ viewBox: "0 0 24 24",
98704
+ fill: "none",
98705
+ stroke: "currentColor",
98706
+ strokeWidth: "2",
98707
+ strokeLinecap: "round",
98708
+ strokeLinejoin: "round",
98709
+ children: /* @__PURE__ */ jsx("path", { d: "M6 20h12M9.5 4h5L18 16H6L9.5 4z" })
98710
+ }
98711
+ ),
98712
+ /* @__PURE__ */ jsx(
98713
+ "div",
98714
+ {
98715
+ className: "w-4 h-1 rounded-sm -mt-0.5",
98716
+ style: { backgroundColor: currentColor }
98717
+ }
98718
+ )
98719
+ ]
98720
+ }
98721
+ ),
98722
+ /* @__PURE__ */ jsx("div", { className: "absolute left-0 top-full z-50 hidden group-hover:block pt-1", children: /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-border bg-popover backdrop-blur-lg shadow-2xl p-2 w-36", children: [
98723
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-5 gap-1.5 mb-2", children: FONT_COLOR_PRESETS.map((c2) => /* @__PURE__ */ jsx(
98724
+ "button",
98725
+ {
98726
+ type: "button",
98727
+ className: `w-5 h-5 rounded-full border transition-transform hover:scale-125 ${currentColor?.toLowerCase() === c2 ? "border-primary ring-1 ring-primary" : "border-border"}`,
98728
+ style: { backgroundColor: c2 },
98729
+ onMouseDown: (e2) => e2.preventDefault(),
98730
+ onClick: () => handleColorChange(c2)
98731
+ },
98732
+ c2
98733
+ )) }),
98583
98734
  /* @__PURE__ */ jsx(
98584
- "path",
98735
+ "button",
98585
98736
  {
98586
- d: "M0 0 L0 16 L4.5 12.5 L8 20 L10.5 19 L7 11.5 L12 11 Z",
98587
- fill: user.userColor,
98588
- stroke: "#fff",
98589
- strokeWidth: 1,
98590
- opacity: 0.9
98737
+ type: "button",
98738
+ className: "w-full text-[10px] text-muted-foreground hover:text-foreground py-1 transition-colors",
98739
+ onMouseDown: (e2) => e2.preventDefault(),
98740
+ onClick: () => colorInputRef.current?.click(),
98741
+ children: "Custom colour..."
98591
98742
  }
98592
98743
  ),
98593
- /* @__PURE__ */ jsxs("g", { transform: "translate(14, 18)", children: [
98594
- /* @__PURE__ */ jsx(
98595
- "rect",
98596
- {
98597
- rx: 3,
98598
- ry: 3,
98599
- x: -2,
98600
- y: -10,
98601
- width: Math.min(user.userName.length * 7 + 8, 150),
98602
- height: 16,
98603
- fill: user.userColor,
98604
- opacity: 0.85
98744
+ /* @__PURE__ */ jsx(
98745
+ "input",
98746
+ {
98747
+ ref: colorInputRef,
98748
+ type: "color",
98749
+ className: "sr-only",
98750
+ value: currentColor,
98751
+ onChange: (e2) => handleColorChange(e2.target.value)
98752
+ }
98753
+ )
98754
+ ] }) })
98755
+ ] }),
98756
+ /* @__PURE__ */ jsxs("div", { className: "relative group", children: [
98757
+ /* @__PURE__ */ jsxs(
98758
+ "button",
98759
+ {
98760
+ type: "button",
98761
+ disabled: !canMut,
98762
+ onMouseDown: (e2) => e2.preventDefault(),
98763
+ className: pill,
98764
+ title: "Text Highlight Color",
98765
+ children: [
98766
+ /* @__PURE__ */ jsx(LuHighlighter, { className: ic2 }),
98767
+ /* @__PURE__ */ jsx(
98768
+ "div",
98769
+ {
98770
+ className: "w-4 h-1 rounded-sm -mt-0.5",
98771
+ style: { backgroundColor: currentHighlight }
98772
+ }
98773
+ )
98774
+ ]
98775
+ }
98776
+ ),
98777
+ /* @__PURE__ */ jsx("div", { className: "absolute left-0 top-full z-50 hidden group-hover:block pt-1", children: /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-border bg-popover backdrop-blur-lg shadow-2xl p-2 w-36", children: [
98778
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-5 gap-1.5 mb-2", children: HIGHLIGHT_COLOR_PRESETS.map((c2) => /* @__PURE__ */ jsx(
98779
+ "button",
98780
+ {
98781
+ type: "button",
98782
+ className: `w-5 h-5 rounded-full border transition-transform hover:scale-125 ${currentHighlight?.toLowerCase() === c2 ? "border-primary ring-1 ring-primary" : "border-border"}`,
98783
+ style: { backgroundColor: c2 },
98784
+ onMouseDown: (e2) => e2.preventDefault(),
98785
+ onClick: () => handleHighlightChange(c2)
98786
+ },
98787
+ c2
98788
+ )) }),
98789
+ /* @__PURE__ */ jsx(
98790
+ "button",
98791
+ {
98792
+ type: "button",
98793
+ className: "w-full text-[10px] text-muted-foreground hover:text-foreground py-1 transition-colors",
98794
+ onMouseDown: (e2) => e2.preventDefault(),
98795
+ onClick: () => highlightInputRef.current?.click(),
98796
+ children: "Custom colour..."
98797
+ }
98798
+ ),
98799
+ /* @__PURE__ */ jsx(
98800
+ "input",
98801
+ {
98802
+ ref: highlightInputRef,
98803
+ type: "color",
98804
+ className: "sr-only",
98805
+ value: currentHighlight,
98806
+ onChange: (e2) => handleHighlightChange(e2.target.value)
98807
+ }
98808
+ )
98809
+ ] }) })
98810
+ ] })
98811
+ ] }),
98812
+ /* @__PURE__ */ jsx("span", { className: "text-[9px] text-muted-foreground leading-none", children: "Font" })
98813
+ ] }),
98814
+ sep,
98815
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-0.5", children: [
98816
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
98817
+ /* @__PURE__ */ jsxs("div", { className: grp, children: [
98818
+ /* @__PURE__ */ jsx(
98819
+ "button",
98820
+ {
98821
+ type: "button",
98822
+ disabled: !canMut,
98823
+ onMouseDown: (e2) => e2.preventDefault(),
98824
+ onClick: () => {
98825
+ if (!canFormat || !p3.selectedElement) {
98826
+ return;
98605
98827
  }
98606
- ),
98607
- /* @__PURE__ */ jsx(
98608
- "text",
98609
- {
98610
- fill: "#fff",
98611
- fontSize: 10,
98612
- fontFamily: "system-ui, sans-serif",
98613
- fontWeight: 500,
98614
- dominantBaseline: "central",
98615
- y: -2,
98616
- x: 2,
98617
- children: user.userName.length > 20 ? `${user.userName.slice(0, 18)}...` : user.userName
98828
+ const ts = hasTextProperties(p3.selectedElement) ? p3.selectedElement.textStyle : void 0;
98829
+ p3.onUpdateTextStyle({
98830
+ listType: ts?.listType === "bullet" ? "none" : "bullet"
98831
+ });
98832
+ },
98833
+ className: gB,
98834
+ title: "Bullet List",
98835
+ children: /* @__PURE__ */ jsx(LuList, { className: ic2 })
98836
+ }
98837
+ ),
98838
+ /* @__PURE__ */ jsx(
98839
+ "button",
98840
+ {
98841
+ type: "button",
98842
+ disabled: !canMut,
98843
+ onMouseDown: (e2) => e2.preventDefault(),
98844
+ onClick: () => {
98845
+ if (!canFormat || !p3.selectedElement) {
98846
+ return;
98618
98847
  }
98619
- )
98620
- ] })
98621
- ]
98622
- },
98623
- user.clientId
98624
- ))
98625
- }
98626
- );
98627
- }
98628
- function getInitials(name) {
98629
- const parts = name.trim().split(/\s+/);
98630
- if (parts.length >= 2) {
98631
- return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
98632
- }
98633
- return name.slice(0, 2).toUpperCase();
98634
- }
98635
- function AvatarCircle({
98636
- name,
98637
- color,
98638
- avatar,
98639
- isLocal
98640
- }) {
98641
- const { t: t2 } = useTranslation();
98642
- const initials = getInitials(name);
98643
- const title = isLocal ? t2("pptx.collaboration.youLabel", { name }) : name;
98644
- return /* @__PURE__ */ jsx(
98645
- "div",
98646
- {
98647
- className: "relative w-7 h-7 rounded-full flex items-center justify-center text-[10px] font-semibold text-white border-2 -ml-1 first:ml-0",
98648
- style: {
98649
- backgroundColor: color,
98650
- borderColor: isLocal ? "#fff" : color
98651
- },
98652
- title,
98653
- "aria-label": title,
98654
- children: avatar ? /* @__PURE__ */ jsx(
98655
- "img",
98656
- {
98657
- src: avatar,
98658
- alt: "",
98659
- className: "w-full h-full rounded-full object-cover",
98660
- onError: (e2) => {
98661
- e2.target.style.display = "none";
98662
- }
98663
- }
98664
- ) : initials
98665
- }
98666
- );
98667
- }
98668
- function UserAvatarBar({
98669
- remoteUsers,
98670
- localUserName,
98671
- localUserColor,
98672
- localUserAvatar,
98673
- status,
98674
- maxVisible = 5
98675
- }) {
98676
- const { t: t2 } = useTranslation();
98677
- if (status === "disconnected" || status === "error") {
98678
- return null;
98679
- }
98680
- const allUsers = [
98681
- { name: localUserName, color: localUserColor, avatar: localUserAvatar, isLocal: true },
98682
- ...remoteUsers.map((u2) => ({
98683
- name: u2.userName,
98684
- color: u2.userColor,
98685
- avatar: u2.userAvatar,
98686
- isLocal: false
98687
- }))
98688
- ];
98689
- const visible = allUsers.slice(0, maxVisible);
98690
- const overflow = allUsers.length - maxVisible;
98691
- return /* @__PURE__ */ jsxs(
98692
- "div",
98693
- {
98694
- "data-testid": "user-avatar-bar",
98695
- className: "flex items-center px-2",
98696
- "aria-label": t2("pptx.collaboration.usersConnected", { count: allUsers.length }),
98697
- children: [
98698
- visible.map((user, i3) => /* @__PURE__ */ jsx(
98699
- AvatarCircle,
98700
- {
98701
- name: user.name,
98702
- color: user.color,
98703
- avatar: user.avatar,
98704
- isLocal: user.isLocal
98705
- },
98706
- user.isLocal ? "local" : `remote-${i3}`
98707
- )),
98708
- overflow > 0 && /* @__PURE__ */ jsxs(
98709
- "div",
98710
- {
98711
- className: "w-7 h-7 rounded-full flex items-center justify-center text-[10px] font-semibold text-gray-300 bg-gray-700 border-2 border-gray-600 -ml-1",
98712
- title: t2("pptx.collaboration.moreUsers", { count: overflow }),
98713
- children: [
98714
- "+",
98715
- overflow
98716
- ]
98717
- }
98718
- )
98719
- ]
98720
- }
98721
- );
98722
- }
98723
- var STATUS_STYLES = {
98724
- connected: {
98725
- dot: "bg-green-400",
98726
- text: "text-green-400",
98727
- label: "Connected"
98728
- },
98729
- connecting: {
98730
- dot: "bg-yellow-400 animate-pulse",
98731
- text: "text-yellow-400",
98732
- label: "Connecting..."
98733
- },
98734
- disconnected: {
98735
- dot: "bg-gray-500",
98736
- text: "text-gray-500",
98737
- label: "Disconnected"
98738
- },
98739
- error: {
98740
- dot: "bg-red-400",
98741
- text: "text-red-400",
98742
- label: "Connection error"
98743
- }
98744
- };
98745
- function CollaborationStatusIndicator({
98746
- status,
98747
- connectedCount
98748
- }) {
98749
- const { t: t2 } = useTranslation();
98750
- const style = STATUS_STYLES[status];
98751
- return /* @__PURE__ */ jsxs(
98752
- "div",
98753
- {
98754
- "data-testid": "collaboration-status",
98755
- className: "flex items-center gap-1.5",
98756
- "aria-label": t2("pptx.collaboration.statusAriaLabel", {
98757
- status: t2(`pptx.collaboration.status.${status}`),
98758
- count: connectedCount
98759
- }),
98760
- children: [
98761
- /* @__PURE__ */ jsx("span", { className: `inline-block w-2 h-2 rounded-full ${style.dot}`, "aria-hidden": "true" }),
98762
- /* @__PURE__ */ jsx("span", { className: `text-[10px] ${style.text}`, children: status === "connected" ? t2("pptx.collaboration.userCount", { count: connectedCount }) : t2(`pptx.collaboration.status.${status}`) })
98763
- ]
98764
- }
98765
- );
98848
+ const ts = hasTextProperties(p3.selectedElement) ? p3.selectedElement.textStyle : void 0;
98849
+ p3.onUpdateTextStyle({
98850
+ listType: ts?.listType === "numbered" ? "none" : "numbered"
98851
+ });
98852
+ },
98853
+ className: gL,
98854
+ title: "Numbered List",
98855
+ children: /* @__PURE__ */ jsx(LuListOrdered, { className: ic2 })
98856
+ }
98857
+ )
98858
+ ] }),
98859
+ /* @__PURE__ */ jsxs("div", { className: grp, children: [
98860
+ /* @__PURE__ */ jsx(
98861
+ "button",
98862
+ {
98863
+ type: "button",
98864
+ disabled: !canMut,
98865
+ onMouseDown: (e2) => e2.preventDefault(),
98866
+ onClick: () => {
98867
+ if (!canFormat || !p3.selectedElement) {
98868
+ return;
98869
+ }
98870
+ const ts = hasTextProperties(p3.selectedElement) ? p3.selectedElement.textStyle : void 0;
98871
+ const current = ts?.paragraphMarginLeft ?? 0;
98872
+ p3.onUpdateTextStyle({
98873
+ paragraphMarginLeft: Math.max(0, current - 24)
98874
+ });
98875
+ },
98876
+ className: gB,
98877
+ title: "Decrease Indent",
98878
+ children: /* @__PURE__ */ jsx(LuIndentDecrease, { className: ic2 })
98879
+ }
98880
+ ),
98881
+ /* @__PURE__ */ jsx(
98882
+ "button",
98883
+ {
98884
+ type: "button",
98885
+ disabled: !canMut,
98886
+ onMouseDown: (e2) => e2.preventDefault(),
98887
+ onClick: () => {
98888
+ if (!canFormat || !p3.selectedElement) {
98889
+ return;
98890
+ }
98891
+ const ts = hasTextProperties(p3.selectedElement) ? p3.selectedElement.textStyle : void 0;
98892
+ const current = ts?.paragraphMarginLeft ?? 0;
98893
+ p3.onUpdateTextStyle({
98894
+ paragraphMarginLeft: current + 24
98895
+ });
98896
+ },
98897
+ className: gL,
98898
+ title: "Increase Indent",
98899
+ children: /* @__PURE__ */ jsx(LuIndentIncrease, { className: ic2 })
98900
+ }
98901
+ )
98902
+ ] }),
98903
+ /* @__PURE__ */ jsx("div", { className: grp, children: ATXT.map((b2, i3, a2) => {
98904
+ const handleClick = () => {
98905
+ if (!canFormat) {
98906
+ return;
98907
+ }
98908
+ const alignMap = {
98909
+ "Align left": "left",
98910
+ "Align center": "center",
98911
+ "Align right": "right",
98912
+ Justify: "justify"
98913
+ };
98914
+ const align = alignMap[b2.t];
98915
+ if (align) {
98916
+ p3.onUpdateTextStyle({ align });
98917
+ }
98918
+ };
98919
+ return /* @__PURE__ */ jsx(
98920
+ "button",
98921
+ {
98922
+ type: "button",
98923
+ disabled: !canMut,
98924
+ onMouseDown: (e2) => e2.preventDefault(),
98925
+ onClick: handleClick,
98926
+ className: i3 < a2.length - 1 ? gB : gL,
98927
+ title: b2.t,
98928
+ children: b2.i
98929
+ },
98930
+ b2.t
98931
+ );
98932
+ }) })
98933
+ ] }),
98934
+ /* @__PURE__ */ jsx("span", { className: "text-[9px] text-muted-foreground leading-none", children: "Paragraph" })
98935
+ ] })
98936
+ ] });
98766
98937
  }
98767
98938
  function CustomShowsControls({
98768
98939
  customShows,
@@ -99202,29 +99373,38 @@ function ToolbarPrimaryRow(p3) {
99202
99373
  ]
99203
99374
  }
99204
99375
  ),
99205
- 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: [
99206
- collab.remoteUsers.slice(0, 4).map((user) => /* @__PURE__ */ jsx(
99207
- "div",
99208
- {
99209
- className: "w-6 h-6 rounded-full border-2 border-background flex items-center justify-center text-[8px] font-semibold text-white shrink-0",
99210
- style: { backgroundColor: user.userColor },
99211
- title: user.userName,
99212
- children: user.userAvatar ? /* @__PURE__ */ jsx(
99213
- "img",
99376
+ collab && (collab.status === "connected" || collab.status === "connecting") && collab.remoteUsers.length > 0 && /* @__PURE__ */ jsxs(
99377
+ "button",
99378
+ {
99379
+ type: "button",
99380
+ onClick: p3.onOpenShareDialog,
99381
+ 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",
99382
+ title: t2("pptx.toolbar.sharingUsers", { count: collab.connectedCount }),
99383
+ children: [
99384
+ collab.remoteUsers.slice(0, 4).map((user) => /* @__PURE__ */ jsx(
99385
+ "div",
99214
99386
  {
99215
- src: user.userAvatar,
99216
- alt: user.userName,
99217
- className: "w-full h-full rounded-full object-cover"
99218
- }
99219
- ) : user.userName.slice(0, 2).toUpperCase()
99220
- },
99221
- user.clientId
99222
- )),
99223
- 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: [
99224
- "+",
99225
- collab.remoteUsers.length - 4
99226
- ] })
99227
- ] }),
99387
+ className: "w-6 h-6 rounded-full border-2 border-background flex items-center justify-center text-[8px] font-semibold text-white shrink-0",
99388
+ style: { backgroundColor: user.userColor },
99389
+ title: user.userName,
99390
+ children: user.userAvatar ? /* @__PURE__ */ jsx(
99391
+ "img",
99392
+ {
99393
+ src: user.userAvatar,
99394
+ alt: user.userName,
99395
+ className: "w-full h-full rounded-full object-cover"
99396
+ }
99397
+ ) : user.userName.slice(0, 2).toUpperCase()
99398
+ },
99399
+ user.clientId
99400
+ )),
99401
+ 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: [
99402
+ "+",
99403
+ collab.remoteUsers.length - 4
99404
+ ] })
99405
+ ]
99406
+ }
99407
+ ),
99228
99408
  /* @__PURE__ */ jsx(
99229
99409
  ModeSwitcher,
99230
99410
  {
@@ -99601,7 +99781,8 @@ function Toolbar(p3) {
99601
99781
  canEdit: p3.canEdit,
99602
99782
  selectedElement: p3.selectedElement,
99603
99783
  isInspectorPaneOpen: p3.isInspectorPaneOpen,
99604
- onToggleInspector: p3.onToggleInspector
99784
+ onToggleInspector: p3.onToggleInspector,
99785
+ onOpenAnimationPanel: p3.onOpenAnimationPanel
99605
99786
  }
99606
99787
  ),
99607
99788
  sSlw && /* @__PURE__ */ jsx(
@@ -107046,9 +107227,75 @@ function SetUpSlideShowDialog({
107046
107227
  }
107047
107228
  function BroadcastDialog({
107048
107229
  open,
107049
- onClose
107230
+ onClose,
107231
+ onStartBroadcast,
107232
+ onStopBroadcast,
107233
+ onStartPresenting,
107234
+ defaultRoomId,
107235
+ defaultUserName,
107236
+ defaultServerUrl
107050
107237
  }) {
107051
107238
  const { t: t2 } = useTranslation();
107239
+ const collab = useCollaboration();
107240
+ const isBroadcasting = collab !== null && collab.status !== "disconnected" && collab.status !== "error" && collab.config.role === "broadcaster";
107241
+ const [roomId, setRoomId] = useState("");
107242
+ const [userName, setUserName] = useState("");
107243
+ const [serverUrl, setServerUrl] = useState("");
107244
+ const [copied, setCopied] = useState(false);
107245
+ const dialogRef = useRef(null);
107246
+ useEffect(() => {
107247
+ if (open && !isBroadcasting) {
107248
+ const broadcastRoom = defaultRoomId ? `broadcast-${defaultRoomId}` : `broadcast-${Math.random().toString(36).slice(2, 10)}`;
107249
+ setRoomId(broadcastRoom);
107250
+ setUserName(defaultUserName ?? "");
107251
+ setServerUrl(defaultServerUrl ?? "ws://localhost:1234");
107252
+ }
107253
+ }, [open, isBroadcasting, defaultRoomId, defaultUserName, defaultServerUrl]);
107254
+ useEffect(() => {
107255
+ if (!open) {
107256
+ return;
107257
+ }
107258
+ function handleKeyDown(e2) {
107259
+ if (e2.key === "Escape") {
107260
+ onClose();
107261
+ }
107262
+ }
107263
+ document.addEventListener("keydown", handleKeyDown);
107264
+ return () => document.removeEventListener("keydown", handleKeyDown);
107265
+ }, [open, onClose]);
107266
+ useEffect(() => {
107267
+ if (open && dialogRef.current) {
107268
+ dialogRef.current.focus();
107269
+ }
107270
+ }, [open]);
107271
+ 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;
107272
+ const handleCopyUrl = useCallback(() => {
107273
+ void navigator.clipboard.writeText(broadcastUrl).then(() => {
107274
+ setCopied(true);
107275
+ setTimeout(() => setCopied(false), 2e3);
107276
+ return void 0;
107277
+ });
107278
+ }, [broadcastUrl]);
107279
+ const handleStartBroadcast = useCallback(() => {
107280
+ if (!roomId.trim() || !userName.trim()) {
107281
+ return;
107282
+ }
107283
+ onStartBroadcast?.({
107284
+ roomId: roomId.trim(),
107285
+ serverUrl: serverUrl.trim(),
107286
+ userName: userName.trim(),
107287
+ role: "broadcaster"
107288
+ });
107289
+ setTimeout(() => {
107290
+ onStartPresenting?.();
107291
+ }, 100);
107292
+ onClose();
107293
+ }, [roomId, userName, serverUrl, onStartBroadcast, onStartPresenting, onClose]);
107294
+ const handleStopBroadcast = useCallback(() => {
107295
+ onStopBroadcast?.();
107296
+ onClose();
107297
+ }, [onStopBroadcast, onClose]);
107298
+ const canStart = roomId.trim().length > 0 && userName.trim().length > 0 && serverUrl.trim().length > 0;
107052
107299
  if (!open) {
107053
107300
  return null;
107054
107301
  }
@@ -107062,42 +107309,225 @@ function BroadcastDialog({
107062
107309
  onClick: onClose
107063
107310
  }
107064
107311
  ),
107065
- /* @__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: [
107066
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-5 py-3 border-b border-border", children: [
107067
- /* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold text-foreground", children: t2("pptx.broadcast.title") }),
107312
+ /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-[201] flex items-center justify-center pointer-events-none", children: /* @__PURE__ */ jsxs(
107313
+ "div",
107314
+ {
107315
+ ref: dialogRef,
107316
+ role: "dialog",
107317
+ "aria-modal": "true",
107318
+ "aria-label": t2("pptx.broadcast.title"),
107319
+ tabIndex: -1,
107320
+ className: "pointer-events-auto w-full max-w-md rounded-xl border border-border bg-popover text-foreground shadow-2xl outline-none",
107321
+ children: [
107322
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-5 py-3 border-b border-border", children: [
107323
+ /* @__PURE__ */ jsxs("h2", { className: "text-sm font-semibold text-foreground flex items-center gap-2", children: [
107324
+ /* @__PURE__ */ jsx(LuCast, { className: "w-4 h-4" }),
107325
+ isBroadcasting ? t2("pptx.broadcast.broadcasting") : t2("pptx.broadcast.title")
107326
+ ] }),
107327
+ /* @__PURE__ */ jsx(
107328
+ "button",
107329
+ {
107330
+ type: "button",
107331
+ onClick: onClose,
107332
+ className: "text-muted-foreground hover:text-foreground text-lg leading-none",
107333
+ "aria-label": "Close",
107334
+ children: "\xD7"
107335
+ }
107336
+ )
107337
+ ] }),
107338
+ /* @__PURE__ */ jsx("div", { className: "px-5 py-4", children: isBroadcasting ? /* @__PURE__ */ jsx(
107339
+ ActiveBroadcastView,
107340
+ {
107341
+ collab,
107342
+ broadcastUrl,
107343
+ copied,
107344
+ onCopyUrl: handleCopyUrl,
107345
+ onStopBroadcast: handleStopBroadcast
107346
+ }
107347
+ ) : /* @__PURE__ */ jsx(
107348
+ StartBroadcastForm,
107349
+ {
107350
+ roomId,
107351
+ userName,
107352
+ serverUrl,
107353
+ onRoomIdChange: setRoomId,
107354
+ onUserNameChange: setUserName,
107355
+ onServerUrlChange: setServerUrl
107356
+ }
107357
+ ) }),
107358
+ !isBroadcasting && /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2 px-5 py-3 border-t border-border", children: [
107359
+ /* @__PURE__ */ jsx(
107360
+ "button",
107361
+ {
107362
+ type: "button",
107363
+ onClick: onClose,
107364
+ className: "px-3 py-1.5 rounded bg-muted hover:bg-accent text-[12px] text-foreground transition-colors",
107365
+ children: t2("common.close")
107366
+ }
107367
+ ),
107368
+ /* @__PURE__ */ jsx(
107369
+ "button",
107370
+ {
107371
+ type: "button",
107372
+ disabled: !canStart,
107373
+ onClick: handleStartBroadcast,
107374
+ 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",
107375
+ children: t2("pptx.broadcast.startBroadcast")
107376
+ }
107377
+ )
107378
+ ] })
107379
+ ]
107380
+ }
107381
+ ) })
107382
+ ] });
107383
+ }
107384
+ function StartBroadcastForm({
107385
+ roomId,
107386
+ userName,
107387
+ serverUrl,
107388
+ onRoomIdChange,
107389
+ onUserNameChange,
107390
+ onServerUrlChange
107391
+ }) {
107392
+ const { t: t2 } = useTranslation();
107393
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
107394
+ /* @__PURE__ */ jsx("p", { className: "text-[13px] text-muted-foreground leading-relaxed", children: t2("pptx.broadcast.description") }),
107395
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
107396
+ /* @__PURE__ */ jsx(
107397
+ "label",
107398
+ {
107399
+ htmlFor: "broadcast-room-id",
107400
+ className: "block text-[12px] font-medium text-foreground",
107401
+ children: t2("pptx.broadcast.sessionName")
107402
+ }
107403
+ ),
107404
+ /* @__PURE__ */ jsx(
107405
+ "input",
107406
+ {
107407
+ id: "broadcast-room-id",
107408
+ type: "text",
107409
+ value: roomId,
107410
+ onChange: (e2) => onRoomIdChange(e2.target.value),
107411
+ placeholder: "broadcast-abc123",
107412
+ 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"
107413
+ }
107414
+ )
107415
+ ] }),
107416
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
107417
+ /* @__PURE__ */ jsx(
107418
+ "label",
107419
+ {
107420
+ htmlFor: "broadcast-user-name",
107421
+ className: "block text-[12px] font-medium text-foreground",
107422
+ children: t2("pptx.broadcast.displayName")
107423
+ }
107424
+ ),
107425
+ /* @__PURE__ */ jsx(
107426
+ "input",
107427
+ {
107428
+ id: "broadcast-user-name",
107429
+ type: "text",
107430
+ value: userName,
107431
+ onChange: (e2) => onUserNameChange(e2.target.value),
107432
+ placeholder: "Presenter",
107433
+ 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"
107434
+ }
107435
+ )
107436
+ ] }),
107437
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
107438
+ /* @__PURE__ */ jsx(
107439
+ "label",
107440
+ {
107441
+ htmlFor: "broadcast-server-url",
107442
+ className: "block text-[12px] font-medium text-foreground",
107443
+ children: t2("pptx.broadcast.serverLabel")
107444
+ }
107445
+ ),
107446
+ /* @__PURE__ */ jsx(
107447
+ "input",
107448
+ {
107449
+ id: "broadcast-server-url",
107450
+ type: "text",
107451
+ value: serverUrl,
107452
+ onChange: (e2) => onServerUrlChange(e2.target.value),
107453
+ placeholder: "ws://localhost:1234",
107454
+ 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"
107455
+ }
107456
+ )
107457
+ ] }),
107458
+ /* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground/70 leading-relaxed", children: t2("pptx.broadcast.hint") })
107459
+ ] });
107460
+ }
107461
+ function ActiveBroadcastView({
107462
+ collab,
107463
+ broadcastUrl,
107464
+ copied,
107465
+ onCopyUrl,
107466
+ onStopBroadcast
107467
+ }) {
107468
+ const { t: t2 } = useTranslation();
107469
+ const statusColor = collab.status === "connected" ? "text-green-400" : collab.status === "connecting" ? "text-yellow-400" : "text-red-400";
107470
+ const statusIcon = collab.status === "connected" || collab.status === "connecting" ? /* @__PURE__ */ jsx(LuWifi, { className: "w-4 h-4" }) : /* @__PURE__ */ jsx(LuWifiOff, { className: "w-4 h-4" });
107471
+ const viewerCount = collab.remoteUsers.filter((u2) => u2.role === "viewer").length;
107472
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
107473
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
107474
+ /* @__PURE__ */ jsx("span", { className: statusColor, children: statusIcon }),
107475
+ /* @__PURE__ */ jsx("span", { className: "text-[13px] font-medium text-foreground capitalize", children: collab.status }),
107476
+ /* @__PURE__ */ jsxs("span", { className: "text-[12px] text-muted-foreground ml-auto flex items-center gap-1", children: [
107477
+ /* @__PURE__ */ jsx(LuUsers, { className: "w-3.5 h-3.5" }),
107478
+ t2("pptx.broadcast.viewerCount", { count: viewerCount })
107479
+ ] })
107480
+ ] }),
107481
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
107482
+ /* @__PURE__ */ jsx("label", { className: "block text-[12px] font-medium text-foreground", children: t2("pptx.broadcast.viewerLink") }),
107483
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
107484
+ /* @__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 }),
107068
107485
  /* @__PURE__ */ jsx(
107069
107486
  "button",
107070
107487
  {
107071
107488
  type: "button",
107072
- onClick: onClose,
107073
- className: "text-muted-foreground hover:text-foreground text-lg leading-none",
107074
- "aria-label": "Close",
107075
- children: "\xD7"
107489
+ onClick: onCopyUrl,
107490
+ 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",
107491
+ title: t2("pptx.broadcast.copyLink"),
107492
+ children: copied ? /* @__PURE__ */ jsxs(Fragment, { children: [
107493
+ /* @__PURE__ */ jsx(LuCheck, { className: "w-3.5 h-3.5 text-green-400" }),
107494
+ /* @__PURE__ */ jsx("span", { children: t2("pptx.share.copied") })
107495
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
107496
+ /* @__PURE__ */ jsx(LuCopy, { className: "w-3.5 h-3.5" }),
107497
+ /* @__PURE__ */ jsx("span", { children: t2("pptx.share.copyUrl") })
107498
+ ] })
107076
107499
  }
107077
107500
  )
107078
107501
  ] }),
107079
- /* @__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") }) }),
107080
- /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2 px-5 py-3 border-t border-border", children: [
107502
+ /* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground", children: t2("pptx.broadcast.shareHint") })
107503
+ ] }),
107504
+ collab.remoteUsers.length > 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
107505
+ /* @__PURE__ */ jsx("label", { className: "block text-[12px] font-medium text-foreground", children: t2("pptx.broadcast.viewers") }),
107506
+ /* @__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: [
107081
107507
  /* @__PURE__ */ jsx(
107082
- "button",
107508
+ "div",
107083
107509
  {
107084
- type: "button",
107085
- onClick: onClose,
107086
- className: "px-3 py-1.5 rounded bg-muted hover:bg-accent text-[12px] text-foreground transition-colors",
107087
- children: t2("common.close")
107510
+ className: "w-5 h-5 rounded-full flex items-center justify-center text-[8px] font-semibold text-white shrink-0",
107511
+ style: { backgroundColor: user.userColor },
107512
+ children: user.userName.slice(0, 2).toUpperCase()
107088
107513
  }
107089
107514
  ),
107090
- /* @__PURE__ */ jsx(
107091
- "button",
107092
- {
107093
- type: "button",
107094
- disabled: true,
107095
- className: "px-3 py-1.5 rounded bg-primary/40 text-[12px] text-white/50 cursor-not-allowed",
107096
- children: t2("pptx.broadcast.startBroadcast")
107097
- }
107098
- )
107099
- ] })
107100
- ] }) })
107515
+ /* @__PURE__ */ jsx("span", { className: "text-[12px] text-foreground truncate", children: user.userName }),
107516
+ /* @__PURE__ */ jsxs("span", { className: "text-[10px] text-muted-foreground ml-auto", children: [
107517
+ "Slide ",
107518
+ user.activeSlideIndex + 1
107519
+ ] })
107520
+ ] }, user.clientId)) })
107521
+ ] }),
107522
+ /* @__PURE__ */ jsx(
107523
+ "button",
107524
+ {
107525
+ type: "button",
107526
+ onClick: onStopBroadcast,
107527
+ 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",
107528
+ children: t2("pptx.broadcast.stopBroadcast")
107529
+ }
107530
+ )
107101
107531
  ] });
107102
107532
  }
107103
107533
  function getInitials2(name) {
@@ -108681,7 +109111,8 @@ function useDrawingOverlay({
108681
109111
  drawingWidth,
108682
109112
  isDrawingRef,
108683
109113
  onAddInkElement,
108684
- onAddFreeformShape
109114
+ onAddFreeformShape,
109115
+ onEraseInkElement
108685
109116
  }) {
108686
109117
  const isDrawing = activeTool !== "select";
108687
109118
  const [currentStrokePoints, setCurrentStrokePoints] = useState(
@@ -108730,6 +109161,7 @@ function useDrawingOverlay({
108730
109161
  continue;
108731
109162
  }
108732
109163
  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) {
109164
+ onEraseInkElement?.(el.id);
108733
109165
  break;
108734
109166
  }
108735
109167
  }
@@ -108748,7 +109180,7 @@ function useDrawingOverlay({
108748
109180
  isDrawingRef.current = true;
108749
109181
  }
108750
109182
  },
108751
- [activeTool, activeSlide, pointerToCanvasCoords, isDrawingRef]
109183
+ [activeTool, activeSlide, pointerToCanvasCoords, isDrawingRef, onEraseInkElement]
108752
109184
  );
108753
109185
  const handleDrawPointerMove = useCallback(
108754
109186
  (e2) => {
@@ -108818,6 +109250,9 @@ function useDrawingOverlay({
108818
109250
  i3 === 0 ? { type: "moveTo", pt: scaledPt } : { type: "lineTo", pt: scaledPt }
108819
109251
  );
108820
109252
  }
109253
+ if (segments.length > 2) {
109254
+ segments.push({ type: "close" });
109255
+ }
108821
109256
  const freeformShape = {
108822
109257
  id: `shape-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
108823
109258
  type: "shape",
@@ -108827,7 +109262,7 @@ function useDrawingOverlay({
108827
109262
  height: h2,
108828
109263
  shapeType: "custom",
108829
109264
  shapeStyle: {
108830
- fillColor: drawingColor,
109265
+ fillColor: "transparent",
108831
109266
  strokeColor: drawingColor,
108832
109267
  strokeWidth: drawingWidth
108833
109268
  },
@@ -108977,6 +109412,7 @@ function SlideCanvas({
108977
109412
  isDrawingRef,
108978
109413
  onAddInkElement,
108979
109414
  onAddFreeformShape,
109415
+ onEraseInkElement,
108980
109416
  onActionClick,
108981
109417
  onHyperlinkClick,
108982
109418
  comments,
@@ -109062,7 +109498,8 @@ function SlideCanvas({
109062
109498
  drawingWidth,
109063
109499
  isDrawingRef,
109064
109500
  onAddInkElement,
109065
- onAddFreeformShape
109501
+ onAddFreeformShape,
109502
+ onEraseInkElement
109066
109503
  });
109067
109504
  const rulerOffset = showRulers ? RULER_THICKNESS : 0;
109068
109505
  return /* @__PURE__ */ jsx(
@@ -109558,6 +109995,10 @@ function ViewerToolbarSection(props) {
109558
109995
  onSetMode,
109559
109996
  onToggleSidebar: () => s.setIsSlidesPaneOpen((p3) => !p3),
109560
109997
  onToggleInspector: () => s.setIsInspectorPaneOpen((p3) => !p3),
109998
+ onOpenAnimationPanel: () => {
109999
+ s.setIsInspectorPaneOpen(true);
110000
+ s.setSidebarPanelMode("properties");
110001
+ },
109561
110002
  onToggleCompactToolbar: () => s.setIsCompactToolbarOpen((p3) => !p3),
109562
110003
  onSetToolbarSection: s.setToolbarSection,
109563
110004
  onZoomIn: zoom.handleZoomIn,
@@ -111229,13 +111670,6 @@ function ViewerDialogGroup(props) {
111229
111670
  slideCount: slides.length
111230
111671
  }
111231
111672
  ),
111232
- /* @__PURE__ */ jsx(
111233
- BroadcastDialog,
111234
- {
111235
- open: dialogs.isBroadcastDialogOpen,
111236
- onClose: () => dialogs.setIsBroadcastDialogOpen(false)
111237
- }
111238
- ),
111239
111673
  /* @__PURE__ */ jsx(
111240
111674
  PrintDialog,
111241
111675
  {
@@ -111478,6 +111912,7 @@ function ViewerCanvasArea(props) {
111478
111912
  isDrawingRef: s.isDrawingRef,
111479
111913
  onAddInkElement: insertHandlers.handleAddInkElement,
111480
111914
  onAddFreeformShape: insertHandlers.handleAddFreeformShape,
111915
+ onEraseInkElement: insertHandlers.handleEraseInkElement,
111481
111916
  onActionClick: handleActionClick,
111482
111917
  onHyperlinkClick: handleHyperlinkClick,
111483
111918
  allSlides: mode === "present" ? slides : void 0,
@@ -111485,6 +111920,24 @@ function ViewerCanvasArea(props) {
111485
111920
  sourceSlideIndex: mode === "present" ? activeSlideIndex : void 0,
111486
111921
  fieldContext,
111487
111922
  tableStyleContext,
111923
+ collaborationOverlay: /* @__PURE__ */ jsxs(Fragment, { children: [
111924
+ /* @__PURE__ */ jsx(
111925
+ RemoteSelectionOverlay,
111926
+ {
111927
+ elements: effectiveSlide?.elements ?? [],
111928
+ activeSlideIndex
111929
+ }
111930
+ ),
111931
+ /* @__PURE__ */ jsx(
111932
+ CollaborationCursorOverlay,
111933
+ {
111934
+ activeSlideIndex,
111935
+ canvasWidth: canvasSize.width,
111936
+ canvasHeight: canvasSize.height,
111937
+ selectedElementId: s.selectedElementId
111938
+ }
111939
+ )
111940
+ ] }),
111488
111941
  comments: activeSlide?.comments,
111489
111942
  showCommentMarkers: s.sidebarPanelMode === "comments",
111490
111943
  onCommentMarkerClick: () => s.setSidebarPanelMode("comments"),
@@ -113095,6 +113548,37 @@ function useYjsDocumentSync({
113095
113548
  };
113096
113549
  }, [doc2, isConnected, getDocMap, setSlides]);
113097
113550
  }
113551
+ function useBroadcastFollower({
113552
+ collab,
113553
+ activeSlideIndex,
113554
+ setActiveSlideIndex,
113555
+ slideCount
113556
+ }) {
113557
+ const lastBroadcasterSlide = useRef(-1);
113558
+ useEffect(() => {
113559
+ if (!collab) {
113560
+ return;
113561
+ }
113562
+ if (collab.config.role !== "viewer") {
113563
+ return;
113564
+ }
113565
+ const broadcaster = collab.remoteUsers.find((u2) => u2.role === "broadcaster");
113566
+ if (!broadcaster) {
113567
+ return;
113568
+ }
113569
+ const targetSlide = broadcaster.activeSlideIndex;
113570
+ if (targetSlide < 0 || targetSlide >= slideCount) {
113571
+ return;
113572
+ }
113573
+ if (targetSlide === lastBroadcasterSlide.current) {
113574
+ return;
113575
+ }
113576
+ lastBroadcasterSlide.current = targetSlide;
113577
+ if (targetSlide !== activeSlideIndex) {
113578
+ setActiveSlideIndex(targetSlide);
113579
+ }
113580
+ }, [collab, activeSlideIndex, setActiveSlideIndex, slideCount]);
113581
+ }
113098
113582
  function computeGridSpacingPx(presentationGridSpacing) {
113099
113583
  if (presentationGridSpacing) {
113100
113584
  const px2 = Math.round(presentationGridSpacing.cx / EMU_PER_PX);
@@ -115435,6 +115919,17 @@ function useInsertElements(input) {
115435
115919
  }
115436
115920
  addElement(shape);
115437
115921
  };
115922
+ const handleEraseInkElement = (elementId) => {
115923
+ if (!activeSlide) {
115924
+ return;
115925
+ }
115926
+ ops.updateSlides(
115927
+ (prev) => prev.map(
115928
+ (s, i3) => i3 === activeSlideIndex ? { ...s, elements: s.elements.filter((el) => el.id !== elementId) } : s
115929
+ )
115930
+ );
115931
+ history.markDirty();
115932
+ };
115438
115933
  return {
115439
115934
  handleAddTextBox,
115440
115935
  handleAddShape,
@@ -115442,6 +115937,7 @@ function useInsertElements(input) {
115442
115937
  ...structured,
115443
115938
  handleAddInkElement,
115444
115939
  handleAddFreeformShape,
115940
+ handleEraseInkElement,
115445
115941
  ...fileHandlers
115446
115942
  };
115447
115943
  }
@@ -116271,7 +116767,8 @@ function useEditorOperations(input) {
116271
116767
  selectedElementIds,
116272
116768
  canvasSize,
116273
116769
  dialogs,
116274
- presentation
116770
+ presentation,
116771
+ userName
116275
116772
  } = input;
116276
116773
  const ops = useElementOperations({
116277
116774
  activeSlide,
@@ -116305,6 +116802,7 @@ function useEditorOperations(input) {
116305
116802
  const comments = useComments({
116306
116803
  slides,
116307
116804
  canEdit,
116805
+ userName,
116308
116806
  selectedElementId: state2.selectedElementId,
116309
116807
  onUpdateSlides: ops.updateSlides,
116310
116808
  onMarkDirty: history.markDirty
@@ -123078,6 +123576,7 @@ var PowerPointViewer = forwardRef(
123078
123576
  onDirtyChange,
123079
123577
  onActiveSlideChange,
123080
123578
  theme,
123579
+ authorName,
123081
123580
  collaboration,
123082
123581
  onStartCollaboration,
123083
123582
  onStopCollaboration,
@@ -123235,7 +123734,8 @@ var PowerPointViewer = forwardRef(
123235
123734
  selectedElementIds,
123236
123735
  canvasSize,
123237
123736
  dialogs,
123238
- presentation
123737
+ presentation,
123738
+ userName: authorName ?? collaboration?.userName
123239
123739
  });
123240
123740
  const {
123241
123741
  exportHandlers,
@@ -123438,6 +123938,19 @@ var PowerPointViewer = forwardRef(
123438
123938
  defaultServerUrl: shareDefaults?.serverUrl
123439
123939
  }
123440
123940
  ),
123941
+ /* @__PURE__ */ jsx(
123942
+ BroadcastDialog,
123943
+ {
123944
+ open: dialogs.isBroadcastDialogOpen,
123945
+ onClose: () => dialogs.setIsBroadcastDialogOpen(false),
123946
+ onStartBroadcast: onStartCollaboration,
123947
+ onStopBroadcast: onStopCollaboration,
123948
+ onStartPresenting: () => handleSetMode("present"),
123949
+ defaultRoomId: shareDefaults?.roomId,
123950
+ defaultUserName: shareDefaults?.userName,
123951
+ defaultServerUrl: shareDefaults?.serverUrl
123952
+ }
123953
+ ),
123441
123954
  /* @__PURE__ */ jsx(
123442
123955
  ViewerOverlays,
123443
123956
  {
@@ -123487,6 +124000,14 @@ var PowerPointViewer = forwardRef(
123487
124000
  canvasHeight: canvasSize.height,
123488
124001
  children: [
123489
124002
  /* @__PURE__ */ jsx(CollaborationDocumentSync, { slides, setSlides: state2.setSlides }),
124003
+ /* @__PURE__ */ jsx(
124004
+ BroadcastFollowerSync,
124005
+ {
124006
+ activeSlideIndex,
124007
+ setActiveSlideIndex: state2.setActiveSlideIndex,
124008
+ slideCount: slides.length
124009
+ }
124010
+ ),
123490
124011
  viewerContent
123491
124012
  ]
123492
124013
  }
@@ -123514,6 +124035,20 @@ function CollaborationDocumentSync({
123514
124035
  });
123515
124036
  return null;
123516
124037
  }
124038
+ function BroadcastFollowerSync({
124039
+ activeSlideIndex,
124040
+ setActiveSlideIndex,
124041
+ slideCount
124042
+ }) {
124043
+ const collab = useCollaboration();
124044
+ useBroadcastFollower({
124045
+ collab,
124046
+ activeSlideIndex,
124047
+ setActiveSlideIndex,
124048
+ slideCount
124049
+ });
124050
+ return null;
124051
+ }
123517
124052
  function colorSchemesMatch(a2, b2) {
123518
124053
  if (!a2) {
123519
124054
  return false;