@opengeni/react 0.46.1 → 0.52.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.
Files changed (341) hide show
  1. package/README.md +158 -14
  2. package/dist/artifacts-document.d.ts +3 -0
  3. package/dist/artifacts-document.js +17 -0
  4. package/dist/artifacts-document.js.map +1 -0
  5. package/dist/artifacts-presentation.d.ts +3 -0
  6. package/dist/artifacts-presentation.js +17 -0
  7. package/dist/artifacts-presentation.js.map +1 -0
  8. package/dist/artifacts-spreadsheet.d.ts +3 -0
  9. package/dist/artifacts-spreadsheet.js +17 -0
  10. package/dist/artifacts-spreadsheet.js.map +1 -0
  11. package/dist/artifacts.d.ts +2 -0
  12. package/dist/artifacts.js +170 -0
  13. package/dist/artifacts.js.map +1 -0
  14. package/dist/chunk-6BBWWOOU.js +82 -0
  15. package/dist/chunk-6BBWWOOU.js.map +1 -0
  16. package/dist/{chunk-HFO4ERGQ.js → chunk-724BW4TW.js} +586 -35
  17. package/dist/chunk-724BW4TW.js.map +1 -0
  18. package/dist/chunk-7I2KB3OK.js +3895 -0
  19. package/dist/chunk-7I2KB3OK.js.map +1 -0
  20. package/dist/chunk-7WCLAEFV.js +2828 -0
  21. package/dist/chunk-7WCLAEFV.js.map +1 -0
  22. package/dist/{chunk-U3CSCMWA.js → chunk-FK6VG4LL.js} +9 -13
  23. package/dist/chunk-FK6VG4LL.js.map +1 -0
  24. package/dist/{chunk-SRFUT2ZU.js → chunk-FWS3KC3Z.js} +120 -64
  25. package/dist/chunk-FWS3KC3Z.js.map +1 -0
  26. package/dist/{chunk-EKZH6IDG.js → chunk-HDBG7XGO.js} +62 -1
  27. package/dist/chunk-HDBG7XGO.js.map +1 -0
  28. package/dist/chunk-HUIKIDM5.js +2148 -0
  29. package/dist/chunk-HUIKIDM5.js.map +1 -0
  30. package/dist/{chunk-LGVMUIRV.js → chunk-JAGDUCZQ.js} +1053 -390
  31. package/dist/chunk-JAGDUCZQ.js.map +1 -0
  32. package/dist/{chunk-LWR4MXSS.js → chunk-JR3QFY6B.js} +420 -35
  33. package/dist/chunk-JR3QFY6B.js.map +1 -0
  34. package/dist/chunk-JVYQRRC4.js +53 -0
  35. package/dist/chunk-JVYQRRC4.js.map +1 -0
  36. package/dist/chunk-KCGXPP5V.js +157 -0
  37. package/dist/chunk-KCGXPP5V.js.map +1 -0
  38. package/dist/{chunk-EB67J347.js → chunk-KZ73RL76.js} +111 -31
  39. package/dist/chunk-KZ73RL76.js.map +1 -0
  40. package/dist/chunk-PEI6RANI.js +3234 -0
  41. package/dist/chunk-PEI6RANI.js.map +1 -0
  42. package/dist/{chunk-3PB7MIT6.js → chunk-ROIRTM37.js} +220 -72
  43. package/dist/chunk-ROIRTM37.js.map +1 -0
  44. package/dist/chunk-VZTQ73XT.js +86 -0
  45. package/dist/chunk-VZTQ73XT.js.map +1 -0
  46. package/dist/{chunk-VCBFUSUZ.js → chunk-WSK7G5LE.js} +72 -4
  47. package/dist/chunk-WSK7G5LE.js.map +1 -0
  48. package/dist/{chunk-FGZUCXZF.js → chunk-WYA65Y3F.js} +16 -16
  49. package/dist/chunk-WYA65Y3F.js.map +1 -0
  50. package/dist/{chunk-6EDV6YFE.js → chunk-X4S56Y2H.js} +147 -35
  51. package/dist/chunk-X4S56Y2H.js.map +1 -0
  52. package/dist/client.d.ts +7 -1
  53. package/dist/clipboard.d.ts +1 -0
  54. package/dist/clipboard.js +7 -0
  55. package/dist/clipboard.js.map +1 -0
  56. package/dist/commands/index.d.ts +3 -3
  57. package/dist/commands/registry.d.ts +1 -1
  58. package/dist/commands/types.d.ts +1 -1
  59. package/dist/components/approval-surface.d.ts +2 -2
  60. package/dist/components/artifacts/artifact-surface.d.ts +18 -0
  61. package/dist/components/artifacts/document-editor.d.ts +172 -0
  62. package/dist/components/artifacts/editable-artifact-ui.d.ts +13 -0
  63. package/dist/components/artifacts/editable-artifact-workbench.d.ts +49 -0
  64. package/dist/components/artifacts/editable-document.d.ts +24 -0
  65. package/dist/components/artifacts/editable-presentation.d.ts +24 -0
  66. package/dist/components/artifacts/editable-spreadsheet.d.ts +32 -0
  67. package/dist/components/artifacts/index.d.ts +8 -0
  68. package/dist/components/artifacts/presentation-editor.d.ts +297 -0
  69. package/dist/components/artifacts/spreadsheet-canvas.d.ts +120 -0
  70. package/dist/components/artifacts/spreadsheet-grid.d.ts +113 -0
  71. package/dist/components/browser-viewer.d.ts +29 -0
  72. package/dist/components/chat-composer.d.ts +13 -8
  73. package/dist/components/code-editor.d.ts +25 -3
  74. package/dist/components/command-palette.d.ts +2 -2
  75. package/dist/components/composer-transcription-control.d.ts +4 -2
  76. package/dist/components/composer.d.ts +29 -15
  77. package/dist/components/computer-viewer.d.ts +23 -0
  78. package/dist/components/copy-button.d.ts +2 -2
  79. package/dist/components/desktop-viewer.d.ts +25 -21
  80. package/dist/components/diff-view.d.ts +1 -1
  81. package/dist/components/enrollment-consent.d.ts +1 -1
  82. package/dist/components/enrollment-device-flow.d.ts +1 -1
  83. package/dist/components/file-browser.d.ts +2 -2
  84. package/dist/components/fleet-tile.d.ts +1 -1
  85. package/dist/components/generated-video-player.d.ts +10 -0
  86. package/dist/components/human-input-form.d.ts +1 -1
  87. package/dist/components/human-input-surface.d.ts +2 -2
  88. package/dist/components/interactive-workbench-viewers.d.ts +2 -0
  89. package/dist/components/machine-card.d.ts +2 -2
  90. package/dist/components/machine-dock-bar.d.ts +3 -3
  91. package/dist/components/machine-health-pill.d.ts +2 -2
  92. package/dist/components/machine-input-display.d.ts +1 -1
  93. package/dist/components/machine-metrics.d.ts +2 -2
  94. package/dist/components/machine-status-pill.d.ts +4 -4
  95. package/dist/components/machines/health.d.ts +1 -1
  96. package/dist/components/machines/machine-detail.d.ts +5 -3
  97. package/dist/components/machines/metric-history-chart.d.ts +1 -1
  98. package/dist/components/machines/metric-sparkline.d.ts +2 -2
  99. package/dist/components/machines/series.d.ts +2 -2
  100. package/dist/components/machines-dashboard.d.ts +5 -5
  101. package/dist/components/markdown.d.ts +1 -1
  102. package/dist/components/message-timeline.d.ts +22 -5
  103. package/dist/components/model-picker.d.ts +2 -2
  104. package/dist/components/model-policy-picker.d.ts +7 -7
  105. package/dist/components/pierre-diff.d.ts +1 -1
  106. package/dist/components/pierre-file.d.ts +1 -1
  107. package/dist/components/queue-draft-policy.d.ts +1 -1
  108. package/dist/components/queue-surface-implementation.d.ts +3 -3
  109. package/dist/components/queue-surface-state.d.ts +4 -4
  110. package/dist/components/queue-surface.d.ts +5 -5
  111. package/dist/components/sandbox-files.d.ts +5 -5
  112. package/dist/components/sandbox-terminal.d.ts +50 -4
  113. package/dist/components/sandbox-workspace.d.ts +30 -10
  114. package/dist/components/session-chrome.d.ts +4 -4
  115. package/dist/components/session-status.d.ts +2 -2
  116. package/dist/components/timeline-annotation-selection.d.ts +9 -0
  117. package/dist/components/timeline-annotations-dialog.d.ts +16 -0
  118. package/dist/components/timeline-annotations.d.ts +13 -0
  119. package/dist/components/tooltip.d.ts +4 -4
  120. package/dist/components/user-message-body.d.ts +2 -2
  121. package/dist/components/workbench-changes.d.ts +2 -2
  122. package/dist/components/workspace-dock.d.ts +3 -1
  123. package/dist/composer.d.ts +11 -11
  124. package/dist/composer.js +7 -7
  125. package/dist/hooks/internal.d.ts +7 -1
  126. package/dist/hooks/use-attached-browsers.d.ts +21 -0
  127. package/dist/hooks/use-available-models.d.ts +2 -2
  128. package/dist/hooks/use-billing-usage.d.ts +1 -1
  129. package/dist/hooks/use-browser-frame-stream.d.ts +28 -0
  130. package/dist/hooks/use-browser-identities.d.ts +26 -0
  131. package/dist/hooks/use-browser-session.d.ts +28 -0
  132. package/dist/hooks/use-browser-sessions.d.ts +32 -0
  133. package/dist/hooks/use-codex-accounts.d.ts +3 -3
  134. package/dist/hooks/use-composer.d.ts +36 -9
  135. package/dist/hooks/use-computer-frame-stream.d.ts +24 -0
  136. package/dist/hooks/use-computer-session.d.ts +28 -0
  137. package/dist/hooks/use-computer-sessions.d.ts +28 -0
  138. package/dist/hooks/use-desktop-stream.d.ts +1 -1
  139. package/dist/hooks/use-environments.d.ts +11 -5
  140. package/dist/hooks/use-file-attachments.d.ts +1 -1
  141. package/dist/hooks/use-goal.d.ts +2 -2
  142. package/dist/hooks/use-human-input.d.ts +2 -2
  143. package/dist/hooks/use-last-started-turn-policy.d.ts +2 -2
  144. package/dist/hooks/use-machine-chip.d.ts +2 -2
  145. package/dist/hooks/use-machines.d.ts +10 -3
  146. package/dist/hooks/use-packs.d.ts +7 -2
  147. package/dist/hooks/use-rigs.d.ts +1 -1
  148. package/dist/hooks/use-sandbox-files.d.ts +25 -4
  149. package/dist/hooks/use-sandbox-git.d.ts +13 -5
  150. package/dist/hooks/use-sandbox-terminal.d.ts +1 -1
  151. package/dist/hooks/use-scheduled-tasks.d.ts +1 -1
  152. package/dist/hooks/use-session-capabilities.d.ts +5 -4
  153. package/dist/hooks/use-session-control.d.ts +1 -1
  154. package/dist/hooks/use-session-events.d.ts +2 -2
  155. package/dist/hooks/use-session-lineage.d.ts +1 -1
  156. package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
  157. package/dist/hooks/use-session.d.ts +2 -2
  158. package/dist/hooks/use-slash-commands.d.ts +1 -1
  159. package/dist/hooks/use-terminal-stream.d.ts +8 -2
  160. package/dist/hooks/use-transcription.d.ts +2 -2
  161. package/dist/hooks/use-turn-queue.d.ts +2 -2
  162. package/dist/hooks/use-video-artifact-playback.d.ts +9 -0
  163. package/dist/hooks/use-voice-input.d.ts +6 -4
  164. package/dist/hooks/use-workspace-capture.d.ts +1 -1
  165. package/dist/hooks/use-workspace-edit.d.ts +1 -1
  166. package/dist/hooks/use-workspace-sessions.d.ts +1 -1
  167. package/dist/hooks/use-workspaces.d.ts +1 -1
  168. package/dist/index.d.ts +158 -154
  169. package/dist/index.js +1718 -908
  170. package/dist/index.js.map +1 -1
  171. package/dist/interaction.d.ts +23 -0
  172. package/dist/interaction.js +36 -0
  173. package/dist/interaction.js.map +1 -0
  174. package/dist/interactive-workbench-viewers-KHNOHWQE.js +13 -0
  175. package/dist/interactive-workbench-viewers-KHNOHWQE.js.map +1 -0
  176. package/dist/lib/composer-responsive-context.d.ts +9 -0
  177. package/dist/lib/relay-wire.d.ts +4 -0
  178. package/dist/lib/sandbox-liveness.d.ts +9 -0
  179. package/dist/lib/terminal-capability.d.ts +5 -0
  180. package/dist/lib/use-portal-token-style.d.ts +14 -3
  181. package/dist/lib/xterm-theme.d.ts +1 -1
  182. package/dist/machines.d.ts +30 -30
  183. package/dist/machines.js +3 -3
  184. package/dist/provider.d.ts +4 -4
  185. package/dist/{queue-surface-implementation-J4XJFKRW.js → queue-surface-implementation-2KVJTYXH.js} +117 -64
  186. package/dist/queue-surface-implementation-2KVJTYXH.js.map +1 -0
  187. package/dist/realtime/dropdown-menu.d.ts +8 -8
  188. package/dist/realtime/realtime-control.d.ts +15 -6
  189. package/dist/realtime.d.ts +4 -4
  190. package/dist/realtime.js +30 -33
  191. package/dist/realtime.js.map +1 -1
  192. package/dist/session-context.d.ts +19 -1
  193. package/dist/session-ui.d.ts +14 -14
  194. package/dist/session-ui.js +6 -5
  195. package/dist/session.d.ts +28 -28
  196. package/dist/session.js +5 -5
  197. package/dist/timeline/activity-rail.d.ts +6 -3
  198. package/dist/timeline/disclosure-context.d.ts +1 -1
  199. package/dist/timeline/fleet-decision-projection.d.ts +1 -1
  200. package/dist/timeline/fleet-decision-row.d.ts +3 -3
  201. package/dist/timeline/index.d.ts +19 -19
  202. package/dist/timeline/parsers.d.ts +6 -3
  203. package/dist/timeline/projection.d.ts +3 -3
  204. package/dist/timeline/registry.d.ts +10 -1
  205. package/dist/timeline/screenshot-lightbox.d.ts +2 -2
  206. package/dist/timeline/shared.d.ts +21 -10
  207. package/dist/timeline/tool-diff.d.ts +2 -2
  208. package/dist/timeline/tool-renderers.d.ts +1 -1
  209. package/dist/timeline/turn-summary.d.ts +3 -3
  210. package/dist/timeline/types.d.ts +42 -2
  211. package/dist/timeline-annotation-selection-3L7LHGG2.js +187 -0
  212. package/dist/timeline-annotation-selection-3L7LHGG2.js.map +1 -0
  213. package/dist/timeline-annotations-dialog-U7EVLR75.js +196 -0
  214. package/dist/timeline-annotations-dialog-U7EVLR75.js.map +1 -0
  215. package/dist/voice-recording-store.d.ts +8 -2
  216. package/package.json +52 -7
  217. package/src/artifacts-document.ts +31 -0
  218. package/src/artifacts-presentation.ts +36 -0
  219. package/src/artifacts-spreadsheet.ts +21 -0
  220. package/src/artifacts.ts +86 -0
  221. package/src/client.ts +90 -1
  222. package/src/clipboard.ts +2 -0
  223. package/src/components/artifacts/artifact-surface.tsx +93 -0
  224. package/src/components/artifacts/document-editor.tsx +2255 -0
  225. package/src/components/artifacts/editable-artifact-ui.tsx +89 -0
  226. package/src/components/artifacts/editable-artifact-workbench.tsx +203 -0
  227. package/src/components/artifacts/editable-document.tsx +657 -0
  228. package/src/components/artifacts/editable-presentation.tsx +1020 -0
  229. package/src/components/artifacts/editable-spreadsheet.tsx +733 -0
  230. package/src/components/artifacts/index.ts +99 -0
  231. package/src/components/artifacts/presentation-editor.tsx +3148 -0
  232. package/src/components/artifacts/spreadsheet-canvas.ts +799 -0
  233. package/src/components/artifacts/spreadsheet-grid.tsx +2129 -0
  234. package/src/components/browser-viewer.tsx +1592 -0
  235. package/src/components/chat-composer.tsx +25 -2
  236. package/src/components/code-editor.tsx +90 -31
  237. package/src/components/command-palette.tsx +1 -1
  238. package/src/components/composer-transcription-control.tsx +51 -22
  239. package/src/components/composer.tsx +49 -23
  240. package/src/components/computer-viewer.tsx +1139 -0
  241. package/src/components/copy-button.tsx +1 -1
  242. package/src/components/desktop-viewer.tsx +103 -75
  243. package/src/components/file-browser.tsx +39 -17
  244. package/src/components/generated-video-player.tsx +89 -0
  245. package/src/components/human-input-form.tsx +2 -2
  246. package/src/components/interactive-workbench-viewers.ts +2 -0
  247. package/src/components/machine-card.tsx +1 -1
  248. package/src/components/machine-input-display.ts +3 -0
  249. package/src/components/machines/machine-detail.tsx +39 -15
  250. package/src/components/machines-dashboard.tsx +7 -6
  251. package/src/components/message-timeline.tsx +327 -37
  252. package/src/components/model-picker.tsx +2 -2
  253. package/src/components/model-policy-picker.tsx +16 -15
  254. package/src/components/queue-surface-implementation.tsx +135 -75
  255. package/src/components/queue-surface-state.tsx +2 -2
  256. package/src/components/sandbox-files.tsx +142 -62
  257. package/src/components/sandbox-terminal.tsx +119 -35
  258. package/src/components/sandbox-workspace.tsx +394 -228
  259. package/src/components/session-chrome.tsx +67 -27
  260. package/src/components/timeline-annotation-selection.tsx +221 -0
  261. package/src/components/timeline-annotations-dialog.tsx +213 -0
  262. package/src/components/timeline-annotations.tsx +84 -0
  263. package/src/components/tooltip.tsx +13 -16
  264. package/src/components/user-message-body.tsx +1 -1
  265. package/src/components/workbench-changes.tsx +192 -35
  266. package/src/components/workspace-dock.tsx +13 -5
  267. package/src/composer.ts +3 -0
  268. package/src/hooks/internal.ts +132 -38
  269. package/src/hooks/use-attached-browsers.ts +152 -0
  270. package/src/hooks/use-browser-frame-stream.ts +316 -0
  271. package/src/hooks/use-browser-identities.ts +225 -0
  272. package/src/hooks/use-browser-session.ts +476 -0
  273. package/src/hooks/use-browser-sessions.ts +281 -0
  274. package/src/hooks/use-codex-accounts.ts +4 -4
  275. package/src/hooks/use-composer.ts +769 -24
  276. package/src/hooks/use-computer-frame-stream.ts +311 -0
  277. package/src/hooks/use-computer-session.ts +442 -0
  278. package/src/hooks/use-computer-sessions.ts +245 -0
  279. package/src/hooks/use-environments.ts +24 -5
  280. package/src/hooks/use-file-attachments.ts +70 -32
  281. package/src/hooks/use-goal.ts +17 -5
  282. package/src/hooks/use-machine-chip.ts +5 -3
  283. package/src/hooks/use-machines.ts +79 -3
  284. package/src/hooks/use-packs.ts +51 -0
  285. package/src/hooks/use-sandbox-files.ts +382 -88
  286. package/src/hooks/use-sandbox-git.ts +208 -46
  287. package/src/hooks/use-sandbox-terminal.ts +3 -2
  288. package/src/hooks/use-session-capabilities.ts +94 -70
  289. package/src/hooks/use-session-events.ts +18 -4
  290. package/src/hooks/use-terminal-stream.ts +102 -23
  291. package/src/hooks/use-transcription.ts +4 -14
  292. package/src/hooks/use-turn-queue.ts +17 -5
  293. package/src/hooks/use-video-artifact-playback.ts +28 -0
  294. package/src/hooks/use-voice-input.ts +163 -19
  295. package/src/hooks/use-windowed-sections.ts +9 -7
  296. package/src/hooks/use-workspace-edit.ts +2 -1
  297. package/src/index.ts +21 -6
  298. package/src/interaction.ts +71 -0
  299. package/src/lib/composer-responsive-context.ts +15 -0
  300. package/src/lib/relay-wire.ts +4 -0
  301. package/src/lib/sandbox-liveness.ts +11 -0
  302. package/src/lib/terminal-capability.ts +10 -0
  303. package/src/lib/use-portal-token-style.ts +60 -15
  304. package/src/provider.tsx +52 -7
  305. package/src/realtime/dropdown-menu.tsx +15 -19
  306. package/src/realtime/realtime-control.tsx +16 -5
  307. package/src/session-context.ts +90 -0
  308. package/src/timeline/activity-rail.tsx +31 -6
  309. package/src/timeline/index.ts +4 -1
  310. package/src/timeline/parsers.ts +46 -20
  311. package/src/timeline/projection.ts +233 -13
  312. package/src/timeline/registry.ts +19 -0
  313. package/src/timeline/screenshot-lightbox.tsx +30 -9
  314. package/src/timeline/shared.tsx +59 -11
  315. package/src/timeline/tool-renderers.tsx +421 -24
  316. package/src/timeline/turn-summary.tsx +2 -2
  317. package/src/timeline/types.ts +58 -2
  318. package/src/voice-recording-store.ts +15 -0
  319. package/styles/compiled.css +5298 -0
  320. package/styles/compiled.d.ts +2 -0
  321. package/styles/effective-tokens.css +27 -0
  322. package/styles/index.css +18 -0
  323. package/styles/responsive.css +208 -0
  324. package/styles/responsive.d.ts +2 -0
  325. package/styles/tokens.css +50 -17
  326. package/dist/chunk-3PB7MIT6.js.map +0 -1
  327. package/dist/chunk-6EDV6YFE.js.map +0 -1
  328. package/dist/chunk-EB67J347.js.map +0 -1
  329. package/dist/chunk-EKZH6IDG.js.map +0 -1
  330. package/dist/chunk-FGZUCXZF.js.map +0 -1
  331. package/dist/chunk-HFO4ERGQ.js.map +0 -1
  332. package/dist/chunk-JB5725BI.js +0 -48
  333. package/dist/chunk-JB5725BI.js.map +0 -1
  334. package/dist/chunk-LGVMUIRV.js.map +0 -1
  335. package/dist/chunk-LWR4MXSS.js.map +0 -1
  336. package/dist/chunk-SRFUT2ZU.js.map +0 -1
  337. package/dist/chunk-U3CSCMWA.js.map +0 -1
  338. package/dist/chunk-VCBFUSUZ.js.map +0 -1
  339. package/dist/fleet-decision-projection-SLNZMS42.js +0 -241
  340. package/dist/fleet-decision-projection-SLNZMS42.js.map +0 -1
  341. package/dist/queue-surface-implementation-J4XJFKRW.js.map +0 -1
@@ -0,0 +1,3234 @@
1
+ import {
2
+ ArtifactSurface,
3
+ EditableArtifactMessage,
4
+ asEditableArtifactError,
5
+ editableArtifactAccessRevoked,
6
+ editableArtifactProjectionKey,
7
+ editableArtifactStatusLabel,
8
+ useEditableArtifactView
9
+ } from "./chunk-KCGXPP5V.js";
10
+ import {
11
+ cn
12
+ } from "./chunk-22KP6LR5.js";
13
+
14
+ // src/components/artifacts/presentation-editor.tsx
15
+ import {
16
+ ChevronLeftIcon,
17
+ ChevronRightIcon,
18
+ ExpandIcon,
19
+ FilePlus2Icon,
20
+ MinusIcon,
21
+ PlusIcon,
22
+ Trash2Icon,
23
+ TypeIcon
24
+ } from "lucide-react";
25
+ import {
26
+ useCallback,
27
+ useEffect,
28
+ useId,
29
+ useLayoutEffect,
30
+ useMemo,
31
+ useRef,
32
+ useState
33
+ } from "react";
34
+ import { jsx, jsxs } from "react/jsx-runtime";
35
+ var MIN_ZOOM = 0.2;
36
+ var MAX_ZOOM = 4;
37
+ var RAIL_ITEM_HEIGHT = 116;
38
+ var RAIL_OVERSCAN = 3;
39
+ var DEFAULT_RAIL_HEIGHT = 640;
40
+ var DEFAULT_VIEWPORT = { width: 960, height: 640 };
41
+ var SPATIAL_TILE = 256;
42
+ var MAX_SPATIAL_BUCKETS_PER_OBJECT = 1024;
43
+ var CANVAS_OVERSCAN_PX = 96;
44
+ var MAX_CANVAS_EDGE = 8192;
45
+ var MAX_CANVAS_PIXELS = 16e6;
46
+ var MAX_EDITOR_GEOMETRY = 1e6;
47
+ var MAX_TABLE_CELLS_PER_PAINT = 2e4;
48
+ var MAX_PROJECTION_SLIDES = 1e5;
49
+ var MAX_PROJECTION_ELEMENTS_PER_SLIDE = 1e5;
50
+ var MAX_PROJECTION_GROUP_DEPTH = 16;
51
+ var MAX_PROJECTION_TABLE_CELLS = 1e6;
52
+ var MAX_PROJECTION_TEXT = 1e6;
53
+ function PresentationEditor(props) {
54
+ return /* @__PURE__ */ jsx(PresentationEditorCore, { ...props });
55
+ }
56
+ function PresentationProjectionEditor({
57
+ projection,
58
+ commit,
59
+ onCommandError,
60
+ onSlideChange,
61
+ onSelectionChange,
62
+ readOnly = false,
63
+ ...props
64
+ }) {
65
+ const view = useMemo(() => presentationViewFromProjection(projection), [projection]);
66
+ return /* @__PURE__ */ jsx(
67
+ PresentationEditorCore,
68
+ {
69
+ ...props,
70
+ presentation: view.presentation,
71
+ revision: projection.revision,
72
+ readOnly: readOnly || !commit,
73
+ onCommit: commit ? (command) => commit({ ...command, revision: projection.revision }) : void 0,
74
+ onCommandError,
75
+ onSlideChange: (slide, index) => {
76
+ const projected = view.slideById.get(slide.id);
77
+ if (projected) onSlideChange?.(projected, index);
78
+ },
79
+ onSelectionChange: (object, slide) => {
80
+ const projectedSlide = view.slideById.get(slide.id);
81
+ if (!projectedSlide) return;
82
+ onSelectionChange?.(
83
+ object ? view.elementById.get(object.id) ?? null : null,
84
+ projectedSlide
85
+ );
86
+ }
87
+ }
88
+ );
89
+ }
90
+ function PresentationEditorCore({
91
+ presentation,
92
+ revision,
93
+ activeSlideId: controlledSlideId,
94
+ defaultActiveSlideId,
95
+ selectedObjectId: controlledSelectionId,
96
+ defaultSelectedObjectId = null,
97
+ zoom: controlledZoom,
98
+ defaultZoom = 0.75,
99
+ readOnly = false,
100
+ className,
101
+ onSlideChange,
102
+ onSelectionChange,
103
+ onCommit,
104
+ onCommandError,
105
+ onZoomChange
106
+ }) {
107
+ const slides = presentation.slides.items;
108
+ const firstSlideId = slides[0]?.id ?? null;
109
+ const [localSlideId, setLocalSlideId] = useState(
110
+ defaultActiveSlideId ?? controlledSlideId ?? firstSlideId
111
+ );
112
+ const requestedSlideId = controlledSlideId ?? localSlideId;
113
+ const activeSlide = slides.find((slide) => slide.id === requestedSlideId) ?? slides[0] ?? null;
114
+ const activeIndex = activeSlide ? slides.indexOf(activeSlide) : -1;
115
+ const [localSelectionId, setLocalSelectionId] = useState(defaultSelectedObjectId);
116
+ const selectedId = controlledSelectionId === void 0 ? localSelectionId : controlledSelectionId;
117
+ const [localZoom, setLocalZoom] = useState(() => clampZoom(defaultZoom));
118
+ const zoom = clampZoom(controlledZoom ?? localZoom);
119
+ const [fitMode, setFitMode] = useState(false);
120
+ const [viewportMeasured, setViewportMeasured] = useState(false);
121
+ const [modelEpoch, setModelEpoch] = useState(0);
122
+ const [pendingCommands, setPendingCommands] = useState(0);
123
+ const [commandFailure, setCommandFailure] = useState(null);
124
+ const [rail, setRail] = useState({
125
+ scrollTop: 0,
126
+ height: DEFAULT_RAIL_HEIGHT
127
+ });
128
+ const [viewport, setViewport] = useState({
129
+ scrollLeft: 0,
130
+ scrollTop: 0,
131
+ ...DEFAULT_VIEWPORT,
132
+ stageLeft: 0,
133
+ stageTop: 0
134
+ });
135
+ const [draftPosition, setDraftPosition] = useState(null);
136
+ const [textEdit, setTextEdit] = useState(null);
137
+ const [textDraft, setTextDraft] = useState("");
138
+ const [paintEpoch, setPaintEpoch] = useState(0);
139
+ const railRef = useRef(null);
140
+ const viewportRef = useRef(null);
141
+ const stageRef = useRef(null);
142
+ const canvasRef = useRef(null);
143
+ const overlayRef = useRef(null);
144
+ const editorRef = useRef(null);
145
+ const dragRef = useRef(null);
146
+ const textEditRef = useRef(null);
147
+ const imageCacheRef = useRef(/* @__PURE__ */ new Map());
148
+ const mountedRef = useRef(true);
149
+ const commandScopeRef = useRef(0);
150
+ const commandVersionsRef = useRef(/* @__PURE__ */ new Map());
151
+ const pendingSlideIdRef = useRef(null);
152
+ const pendingObjectIdRef = useRef(null);
153
+ const domId = useId().replaceAll(":", "");
154
+ const selectionProxyId = `${domId}-presentation-selection`;
155
+ const instructionsId = `${domId}-presentation-instructions`;
156
+ const objects = useMemo(
157
+ () => activeSlide ? listSlideObjects(activeSlide) : [],
158
+ // revision is a deliberate host invalidation token; modelEpoch covers local commits.
159
+ // oxlint-disable-next-line react-hooks/exhaustive-deps
160
+ [activeSlide, modelEpoch, revision]
161
+ );
162
+ const objectById = useMemo(
163
+ () => new Map(objects.map((entry) => [entry.id, entry])),
164
+ [objects]
165
+ );
166
+ const objectIndexById = useMemo(
167
+ () => new Map(objects.map((entry, index) => [entry.id, index])),
168
+ [objects]
169
+ );
170
+ const selected = selectedId ? objectById.get(selectedId) ?? null : null;
171
+ const spatialIndex = useMemo(
172
+ () => buildSpatialIndex(objects, presentation.slideSize),
173
+ [objects, presentation.slideSize]
174
+ );
175
+ const effectiveSelectedPosition = selected && draftPosition?.objectId === selected.id ? draftPosition.position : selected ? copyPosition(selected.object.position) : null;
176
+ useEffect(() => {
177
+ const pendingSlideId = pendingSlideIdRef.current;
178
+ if (pendingSlideId && controlledSlideId === void 0 && slides.some((slide) => slide.id === pendingSlideId)) {
179
+ pendingSlideIdRef.current = null;
180
+ setLocalSlideId(pendingSlideId);
181
+ return;
182
+ }
183
+ if (activeSlide && requestedSlideId !== activeSlide.id && controlledSlideId === void 0 && pendingSlideId === null) {
184
+ setLocalSlideId(activeSlide.id);
185
+ }
186
+ }, [activeSlide, controlledSlideId, requestedSlideId, slides]);
187
+ useEffect(() => {
188
+ if (selectedId && !objectById.has(selectedId) && controlledSelectionId === void 0) {
189
+ setLocalSelectionId(null);
190
+ }
191
+ }, [controlledSelectionId, objectById, selectedId]);
192
+ useEffect(() => {
193
+ const pendingObjectId = pendingObjectIdRef.current;
194
+ if (pendingObjectId && controlledSelectionId === void 0 && objectById.has(pendingObjectId)) {
195
+ pendingObjectIdRef.current = null;
196
+ setLocalSelectionId(pendingObjectId);
197
+ }
198
+ }, [controlledSelectionId, objectById]);
199
+ useLayoutEffect(() => {
200
+ const element = railRef.current;
201
+ if (!element || typeof ResizeObserver === "undefined") return;
202
+ const measure = () => setRail((current) => ({
203
+ scrollTop: element.scrollTop,
204
+ height: element.clientHeight || current.height
205
+ }));
206
+ measure();
207
+ const observer = new ResizeObserver(measure);
208
+ observer.observe(element);
209
+ return () => observer.disconnect();
210
+ }, []);
211
+ useLayoutEffect(() => {
212
+ const element = viewportRef.current;
213
+ if (!element) return;
214
+ const measure = () => {
215
+ const stage = stageRef.current;
216
+ const measuredWidth = element.clientWidth;
217
+ const measuredHeight = element.clientHeight;
218
+ const next = {
219
+ scrollLeft: element.scrollLeft,
220
+ scrollTop: element.scrollTop,
221
+ width: measuredWidth || DEFAULT_VIEWPORT.width,
222
+ height: measuredHeight || DEFAULT_VIEWPORT.height,
223
+ stageLeft: stage?.offsetLeft ?? 0,
224
+ stageTop: stage?.offsetTop ?? 0
225
+ };
226
+ setViewport((current) => sameViewport(current, next) ? current : next);
227
+ if (measuredWidth > 0 && measuredHeight > 0) setViewportMeasured(true);
228
+ };
229
+ measure();
230
+ if (typeof ResizeObserver === "undefined") return;
231
+ const observer = new ResizeObserver(measure);
232
+ observer.observe(element);
233
+ if (stageRef.current) observer.observe(stageRef.current);
234
+ return () => observer.disconnect();
235
+ }, [activeSlide?.id, presentation.slideSize.height, presentation.slideSize.width, zoom]);
236
+ useLayoutEffect(() => {
237
+ const imageCache = imageCacheRef.current;
238
+ return () => disposeCanvasImageCache(imageCache);
239
+ }, [activeSlide]);
240
+ useEffect(() => {
241
+ const edit = textEditRef.current;
242
+ const currentEntry = edit ? objectById.get(edit.shape.id) : null;
243
+ if (edit && activeSlide?.id === edit.slideId && currentEntry?.kind === "shape" && !currentEntry.readOnly && currentEntry.object.geometry !== "line") {
244
+ const rebound = {
245
+ ...edit,
246
+ shape: currentEntry.object
247
+ };
248
+ textEditRef.current = rebound;
249
+ setTextEdit(rebound);
250
+ } else {
251
+ textEditRef.current = null;
252
+ setTextEdit(null);
253
+ }
254
+ }, [activeSlide?.id, objectById]);
255
+ useEffect(() => {
256
+ dragRef.current = null;
257
+ setDraftPosition(null);
258
+ commandScopeRef.current += 1;
259
+ commandVersionsRef.current.clear();
260
+ setPendingCommands(0);
261
+ setCommandFailure(null);
262
+ }, [activeSlide?.id, revision]);
263
+ useEffect(() => {
264
+ const edit = textEditRef.current;
265
+ if (edit && edit.shape.id !== selectedId) {
266
+ textEditRef.current = null;
267
+ setTextEdit(null);
268
+ }
269
+ const drag = dragRef.current;
270
+ if (drag && drag.object.id !== selectedId) {
271
+ dragRef.current = null;
272
+ setDraftPosition(null);
273
+ }
274
+ }, [selectedId]);
275
+ useEffect(() => {
276
+ if (!readOnly) return;
277
+ dragRef.current = null;
278
+ setDraftPosition(null);
279
+ textEditRef.current = null;
280
+ setTextEdit(null);
281
+ }, [readOnly]);
282
+ useEffect(() => {
283
+ mountedRef.current = true;
284
+ return () => {
285
+ mountedRef.current = false;
286
+ dragRef.current = null;
287
+ textEditRef.current = null;
288
+ };
289
+ }, []);
290
+ useEffect(() => {
291
+ const element = railRef.current;
292
+ if (!element || activeIndex < 0) return;
293
+ const top = activeIndex * RAIL_ITEM_HEIGHT;
294
+ const bottom = top + RAIL_ITEM_HEIGHT;
295
+ const height = element.clientHeight || rail.height;
296
+ if (top >= element.scrollTop && bottom <= element.scrollTop + height) return;
297
+ const nextTop = Math.max(0, top - height / 2);
298
+ element.scrollTop = nextTop;
299
+ setRail((current) => ({ ...current, scrollTop: nextTop }));
300
+ }, [activeIndex, rail.height]);
301
+ const visibleSlideRegion = useMemo(() => {
302
+ const slideWidth = presentation.slideSize.width;
303
+ const slideHeight = presentation.slideSize.height;
304
+ const stageWidth2 = slideWidth * zoom;
305
+ const stageHeight2 = slideHeight * zoom;
306
+ if (stageWidth2 <= viewport.width && stageHeight2 <= viewport.height) {
307
+ return { left: 0, top: 0, width: slideWidth, height: slideHeight };
308
+ }
309
+ const overscan = CANVAS_OVERSCAN_PX / zoom;
310
+ const visibleLeft = Math.max(0, (viewport.scrollLeft - viewport.stageLeft) / zoom);
311
+ const visibleTop = Math.max(0, (viewport.scrollTop - viewport.stageTop) / zoom);
312
+ const left = Math.max(0, visibleLeft - overscan);
313
+ const top = Math.max(0, visibleTop - overscan);
314
+ return {
315
+ left,
316
+ top,
317
+ width: Math.max(0, Math.min(slideWidth - left, viewport.width / zoom + overscan * 2)),
318
+ height: Math.max(0, Math.min(slideHeight - top, viewport.height / zoom + overscan * 2))
319
+ };
320
+ }, [presentation.slideSize.height, presentation.slideSize.width, viewport, zoom]);
321
+ const visibleObjectIndexes = useMemo(() => {
322
+ if (!activeSlide) return [];
323
+ if (visibleSlideRegion.left === 0 && visibleSlideRegion.top === 0 && visibleSlideRegion.width === presentation.slideSize.width && visibleSlideRegion.height === presentation.slideSize.height) {
324
+ return objects.map((_, index) => index);
325
+ }
326
+ const visible = querySpatialIndex(spatialIndex, visibleSlideRegion);
327
+ const selectedIndex = selectedId ? objectIndexById.get(selectedId) ?? -1 : -1;
328
+ if (selectedIndex >= 0 && !visible.includes(selectedIndex)) visible.push(selectedIndex);
329
+ const draftIndex = draftPosition ? objectIndexById.get(draftPosition.objectId) ?? -1 : -1;
330
+ if (draftIndex >= 0 && !visible.includes(draftIndex)) visible.push(draftIndex);
331
+ return visible.sort((a, b) => a - b);
332
+ }, [
333
+ activeSlide,
334
+ objects,
335
+ objectIndexById,
336
+ presentation.slideSize,
337
+ draftPosition,
338
+ selectedId,
339
+ spatialIndex,
340
+ visibleSlideRegion
341
+ ]);
342
+ useLayoutEffect(() => {
343
+ const canvas = canvasRef.current;
344
+ if (!canvas || !activeSlide) return;
345
+ paintSlideCanvas({
346
+ canvas,
347
+ slide: activeSlide,
348
+ slideSize: presentation.slideSize,
349
+ zoom,
350
+ objects,
351
+ objectIndexes: visibleObjectIndexes,
352
+ renderRegion: visibleSlideRegion,
353
+ draftPosition,
354
+ imageCache: imageCacheRef.current,
355
+ requestRepaint: () => setPaintEpoch((value) => value + 1)
356
+ });
357
+ }, [
358
+ activeSlide,
359
+ draftPosition,
360
+ objects,
361
+ paintEpoch,
362
+ presentation.slideSize,
363
+ visibleObjectIndexes,
364
+ visibleSlideRegion,
365
+ zoom
366
+ ]);
367
+ const selectObject = useCallback(
368
+ (entry) => {
369
+ textEditRef.current = null;
370
+ setTextEdit(null);
371
+ setDraftPosition(null);
372
+ if (controlledSelectionId === void 0) setLocalSelectionId(entry?.id ?? null);
373
+ if (activeSlide) onSelectionChange?.(entry?.object ?? null, activeSlide);
374
+ },
375
+ [activeSlide, controlledSelectionId, onSelectionChange]
376
+ );
377
+ const selectSlide = useCallback(
378
+ (slide, index) => {
379
+ if (controlledSlideId === void 0) setLocalSlideId(slide.id);
380
+ selectObject(null);
381
+ onSlideChange?.(slide, index);
382
+ },
383
+ [controlledSlideId, onSlideChange, selectObject]
384
+ );
385
+ const changeZoom = useCallback(
386
+ (next) => {
387
+ const clamped = clampZoom(next);
388
+ setFitMode(false);
389
+ if (clamped === zoom) return;
390
+ if (controlledZoom === void 0) setLocalZoom(clamped);
391
+ onZoomChange?.(clamped);
392
+ },
393
+ [controlledZoom, onZoomChange, zoom]
394
+ );
395
+ const applyFittedZoom = useCallback(() => {
396
+ const next = fittedPresentationZoom(viewport, presentation.slideSize);
397
+ if (next === zoom) return;
398
+ if (controlledZoom === void 0) setLocalZoom(next);
399
+ onZoomChange?.(next);
400
+ }, [controlledZoom, onZoomChange, presentation.slideSize, viewport, zoom]);
401
+ const fitSlide = useCallback(() => {
402
+ setFitMode(true);
403
+ applyFittedZoom();
404
+ }, [applyFittedZoom]);
405
+ useLayoutEffect(() => {
406
+ if (!viewportMeasured || controlledZoom !== void 0) return;
407
+ const fitted = fittedPresentationZoom(viewport, presentation.slideSize);
408
+ if (!fitMode && zoom <= fitted) return;
409
+ if (!fitMode) setFitMode(true);
410
+ applyFittedZoom();
411
+ }, [
412
+ applyFittedZoom,
413
+ controlledZoom,
414
+ fitMode,
415
+ presentation.slideSize,
416
+ viewport,
417
+ viewportMeasured,
418
+ zoom
419
+ ]);
420
+ const runCommit = useCallback(
421
+ (key, command, rollback, retry) => {
422
+ if (!onCommit) return;
423
+ const version = (commandVersionsRef.current.get(key) ?? 0) + 1;
424
+ commandVersionsRef.current.set(key, version);
425
+ const scope = commandScopeRef.current;
426
+ setCommandFailure(null);
427
+ let result;
428
+ try {
429
+ result = onCommit(command);
430
+ } catch (cause) {
431
+ if (!mountedRef.current || commandScopeRef.current !== scope || commandVersionsRef.current.get(key) !== version) {
432
+ return;
433
+ }
434
+ rollback();
435
+ const error = asPresentationError(cause);
436
+ setCommandFailure({ message: error.message, retry });
437
+ onCommandError?.(error);
438
+ return;
439
+ }
440
+ if (!result || typeof result.then !== "function") return;
441
+ setPendingCommands((current) => current + 1);
442
+ void Promise.resolve(result).then(
443
+ () => {
444
+ if (!mountedRef.current || commandScopeRef.current !== scope) return;
445
+ setPendingCommands((current) => Math.max(0, current - 1));
446
+ },
447
+ (cause) => {
448
+ if (!mountedRef.current || commandScopeRef.current !== scope) return;
449
+ setPendingCommands((current) => Math.max(0, current - 1));
450
+ if (commandVersionsRef.current.get(key) !== version) return;
451
+ rollback();
452
+ const error = asPresentationError(cause);
453
+ setCommandFailure({ message: error.message, retry });
454
+ onCommandError?.(error);
455
+ }
456
+ );
457
+ },
458
+ [onCommandError, onCommit]
459
+ );
460
+ const commitPosition = useCallback(
461
+ (entry, kind, next) => {
462
+ if (!activeSlide || readOnly || entry.readOnly) return;
463
+ const before = copyPosition(entry.object.position);
464
+ const after = normalizeEditorPosition(next);
465
+ if (samePosition(before, after)) return;
466
+ entry.object.position = after;
467
+ setModelEpoch((value) => value + 1);
468
+ const command = {
469
+ kind,
470
+ slideId: activeSlide.id,
471
+ objectId: entry.id,
472
+ before,
473
+ after
474
+ };
475
+ const submit = () => runCommit(
476
+ `position:${activeSlide.id}:${entry.id}`,
477
+ command,
478
+ () => {
479
+ entry.object.position = before;
480
+ setModelEpoch((value) => value + 1);
481
+ },
482
+ () => {
483
+ entry.object.position = after;
484
+ setModelEpoch((value) => value + 1);
485
+ submit();
486
+ }
487
+ );
488
+ submit();
489
+ },
490
+ [activeSlide, readOnly, runCommit]
491
+ );
492
+ const insertSlide = useCallback(() => {
493
+ if (readOnly) return;
494
+ const slideId = randomPresentationId();
495
+ const index = activeIndex < 0 ? slides.length : activeIndex + 1;
496
+ const command = { kind: "slide-insert", slideId, index };
497
+ const submit = () => {
498
+ pendingSlideIdRef.current = slideId;
499
+ runCommit(
500
+ `slide-insert:${slideId}`,
501
+ command,
502
+ () => {
503
+ if (pendingSlideIdRef.current === slideId) pendingSlideIdRef.current = null;
504
+ },
505
+ submit
506
+ );
507
+ };
508
+ submit();
509
+ }, [activeIndex, readOnly, runCommit, slides.length]);
510
+ const deleteSlide = useCallback(() => {
511
+ if (readOnly || !activeSlide) return;
512
+ const command = {
513
+ kind: "slide-delete",
514
+ slideId: activeSlide.id,
515
+ index: activeIndex
516
+ };
517
+ const submit = () => runCommit(`slide-delete:${activeSlide.id}`, command, () => void 0, submit);
518
+ submit();
519
+ }, [activeIndex, activeSlide, readOnly, runCommit]);
520
+ const insertTextBox = useCallback(() => {
521
+ if (readOnly || !activeSlide) return;
522
+ const objectId = randomPresentationId();
523
+ const position = nextTextBoxPosition(objects, presentation.slideSize);
524
+ const command = {
525
+ kind: "object-insert",
526
+ slideId: activeSlide.id,
527
+ objectId,
528
+ index: activeSlide.elements.length,
529
+ position
530
+ };
531
+ const submit = () => {
532
+ pendingObjectIdRef.current = objectId;
533
+ runCommit(
534
+ `object-insert:${activeSlide.id}:${objectId}`,
535
+ command,
536
+ () => {
537
+ if (pendingObjectIdRef.current === objectId) pendingObjectIdRef.current = null;
538
+ },
539
+ submit
540
+ );
541
+ };
542
+ submit();
543
+ }, [activeSlide, objects, presentation.slideSize, readOnly, runCommit]);
544
+ const deleteObject = useCallback(() => {
545
+ if (readOnly || !activeSlide || !selected || selected.readOnly) return;
546
+ const command = {
547
+ kind: "object-delete",
548
+ slideId: activeSlide.id,
549
+ objectId: selected.id
550
+ };
551
+ const submit = () => runCommit(`object-delete:${activeSlide.id}:${selected.id}`, command, () => void 0, submit);
552
+ submit();
553
+ }, [activeSlide, readOnly, runCommit, selected]);
554
+ const startTextEdit = useCallback(
555
+ (entry) => {
556
+ if (readOnly || !entry || entry.readOnly || entry.kind !== "shape") return;
557
+ const shape = entry.object;
558
+ if (shape.geometry === "line") return;
559
+ const before = shape.text.toString();
560
+ if (!activeSlide) return;
561
+ const edit = { slideId: activeSlide.id, shape, before };
562
+ textEditRef.current = edit;
563
+ setTextEdit(edit);
564
+ setTextDraft(before);
565
+ },
566
+ [activeSlide, readOnly]
567
+ );
568
+ useEffect(() => {
569
+ if (!textEdit) return;
570
+ editorRef.current?.focus();
571
+ editorRef.current?.select();
572
+ }, [textEdit]);
573
+ const finishTextEdit = useCallback(
574
+ (cancel = false) => {
575
+ const edit = textEditRef.current;
576
+ if (!edit) return;
577
+ textEditRef.current = null;
578
+ setTextEdit(null);
579
+ const { shape, before, slideId } = edit;
580
+ if (!activeSlide || activeSlide.id !== slideId) return;
581
+ if (cancel || readOnly || before === textDraft) return;
582
+ shape.text.set(textDraft);
583
+ setModelEpoch((value) => value + 1);
584
+ const command = {
585
+ kind: "text",
586
+ slideId,
587
+ objectId: shape.id,
588
+ before,
589
+ after: textDraft
590
+ };
591
+ const submit = () => runCommit(
592
+ `text:${slideId}:${shape.id}`,
593
+ command,
594
+ () => {
595
+ shape.text.set(before);
596
+ setModelEpoch((value) => value + 1);
597
+ },
598
+ () => {
599
+ shape.text.set(textDraft);
600
+ setModelEpoch((value) => value + 1);
601
+ submit();
602
+ }
603
+ );
604
+ submit();
605
+ },
606
+ [activeSlide, readOnly, runCommit, textDraft]
607
+ );
608
+ const cycleObject = useCallback(
609
+ (direction) => {
610
+ if (objects.length === 0) return;
611
+ const current = selected ? objects.indexOf(selected) : direction === 1 ? -1 : 0;
612
+ const next = (current + direction + objects.length) % objects.length;
613
+ selectObject(objects[next] ?? null);
614
+ },
615
+ [objects, selectObject, selected]
616
+ );
617
+ const changeSlideBy = useCallback(
618
+ (amount) => {
619
+ if (activeIndex < 0) return;
620
+ const nextIndex = Math.max(0, Math.min(slides.length - 1, activeIndex + amount));
621
+ const next = slides[nextIndex];
622
+ if (next && next !== activeSlide) selectSlide(next, nextIndex);
623
+ },
624
+ [activeIndex, activeSlide, selectSlide, slides]
625
+ );
626
+ const handleCanvasKeyDown = useCallback(
627
+ (event) => {
628
+ if (event.metaKey || event.ctrlKey) {
629
+ if (event.key === "+" || event.key === "=") {
630
+ event.preventDefault();
631
+ changeZoom(zoom * 1.15);
632
+ } else if (event.key === "-") {
633
+ event.preventDefault();
634
+ changeZoom(zoom / 1.15);
635
+ } else if (event.key === "0") {
636
+ event.preventDefault();
637
+ fitSlide();
638
+ }
639
+ return;
640
+ }
641
+ if (event.key === "]") {
642
+ event.preventDefault();
643
+ cycleObject(1);
644
+ return;
645
+ }
646
+ if (event.key === "[") {
647
+ event.preventDefault();
648
+ cycleObject(-1);
649
+ return;
650
+ }
651
+ if (event.key === "PageDown") {
652
+ event.preventDefault();
653
+ changeSlideBy(1);
654
+ return;
655
+ }
656
+ if (event.key === "PageUp") {
657
+ event.preventDefault();
658
+ changeSlideBy(-1);
659
+ return;
660
+ }
661
+ if (event.key === "Escape") {
662
+ event.preventDefault();
663
+ selectObject(null);
664
+ return;
665
+ }
666
+ if (event.key === "Enter" || event.key === "F2") {
667
+ if (selected?.kind === "shape") {
668
+ event.preventDefault();
669
+ startTextEdit(selected);
670
+ }
671
+ return;
672
+ }
673
+ if (!selected || !event.key.startsWith("Arrow")) return;
674
+ event.preventDefault();
675
+ if (readOnly || selected.readOnly) return;
676
+ const amount = event.shiftKey ? 10 : 1;
677
+ const dx = event.key === "ArrowLeft" ? -amount : event.key === "ArrowRight" ? amount : 0;
678
+ const dy = event.key === "ArrowUp" ? -amount : event.key === "ArrowDown" ? amount : 0;
679
+ const before = selected.object.position;
680
+ commitPosition(
681
+ selected,
682
+ event.altKey ? "resize" : "move",
683
+ event.altKey ? {
684
+ ...before,
685
+ width: Math.max(8, before.width + dx),
686
+ height: Math.max(8, before.height + dy)
687
+ } : { ...before, left: before.left + dx, top: before.top + dy }
688
+ );
689
+ },
690
+ [
691
+ changeSlideBy,
692
+ changeZoom,
693
+ commitPosition,
694
+ cycleObject,
695
+ fitSlide,
696
+ readOnly,
697
+ selectObject,
698
+ selected,
699
+ startTextEdit,
700
+ zoom
701
+ ]
702
+ );
703
+ const pointFromPointer = useCallback(
704
+ (event) => {
705
+ const rect = overlayRef.current?.getBoundingClientRect() ?? event.currentTarget.getBoundingClientRect();
706
+ const width = rect.width || presentation.slideSize.width * zoom;
707
+ const height = rect.height || presentation.slideSize.height * zoom;
708
+ return {
709
+ x: (event.clientX - rect.left) / Math.max(1, width) * presentation.slideSize.width,
710
+ y: (event.clientY - rect.top) / Math.max(1, height) * presentation.slideSize.height
711
+ };
712
+ },
713
+ [presentation.slideSize.height, presentation.slideSize.width, zoom]
714
+ );
715
+ const beginDrag = useCallback(
716
+ (event, entry, mode) => {
717
+ event.preventDefault();
718
+ event.stopPropagation();
719
+ selectObject(entry);
720
+ if (readOnly || entry.readOnly) return;
721
+ const start = pointFromPointer(event);
722
+ const before = copyPosition(entry.object.position);
723
+ dragRef.current = {
724
+ pointerId: event.pointerId,
725
+ mode,
726
+ object: entry.object,
727
+ rotation: objectRotation(entry),
728
+ start,
729
+ before
730
+ };
731
+ setDraftPosition({ objectId: entry.id, position: before });
732
+ overlayRef.current?.setPointerCapture?.(event.pointerId);
733
+ },
734
+ [pointFromPointer, readOnly, selectObject]
735
+ );
736
+ const handlePointerDown = useCallback(
737
+ (event) => {
738
+ if (event.button !== 0) return;
739
+ const point = pointFromPointer(event);
740
+ const hit = hitTest(spatialIndex, point.x, point.y);
741
+ if (!hit) {
742
+ selectObject(null);
743
+ return;
744
+ }
745
+ beginDrag(event, hit, "move");
746
+ },
747
+ [beginDrag, pointFromPointer, selectObject, spatialIndex]
748
+ );
749
+ const handlePointerMove = useCallback(
750
+ (event) => {
751
+ const drag = dragRef.current;
752
+ if (!drag || drag.pointerId !== event.pointerId) return;
753
+ const point = pointFromPointer(event);
754
+ setDraftPosition({ objectId: drag.object.id, position: positionFromDrag(drag, point) });
755
+ },
756
+ [pointFromPointer]
757
+ );
758
+ const finishDrag = useCallback(
759
+ (event, cancel = false) => {
760
+ const drag = dragRef.current;
761
+ if (!drag || drag.pointerId !== event.pointerId) return;
762
+ dragRef.current = null;
763
+ const captureTarget = overlayRef.current;
764
+ if (captureTarget?.hasPointerCapture?.(event.pointerId)) {
765
+ captureTarget.releasePointerCapture(event.pointerId);
766
+ }
767
+ setDraftPosition(null);
768
+ if (cancel) return;
769
+ const entry = objectById.get(drag.object.id);
770
+ if (!entry) return;
771
+ const point = pointFromPointer(event);
772
+ commitPosition(entry, drag.mode, positionFromDrag(drag, point));
773
+ },
774
+ [commitPosition, objectById, pointFromPointer]
775
+ );
776
+ const handleLostPointerCapture = useCallback((event) => {
777
+ const drag = dragRef.current;
778
+ if (!drag || drag.pointerId !== event.pointerId) return;
779
+ dragRef.current = null;
780
+ setDraftPosition(null);
781
+ }, []);
782
+ const railStart = Math.max(0, Math.floor(rail.scrollTop / RAIL_ITEM_HEIGHT) - RAIL_OVERSCAN);
783
+ const railEnd = Math.min(
784
+ slides.length,
785
+ Math.ceil((rail.scrollTop + rail.height) / RAIL_ITEM_HEIGHT) + RAIL_OVERSCAN
786
+ );
787
+ const stageWidth = presentation.slideSize.width * zoom;
788
+ const stageHeight = presentation.slideSize.height * zoom;
789
+ return /* @__PURE__ */ jsxs(
790
+ "div",
791
+ {
792
+ className: cn("flex size-full min-w-0 flex-col bg-og-surface-1", className),
793
+ style: { minHeight: "22rem" },
794
+ "data-og-presentation-editor": true,
795
+ "data-og-painted-object-count": visibleObjectIndexes.length,
796
+ "data-og-command-state": commandFailure ? "error" : pendingCommands > 0 ? "pending" : "idle",
797
+ "aria-busy": pendingCommands > 0 ? "true" : void 0,
798
+ children: [
799
+ /* @__PURE__ */ jsxs(
800
+ "div",
801
+ {
802
+ role: "toolbar",
803
+ "aria-label": "Presentation controls",
804
+ className: "flex h-10 shrink-0 items-center border-b border-og-border bg-og-surface-2",
805
+ children: [
806
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-1 overflow-x-auto px-2 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden", children: [
807
+ /* @__PURE__ */ jsx(
808
+ "button",
809
+ {
810
+ type: "button",
811
+ "aria-label": "Previous slide",
812
+ disabled: activeIndex <= 0,
813
+ onClick: () => changeSlideBy(-1),
814
+ className: "grid size-7 shrink-0 place-items-center rounded-og-sm text-og-fg-muted hover:bg-og-surface-3 hover:text-og-fg disabled:opacity-35 [&>svg]:size-3.5",
815
+ children: /* @__PURE__ */ jsx(ChevronLeftIcon, {})
816
+ }
817
+ ),
818
+ /* @__PURE__ */ jsx("span", { className: "min-w-16 shrink-0 text-center text-og-xs tabular-nums text-og-fg-muted", children: activeIndex >= 0 ? `${activeIndex + 1} / ${slides.length}` : `0 / ${slides.length}` }),
819
+ /* @__PURE__ */ jsx(
820
+ "button",
821
+ {
822
+ type: "button",
823
+ "aria-label": "Next slide",
824
+ disabled: activeIndex < 0 || activeIndex >= slides.length - 1,
825
+ onClick: () => changeSlideBy(1),
826
+ className: "grid size-7 shrink-0 place-items-center rounded-og-sm text-og-fg-muted hover:bg-og-surface-3 hover:text-og-fg disabled:opacity-35 [&>svg]:size-3.5",
827
+ children: /* @__PURE__ */ jsx(ChevronRightIcon, {})
828
+ }
829
+ ),
830
+ /* @__PURE__ */ jsx(
831
+ "button",
832
+ {
833
+ type: "button",
834
+ "aria-label": "Add slide",
835
+ disabled: readOnly,
836
+ onClick: insertSlide,
837
+ className: "ml-0.5 grid size-7 shrink-0 place-items-center rounded-og-sm text-og-fg-muted hover:bg-og-surface-3 hover:text-og-fg disabled:opacity-35 [&>svg]:size-3.5",
838
+ children: /* @__PURE__ */ jsx(FilePlus2Icon, {})
839
+ }
840
+ ),
841
+ /* @__PURE__ */ jsx(
842
+ "button",
843
+ {
844
+ type: "button",
845
+ "aria-label": "Delete slide",
846
+ disabled: readOnly || !activeSlide,
847
+ onClick: deleteSlide,
848
+ className: "grid size-7 shrink-0 place-items-center rounded-og-sm text-og-fg-muted hover:bg-og-surface-3 hover:text-og-status-failed disabled:opacity-35 [&>svg]:size-3.5",
849
+ children: /* @__PURE__ */ jsx(Trash2Icon, {})
850
+ }
851
+ ),
852
+ /* @__PURE__ */ jsx("span", { className: "mx-1 h-4 w-px shrink-0 bg-og-border" }),
853
+ /* @__PURE__ */ jsx(
854
+ "button",
855
+ {
856
+ type: "button",
857
+ "aria-label": "Zoom out",
858
+ onClick: () => changeZoom(zoom / 1.15),
859
+ className: "grid size-7 shrink-0 place-items-center rounded-og-sm text-og-fg-muted hover:bg-og-surface-3 hover:text-og-fg [&>svg]:size-3.5",
860
+ children: /* @__PURE__ */ jsx(MinusIcon, {})
861
+ }
862
+ ),
863
+ /* @__PURE__ */ jsxs("span", { className: "min-w-11 shrink-0 text-center text-og-xs tabular-nums text-og-fg-muted", children: [
864
+ Math.round(zoom * 100),
865
+ "%"
866
+ ] }),
867
+ /* @__PURE__ */ jsx(
868
+ "button",
869
+ {
870
+ type: "button",
871
+ "aria-label": "Zoom in",
872
+ onClick: () => changeZoom(zoom * 1.15),
873
+ className: "grid size-7 shrink-0 place-items-center rounded-og-sm text-og-fg-muted hover:bg-og-surface-3 hover:text-og-fg [&>svg]:size-3.5",
874
+ children: /* @__PURE__ */ jsx(PlusIcon, {})
875
+ }
876
+ ),
877
+ /* @__PURE__ */ jsx(
878
+ "button",
879
+ {
880
+ type: "button",
881
+ "aria-label": "Fit slide",
882
+ onClick: fitSlide,
883
+ className: "ml-0.5 grid size-7 shrink-0 place-items-center rounded-og-sm text-og-fg-muted hover:bg-og-surface-3 hover:text-og-fg [&>svg]:size-3.5",
884
+ children: /* @__PURE__ */ jsx(ExpandIcon, {})
885
+ }
886
+ ),
887
+ /* @__PURE__ */ jsx("span", { className: "mx-1 h-4 w-px shrink-0 bg-og-border" }),
888
+ /* @__PURE__ */ jsx(
889
+ "button",
890
+ {
891
+ type: "button",
892
+ "aria-label": "Add text box",
893
+ disabled: readOnly || !activeSlide,
894
+ onClick: insertTextBox,
895
+ className: "grid size-7 shrink-0 place-items-center rounded-og-sm text-og-fg-muted hover:bg-og-surface-3 hover:text-og-fg disabled:opacity-35 [&>svg]:size-3.5",
896
+ children: /* @__PURE__ */ jsx(TypeIcon, {})
897
+ }
898
+ ),
899
+ /* @__PURE__ */ jsx(
900
+ "button",
901
+ {
902
+ type: "button",
903
+ "aria-label": "Delete selected object",
904
+ disabled: readOnly || !selected || selected.readOnly,
905
+ onClick: deleteObject,
906
+ className: "grid size-7 shrink-0 place-items-center rounded-og-sm text-og-fg-muted hover:bg-og-surface-3 hover:text-og-status-failed disabled:opacity-35 [&>svg]:size-3.5",
907
+ children: /* @__PURE__ */ jsx(Trash2Icon, {})
908
+ }
909
+ )
910
+ ] }),
911
+ /* @__PURE__ */ jsx(
912
+ "span",
913
+ {
914
+ className: "hidden max-w-44 shrink-0 truncate border-l border-og-border px-2 text-og-xs text-og-fg-subtle sm:block",
915
+ title: commandFailure ? "Change not saved" : pendingCommands > 0 ? "Saving\u2026" : readOnly ? "View only" : selected ? `${objectLabel(selected)}${selected.readOnly ? " \xB7 View only" : ""}` : "Select an object to edit",
916
+ children: commandFailure ? "Change not saved" : pendingCommands > 0 ? "Saving\u2026" : readOnly ? "View only" : selected ? `${objectLabel(selected)}${selected.readOnly ? " \xB7 View only" : ""}` : "Select an object to edit"
917
+ }
918
+ )
919
+ ]
920
+ }
921
+ ),
922
+ /* @__PURE__ */ jsx("div", { role: "status", "aria-live": "polite", className: "sr-only", children: commandFailure?.message ?? (pendingCommands > 0 ? "Saving presentation changes" : "") }),
923
+ /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 min-w-0 flex-1", children: [
924
+ /* @__PURE__ */ jsx(
925
+ "div",
926
+ {
927
+ ref: railRef,
928
+ role: "listbox",
929
+ tabIndex: 0,
930
+ "aria-label": "Slides",
931
+ "aria-orientation": "vertical",
932
+ "aria-activedescendant": activeSlide && activeIndex >= railStart && activeIndex < railEnd ? `${domId}-slide-${safeDomId(activeSlide.id)}` : void 0,
933
+ className: "relative hidden w-40 shrink-0 overflow-y-auto border-r border-og-border bg-og-surface-2/70 outline-hidden focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-og-accent sm:block",
934
+ "data-og-slide-rail": true,
935
+ "data-og-window-start": railStart,
936
+ "data-og-window-end": railEnd,
937
+ onScroll: (event) => {
938
+ const scrollTop = event.currentTarget.scrollTop;
939
+ const height = event.currentTarget.clientHeight;
940
+ setRail((current) => ({
941
+ scrollTop,
942
+ height: height || current.height
943
+ }));
944
+ },
945
+ onKeyDown: (event) => {
946
+ if (slides.length === 0) return;
947
+ let nextIndex = null;
948
+ if (event.key === "ArrowDown") nextIndex = Math.min(slides.length - 1, activeIndex + 1);
949
+ else if (event.key === "ArrowUp") nextIndex = Math.max(0, activeIndex - 1);
950
+ else if (event.key === "Home") nextIndex = 0;
951
+ else if (event.key === "End") nextIndex = slides.length - 1;
952
+ if (nextIndex === null) return;
953
+ event.preventDefault();
954
+ const next = slides[nextIndex];
955
+ if (next && next !== activeSlide) selectSlide(next, nextIndex);
956
+ },
957
+ children: /* @__PURE__ */ jsx("div", { className: "relative w-full", style: { height: slides.length * RAIL_ITEM_HEIGHT }, children: slides.slice(railStart, railEnd).map((slide, offset) => {
958
+ const index = railStart + offset;
959
+ const active = slide === activeSlide;
960
+ return /* @__PURE__ */ jsxs(
961
+ "button",
962
+ {
963
+ id: `${domId}-slide-${safeDomId(slide.id)}`,
964
+ type: "button",
965
+ role: "option",
966
+ tabIndex: -1,
967
+ "aria-selected": active,
968
+ "aria-label": `Slide ${index + 1}${slide.title ? `: ${slide.title}` : ""}`,
969
+ onClick: () => selectSlide(slide, index),
970
+ className: cn(
971
+ "absolute left-0 flex w-full items-start gap-2 px-2 py-2 text-left transition-colors",
972
+ active ? "bg-og-surface-3 text-og-fg" : "text-og-fg-muted hover:bg-og-surface-3/70"
973
+ ),
974
+ style: { top: index * RAIL_ITEM_HEIGHT, height: 116 },
975
+ "data-og-slide-index": index,
976
+ children: [
977
+ /* @__PURE__ */ jsx("span", { className: "w-5 shrink-0 pt-0.5 text-right text-og-xs tabular-nums text-og-fg-subtle", children: index + 1 }),
978
+ /* @__PURE__ */ jsx(
979
+ "span",
980
+ {
981
+ className: cn(
982
+ "block min-w-0 flex-1 overflow-hidden border bg-white shadow-og-sm",
983
+ active ? "border-og-accent" : "border-og-border"
984
+ ),
985
+ style: { aspectRatio: "16 / 9", borderRadius: 3 },
986
+ children: /* @__PURE__ */ jsx(
987
+ SlideThumbnail,
988
+ {
989
+ slide,
990
+ width: presentation.slideSize.width,
991
+ height: presentation.slideSize.height,
992
+ paintRevision: `${String(revision ?? "")}:${modelEpoch}`
993
+ }
994
+ )
995
+ }
996
+ )
997
+ ]
998
+ },
999
+ slide.id
1000
+ );
1001
+ }) })
1002
+ }
1003
+ ),
1004
+ /* @__PURE__ */ jsxs(
1005
+ "div",
1006
+ {
1007
+ ref: viewportRef,
1008
+ className: "relative min-h-0 min-w-0 flex-1 overflow-auto bg-og-surface-3/60 p-6",
1009
+ onScroll: (event) => {
1010
+ const element = event.currentTarget;
1011
+ const width = element.clientWidth || viewport.width;
1012
+ const height = element.clientHeight || viewport.height;
1013
+ const next = {
1014
+ scrollLeft: element.scrollLeft,
1015
+ scrollTop: element.scrollTop,
1016
+ width,
1017
+ height,
1018
+ stageLeft: stageRef.current?.offsetLeft ?? 0,
1019
+ stageTop: stageRef.current?.offsetTop ?? 0
1020
+ };
1021
+ setViewport((current) => sameViewport(current, next) ? current : next);
1022
+ if (element.clientWidth > 0 && element.clientHeight > 0) setViewportMeasured(true);
1023
+ },
1024
+ "data-og-presentation-viewport": true,
1025
+ children: [
1026
+ commandFailure ? /* @__PURE__ */ jsxs(
1027
+ "div",
1028
+ {
1029
+ role: "alert",
1030
+ className: "absolute left-2 top-2 z-30 flex w-fit items-center gap-2 rounded-og-sm border border-og-status-failed/30 bg-og-surface-1/95 px-2 py-1 text-og-xs text-og-status-failed shadow-og-sm",
1031
+ style: { maxWidth: "calc(100% - 1rem)" },
1032
+ children: [
1033
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: commandFailure.message }),
1034
+ /* @__PURE__ */ jsx(
1035
+ "button",
1036
+ {
1037
+ type: "button",
1038
+ onClick: commandFailure.retry,
1039
+ className: "shrink-0 rounded-og-xs px-1.5 py-0.5 font-medium outline-hidden hover:bg-og-surface-3 focus-visible:ring-2 focus-visible:ring-og-accent",
1040
+ children: "Retry"
1041
+ }
1042
+ )
1043
+ ]
1044
+ }
1045
+ ) : null,
1046
+ activeSlide ? /* @__PURE__ */ jsxs(
1047
+ "div",
1048
+ {
1049
+ ref: stageRef,
1050
+ className: "relative mx-auto shrink-0 overflow-hidden shadow-og-lg",
1051
+ style: { width: stageWidth, height: stageHeight, backgroundColor: "#fff" },
1052
+ "data-og-presentation-stage": true,
1053
+ children: [
1054
+ /* @__PURE__ */ jsx(
1055
+ "canvas",
1056
+ {
1057
+ ref: canvasRef,
1058
+ "aria-hidden": true,
1059
+ className: "absolute inset-0 size-full",
1060
+ style: { width: stageWidth, height: stageHeight }
1061
+ }
1062
+ ),
1063
+ /* @__PURE__ */ jsx(
1064
+ "svg",
1065
+ {
1066
+ ref: overlayRef,
1067
+ role: "application",
1068
+ tabIndex: 0,
1069
+ "aria-label": `Slide ${activeIndex + 1} editor`,
1070
+ "aria-describedby": selected ? `${instructionsId} ${selectionProxyId}` : instructionsId,
1071
+ "aria-readonly": readOnly || selected?.readOnly || void 0,
1072
+ viewBox: `0 0 ${presentation.slideSize.width} ${presentation.slideSize.height}`,
1073
+ className: "absolute inset-0 size-full touch-none outline-hidden focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-og-accent",
1074
+ onKeyDown: handleCanvasKeyDown,
1075
+ onPointerDown: handlePointerDown,
1076
+ onPointerMove: handlePointerMove,
1077
+ onPointerUp: (event) => finishDrag(event),
1078
+ onPointerCancel: (event) => finishDrag(event, true),
1079
+ onLostPointerCapture: handleLostPointerCapture,
1080
+ onDoubleClick: () => startTextEdit(selected),
1081
+ children: selected && effectiveSelectedPosition ? /* @__PURE__ */ jsxs(
1082
+ "g",
1083
+ {
1084
+ "data-og-presentation-object": selected.id,
1085
+ transform: selectionTransform(selected, effectiveSelectedPosition),
1086
+ children: [
1087
+ /* @__PURE__ */ jsx(
1088
+ "rect",
1089
+ {
1090
+ x: effectiveSelectedPosition.left,
1091
+ y: effectiveSelectedPosition.top,
1092
+ width: effectiveSelectedPosition.width,
1093
+ height: effectiveSelectedPosition.height,
1094
+ fill: "transparent",
1095
+ stroke: "var(--og-accent, #3b82f6)",
1096
+ strokeWidth: 2,
1097
+ vectorEffect: "non-scaling-stroke",
1098
+ pointerEvents: "none"
1099
+ }
1100
+ ),
1101
+ !readOnly && !selected.readOnly ? /* @__PURE__ */ jsx(
1102
+ "circle",
1103
+ {
1104
+ cx: effectiveSelectedPosition.left + effectiveSelectedPosition.width,
1105
+ cy: effectiveSelectedPosition.top + effectiveSelectedPosition.height,
1106
+ r: 6 / zoom,
1107
+ fill: "var(--og-accent, #3b82f6)",
1108
+ stroke: "white",
1109
+ strokeWidth: 1.5,
1110
+ vectorEffect: "non-scaling-stroke",
1111
+ className: "cursor-nwse-resize",
1112
+ onPointerDown: (event) => beginDrag(event, selected, "resize")
1113
+ }
1114
+ ) : null
1115
+ ]
1116
+ }
1117
+ ) : null
1118
+ }
1119
+ ),
1120
+ selected ? /* @__PURE__ */ jsx("span", { id: selectionProxyId, role: "status", "aria-live": "polite", className: "sr-only", children: accessibleObjectLabel(selected) }) : null,
1121
+ /* @__PURE__ */ jsx("span", { id: instructionsId, className: "sr-only", children: "Use bracket keys to select objects, arrows to move, Alt plus arrows to resize, Enter to edit text, and Page Up or Page Down to change slides." }),
1122
+ textEdit && selected?.id === textEdit.shape.id && effectiveSelectedPosition ? /* @__PURE__ */ jsx(
1123
+ "textarea",
1124
+ {
1125
+ ref: editorRef,
1126
+ "aria-label": `Edit ${textEdit.shape.name}`,
1127
+ value: textDraft,
1128
+ maxLength: 1e6,
1129
+ onChange: (event) => setTextDraft(event.currentTarget.value),
1130
+ onInput: (event) => setTextDraft(event.currentTarget.value),
1131
+ onBlur: () => finishTextEdit(),
1132
+ onKeyDown: (event) => {
1133
+ if (event.key === "Escape") {
1134
+ event.preventDefault();
1135
+ finishTextEdit(true);
1136
+ } else if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
1137
+ event.preventDefault();
1138
+ finishTextEdit();
1139
+ }
1140
+ },
1141
+ className: "absolute resize-none overflow-hidden border-2 border-og-accent p-1 outline-hidden",
1142
+ style: {
1143
+ left: effectiveSelectedPosition.left * zoom,
1144
+ top: effectiveSelectedPosition.top * zoom,
1145
+ width: effectiveSelectedPosition.width * zoom,
1146
+ height: effectiveSelectedPosition.height * zoom,
1147
+ backgroundColor: "#fffffff2",
1148
+ color: resolveColor(textEdit.shape.text.style.color ?? "slate-950"),
1149
+ fontFamily: textEdit.shape.text.style.fontFamily ?? "Arial, sans-serif",
1150
+ fontSize: Math.max(8, (textEdit.shape.text.style.fontSize ?? 18) * zoom),
1151
+ fontWeight: textEdit.shape.text.style.bold ? 700 : 400,
1152
+ fontStyle: textEdit.shape.text.style.italic ? "italic" : "normal",
1153
+ textAlign: textEdit.shape.text.style.alignment ?? "left",
1154
+ transform: textEdit.shape.rotation === 0 ? void 0 : `rotate(${textEdit.shape.rotation}deg)`,
1155
+ transformOrigin: "center"
1156
+ }
1157
+ }
1158
+ ) : null
1159
+ ]
1160
+ }
1161
+ ) : /* @__PURE__ */ jsx("div", { className: "grid size-full min-h-72 place-items-center text-og-sm text-og-fg-muted", children: "This presentation has no slides." })
1162
+ ]
1163
+ }
1164
+ )
1165
+ ] })
1166
+ ]
1167
+ }
1168
+ );
1169
+ }
1170
+ function PresentationArtifactSurface({
1171
+ presentation,
1172
+ title = "Presentation",
1173
+ subtitle,
1174
+ actions,
1175
+ busy,
1176
+ className,
1177
+ ...editorProps
1178
+ }) {
1179
+ const count = presentation.slides.items.length;
1180
+ return /* @__PURE__ */ jsx(
1181
+ ArtifactSurface,
1182
+ {
1183
+ modality: "presentation",
1184
+ title,
1185
+ subtitle: subtitle ?? `${count} slide${count === 1 ? "" : "s"}`,
1186
+ actions,
1187
+ busy,
1188
+ className,
1189
+ children: /* @__PURE__ */ jsx(PresentationEditor, { ...editorProps, presentation })
1190
+ }
1191
+ );
1192
+ }
1193
+ function PresentationProjectionArtifactSurface({
1194
+ projection,
1195
+ title = "Presentation",
1196
+ subtitle,
1197
+ actions,
1198
+ busy,
1199
+ className,
1200
+ ...editorProps
1201
+ }) {
1202
+ const count = projection.slides.length;
1203
+ return /* @__PURE__ */ jsx(
1204
+ ArtifactSurface,
1205
+ {
1206
+ modality: "presentation",
1207
+ title,
1208
+ subtitle: subtitle ?? `${count} slide${count === 1 ? "" : "s"}`,
1209
+ actions,
1210
+ busy,
1211
+ className,
1212
+ children: /* @__PURE__ */ jsx(PresentationProjectionEditor, { ...editorProps, projection })
1213
+ }
1214
+ );
1215
+ }
1216
+ function SlideThumbnail({
1217
+ slide,
1218
+ width,
1219
+ height,
1220
+ paintRevision
1221
+ }) {
1222
+ const canvasRef = useRef(null);
1223
+ const imageCacheRef = useRef(/* @__PURE__ */ new Map());
1224
+ const [paintEpoch, setPaintEpoch] = useState(0);
1225
+ useLayoutEffect(() => {
1226
+ const imageCache = imageCacheRef.current;
1227
+ return () => disposeCanvasImageCache(imageCache);
1228
+ }, []);
1229
+ useLayoutEffect(() => {
1230
+ const canvas = canvasRef.current;
1231
+ const context = canvas?.getContext("2d");
1232
+ if (!canvas || !context) return;
1233
+ const displayWidth = 112;
1234
+ const scale = displayWidth / width;
1235
+ canvas.width = Math.max(1, Math.round(width * scale));
1236
+ canvas.height = Math.max(1, Math.round(height * scale));
1237
+ context.setTransform(scale, 0, 0, scale, 0, 0);
1238
+ context.fillStyle = resolveColor(slide.background.fill);
1239
+ context.fillRect(0, 0, width, height);
1240
+ const objects = listSlideObjects(slide);
1241
+ const usedImageSources = /* @__PURE__ */ new Set();
1242
+ for (const entry of objects) {
1243
+ drawObject(
1244
+ context,
1245
+ entry,
1246
+ entry.object.position,
1247
+ imageCacheRef.current,
1248
+ () => setPaintEpoch((value) => value + 1),
1249
+ { left: 0, top: 0, width, height },
1250
+ scale,
1251
+ usedImageSources
1252
+ );
1253
+ }
1254
+ disposeUnusedCanvasImages(imageCacheRef.current, usedImageSources);
1255
+ }, [height, paintEpoch, paintRevision, slide, width]);
1256
+ return /* @__PURE__ */ jsx("canvas", { ref: canvasRef, "aria-hidden": true, className: "block size-full" });
1257
+ }
1258
+ function presentationViewFromProjection(projection) {
1259
+ if (projection.slides.length > MAX_PROJECTION_SLIDES) {
1260
+ throw new Error(`Presentation projection exceeds ${MAX_PROJECTION_SLIDES} slides`);
1261
+ }
1262
+ const slideById = /* @__PURE__ */ new Map();
1263
+ const elementById = /* @__PURE__ */ new Map();
1264
+ const slideSize = {
1265
+ width: Math.min(MAX_EDITOR_GEOMETRY, Math.max(1, finite(projection.slideSize.width, 1))),
1266
+ height: Math.min(MAX_EDITOR_GEOMETRY, Math.max(1, finite(projection.slideSize.height, 1)))
1267
+ };
1268
+ const slides = projection.slides.map((slideProjection) => {
1269
+ if (slideProjection.elements.length > MAX_PROJECTION_ELEMENTS_PER_SLIDE) {
1270
+ throw new Error(
1271
+ `Slide ${slideProjection.id} exceeds ${MAX_PROJECTION_ELEMENTS_PER_SLIDE} projected objects`
1272
+ );
1273
+ }
1274
+ slideById.set(slideProjection.id, slideProjection);
1275
+ const elements = slideProjection.elements.map(
1276
+ (element) => presentationElementView(element, elementById, 0)
1277
+ );
1278
+ return {
1279
+ id: slideProjection.id,
1280
+ title: boundedProjectionText(slideProjection.title ?? ""),
1281
+ background: { fill: cloneProjectionFill(slideProjection.background ?? "white") },
1282
+ elements
1283
+ };
1284
+ });
1285
+ return {
1286
+ presentation: {
1287
+ slideSize,
1288
+ slides: { items: slides }
1289
+ },
1290
+ slideById,
1291
+ elementById
1292
+ };
1293
+ }
1294
+ function presentationElementView(element, byId, depth) {
1295
+ if (depth > MAX_PROJECTION_GROUP_DEPTH) {
1296
+ throw new Error(`Presentation group depth exceeds ${MAX_PROJECTION_GROUP_DEPTH}`);
1297
+ }
1298
+ byId.set(element.id, element);
1299
+ const position = normalizeEditorPosition(element.position);
1300
+ switch (element.kind) {
1301
+ case "shape": {
1302
+ const text = mutableProjectionText(element.text, element.textStyle);
1303
+ return {
1304
+ id: element.id,
1305
+ name: boundedProjectionText(element.name),
1306
+ __projectionReadOnly: element.readOnly ?? false,
1307
+ geometry: element.geometry,
1308
+ position,
1309
+ fill: cloneProjectionFill(
1310
+ element.fill ?? (element.geometry === "textbox" ? "none" : "white")
1311
+ ),
1312
+ line: cloneProjectionLine(element.line),
1313
+ rotation: finite(element.rotation ?? 0, 0),
1314
+ borderRadius: element.borderRadius,
1315
+ text
1316
+ };
1317
+ }
1318
+ case "chart":
1319
+ return {
1320
+ id: element.id,
1321
+ name: boundedProjectionText(element.name),
1322
+ __projectionReadOnly: element.readOnly ?? false,
1323
+ type: element.type,
1324
+ position,
1325
+ title: boundedProjectionText(element.title ?? ""),
1326
+ series: {
1327
+ items: element.series.map((series) => ({
1328
+ name: boundedProjectionText(series.name),
1329
+ categories: (series.categories ?? []).map(boundedProjectionText),
1330
+ values: series.values.map((value) => finite(value, 0)),
1331
+ xValues: (series.xValues ?? []).map((value) => finite(value, 0)),
1332
+ bubbleSizes: (series.bubbleSizes ?? []).map((value) => finite(value, 0)),
1333
+ fill: series.fill ? cloneProjectionFill(series.fill) : void 0,
1334
+ line: cloneProjectionLine(series.line)
1335
+ }))
1336
+ },
1337
+ xAxis: element.xAxis ? { ...element.xAxis } : void 0,
1338
+ yAxis: element.yAxis ? { ...element.yAxis } : void 0,
1339
+ dataLabels: element.dataLabels ? { ...element.dataLabels } : void 0
1340
+ };
1341
+ case "image": {
1342
+ const source = safeProjectionImageSource(element.source);
1343
+ return {
1344
+ id: element.id,
1345
+ name: boundedProjectionText(element.name),
1346
+ __projectionReadOnly: element.readOnly ?? false,
1347
+ position,
1348
+ alt: boundedProjectionText(element.alt ?? ""),
1349
+ prompt: element.prompt ? boundedProjectionText(element.prompt) : void 0,
1350
+ fit: element.fit ?? "contain",
1351
+ crop: element.crop ? { ...element.crop } : void 0,
1352
+ geometry: element.geometry ?? "rect",
1353
+ borderRadius: element.borderRadius,
1354
+ rotation: finite(element.rotation ?? 0, 0),
1355
+ flipHorizontal: element.flipHorizontal ?? false,
1356
+ flipVertical: element.flipVertical ?? false,
1357
+ sourceForSvg: () => source
1358
+ };
1359
+ }
1360
+ case "table": {
1361
+ const columns = element.rows[0]?.length ?? 0;
1362
+ if (columns <= 0 || element.rows.some((row) => row.length !== columns) || element.rows.length * columns > MAX_PROJECTION_TABLE_CELLS) {
1363
+ throw new Error(`Table ${element.id} has an invalid or oversized projected grid`);
1364
+ }
1365
+ return {
1366
+ id: element.id,
1367
+ name: boundedProjectionText(element.name),
1368
+ __projectionReadOnly: element.readOnly ?? false,
1369
+ position,
1370
+ rows: element.rows.map(
1371
+ (row) => row.map(
1372
+ (cell) => cell ? {
1373
+ text: mutableProjectionText(cell.text, cell.textStyle),
1374
+ fill: cloneProjectionFill(cell.fill ?? "white"),
1375
+ rowSpan: boundedSpan(cell.rowSpan),
1376
+ colSpan: boundedSpan(cell.colSpan)
1377
+ } : null
1378
+ )
1379
+ ),
1380
+ columnWidths: [...element.columnWidths ?? []],
1381
+ rowHeights: [...element.rowHeights ?? []],
1382
+ fill: cloneProjectionFill(element.fill ?? "white"),
1383
+ line: cloneProjectionLine(element.line),
1384
+ textStyle: { ...element.textStyle ?? {} }
1385
+ };
1386
+ }
1387
+ case "group":
1388
+ return {
1389
+ id: element.id,
1390
+ name: boundedProjectionText(element.name),
1391
+ __projectionReadOnly: element.readOnly ?? false,
1392
+ position,
1393
+ childOffset: {
1394
+ left: finite(element.childOffset.left, 0),
1395
+ top: finite(element.childOffset.top, 0)
1396
+ },
1397
+ childExtent: {
1398
+ width: Math.max(1, finite(element.childExtent.width, 1)),
1399
+ height: Math.max(1, finite(element.childExtent.height, 1))
1400
+ },
1401
+ rotation: finite(element.rotation ?? 0, 0),
1402
+ flipHorizontal: element.flipHorizontal ?? false,
1403
+ flipVertical: element.flipVertical ?? false,
1404
+ children: element.children.map((child) => presentationElementView(child, byId, depth + 1))
1405
+ };
1406
+ case "connector":
1407
+ return {
1408
+ id: element.id,
1409
+ name: boundedProjectionText(element.name),
1410
+ __projectionReadOnly: element.readOnly ?? false,
1411
+ geometry: "line",
1412
+ position,
1413
+ fill: "none",
1414
+ line: cloneProjectionLine(element.line),
1415
+ rotation: 0,
1416
+ text: mutableProjectionText("", void 0)
1417
+ };
1418
+ }
1419
+ }
1420
+ function mutableProjectionText(initial, style) {
1421
+ let value = boundedProjectionText(initial);
1422
+ return {
1423
+ style: { ...style ?? {} },
1424
+ set(next) {
1425
+ value = boundedProjectionText(next);
1426
+ },
1427
+ toString() {
1428
+ return value;
1429
+ }
1430
+ };
1431
+ }
1432
+ function cloneProjectionFill(fill) {
1433
+ return typeof fill === "string" ? fill : { ...fill };
1434
+ }
1435
+ function cloneProjectionLine(line) {
1436
+ return {
1437
+ style: line?.style ?? "solid",
1438
+ fill: line?.fill ? cloneProjectionFill(line.fill) : "slate-900",
1439
+ width: Math.max(0, finite(line?.width ?? 1, 1))
1440
+ };
1441
+ }
1442
+ function boundedProjectionText(value) {
1443
+ return value.slice(0, MAX_PROJECTION_TEXT);
1444
+ }
1445
+ function boundedSpan(value) {
1446
+ return Math.max(1, Math.min(1e4, Math.floor(finite(value ?? 1, 1))));
1447
+ }
1448
+ function safeProjectionImageSource(source) {
1449
+ if (!source || source.length > 64 * 1024 * 1024) return void 0;
1450
+ if (/^data:image\/(?:png|jpeg|gif|webp);base64,/i.test(source) || source.startsWith("blob:")) {
1451
+ return source;
1452
+ }
1453
+ try {
1454
+ const base = globalThis.location?.href;
1455
+ if (!base) return void 0;
1456
+ const url = new URL(source, base);
1457
+ return url.origin === globalThis.location.origin && /^https?:$/.test(url.protocol) ? url.href : void 0;
1458
+ } catch {
1459
+ return void 0;
1460
+ }
1461
+ }
1462
+ function asPresentationError(cause) {
1463
+ return cause instanceof Error ? cause : new Error("Presentation change failed");
1464
+ }
1465
+ function listSlideObjects(slide) {
1466
+ return slide.elements.map(displayObjectFromElement);
1467
+ }
1468
+ function displayObjectFromElement(object) {
1469
+ const readOnly = object.__projectionReadOnly === true;
1470
+ if ("geometry" in object && "text" in object)
1471
+ return { id: object.id, kind: "shape", object, readOnly };
1472
+ if ("series" in object) return { id: object.id, kind: "chart", object, readOnly };
1473
+ if ("sourceForSvg" in object) return { id: object.id, kind: "image", object, readOnly };
1474
+ if ("rows" in object) return { id: object.id, kind: "table", object, readOnly };
1475
+ return { id: object.id, kind: "group", object, readOnly };
1476
+ }
1477
+ function buildSpatialIndex(objects, slideSize) {
1478
+ const buckets = /* @__PURE__ */ new Map();
1479
+ const overflowObjects = [];
1480
+ const bounds = objects.map(objectBounds);
1481
+ const maxX = Math.max(0, Math.ceil(slideSize.width / SPATIAL_TILE) - 1);
1482
+ const maxY = Math.max(0, Math.ceil(slideSize.height / SPATIAL_TILE) - 1);
1483
+ for (const [index, entry] of objects.entries()) {
1484
+ const objectBound = bounds[index] ?? entry.object.position;
1485
+ if (!intersects(objectBound, { left: 0, top: 0, ...slideSize })) continue;
1486
+ const left = Math.max(0, Math.floor(objectBound.left / SPATIAL_TILE));
1487
+ const top = Math.max(0, Math.floor(objectBound.top / SPATIAL_TILE));
1488
+ const right = Math.min(maxX, Math.floor((objectBound.left + objectBound.width) / SPATIAL_TILE));
1489
+ const bottom = Math.min(
1490
+ maxY,
1491
+ Math.floor((objectBound.top + objectBound.height) / SPATIAL_TILE)
1492
+ );
1493
+ const bucketCount = (right - left + 1) * (bottom - top + 1);
1494
+ if (!Number.isSafeInteger(bucketCount) || bucketCount > MAX_SPATIAL_BUCKETS_PER_OBJECT) {
1495
+ overflowObjects.push(index);
1496
+ continue;
1497
+ }
1498
+ for (let y = top; y <= bottom; y += 1) {
1499
+ for (let x = left; x <= right; x += 1) {
1500
+ const key = `${x}:${y}`;
1501
+ const bucket = buckets.get(key);
1502
+ if (bucket) bucket.push(index);
1503
+ else buckets.set(key, [index]);
1504
+ }
1505
+ }
1506
+ }
1507
+ return { objects, bounds, buckets, overflowObjects };
1508
+ }
1509
+ function querySpatialIndex(index, viewport) {
1510
+ const seen = new Set(index.overflowObjects);
1511
+ const left = Math.floor(viewport.left / SPATIAL_TILE);
1512
+ const top = Math.floor(viewport.top / SPATIAL_TILE);
1513
+ const right = Math.floor((viewport.left + viewport.width) / SPATIAL_TILE);
1514
+ const bottom = Math.floor((viewport.top + viewport.height) / SPATIAL_TILE);
1515
+ for (let y = top; y <= bottom; y += 1) {
1516
+ for (let x = left; x <= right; x += 1) {
1517
+ for (const objectIndex of index.buckets.get(`${x}:${y}`) ?? []) {
1518
+ if (intersects(index.bounds[objectIndex], viewport)) seen.add(objectIndex);
1519
+ }
1520
+ }
1521
+ }
1522
+ return [...seen].filter((objectIndex) => {
1523
+ return !!index.objects[objectIndex] && intersects(index.bounds[objectIndex], viewport);
1524
+ }).sort((a, b) => a - b);
1525
+ }
1526
+ function paintSlideCanvas(options) {
1527
+ const { canvas, slide, slideSize, zoom } = options;
1528
+ const context = canvas.getContext("2d");
1529
+ if (!context) return;
1530
+ const cssWidth = slideSize.width * zoom;
1531
+ const cssHeight = slideSize.height * zoom;
1532
+ const ratio = Math.max(
1533
+ 1e-4,
1534
+ Math.min(
1535
+ 2,
1536
+ Math.max(1, globalThis.devicePixelRatio || 1),
1537
+ MAX_CANVAS_EDGE / Math.max(1, cssWidth),
1538
+ MAX_CANVAS_EDGE / Math.max(1, cssHeight),
1539
+ Math.sqrt(MAX_CANVAS_PIXELS / Math.max(1, cssWidth * cssHeight))
1540
+ )
1541
+ );
1542
+ const pixelWidth = Math.max(1, Math.round(slideSize.width * zoom * ratio));
1543
+ const pixelHeight = Math.max(1, Math.round(slideSize.height * zoom * ratio));
1544
+ if (canvas.width !== pixelWidth) canvas.width = pixelWidth;
1545
+ if (canvas.height !== pixelHeight) canvas.height = pixelHeight;
1546
+ context.setTransform(zoom * ratio, 0, 0, zoom * ratio, 0, 0);
1547
+ context.clearRect(0, 0, slideSize.width, slideSize.height);
1548
+ context.fillStyle = resolveColor(slide.background.fill);
1549
+ context.fillRect(0, 0, slideSize.width, slideSize.height);
1550
+ const usedImageSources = /* @__PURE__ */ new Set();
1551
+ for (const index of options.objectIndexes) {
1552
+ const entry = options.objects[index];
1553
+ if (!entry) continue;
1554
+ const position = entry.id === options.draftPosition?.objectId ? options.draftPosition.position : entry.object.position;
1555
+ drawObject(
1556
+ context,
1557
+ entry,
1558
+ position,
1559
+ options.imageCache,
1560
+ options.requestRepaint,
1561
+ options.renderRegion,
1562
+ zoom * ratio,
1563
+ usedImageSources
1564
+ );
1565
+ }
1566
+ disposeUnusedCanvasImages(options.imageCache, usedImageSources);
1567
+ }
1568
+ function drawObject(context, entry, position, imageCache, requestRepaint, renderRegion, pixelScale, usedImageSources) {
1569
+ switch (entry.kind) {
1570
+ case "shape":
1571
+ drawShape(context, entry.object, position);
1572
+ return;
1573
+ case "chart":
1574
+ drawChart(context, entry.object, position);
1575
+ return;
1576
+ case "image": {
1577
+ const source = drawImage(
1578
+ context,
1579
+ entry.object,
1580
+ position,
1581
+ imageCache,
1582
+ requestRepaint
1583
+ );
1584
+ if (source) usedImageSources.add(source);
1585
+ return;
1586
+ }
1587
+ case "table":
1588
+ drawTable(context, entry.object, position, renderRegion, pixelScale);
1589
+ return;
1590
+ case "group":
1591
+ drawGroup(
1592
+ context,
1593
+ entry.object,
1594
+ position,
1595
+ imageCache,
1596
+ requestRepaint,
1597
+ renderRegion,
1598
+ pixelScale,
1599
+ usedImageSources
1600
+ );
1601
+ }
1602
+ }
1603
+ function drawTable(context, table, position, renderRegion, pixelScale) {
1604
+ const rowCount = table.rows.length;
1605
+ const columnCount = table.rows[0]?.length ?? 0;
1606
+ if (rowCount === 0 || columnCount === 0 || !intersects(position, renderRegion)) return;
1607
+ const columnWidths = normalizeTableTracks(table.columnWidths, columnCount, position.width);
1608
+ const rowHeights = normalizeTableTracks(table.rowHeights, rowCount, position.height);
1609
+ const columnOffsets = cumulativeOffsets(columnWidths, position.left);
1610
+ const rowOffsets = cumulativeOffsets(rowHeights, position.top);
1611
+ const columnStart = visibleTrackStart(columnOffsets, renderRegion.left);
1612
+ const columnEnd = visibleTrackEnd(columnOffsets, renderRegion.left + renderRegion.width);
1613
+ const rowStart = visibleTrackStart(rowOffsets, renderRegion.top);
1614
+ const rowEnd = visibleTrackEnd(rowOffsets, renderRegion.top + renderRegion.height);
1615
+ const visibleCellCount = (rowEnd - rowStart) * (columnEnd - columnStart);
1616
+ if (visibleCellCount > MAX_TABLE_CELLS_PER_PAINT) {
1617
+ drawTableOverview(context, table, position, columnOffsets, rowOffsets, {
1618
+ columnStart,
1619
+ columnEnd,
1620
+ rowStart,
1621
+ rowEnd
1622
+ });
1623
+ return;
1624
+ }
1625
+ context.save();
1626
+ for (let rowIndex = rowStart; rowIndex < rowEnd; rowIndex += 1) {
1627
+ const row = table.rows[rowIndex];
1628
+ if (!row) continue;
1629
+ for (let columnIndex = columnStart; columnIndex < columnEnd; columnIndex += 1) {
1630
+ const cell = row[columnIndex];
1631
+ if (!cell) continue;
1632
+ const rightIndex = Math.min(columnCount, columnIndex + cell.colSpan);
1633
+ const bottomIndex = Math.min(rowCount, rowIndex + cell.rowSpan);
1634
+ const left = columnOffsets[columnIndex] ?? position.left;
1635
+ const top = rowOffsets[rowIndex] ?? position.top;
1636
+ const right = columnOffsets[rightIndex] ?? position.left + position.width;
1637
+ const bottom = rowOffsets[bottomIndex] ?? position.top + position.height;
1638
+ const width = Math.max(1, right - left);
1639
+ const height = Math.max(1, bottom - top);
1640
+ context.beginPath();
1641
+ context.rect(left, top, width, height);
1642
+ applyFillAndLine(context, cell.fill, table.line, false);
1643
+ if (width * pixelScale >= 8 && height * pixelScale >= 8) {
1644
+ drawText(context, cell.text.toString(), cell.text.style, {
1645
+ left: left + 4,
1646
+ top: top + 2,
1647
+ width: Math.max(1, width - 8),
1648
+ height: Math.max(1, height - 4)
1649
+ });
1650
+ }
1651
+ }
1652
+ }
1653
+ context.restore();
1654
+ }
1655
+ function visibleTrackStart(offsets, value) {
1656
+ return Math.max(0, Math.min(offsets.length - 2, lowerBound(offsets, value) - 1));
1657
+ }
1658
+ function visibleTrackEnd(offsets, value) {
1659
+ return Math.max(0, Math.min(offsets.length - 1, lowerBound(offsets, value) + 1));
1660
+ }
1661
+ function lowerBound(values, target) {
1662
+ let low = 0;
1663
+ let high = values.length;
1664
+ while (low < high) {
1665
+ const middle = low + Math.floor((high - low) / 2);
1666
+ if ((values[middle] ?? Number.POSITIVE_INFINITY) < target) low = middle + 1;
1667
+ else high = middle;
1668
+ }
1669
+ return low;
1670
+ }
1671
+ function drawTableOverview(context, table, position, columnOffsets, rowOffsets, visible) {
1672
+ context.save();
1673
+ context.beginPath();
1674
+ context.rect(position.left, position.top, position.width, position.height);
1675
+ applyFillAndLine(context, table.fill, table.line, false);
1676
+ const columnStep = Math.max(1, Math.ceil((visible.columnEnd - visible.columnStart) / 96));
1677
+ const rowStep = Math.max(1, Math.ceil((visible.rowEnd - visible.rowStart) / 96));
1678
+ context.beginPath();
1679
+ for (let index = visible.columnStart; index <= visible.columnEnd; index += columnStep) {
1680
+ const x = columnOffsets[index];
1681
+ if (x === void 0) continue;
1682
+ context.moveTo(x, position.top);
1683
+ context.lineTo(x, position.top + position.height);
1684
+ }
1685
+ for (let index = visible.rowStart; index <= visible.rowEnd; index += rowStep) {
1686
+ const y = rowOffsets[index];
1687
+ if (y === void 0) continue;
1688
+ context.moveTo(position.left, y);
1689
+ context.lineTo(position.left + position.width, y);
1690
+ }
1691
+ applyFillAndLine(context, "none", table.line, true);
1692
+ context.restore();
1693
+ }
1694
+ function normalizeTableTracks(values, count, total) {
1695
+ if (values.length !== count) return Array.from({ length: count }, () => total / count);
1696
+ const sum = values.reduce((value, next) => value + next, 0);
1697
+ if (!Number.isFinite(sum) || sum <= 0) {
1698
+ return Array.from({ length: count }, () => total / count);
1699
+ }
1700
+ return values.map((value) => value / sum * total);
1701
+ }
1702
+ function cumulativeOffsets(tracks, start) {
1703
+ const offsets = [start];
1704
+ for (const track of tracks) offsets.push((offsets.at(-1) ?? start) + track);
1705
+ return offsets;
1706
+ }
1707
+ function drawGroup(context, group, position, imageCache, requestRepaint, renderRegion, pixelScale, usedImageSources) {
1708
+ const scaleX = position.width / group.childExtent.width;
1709
+ const scaleY = position.height / group.childExtent.height;
1710
+ const translatedX = position.left - group.childOffset.left * scaleX;
1711
+ const translatedY = position.top - group.childOffset.top * scaleY;
1712
+ const centerX = position.left + position.width / 2;
1713
+ const centerY = position.top + position.height / 2;
1714
+ context.save();
1715
+ context.translate(centerX, centerY);
1716
+ context.rotate(group.rotation * Math.PI / 180);
1717
+ context.scale(group.flipHorizontal ? -1 : 1, group.flipVertical ? -1 : 1);
1718
+ context.translate(-centerX, -centerY);
1719
+ context.translate(translatedX, translatedY);
1720
+ context.scale(scaleX, scaleY);
1721
+ const childRenderRegion = inverseGroupRegion(group, position, renderRegion);
1722
+ for (const child of group.children) {
1723
+ const entry = displayObjectFromElement(child);
1724
+ if (!intersects(objectBounds(entry), childRenderRegion)) continue;
1725
+ drawObject(
1726
+ context,
1727
+ entry,
1728
+ child.position,
1729
+ imageCache,
1730
+ requestRepaint,
1731
+ childRenderRegion,
1732
+ pixelScale * Math.min(Math.abs(scaleX), Math.abs(scaleY)),
1733
+ usedImageSources
1734
+ );
1735
+ }
1736
+ context.restore();
1737
+ }
1738
+ function inverseGroupRegion(group, position, region) {
1739
+ const scaleX = position.width / group.childExtent.width;
1740
+ const scaleY = position.height / group.childExtent.height;
1741
+ const translatedX = position.left - group.childOffset.left * scaleX;
1742
+ const translatedY = position.top - group.childOffset.top * scaleY;
1743
+ const centerX = position.left + position.width / 2;
1744
+ const centerY = position.top + position.height / 2;
1745
+ const radians = -group.rotation * Math.PI / 180;
1746
+ const cos = Math.cos(radians);
1747
+ const sin = Math.sin(radians);
1748
+ const points = [
1749
+ [region.left, region.top],
1750
+ [region.left + region.width, region.top],
1751
+ [region.left, region.top + region.height],
1752
+ [region.left + region.width, region.top + region.height]
1753
+ ].map(([x = 0, y = 0]) => {
1754
+ const dx = x - centerX;
1755
+ const dy = y - centerY;
1756
+ const rotatedX = dx * cos - dy * sin;
1757
+ const rotatedY = dx * sin + dy * cos;
1758
+ const unflippedX = rotatedX * (group.flipHorizontal ? -1 : 1) + centerX;
1759
+ const unflippedY = rotatedY * (group.flipVertical ? -1 : 1) + centerY;
1760
+ return {
1761
+ x: (unflippedX - translatedX) / scaleX,
1762
+ y: (unflippedY - translatedY) / scaleY
1763
+ };
1764
+ });
1765
+ const xs = points.map((point) => point.x);
1766
+ const ys = points.map((point) => point.y);
1767
+ const left = Math.min(...xs);
1768
+ const right = Math.max(...xs);
1769
+ const top = Math.min(...ys);
1770
+ const bottom = Math.max(...ys);
1771
+ return { left, top, width: right - left, height: bottom - top };
1772
+ }
1773
+ function drawShape(context, shape, position) {
1774
+ context.save();
1775
+ context.translate(position.left + position.width / 2, position.top + position.height / 2);
1776
+ context.rotate(shape.rotation * Math.PI / 180);
1777
+ const x = -position.width / 2;
1778
+ const y = -position.height / 2;
1779
+ context.beginPath();
1780
+ switch (shape.geometry) {
1781
+ case "ellipse":
1782
+ context.ellipse(0, 0, position.width / 2, position.height / 2, 0, 0, Math.PI * 2);
1783
+ break;
1784
+ case "triangle":
1785
+ context.moveTo(0, y);
1786
+ context.lineTo(x + position.width, y + position.height);
1787
+ context.lineTo(x, y + position.height);
1788
+ context.closePath();
1789
+ break;
1790
+ case "rightArrow": {
1791
+ const body = position.width * 0.65;
1792
+ context.moveTo(x, y + position.height * 0.25);
1793
+ context.lineTo(x + body, y + position.height * 0.25);
1794
+ context.lineTo(x + body, y);
1795
+ context.lineTo(x + position.width, 0);
1796
+ context.lineTo(x + body, y + position.height);
1797
+ context.lineTo(x + body, y + position.height * 0.75);
1798
+ context.lineTo(x, y + position.height * 0.75);
1799
+ context.closePath();
1800
+ break;
1801
+ }
1802
+ case "line":
1803
+ context.moveTo(x, y);
1804
+ context.lineTo(x + position.width, y + position.height);
1805
+ break;
1806
+ case "roundRect":
1807
+ roundedRect(
1808
+ context,
1809
+ x,
1810
+ y,
1811
+ position.width,
1812
+ position.height,
1813
+ resolveRadius(shape.borderRadius, position)
1814
+ );
1815
+ break;
1816
+ default:
1817
+ context.rect(x, y, position.width, position.height);
1818
+ }
1819
+ applyFillAndLine(context, shape.fill, shape.line, shape.geometry === "line");
1820
+ const text = shape.text.toString();
1821
+ if (text)
1822
+ drawText(context, text, shape.text.style, {
1823
+ left: x,
1824
+ top: y,
1825
+ width: position.width,
1826
+ height: position.height
1827
+ });
1828
+ context.restore();
1829
+ }
1830
+ function drawChart(context, chart, position) {
1831
+ context.save();
1832
+ context.fillStyle = "#ffffff";
1833
+ context.strokeStyle = "#e2e8f0";
1834
+ context.lineWidth = 1;
1835
+ context.fillRect(position.left, position.top, position.width, position.height);
1836
+ context.strokeRect(position.left, position.top, position.width, position.height);
1837
+ if (chart.title) {
1838
+ drawText(
1839
+ context,
1840
+ chart.title,
1841
+ { fontSize: 18, bold: true, alignment: "center" },
1842
+ {
1843
+ left: position.left + 16,
1844
+ top: position.top + 8,
1845
+ width: Math.max(1, position.width - 32),
1846
+ height: 28
1847
+ }
1848
+ );
1849
+ }
1850
+ const plot = {
1851
+ left: position.left + 36,
1852
+ top: position.top + (chart.title ? 46 : 20),
1853
+ width: Math.max(1, position.width - 56),
1854
+ height: Math.max(1, position.height - (chart.title ? 72 : 48))
1855
+ };
1856
+ const values = chart.series.items.flatMap((series) => series.values).filter(Number.isFinite);
1857
+ const minValue = Math.min(0, chart.yAxis?.min ?? Math.min(...values, 0));
1858
+ const maxValue = Math.max(1, chart.yAxis?.max ?? Math.max(...values, 1));
1859
+ const valueRange = Math.max(Number.EPSILON, maxValue - minValue);
1860
+ const yForValue = (value) => plot.top + plot.height - (value - minValue) / valueRange * plot.height;
1861
+ const palette = ["#2563eb", "#0f766e", "#f59e0b", "#dc2626", "#7c3aed"];
1862
+ if (chart.type === "pie" || chart.type === "doughnut") {
1863
+ const series = chart.series.items[0];
1864
+ const slices = series?.values.map((value) => Math.max(0, value)) ?? [];
1865
+ const total = slices.reduce((sum, value) => sum + value, 0);
1866
+ let angle = -Math.PI / 2;
1867
+ const radius = Math.min(plot.width, plot.height) * 0.42;
1868
+ const cx = plot.left + plot.width / 2;
1869
+ const cy = plot.top + plot.height / 2;
1870
+ slices.forEach((value, index) => {
1871
+ const next = angle + (total > 0 ? value / total * Math.PI * 2 : 0);
1872
+ context.beginPath();
1873
+ context.moveTo(cx, cy);
1874
+ context.arc(cx, cy, radius, angle, next);
1875
+ context.closePath();
1876
+ context.fillStyle = palette[index % palette.length] ?? "#2563eb";
1877
+ context.fill();
1878
+ angle = next;
1879
+ });
1880
+ if (chart.type === "doughnut") {
1881
+ context.beginPath();
1882
+ context.arc(cx, cy, radius * 0.55, 0, Math.PI * 2);
1883
+ context.fillStyle = "#ffffff";
1884
+ context.fill();
1885
+ }
1886
+ } else if (chart.type === "line" || chart.type === "area") {
1887
+ drawChartAxes(context, chart, plot, yForValue(0));
1888
+ chart.series.items.forEach((series, seriesIndex) => {
1889
+ if (series.values.length === 0) return;
1890
+ const points = series.values.map((value, index) => ({
1891
+ x: plot.left + index / Math.max(1, series.values.length - 1) * plot.width,
1892
+ y: yForValue(value)
1893
+ }));
1894
+ const color = resolveColor(
1895
+ series.line?.fill ?? series.fill ?? palette[seriesIndex % palette.length] ?? "#2563eb"
1896
+ );
1897
+ if (chart.type === "area") {
1898
+ context.beginPath();
1899
+ context.moveTo(points[0]?.x ?? plot.left, yForValue(0));
1900
+ for (const point of points) context.lineTo(point.x, point.y);
1901
+ context.lineTo(points.at(-1)?.x ?? plot.left + plot.width, yForValue(0));
1902
+ context.closePath();
1903
+ context.globalAlpha = 0.22;
1904
+ context.fillStyle = color;
1905
+ context.fill();
1906
+ context.globalAlpha = 1;
1907
+ }
1908
+ context.beginPath();
1909
+ points.forEach((point, index) => {
1910
+ if (index === 0) context.moveTo(point.x, point.y);
1911
+ else context.lineTo(point.x, point.y);
1912
+ });
1913
+ context.strokeStyle = color;
1914
+ context.lineWidth = series.line?.width ?? 3;
1915
+ context.stroke();
1916
+ for (const point of points) {
1917
+ context.beginPath();
1918
+ context.arc(point.x, point.y, 3, 0, Math.PI * 2);
1919
+ context.fillStyle = color;
1920
+ context.fill();
1921
+ }
1922
+ });
1923
+ } else if (chart.type === "scatter" || chart.type === "bubble") {
1924
+ drawChartAxes(context, chart, plot, yForValue(0));
1925
+ const xValues = chart.series.items.flatMap(
1926
+ (series) => series.values.map((_value, index) => series.xValues[index] ?? index)
1927
+ );
1928
+ const minX = chart.xAxis?.min ?? Math.min(...xValues, 0);
1929
+ const maxX = chart.xAxis?.max ?? Math.max(...xValues, 1);
1930
+ const xRange = Math.max(Number.EPSILON, maxX - minX);
1931
+ chart.series.items.forEach((series, seriesIndex) => {
1932
+ context.fillStyle = resolveColor(
1933
+ series.fill ?? palette[seriesIndex % palette.length] ?? "#2563eb"
1934
+ );
1935
+ series.values.forEach((value, index) => {
1936
+ const xValue = series.xValues[index] ?? index;
1937
+ const x = plot.left + (xValue - minX) / xRange * plot.width;
1938
+ const radius = chart.type === "bubble" ? Math.max(3, Math.sqrt(Math.max(0, series.bubbleSizes[index] ?? 9))) : 4;
1939
+ context.beginPath();
1940
+ context.arc(x, yForValue(value), radius, 0, Math.PI * 2);
1941
+ context.fill();
1942
+ });
1943
+ });
1944
+ } else if (chart.type === "radar") {
1945
+ drawRadarChart(context, chart, plot, palette);
1946
+ } else {
1947
+ drawChartAxes(context, chart, plot, yForValue(0));
1948
+ const count = Math.max(1, ...chart.series.items.map((series) => series.values.length));
1949
+ const groupWidth = plot.width / count;
1950
+ const barWidth = groupWidth * 0.72 / Math.max(1, chart.series.items.length);
1951
+ const baseline = yForValue(0);
1952
+ chart.series.items.forEach((series, seriesIndex) => {
1953
+ series.values.forEach((value, valueIndex) => {
1954
+ const valueY = yForValue(value);
1955
+ const height = Math.abs(baseline - valueY);
1956
+ context.fillStyle = resolveColor(
1957
+ series.fill ?? palette[seriesIndex % palette.length] ?? "#2563eb"
1958
+ );
1959
+ const left = plot.left + valueIndex * groupWidth + groupWidth * 0.14 + seriesIndex * barWidth;
1960
+ context.fillRect(left, Math.min(baseline, valueY), Math.max(1, barWidth), height);
1961
+ if (chart.dataLabels?.showValue) {
1962
+ drawText(
1963
+ context,
1964
+ String(value),
1965
+ { fontSize: 11, alignment: "center" },
1966
+ {
1967
+ left,
1968
+ top: Math.max(plot.top, Math.min(baseline, valueY) - 14),
1969
+ width: Math.max(1, barWidth),
1970
+ height: 14
1971
+ }
1972
+ );
1973
+ }
1974
+ });
1975
+ });
1976
+ }
1977
+ context.restore();
1978
+ }
1979
+ function drawChartAxes(context, chart, plot, baseline) {
1980
+ context.save();
1981
+ context.strokeStyle = "#94a3b8";
1982
+ context.lineWidth = 1;
1983
+ if (chart.xAxis?.visible !== false) {
1984
+ context.beginPath();
1985
+ context.moveTo(plot.left, Math.max(plot.top, Math.min(plot.top + plot.height, baseline)));
1986
+ context.lineTo(
1987
+ plot.left + plot.width,
1988
+ Math.max(plot.top, Math.min(plot.top + plot.height, baseline))
1989
+ );
1990
+ context.stroke();
1991
+ }
1992
+ if (chart.yAxis?.visible !== false) {
1993
+ context.beginPath();
1994
+ context.moveTo(plot.left, plot.top);
1995
+ context.lineTo(plot.left, plot.top + plot.height);
1996
+ context.stroke();
1997
+ }
1998
+ context.restore();
1999
+ }
2000
+ function drawRadarChart(context, chart, plot, palette) {
2001
+ const count = Math.max(3, ...chart.series.items.map((series) => series.values.length));
2002
+ const maximum = Math.max(
2003
+ 1,
2004
+ ...chart.series.items.flatMap((series) => series.values.map((value) => Math.max(0, value)))
2005
+ );
2006
+ const centerX = plot.left + plot.width / 2;
2007
+ const centerY = plot.top + plot.height / 2;
2008
+ const radius = Math.min(plot.width, plot.height) * 0.42;
2009
+ const point = (index, value) => {
2010
+ const angle = -Math.PI / 2 + index / count * Math.PI * 2;
2011
+ const scaled = Math.max(0, value) / maximum * radius;
2012
+ return { x: centerX + Math.cos(angle) * scaled, y: centerY + Math.sin(angle) * scaled };
2013
+ };
2014
+ context.save();
2015
+ context.strokeStyle = "#cbd5e1";
2016
+ for (let ring = 1; ring <= 4; ring += 1) {
2017
+ context.beginPath();
2018
+ for (let index = 0; index < count; index += 1) {
2019
+ const next = point(index, maximum * ring / 4);
2020
+ if (index === 0) context.moveTo(next.x, next.y);
2021
+ else context.lineTo(next.x, next.y);
2022
+ }
2023
+ context.closePath();
2024
+ context.stroke();
2025
+ }
2026
+ chart.series.items.forEach((series, seriesIndex) => {
2027
+ const color = resolveColor(series.fill ?? palette[seriesIndex % palette.length] ?? "#2563eb");
2028
+ context.beginPath();
2029
+ for (let index = 0; index < count; index += 1) {
2030
+ const next = point(index, series.values[index] ?? 0);
2031
+ if (index === 0) context.moveTo(next.x, next.y);
2032
+ else context.lineTo(next.x, next.y);
2033
+ }
2034
+ context.closePath();
2035
+ context.globalAlpha = 0.18;
2036
+ context.fillStyle = color;
2037
+ context.fill();
2038
+ context.globalAlpha = 1;
2039
+ context.strokeStyle = color;
2040
+ context.lineWidth = series.line?.width ?? 2;
2041
+ context.stroke();
2042
+ });
2043
+ context.restore();
2044
+ }
2045
+ function drawImage(context, image, position, imageCache, requestRepaint) {
2046
+ context.save();
2047
+ applyImageTransform(context, image, position);
2048
+ imageClipPath(context, image, position);
2049
+ context.clip();
2050
+ context.fillStyle = "#f1f5f9";
2051
+ context.fillRect(position.left, position.top, position.width, position.height);
2052
+ const source = imageSourceForCanvas(image);
2053
+ if (source && typeof Image !== "undefined") {
2054
+ let cached = imageCache.get(source);
2055
+ if (!cached) {
2056
+ const element = new Image();
2057
+ cached = { image: element, ready: false };
2058
+ imageCache.set(source, cached);
2059
+ element.onload = () => {
2060
+ const current = imageCache.get(source);
2061
+ if (!current || current.image !== element) return;
2062
+ current.ready = true;
2063
+ requestRepaint();
2064
+ };
2065
+ element.onerror = () => {
2066
+ const current = imageCache.get(source);
2067
+ if (current?.image === element) current.ready = false;
2068
+ };
2069
+ element.src = source;
2070
+ }
2071
+ if (cached.ready && cached.image.naturalWidth > 0 && cached.image.naturalHeight > 0) {
2072
+ drawFittedImage(context, cached.image, image, position);
2073
+ context.restore();
2074
+ return source;
2075
+ }
2076
+ }
2077
+ context.strokeStyle = "#94a3b8";
2078
+ context.lineWidth = 1;
2079
+ context.setLineDash([8, 6]);
2080
+ imageClipPath(context, image, position);
2081
+ context.stroke();
2082
+ context.setLineDash([]);
2083
+ drawText(
2084
+ context,
2085
+ image.alt || image.prompt || "Image",
2086
+ { fontSize: 14, color: "slate-600" },
2087
+ {
2088
+ left: position.left + 12,
2089
+ top: position.top + 12,
2090
+ width: Math.max(1, position.width - 24),
2091
+ height: Math.max(1, position.height - 24)
2092
+ }
2093
+ );
2094
+ context.restore();
2095
+ return source;
2096
+ }
2097
+ function imageSourceForCanvas(image) {
2098
+ try {
2099
+ return image.sourceForSvg();
2100
+ } catch {
2101
+ return void 0;
2102
+ }
2103
+ }
2104
+ function applyImageTransform(context, image, position) {
2105
+ const centerX = position.left + position.width / 2;
2106
+ const centerY = position.top + position.height / 2;
2107
+ context.translate(centerX, centerY);
2108
+ context.rotate(image.rotation * Math.PI / 180);
2109
+ context.scale(image.flipHorizontal ? -1 : 1, image.flipVertical ? -1 : 1);
2110
+ context.translate(-centerX, -centerY);
2111
+ }
2112
+ function imageClipPath(context, image, position) {
2113
+ context.beginPath();
2114
+ if (image.geometry === "ellipse") {
2115
+ context.ellipse(
2116
+ position.left + position.width / 2,
2117
+ position.top + position.height / 2,
2118
+ position.width / 2,
2119
+ position.height / 2,
2120
+ 0,
2121
+ 0,
2122
+ Math.PI * 2
2123
+ );
2124
+ } else if (image.geometry === "roundRect") {
2125
+ roundedRect(
2126
+ context,
2127
+ position.left,
2128
+ position.top,
2129
+ position.width,
2130
+ position.height,
2131
+ resolveRadius(image.borderRadius, position)
2132
+ );
2133
+ } else {
2134
+ context.rect(position.left, position.top, position.width, position.height);
2135
+ }
2136
+ context.closePath();
2137
+ }
2138
+ function drawFittedImage(context, source, image, position) {
2139
+ const crop = image.crop ?? { left: 0, top: 0, right: 0, bottom: 0 };
2140
+ const sourceX = source.naturalWidth * crop.left;
2141
+ const sourceY = source.naturalHeight * crop.top;
2142
+ const sourceWidth = source.naturalWidth * (1 - crop.left - crop.right);
2143
+ const sourceHeight = source.naturalHeight * (1 - crop.top - crop.bottom);
2144
+ if (sourceWidth <= 0 || sourceHeight <= 0) return;
2145
+ const scale = image.fit === "cover" ? Math.max(position.width / sourceWidth, position.height / sourceHeight) : Math.min(position.width / sourceWidth, position.height / sourceHeight);
2146
+ const width = sourceWidth * scale;
2147
+ const height = sourceHeight * scale;
2148
+ const left = position.left + (position.width - width) / 2;
2149
+ const top = position.top + (position.height - height) / 2;
2150
+ context.drawImage(source, sourceX, sourceY, sourceWidth, sourceHeight, left, top, width, height);
2151
+ }
2152
+ function disposeCanvasImageCache(cache) {
2153
+ for (const entry of cache.values()) disposeCanvasImageEntry(entry);
2154
+ cache.clear();
2155
+ }
2156
+ function disposeUnusedCanvasImages(cache, usedSources) {
2157
+ for (const [source, entry] of cache) {
2158
+ if (usedSources.has(source)) continue;
2159
+ disposeCanvasImageEntry(entry);
2160
+ cache.delete(source);
2161
+ }
2162
+ }
2163
+ function disposeCanvasImageEntry(entry) {
2164
+ entry.image.onload = null;
2165
+ entry.image.onerror = null;
2166
+ entry.image.removeAttribute("src");
2167
+ }
2168
+ function drawText(context, text, style, position) {
2169
+ const fontSize = style.fontSize ?? 18;
2170
+ const fontStyle = style.italic ? "italic " : "";
2171
+ const fontWeight = style.bold ? "700 " : "400 ";
2172
+ context.font = `${fontStyle}${fontWeight}${fontSize}px ${style.fontFamily ?? "Arial, sans-serif"}`;
2173
+ context.fillStyle = resolveColor(style.color ?? "slate-950");
2174
+ context.textAlign = style.alignment === "center" ? "center" : style.alignment === "right" ? "right" : "left";
2175
+ context.textBaseline = "top";
2176
+ const x = style.alignment === "center" ? position.left + position.width / 2 : style.alignment === "right" ? position.left + position.width : position.left;
2177
+ const lineHeight = fontSize * 1.2;
2178
+ const lines = wrapCanvasText(context, text, position.width);
2179
+ const totalHeight = lines.length * lineHeight;
2180
+ const top = style.verticalAlignment === "middle" ? position.top + (position.height - totalHeight) / 2 : style.verticalAlignment === "bottom" ? position.top + position.height - totalHeight : position.top;
2181
+ context.save();
2182
+ context.beginPath();
2183
+ context.rect(position.left, position.top, position.width, position.height);
2184
+ context.clip();
2185
+ lines.forEach((line, index) => context.fillText(line, x, top + index * lineHeight));
2186
+ context.restore();
2187
+ }
2188
+ function wrapCanvasText(context, text, width) {
2189
+ const result = [];
2190
+ for (const paragraph of text.split("\n")) {
2191
+ const words = paragraph.split(/\s+/).filter(Boolean);
2192
+ if (words.length === 0) {
2193
+ result.push("");
2194
+ continue;
2195
+ }
2196
+ let line = words[0] ?? "";
2197
+ for (let index = 1; index < words.length; index += 1) {
2198
+ const word = words[index] ?? "";
2199
+ const candidate = `${line} ${word}`;
2200
+ if (context.measureText(candidate).width <= width || line.length === 0) line = candidate;
2201
+ else {
2202
+ result.push(line);
2203
+ line = word;
2204
+ }
2205
+ }
2206
+ result.push(line);
2207
+ }
2208
+ return result;
2209
+ }
2210
+ function applyFillAndLine(context, fill, line, lineOnly) {
2211
+ if (!lineOnly && resolveColor(fill) !== "transparent") {
2212
+ context.fillStyle = resolveColor(fill);
2213
+ context.fill();
2214
+ }
2215
+ if (line.style !== "none" && resolveColor(line.fill ?? "slate-900") !== "transparent" && (line.width ?? 1) > 0) {
2216
+ context.strokeStyle = resolveColor(line.fill ?? "slate-900");
2217
+ context.lineWidth = line.width ?? 1;
2218
+ context.setLineDash(line.style === "dash" ? [8, 5] : line.style === "dot" ? [2, 4] : []);
2219
+ context.stroke();
2220
+ context.setLineDash([]);
2221
+ }
2222
+ }
2223
+ function roundedRect(context, x, y, width, height, radius) {
2224
+ const r = Math.min(Math.max(0, radius), width / 2, height / 2);
2225
+ context.moveTo(x + r, y);
2226
+ context.lineTo(x + width - r, y);
2227
+ context.quadraticCurveTo(x + width, y, x + width, y + r);
2228
+ context.lineTo(x + width, y + height - r);
2229
+ context.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
2230
+ context.lineTo(x + r, y + height);
2231
+ context.quadraticCurveTo(x, y + height, x, y + height - r);
2232
+ context.lineTo(x, y + r);
2233
+ context.quadraticCurveTo(x, y, x + r, y);
2234
+ context.closePath();
2235
+ }
2236
+ function hitTest(index, x, y) {
2237
+ const candidates = querySpatialIndex(index, { left: x, top: y, width: 0, height: 0 });
2238
+ for (let candidateIndex = candidates.length - 1; candidateIndex >= 0; candidateIndex -= 1) {
2239
+ const entry = index.objects[candidates[candidateIndex] ?? -1];
2240
+ if (entry && objectContainsPoint(entry, x, y)) return entry;
2241
+ }
2242
+ return null;
2243
+ }
2244
+ function objectContainsPoint(entry, x, y) {
2245
+ const position = entry.object.position;
2246
+ const rotation = objectRotation(entry);
2247
+ if (rotation === 0) {
2248
+ return x >= position.left && x <= position.left + position.width && y >= position.top && y <= position.top + position.height;
2249
+ }
2250
+ const centerX = position.left + position.width / 2;
2251
+ const centerY = position.top + position.height / 2;
2252
+ const radians = -rotation * Math.PI / 180;
2253
+ const dx = x - centerX;
2254
+ const dy = y - centerY;
2255
+ const localX = dx * Math.cos(radians) - dy * Math.sin(radians) + centerX;
2256
+ const localY = dx * Math.sin(radians) + dy * Math.cos(radians) + centerY;
2257
+ return localX >= position.left && localX <= position.left + position.width && localY >= position.top && localY <= position.top + position.height;
2258
+ }
2259
+ function objectBounds(entry) {
2260
+ const position = entry.object.position;
2261
+ const rotation = objectRotation(entry);
2262
+ if (rotation === 0) return position;
2263
+ const radians = rotation * Math.PI / 180;
2264
+ const width = Math.abs(position.width * Math.cos(radians)) + Math.abs(position.height * Math.sin(radians));
2265
+ const height = Math.abs(position.width * Math.sin(radians)) + Math.abs(position.height * Math.cos(radians));
2266
+ return {
2267
+ left: position.left + (position.width - width) / 2,
2268
+ top: position.top + (position.height - height) / 2,
2269
+ width,
2270
+ height
2271
+ };
2272
+ }
2273
+ function objectRotation(entry) {
2274
+ if (entry.kind === "shape") return finite(entry.object.rotation, 0);
2275
+ if (entry.kind === "image") return finite(entry.object.rotation, 0);
2276
+ if (entry.kind === "group") return finite(entry.object.rotation, 0);
2277
+ return 0;
2278
+ }
2279
+ function objectLabel(entry) {
2280
+ const object = entry.object;
2281
+ if (entry.kind === "shape") {
2282
+ const shape = object;
2283
+ return shape.text.toString().trim() || shape.name || "Shape";
2284
+ }
2285
+ if (entry.kind === "chart") return object.title || object.name || "Chart";
2286
+ if (entry.kind === "image") return object.alt || object.name || "Image";
2287
+ if (entry.kind === "table") return object.name || "Table";
2288
+ return object.name || "Group";
2289
+ }
2290
+ function accessibleObjectLabel(entry) {
2291
+ const position = entry.object.position;
2292
+ return `${objectLabel(entry)}. Position ${Math.round(position.left)}, ${Math.round(position.top)}. Size ${Math.round(position.width)} by ${Math.round(position.height)}.`;
2293
+ }
2294
+ function nextTextBoxPosition(objects, slideSize) {
2295
+ const width = slideSize.width * 0.6;
2296
+ const height = Math.max(48, slideSize.height * 0.16);
2297
+ const lefts = [0.2, 0.08, 0.32].map(
2298
+ (ratio) => Math.max(0, Math.min(slideSize.width - width, slideSize.width * ratio))
2299
+ );
2300
+ const tops = [0.2, 0.42, 0.64, 0.06].map(
2301
+ (ratio) => Math.max(0, Math.min(slideSize.height - height, slideSize.height * ratio))
2302
+ );
2303
+ const occupied = objects.map(objectBounds);
2304
+ let best = null;
2305
+ let bestOverlap = Number.POSITIVE_INFINITY;
2306
+ for (const left of lefts) {
2307
+ for (const top of tops) {
2308
+ const candidate = normalizeEditorPosition({ left, top, width, height });
2309
+ const overlap = occupied.reduce(
2310
+ (total, bounds) => total + intersectionArea(candidate, bounds),
2311
+ 0
2312
+ );
2313
+ if (overlap < bestOverlap) {
2314
+ best = candidate;
2315
+ bestOverlap = overlap;
2316
+ if (overlap === 0) return candidate;
2317
+ }
2318
+ }
2319
+ }
2320
+ return best ?? normalizeEditorPosition({ left: 0, top: 0, width, height });
2321
+ }
2322
+ function intersectionArea(a, b) {
2323
+ const width = Math.max(
2324
+ 0,
2325
+ Math.min(a.left + a.width, b.left + b.width) - Math.max(a.left, b.left)
2326
+ );
2327
+ const height = Math.max(0, Math.min(a.top + a.height, b.top + b.height) - Math.max(a.top, b.top));
2328
+ return width * height;
2329
+ }
2330
+ function normalizeEditorPosition(position) {
2331
+ return {
2332
+ left: clampGeometry(finite(position.left, 0)),
2333
+ top: clampGeometry(finite(position.top, 0)),
2334
+ width: Math.min(MAX_EDITOR_GEOMETRY, Math.max(8, finite(position.width, 8))),
2335
+ height: Math.min(MAX_EDITOR_GEOMETRY, Math.max(8, finite(position.height, 8)))
2336
+ };
2337
+ }
2338
+ function clampGeometry(value) {
2339
+ return Math.max(-MAX_EDITOR_GEOMETRY, Math.min(MAX_EDITOR_GEOMETRY, value));
2340
+ }
2341
+ function positionFromDrag(drag, point) {
2342
+ const dx = point.x - drag.start.x;
2343
+ const dy = point.y - drag.start.y;
2344
+ const radians = -drag.rotation * Math.PI / 180;
2345
+ const resizeDx = dx * Math.cos(radians) - dy * Math.sin(radians);
2346
+ const resizeDy = dx * Math.sin(radians) + dy * Math.cos(radians);
2347
+ return normalizeEditorPosition(
2348
+ drag.mode === "move" ? {
2349
+ ...drag.before,
2350
+ left: drag.before.left + dx,
2351
+ top: drag.before.top + dy
2352
+ } : {
2353
+ ...drag.before,
2354
+ width: Math.max(8, drag.before.width + resizeDx),
2355
+ height: Math.max(8, drag.before.height + resizeDy)
2356
+ }
2357
+ );
2358
+ }
2359
+ function selectionTransform(entry, position) {
2360
+ const rotation = objectRotation(entry);
2361
+ if (rotation === 0) return void 0;
2362
+ const centerX = position.left + position.width / 2;
2363
+ const centerY = position.top + position.height / 2;
2364
+ return `rotate(${rotation} ${centerX} ${centerY})`;
2365
+ }
2366
+ function copyPosition(position) {
2367
+ return {
2368
+ left: position.left,
2369
+ top: position.top,
2370
+ width: position.width,
2371
+ height: position.height
2372
+ };
2373
+ }
2374
+ function samePosition(a, b) {
2375
+ return a.left === b.left && a.top === b.top && a.width === b.width && a.height === b.height;
2376
+ }
2377
+ function sameViewport(a, b) {
2378
+ return a.scrollLeft === b.scrollLeft && a.scrollTop === b.scrollTop && a.width === b.width && a.height === b.height && a.stageLeft === b.stageLeft && a.stageTop === b.stageTop;
2379
+ }
2380
+ function intersects(a, b) {
2381
+ return !!a && a.left <= b.left + b.width && a.left + a.width >= b.left && a.top <= b.top + b.height && a.top + a.height >= b.top;
2382
+ }
2383
+ function clampZoom(value) {
2384
+ return Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, finite(value, 0.75)));
2385
+ }
2386
+ function fittedPresentationZoom(viewport, slideSize) {
2387
+ const horizontalRoom = Math.max(1, viewport.width - 48);
2388
+ const verticalRoom = Math.max(1, viewport.height - 48);
2389
+ return clampZoom(Math.min(horizontalRoom / slideSize.width, verticalRoom / slideSize.height));
2390
+ }
2391
+ function finite(value, fallback) {
2392
+ return Number.isFinite(value) ? value : fallback;
2393
+ }
2394
+ function safeDomId(value) {
2395
+ return value.replaceAll(/[^a-zA-Z0-9_-]/g, "-");
2396
+ }
2397
+ function randomPresentationId() {
2398
+ for (; ; ) {
2399
+ const bytes = crypto.getRandomValues(new Uint8Array(16));
2400
+ if (bytes.some((byte) => byte !== 0)) {
2401
+ return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
2402
+ }
2403
+ }
2404
+ }
2405
+ function resolveRadius(value, position) {
2406
+ if (typeof value === "number") return value;
2407
+ const min = Math.min(position.width, position.height);
2408
+ const preset = value?.startsWith("rounded-") ? value.slice("rounded-".length) : void 0;
2409
+ if (preset === "full") return min / 2;
2410
+ if (preset === "2xl") return 16;
2411
+ if (preset === "xl") return 12;
2412
+ if (preset === "lg") return 8;
2413
+ return 6;
2414
+ }
2415
+ var COLORS = {
2416
+ none: "transparent",
2417
+ transparent: "transparent",
2418
+ white: "#ffffff",
2419
+ black: "#000000",
2420
+ "slate-50": "#f8fafc",
2421
+ "slate-100": "#f1f5f9",
2422
+ "slate-200": "#e2e8f0",
2423
+ "slate-300": "#cbd5e1",
2424
+ "slate-400": "#94a3b8",
2425
+ "slate-500": "#64748b",
2426
+ "slate-600": "#475569",
2427
+ "slate-700": "#334155",
2428
+ "slate-800": "#1e293b",
2429
+ "slate-900": "#0f172a",
2430
+ "slate-950": "#020617"
2431
+ };
2432
+ function resolveColor(fill) {
2433
+ const value = typeof fill === "string" ? fill : fill.color;
2434
+ return COLORS[value] ?? value;
2435
+ }
2436
+
2437
+ // src/components/artifacts/editable-presentation.tsx
2438
+ import {
2439
+ PRESENTATION_ARTIFACT_COMMAND_VERSION,
2440
+ PRESENTATION_ARTIFACT_QUERY_MAX_NODES,
2441
+ PRESENTATION_ARTIFACT_QUERY_MAX_SLIDES,
2442
+ PRESENTATION_ARTIFACT_QUERY_MAX_TEXT_BYTES,
2443
+ PRESENTATION_ARTIFACT_QUERY_RESPONSE_MAX_BYTES
2444
+ } from "@opengeni/sdk/editable-artifacts";
2445
+ import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
2446
+ import { jsx as jsx2 } from "react/jsx-runtime";
2447
+ var EMU_PER_CSS_PIXEL = 9525;
2448
+ var PRESENTATION_CATALOG_PAGE = 128;
2449
+ var PRESENTATION_MAX_INTERACTIVE_SLIDES = 256;
2450
+ var PRESENTATION_MAX_INTERACTIVE_NODES = 16384;
2451
+ var PRESENTATION_MAX_INTERACTIVE_TEXT_BYTES = 4 * 1024 * 1024;
2452
+ var PRESENTATION_MAX_CATALOG_PAGES = 32;
2453
+ var PRESENTATION_SCENE_CONCURRENCY = 4;
2454
+ var PRESENTATION_COMPOSITION_ATTEMPTS = 3;
2455
+ var DEFAULT_PRESENTATION_TEXT_STYLE = Object.freeze({
2456
+ fontFamily: "Arial",
2457
+ fontSizeCentipoints: 1800,
2458
+ color: 255,
2459
+ bold: false,
2460
+ italic: false,
2461
+ underline: false,
2462
+ language: null
2463
+ });
2464
+ function EditablePresentationArtifactSurface({
2465
+ session,
2466
+ title = "Presentation",
2467
+ subtitle,
2468
+ readOnly = false,
2469
+ onCommit,
2470
+ onCommandError,
2471
+ ...surfaceProps
2472
+ }) {
2473
+ const view = useEditableArtifactView(session);
2474
+ const invalidator = editableArtifactProjectionKey(view);
2475
+ const [retryEpoch, setRetryEpoch] = useState2(0);
2476
+ const [state, setState] = useState2({
2477
+ composed: null,
2478
+ loading: true,
2479
+ error: null
2480
+ });
2481
+ const loadGeneration = useRef2(0);
2482
+ const sceneNodes = useRef2(/* @__PURE__ */ new Map());
2483
+ useEffect2(() => {
2484
+ const generation = ++loadGeneration.current;
2485
+ let cancelled = false;
2486
+ setState((current) => ({ ...current, loading: true, error: null }));
2487
+ void composePresentationEditorProjection(session).then(
2488
+ (composed) => {
2489
+ if (cancelled || generation !== loadGeneration.current) return;
2490
+ sceneNodes.current = new Map(composed.sceneNodes);
2491
+ setState({ composed, loading: false, error: null });
2492
+ },
2493
+ (cause) => {
2494
+ if (cancelled || generation !== loadGeneration.current) return;
2495
+ setState({
2496
+ composed: null,
2497
+ loading: false,
2498
+ error: asEditableArtifactError(cause, "Could not open this presentation")
2499
+ });
2500
+ }
2501
+ );
2502
+ return () => {
2503
+ cancelled = true;
2504
+ };
2505
+ }, [invalidator, retryEpoch, session]);
2506
+ const refresh = useCallback2(() => setRetryEpoch((value) => value + 1), []);
2507
+ const writable = !readOnly && view.writable;
2508
+ const applyCommit = useCallback2(
2509
+ async (commit) => {
2510
+ const composed = state.composed;
2511
+ if (!composed) throw new Error("Presentation projection is not ready");
2512
+ if (!writable) throw new Error("This presentation is read only");
2513
+ if (commit.revision !== void 0 && String(commit.revision) !== String(composed.projection.revision)) {
2514
+ throw new Error("The presentation changed; retry this edit on the latest version");
2515
+ }
2516
+ if (commit.kind === "slide-insert") {
2517
+ await session.createPresentationSlide({
2518
+ slideId: commit.slideId,
2519
+ index: commit.index
2520
+ });
2521
+ notifyPresentationCommit(onCommit, onCommandError, commit);
2522
+ refresh();
2523
+ return;
2524
+ }
2525
+ if (commit.kind === "slide-delete") {
2526
+ await session.applyPresentationCommands({
2527
+ version: PRESENTATION_ARTIFACT_COMMAND_VERSION,
2528
+ commands: [{ kind: "slide.delete", id: commit.slideId }]
2529
+ });
2530
+ notifyPresentationCommit(onCommit, onCommandError, commit);
2531
+ refresh();
2532
+ return;
2533
+ }
2534
+ if (commit.kind === "object-insert") {
2535
+ await session.applyPresentationCommands({
2536
+ version: PRESENTATION_ARTIFACT_COMMAND_VERSION,
2537
+ commands: [
2538
+ {
2539
+ kind: "node.insert",
2540
+ owner: { kind: "slide", id: commit.slideId },
2541
+ parentId: null,
2542
+ index: commit.index,
2543
+ node: {
2544
+ id: commit.objectId,
2545
+ name: "Text box",
2546
+ bounds: {
2547
+ x: canonicalCoordinate(commit.position.left),
2548
+ y: canonicalCoordinate(commit.position.top),
2549
+ width: Math.max(1, canonicalCoordinate(commit.position.width)),
2550
+ height: Math.max(1, canonicalCoordinate(commit.position.height))
2551
+ },
2552
+ transform: { rotation: 0, flipHorizontal: false, flipVertical: false },
2553
+ content: {
2554
+ kind: "shape",
2555
+ geometry: "text-box",
2556
+ fill: { kind: "none" },
2557
+ line: { fill: { kind: "none" }, width: 0, dash: "solid" },
2558
+ text: emptyPresentationText(),
2559
+ placeholder: null
2560
+ }
2561
+ }
2562
+ }
2563
+ ]
2564
+ });
2565
+ notifyPresentationCommit(onCommit, onCommandError, commit);
2566
+ refresh();
2567
+ return;
2568
+ }
2569
+ if (commit.kind === "object-delete") {
2570
+ await session.applyPresentationCommands({
2571
+ version: PRESENTATION_ARTIFACT_COMMAND_VERSION,
2572
+ commands: [{ kind: "node.delete", id: commit.objectId }]
2573
+ });
2574
+ notifyPresentationCommit(onCommit, onCommandError, commit);
2575
+ refresh();
2576
+ return;
2577
+ }
2578
+ const key = sceneNodeKey(commit.slideId, commit.objectId);
2579
+ const node = sceneNodes.current.get(key);
2580
+ if (!node) throw new Error("The edited presentation object is no longer available");
2581
+ if (node.inherited || node.source.kind !== "slide" || node.source.id !== commit.slideId) {
2582
+ throw new Error("Inherited master and layout objects are view only in the slide editor");
2583
+ }
2584
+ let command;
2585
+ let nextNode;
2586
+ if (commit.kind === "text") {
2587
+ if (node.content.kind !== "shape" || node.content.text === null) {
2588
+ throw new Error("This presentation object does not contain editable text");
2589
+ }
2590
+ if (flattenPresentationRichText(node.content.text) !== commit.before) {
2591
+ throw new Error("The presentation text changed; retry this edit");
2592
+ }
2593
+ const text = replacePresentationRichText(node.content.text, commit.before, commit.after);
2594
+ const content = Object.freeze({ ...node.content, text });
2595
+ command = { kind: "node.content.set", id: node.id, content };
2596
+ nextNode = Object.freeze({ ...node, content });
2597
+ } else {
2598
+ const current = normalizeProjectedEditorPosition(projectPosition(node.bounds));
2599
+ if (!sameProjectedPosition(current, commit.before)) {
2600
+ throw new Error("The presentation object changed; retry this edit");
2601
+ }
2602
+ const bounds = commit.kind === "move" ? Object.freeze({
2603
+ ...node.bounds,
2604
+ x: canonicalCoordinate(commit.after.left),
2605
+ y: canonicalCoordinate(commit.after.top)
2606
+ }) : Object.freeze({
2607
+ ...node.bounds,
2608
+ width: Math.max(1, canonicalCoordinate(commit.after.width)),
2609
+ height: Math.max(1, canonicalCoordinate(commit.after.height))
2610
+ });
2611
+ command = { kind: "node.bounds.set", id: node.id, bounds };
2612
+ nextNode = Object.freeze({ ...node, bounds });
2613
+ }
2614
+ await session.applyPresentationCommands({
2615
+ version: PRESENTATION_ARTIFACT_COMMAND_VERSION,
2616
+ commands: [command]
2617
+ });
2618
+ sceneNodes.current.set(key, nextNode);
2619
+ notifyPresentationCommit(onCommit, onCommandError, commit);
2620
+ refresh();
2621
+ },
2622
+ [onCommandError, onCommit, refresh, session, state.composed, writable]
2623
+ );
2624
+ const projection = state.composed?.projection ?? null;
2625
+ const status = editableArtifactStatusLabel(view);
2626
+ const accessRevoked = editableArtifactAccessRevoked(view);
2627
+ return projection && !accessRevoked ? /* @__PURE__ */ jsx2(
2628
+ PresentationProjectionArtifactSurface,
2629
+ {
2630
+ ...surfaceProps,
2631
+ title,
2632
+ subtitle: subtitle ?? `${projection.slides.length} slide${projection.slides.length === 1 ? "" : "s"}${writable ? "" : " \xB7 Read only"}`,
2633
+ projection,
2634
+ commit: writable ? applyCommit : void 0,
2635
+ readOnly: !writable,
2636
+ busy: state.loading,
2637
+ onCommandError
2638
+ }
2639
+ ) : /* @__PURE__ */ jsx2(
2640
+ ArtifactSurface,
2641
+ {
2642
+ modality: "presentation",
2643
+ title,
2644
+ subtitle: subtitle ?? status,
2645
+ actions: surfaceProps.actions,
2646
+ busy: !accessRevoked && !state.error,
2647
+ className: surfaceProps.className,
2648
+ children: /* @__PURE__ */ jsx2(
2649
+ EditableArtifactMessage,
2650
+ {
2651
+ title: accessRevoked ? "Access changed" : state.error ? "Could not open this presentation" : "Opening presentation",
2652
+ detail: accessRevoked ? status : state.error?.message ?? status,
2653
+ retry: !accessRevoked && state.error ? refresh : void 0
2654
+ }
2655
+ )
2656
+ }
2657
+ );
2658
+ }
2659
+ async function composePresentationEditorProjection(session) {
2660
+ if (session.modality !== "presentation") {
2661
+ throw new Error("Expected a presentation artifact session");
2662
+ }
2663
+ let lastRevisionError = null;
2664
+ for (let attempt = 0; attempt < PRESENTATION_COMPOSITION_ATTEMPTS; attempt += 1) {
2665
+ try {
2666
+ return await composePresentationAttempt(session);
2667
+ } catch (cause) {
2668
+ const error = asEditableArtifactError(cause, "Could not project this presentation");
2669
+ if (!isPresentationRevisionDrift(error) || attempt === PRESENTATION_COMPOSITION_ATTEMPTS - 1) {
2670
+ throw error;
2671
+ }
2672
+ lastRevisionError = error;
2673
+ }
2674
+ }
2675
+ throw lastRevisionError ?? new Error("Presentation changed while it was opening");
2676
+ }
2677
+ async function composePresentationAttempt(session) {
2678
+ const metadata = await session.queryPresentation({
2679
+ kind: "metadata",
2680
+ maxBytes: PRESENTATION_ARTIFACT_QUERY_RESPONSE_MAX_BYTES
2681
+ });
2682
+ if (metadata.kind !== "metadata") throw new Error("Presentation metadata response is malformed");
2683
+ if (metadata.slides > PRESENTATION_MAX_INTERACTIVE_SLIDES) {
2684
+ throw new Error(
2685
+ `Presentation exceeds the interactive limit of ${PRESENTATION_MAX_INTERACTIVE_SLIDES} slides`
2686
+ );
2687
+ }
2688
+ const catalog = await collectSlideCatalog(session, metadata.revision, metadata.slides);
2689
+ const scenes = [];
2690
+ for (let start = 0; start < catalog.length; start += PRESENTATION_SCENE_CONCURRENCY) {
2691
+ const batch = catalog.slice(start, start + PRESENTATION_SCENE_CONCURRENCY);
2692
+ const projected = await Promise.all(
2693
+ batch.map(
2694
+ (slide) => session.queryPresentationEditorSlide({
2695
+ kind: "editor-slide",
2696
+ slideId: slide.id,
2697
+ maxNodes: PRESENTATION_ARTIFACT_QUERY_MAX_NODES,
2698
+ maxTextBytes: PRESENTATION_ARTIFACT_QUERY_MAX_TEXT_BYTES,
2699
+ maxBytes: PRESENTATION_ARTIFACT_QUERY_RESPONSE_MAX_BYTES
2700
+ })
2701
+ )
2702
+ );
2703
+ for (const scene of projected) {
2704
+ if (scene.revision !== metadata.revision) {
2705
+ throw new Error("Presentation projection revision drifted");
2706
+ }
2707
+ if (scene.truncated) {
2708
+ throw new Error(`Slide ${scene.slide.index + 1} exceeds the bounded editor projection`);
2709
+ }
2710
+ scenes.push(scene);
2711
+ }
2712
+ }
2713
+ const totalNodes = scenes.reduce((sum, scene) => sum + scene.nodes.length, 0);
2714
+ const totalText = scenes.reduce((sum, scene) => sum + scene.projectedTextBytes, 0);
2715
+ if (totalNodes > PRESENTATION_MAX_INTERACTIVE_NODES) {
2716
+ throw new Error(
2717
+ `Presentation exceeds the interactive limit of ${PRESENTATION_MAX_INTERACTIVE_NODES} scene nodes`
2718
+ );
2719
+ }
2720
+ if (totalText > PRESENTATION_MAX_INTERACTIVE_TEXT_BYTES) {
2721
+ throw new Error(
2722
+ `Presentation exceeds the interactive limit of ${PRESENTATION_MAX_INTERACTIVE_TEXT_BYTES} text bytes`
2723
+ );
2724
+ }
2725
+ if (scenes.length !== catalog.length) {
2726
+ throw new Error("Presentation slides changed while they were opening");
2727
+ }
2728
+ const sceneNodeMap = /* @__PURE__ */ new Map();
2729
+ const slides = scenes.map((scene, index) => {
2730
+ if (scene.slide.id !== catalog[index]?.id || scene.slide.index !== index) {
2731
+ throw new Error("Presentation slide catalog changed while it was opening");
2732
+ }
2733
+ for (const node of scene.nodes) {
2734
+ sceneNodeMap.set(sceneNodeKey(scene.slide.id, node.id), node);
2735
+ }
2736
+ return projectPresentationSlide(scene);
2737
+ });
2738
+ return Object.freeze({
2739
+ projection: Object.freeze({
2740
+ revision: metadata.revision.toString(),
2741
+ slideSize: Object.freeze({
2742
+ width: displayCoordinate(metadata.slideSize.width),
2743
+ height: displayCoordinate(metadata.slideSize.height)
2744
+ }),
2745
+ slides: Object.freeze(slides)
2746
+ }),
2747
+ sceneNodes: sceneNodeMap
2748
+ });
2749
+ }
2750
+ async function collectSlideCatalog(session, revision, expectedSlides) {
2751
+ const slides = [];
2752
+ let startSlide = 0;
2753
+ let projectedText = 0;
2754
+ for (let page = 0; page < PRESENTATION_MAX_CATALOG_PAGES; page += 1) {
2755
+ const response = await session.queryPresentationSlideCatalog({
2756
+ kind: "slide-catalog",
2757
+ startSlide,
2758
+ maxSlides: Math.min(
2759
+ PRESENTATION_CATALOG_PAGE,
2760
+ PRESENTATION_ARTIFACT_QUERY_MAX_SLIDES,
2761
+ Math.max(1, expectedSlides - slides.length)
2762
+ ),
2763
+ maxTextBytes: PRESENTATION_ARTIFACT_QUERY_MAX_TEXT_BYTES,
2764
+ maxBytes: PRESENTATION_ARTIFACT_QUERY_RESPONSE_MAX_BYTES
2765
+ });
2766
+ if (response.revision !== revision) {
2767
+ throw new Error("Presentation projection revision drifted");
2768
+ }
2769
+ if (response.startSlide !== startSlide) {
2770
+ throw new Error("Presentation slide catalog cursor is malformed");
2771
+ }
2772
+ if (response.slides.some((slide, offset) => slide.index !== startSlide + offset)) {
2773
+ throw new Error("Presentation slide catalog order is malformed");
2774
+ }
2775
+ slides.push(...response.slides);
2776
+ projectedText += response.projectedTextBytes;
2777
+ if (slides.length > PRESENTATION_MAX_INTERACTIVE_SLIDES || projectedText > PRESENTATION_MAX_INTERACTIVE_TEXT_BYTES) {
2778
+ throw new Error("Presentation slide catalog exceeds its interactive limit");
2779
+ }
2780
+ if (response.nextSlide === null) {
2781
+ if (response.truncated || slides.length !== expectedSlides) {
2782
+ throw new Error("Presentation slide catalog is incomplete");
2783
+ }
2784
+ return slides;
2785
+ }
2786
+ if (!response.truncated || response.nextSlide <= startSlide || response.nextSlide !== startSlide + response.slides.length || response.slides.length === 0) {
2787
+ throw new Error("Presentation slide catalog did not advance");
2788
+ }
2789
+ startSlide = response.nextSlide;
2790
+ }
2791
+ throw new Error(`Presentation catalog exceeds ${PRESENTATION_MAX_CATALOG_PAGES} pages`);
2792
+ }
2793
+ function projectPresentationSlide(scene) {
2794
+ const byId = new Map(scene.nodes.map((node) => [node.id, node]));
2795
+ if (byId.size !== scene.nodes.length)
2796
+ throw new Error("Presentation slide contains duplicate IDs");
2797
+ const children = /* @__PURE__ */ new Map();
2798
+ for (const node of scene.nodes) {
2799
+ const entries = children.get(node.parentId) ?? [];
2800
+ entries.push(node);
2801
+ children.set(node.parentId, entries);
2802
+ }
2803
+ for (const entries of children.values()) {
2804
+ entries.sort((left, right) => left.order - right.order || left.id.localeCompare(right.id));
2805
+ }
2806
+ const visited = /* @__PURE__ */ new Set();
2807
+ const stack = /* @__PURE__ */ new Set();
2808
+ const projectNode = (node) => {
2809
+ if (stack.has(node.id)) throw new Error("Presentation group hierarchy contains a cycle");
2810
+ if (visited.has(node.id)) throw new Error("Presentation node appears more than once");
2811
+ stack.add(node.id);
2812
+ visited.add(node.id);
2813
+ const directChildren = children.get(node.id) ?? [];
2814
+ let orderedChildren = directChildren;
2815
+ if (node.content.kind === "group") {
2816
+ const directIds = new Set(directChildren.map((child) => child.id));
2817
+ if (node.content.children.length !== directChildren.length || node.content.children.some((id) => !directIds.has(id))) {
2818
+ throw new Error(`Presentation group ${node.id} hierarchy is inconsistent`);
2819
+ }
2820
+ orderedChildren = node.content.children.map((id) => byId.get(id));
2821
+ } else if (directChildren.length > 0) {
2822
+ throw new Error(`Presentation non-group node ${node.id} contains children`);
2823
+ }
2824
+ const result = projectPresentationNode(node, orderedChildren.map(projectNode));
2825
+ stack.delete(node.id);
2826
+ return result;
2827
+ };
2828
+ const elements = (children.get(null) ?? []).map(projectNode);
2829
+ if (visited.size !== scene.nodes.length) {
2830
+ throw new Error("Presentation slide contains orphaned hierarchy nodes");
2831
+ }
2832
+ return Object.freeze({
2833
+ id: scene.slide.id,
2834
+ title: scene.slide.title,
2835
+ background: projectFill(scene.slide.background),
2836
+ elements: Object.freeze(elements),
2837
+ layout: scene.slide.layout ? Object.freeze({
2838
+ id: scene.slide.layout.id,
2839
+ name: scene.slide.layout.name,
2840
+ masterId: scene.slide.layout.masterId,
2841
+ background: projectFill(scene.slide.layout.background)
2842
+ }) : null,
2843
+ notes: scene.notes ? projectRichText(scene.notes) : null
2844
+ });
2845
+ }
2846
+ function projectPresentationNode(node, children) {
2847
+ const metadata = {
2848
+ nodeSource: { ...node.source },
2849
+ inherited: node.inherited,
2850
+ parentId: node.parentId,
2851
+ order: node.order,
2852
+ readOnly: node.inherited || node.source.kind !== "slide"
2853
+ };
2854
+ const position = projectPosition(node.bounds);
2855
+ const rotation = node.transform.rotation / 6e4;
2856
+ const content = node.content;
2857
+ if (content.kind === "shape") {
2858
+ const richText = content.text ? projectRichText(content.text) : void 0;
2859
+ const textStyle = richText ? firstTextStyle(richText) : void 0;
2860
+ return Object.freeze({
2861
+ ...metadata,
2862
+ kind: "shape",
2863
+ id: node.id,
2864
+ name: node.name,
2865
+ geometry: projectShapeGeometry(content.geometry),
2866
+ position,
2867
+ fill: projectFill(content.fill),
2868
+ line: projectLine(content.line),
2869
+ rotation,
2870
+ text: content.text ? flattenPresentationRichText(content.text) : "",
2871
+ ...richText ? { richText } : {},
2872
+ ...textStyle ? { textStyle } : {}
2873
+ });
2874
+ }
2875
+ if (content.kind === "group") {
2876
+ return Object.freeze({
2877
+ ...metadata,
2878
+ kind: "group",
2879
+ id: node.id,
2880
+ name: node.name,
2881
+ position,
2882
+ childOffset: Object.freeze({
2883
+ left: displayCoordinate(content.childOffsetX),
2884
+ top: displayCoordinate(content.childOffsetY)
2885
+ }),
2886
+ childExtent: Object.freeze({
2887
+ width: displayCoordinate(content.childExtentWidth),
2888
+ height: displayCoordinate(content.childExtentHeight)
2889
+ }),
2890
+ rotation,
2891
+ flipHorizontal: node.transform.flipHorizontal,
2892
+ flipVertical: node.transform.flipVertical,
2893
+ children: Object.freeze([...children])
2894
+ });
2895
+ }
2896
+ if (content.kind === "connector") {
2897
+ return Object.freeze({
2898
+ ...metadata,
2899
+ kind: "connector",
2900
+ id: node.id,
2901
+ name: node.name,
2902
+ connectorKind: content.connectorKind,
2903
+ position,
2904
+ start: Object.freeze({
2905
+ nodeId: content.start.nodeId,
2906
+ x: displayCoordinate(content.start.x),
2907
+ y: displayCoordinate(content.start.y)
2908
+ }),
2909
+ end: Object.freeze({
2910
+ nodeId: content.end.nodeId,
2911
+ x: displayCoordinate(content.end.x),
2912
+ y: displayCoordinate(content.end.y)
2913
+ }),
2914
+ line: projectLine(content.line)
2915
+ });
2916
+ }
2917
+ if (content.kind === "chart") {
2918
+ const titleRichText = projectRichText(content.title);
2919
+ return Object.freeze({
2920
+ ...metadata,
2921
+ kind: "chart",
2922
+ id: node.id,
2923
+ name: node.name,
2924
+ type: content.chartType,
2925
+ position,
2926
+ title: flattenPresentationRichText(content.title),
2927
+ titleRichText,
2928
+ hasLegend: content.hasLegend,
2929
+ series: Object.freeze(
2930
+ content.series.map((series) => ({
2931
+ name: series.name,
2932
+ categories: Object.freeze([...series.categories]),
2933
+ values: Object.freeze([...series.values]),
2934
+ xValues: Object.freeze([...series.xValues]),
2935
+ bubbleSizes: Object.freeze([...series.bubbleSizes])
2936
+ }))
2937
+ )
2938
+ });
2939
+ }
2940
+ if (content.kind === "table") {
2941
+ return Object.freeze({
2942
+ ...metadata,
2943
+ kind: "table",
2944
+ id: node.id,
2945
+ name: node.name,
2946
+ position,
2947
+ rows: Object.freeze(
2948
+ content.rows.map(
2949
+ (row) => Object.freeze(
2950
+ row.map(
2951
+ (cell) => cell ? Object.freeze({
2952
+ text: flattenPresentationRichText(cell.text),
2953
+ richText: projectRichText(cell.text),
2954
+ fill: projectFill(cell.fill),
2955
+ rowSpan: cell.rowSpan,
2956
+ colSpan: cell.columnSpan
2957
+ }) : null
2958
+ )
2959
+ )
2960
+ )
2961
+ ),
2962
+ columnWidths: Object.freeze(content.columnWidths.map(displayCoordinate)),
2963
+ rowHeights: Object.freeze(content.rowHeights.map(displayCoordinate)),
2964
+ line: projectLine(content.line)
2965
+ });
2966
+ }
2967
+ return Object.freeze({
2968
+ ...metadata,
2969
+ kind: "image",
2970
+ id: node.id,
2971
+ name: node.name,
2972
+ position,
2973
+ alt: content.altText,
2974
+ fit: content.fit,
2975
+ rotation,
2976
+ flipHorizontal: node.transform.flipHorizontal,
2977
+ flipVertical: node.transform.flipVertical,
2978
+ digest: bytesToHex(content.digest),
2979
+ contentType: content.contentType,
2980
+ intrinsicWidth: content.intrinsicWidth,
2981
+ intrinsicHeight: content.intrinsicHeight
2982
+ });
2983
+ }
2984
+ function projectRichText(text) {
2985
+ return Object.freeze({
2986
+ verticalAlignment: text.verticalAlignment,
2987
+ paragraphs: Object.freeze(
2988
+ text.paragraphs.map(
2989
+ (paragraph) => Object.freeze({
2990
+ alignment: paragraph.alignment,
2991
+ runs: Object.freeze(
2992
+ paragraph.runs.map(
2993
+ (run) => Object.freeze({
2994
+ text: run.text,
2995
+ style: projectTextStyle(run.style, paragraph.alignment, text.verticalAlignment),
2996
+ language: run.style.language
2997
+ })
2998
+ )
2999
+ )
3000
+ })
3001
+ )
3002
+ )
3003
+ });
3004
+ }
3005
+ function projectTextStyle(style, alignment, verticalAlignment) {
3006
+ return Object.freeze({
3007
+ fontFamily: style.fontFamily,
3008
+ fontSize: style.fontSizeCentipoints / 100,
3009
+ color: rgbaCss(style.color),
3010
+ bold: style.bold,
3011
+ italic: style.italic,
3012
+ underline: style.underline,
3013
+ ...alignment ? { alignment } : {},
3014
+ ...verticalAlignment ? { verticalAlignment } : {}
3015
+ });
3016
+ }
3017
+ function firstTextStyle(text) {
3018
+ return text.paragraphs[0]?.runs[0]?.style;
3019
+ }
3020
+ function projectFill(fill) {
3021
+ return fill.kind === "none" ? "none" : Object.freeze({ type: "solid", color: rgbaCss(fill.color) });
3022
+ }
3023
+ function projectLine(line) {
3024
+ return Object.freeze({
3025
+ style: line.dash,
3026
+ fill: projectFill(line.fill),
3027
+ width: displayCoordinate(line.width)
3028
+ });
3029
+ }
3030
+ function projectPosition(bounds) {
3031
+ return Object.freeze({
3032
+ left: displayCoordinate(bounds.x),
3033
+ top: displayCoordinate(bounds.y),
3034
+ width: displayCoordinate(bounds.width),
3035
+ height: displayCoordinate(bounds.height)
3036
+ });
3037
+ }
3038
+ function displayCoordinate(value) {
3039
+ return value / EMU_PER_CSS_PIXEL;
3040
+ }
3041
+ function normalizeProjectedEditorPosition(position) {
3042
+ const clamp = (value) => Math.max(-1e6, Math.min(1e6, value));
3043
+ return {
3044
+ left: clamp(position.left),
3045
+ top: clamp(position.top),
3046
+ width: Math.min(1e6, Math.max(8, position.width)),
3047
+ height: Math.min(1e6, Math.max(8, position.height))
3048
+ };
3049
+ }
3050
+ function canonicalCoordinate(value) {
3051
+ if (!Number.isFinite(value)) throw new Error("Presentation coordinate must be finite");
3052
+ const canonical = Math.round(value * EMU_PER_CSS_PIXEL);
3053
+ if (!Number.isSafeInteger(canonical)) throw new Error("Presentation coordinate is out of range");
3054
+ return canonical;
3055
+ }
3056
+ function sameProjectedPosition(left, right) {
3057
+ return canonicalCoordinate(left.left) === canonicalCoordinate(right.left) && canonicalCoordinate(left.top) === canonicalCoordinate(right.top) && canonicalCoordinate(left.width) === canonicalCoordinate(right.width) && canonicalCoordinate(left.height) === canonicalCoordinate(right.height);
3058
+ }
3059
+ function projectShapeGeometry(geometry) {
3060
+ return {
3061
+ "text-box": "textbox",
3062
+ rectangle: "rect",
3063
+ "rounded-rectangle": "roundRect",
3064
+ ellipse: "ellipse",
3065
+ triangle: "triangle",
3066
+ "right-arrow": "rightArrow",
3067
+ line: "line"
3068
+ }[geometry];
3069
+ }
3070
+ function emptyPresentationText() {
3071
+ return Object.freeze({
3072
+ verticalAlignment: "middle",
3073
+ paragraphs: Object.freeze([
3074
+ Object.freeze({
3075
+ alignment: "left",
3076
+ runs: Object.freeze([Object.freeze({ text: "", style: DEFAULT_PRESENTATION_TEXT_STYLE })])
3077
+ })
3078
+ ])
3079
+ });
3080
+ }
3081
+ function flattenPresentationRichText(text) {
3082
+ return text.paragraphs.map((paragraph) => paragraph.runs.map((run) => run.text).join("")).join("\n");
3083
+ }
3084
+ function replacePresentationRichText(richText, before, after) {
3085
+ const canonicalBefore = flattenPresentationRichText(richText);
3086
+ if (canonicalBefore !== before) throw new Error("Presentation rich text does not match its edit");
3087
+ if (before === after) return richText;
3088
+ const diff = textReplacementRange(before, after);
3089
+ const spans = presentationStyleSpans(richText);
3090
+ const paragraphStarts = presentationParagraphStarts(richText);
3091
+ const insertionStyle = styleAtOffset(spans, diff.start) ?? DEFAULT_PRESENTATION_TEXT_STYLE;
3092
+ const insertionAlignment = alignmentAtOffset(richText, paragraphStarts, diff.start);
3093
+ const paragraphs = [];
3094
+ let newOffset = 0;
3095
+ for (const paragraphText of after.split("\n")) {
3096
+ const alignment = alignmentForNewOffset(
3097
+ richText,
3098
+ paragraphStarts,
3099
+ diff,
3100
+ newOffset,
3101
+ insertionAlignment
3102
+ );
3103
+ const runs = [];
3104
+ let localOffset = 0;
3105
+ for (const character of paragraphText) {
3106
+ const offset = newOffset + localOffset;
3107
+ const oldOffset = oldOffsetForNewOffset(diff, offset);
3108
+ const style = oldOffset === null ? insertionStyle : styleAtOffset(spans, oldOffset) ?? insertionStyle;
3109
+ const previous = runs.at(-1);
3110
+ if (previous && samePresentationTextStyle(previous.style, style)) previous.text += character;
3111
+ else runs.push({ text: character, style });
3112
+ localOffset += character.length;
3113
+ }
3114
+ paragraphs.push(
3115
+ Object.freeze({
3116
+ alignment,
3117
+ runs: Object.freeze(
3118
+ runs.map(
3119
+ (run) => Object.freeze({ text: run.text, style: Object.freeze({ ...run.style }) })
3120
+ )
3121
+ )
3122
+ })
3123
+ );
3124
+ newOffset += paragraphText.length + 1;
3125
+ }
3126
+ return Object.freeze({
3127
+ verticalAlignment: richText.verticalAlignment,
3128
+ paragraphs: Object.freeze(paragraphs)
3129
+ });
3130
+ }
3131
+ function textReplacementRange(before, after) {
3132
+ let start = 0;
3133
+ while (start < before.length && start < after.length && before.charCodeAt(start) === after.charCodeAt(start)) {
3134
+ start += 1;
3135
+ }
3136
+ while (start > 0 && (splitsSurrogate(before, start) || splitsSurrogate(after, start))) start -= 1;
3137
+ let suffix = 0;
3138
+ while (suffix < before.length - start && suffix < after.length - start && before.charCodeAt(before.length - suffix - 1) === after.charCodeAt(after.length - suffix - 1)) {
3139
+ suffix += 1;
3140
+ }
3141
+ while (suffix > 0 && (splitsSurrogate(before, before.length - suffix) || splitsSurrogate(after, after.length - suffix))) {
3142
+ suffix -= 1;
3143
+ }
3144
+ const oldEnd = before.length - suffix;
3145
+ const newEnd = after.length - suffix;
3146
+ return { start, oldEnd, newEnd, delta: oldEnd - newEnd };
3147
+ }
3148
+ function oldOffsetForNewOffset(diff, offset) {
3149
+ if (offset < diff.start) return offset;
3150
+ if (offset >= diff.newEnd) return offset + diff.delta;
3151
+ return null;
3152
+ }
3153
+ function presentationStyleSpans(text) {
3154
+ const spans = [];
3155
+ let offset = 0;
3156
+ text.paragraphs.forEach((paragraph, paragraphIndex) => {
3157
+ for (const run of paragraph.runs) {
3158
+ spans.push({ start: offset, end: offset + run.text.length, style: run.style });
3159
+ offset += run.text.length;
3160
+ }
3161
+ if (paragraphIndex < text.paragraphs.length - 1) offset += 1;
3162
+ });
3163
+ return spans;
3164
+ }
3165
+ function presentationParagraphStarts(text) {
3166
+ const starts = [];
3167
+ let offset = 0;
3168
+ text.paragraphs.forEach((paragraph, index) => {
3169
+ starts.push(offset);
3170
+ offset += paragraph.runs.reduce((sum, run) => sum + run.text.length, 0);
3171
+ if (index < text.paragraphs.length - 1) offset += 1;
3172
+ });
3173
+ return starts;
3174
+ }
3175
+ function styleAtOffset(spans, offset) {
3176
+ const containing = spans.find((span) => offset >= span.start && offset < span.end);
3177
+ if (containing) return containing.style;
3178
+ const before = [...spans].reverse().find((span) => span.end <= offset);
3179
+ return before?.style ?? spans[0]?.style ?? null;
3180
+ }
3181
+ function alignmentAtOffset(text, starts, offset) {
3182
+ let index = 0;
3183
+ while (index + 1 < starts.length && (starts[index + 1] ?? Number.POSITIVE_INFINITY) <= offset) {
3184
+ index += 1;
3185
+ }
3186
+ return text.paragraphs[index]?.alignment ?? "left";
3187
+ }
3188
+ function alignmentForNewOffset(text, starts, diff, newOffset, fallback) {
3189
+ const oldOffset = oldOffsetForNewOffset(diff, newOffset);
3190
+ return oldOffset === null ? fallback : alignmentAtOffset(text, starts, oldOffset);
3191
+ }
3192
+ function samePresentationTextStyle(left, right) {
3193
+ return left.fontFamily === right.fontFamily && left.fontSizeCentipoints === right.fontSizeCentipoints && left.color === right.color && left.bold === right.bold && left.italic === right.italic && left.underline === right.underline && left.language === right.language;
3194
+ }
3195
+ function splitsSurrogate(value, index) {
3196
+ if (index <= 0 || index >= value.length) return false;
3197
+ const left = value.charCodeAt(index - 1);
3198
+ const right = value.charCodeAt(index);
3199
+ return left >= 55296 && left <= 56319 && right >= 56320 && right <= 57343;
3200
+ }
3201
+ function rgbaCss(value) {
3202
+ const hex = (value >>> 0).toString(16).padStart(8, "0");
3203
+ return hex.endsWith("ff") ? `#${hex.slice(0, 6)}` : `#${hex}`;
3204
+ }
3205
+ function bytesToHex(value) {
3206
+ return [...value].map((byte) => byte.toString(16).padStart(2, "0")).join("");
3207
+ }
3208
+ function sceneNodeKey(slideId, nodeId) {
3209
+ return `${slideId}:${nodeId}`;
3210
+ }
3211
+ function isPresentationRevisionDrift(error) {
3212
+ return error.message.includes("changed while") || error.message.includes("revision drifted");
3213
+ }
3214
+ function notifyPresentationCommit(observer, onError, commit) {
3215
+ try {
3216
+ const result = observer?.(commit);
3217
+ if (result && typeof result.then === "function") {
3218
+ void Promise.resolve(result).catch(
3219
+ (cause) => onError?.(asEditableArtifactError(cause, "Presentation commit observer failed"))
3220
+ );
3221
+ }
3222
+ } catch (cause) {
3223
+ onError?.(asEditableArtifactError(cause, "Presentation commit observer failed"));
3224
+ }
3225
+ }
3226
+
3227
+ export {
3228
+ PresentationEditor,
3229
+ PresentationProjectionEditor,
3230
+ PresentationArtifactSurface,
3231
+ PresentationProjectionArtifactSurface,
3232
+ EditablePresentationArtifactSurface
3233
+ };
3234
+ //# sourceMappingURL=chunk-PEI6RANI.js.map