@q9labsai/chalk-react 5.0.0 → 5.1.0

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 (294) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/dist/bindings/context.d.ts +0 -1
  3. package/dist/bindings/hooks.d.ts +0 -1
  4. package/dist/{chunk-NPOO7BOX.js → chunk-QDEKMRH2.js} +6 -5
  5. package/dist/{chunk-XN2E352E.js → chunk-XOHNIYSR.js} +1768 -129
  6. package/dist/{chunk-WRAEH24P.js → chunk-ZFWIMKUC.js} +4 -2
  7. package/dist/components/admission-panel/AdmissionPanel.d.ts +0 -1
  8. package/dist/components/atomic/AudioIndicator.d.ts +0 -1
  9. package/dist/components/atomic/Avatar.d.ts +0 -1
  10. package/dist/components/atomic/HandRaiseIndicator.d.ts +0 -1
  11. package/dist/components/atomic/NameTag.d.ts +0 -1
  12. package/dist/components/atomic/ReactionBubble.d.ts +0 -1
  13. package/dist/components/atomic/StatusBadge.d.ts +0 -1
  14. package/dist/components/atomic/VolumeSlider.d.ts +0 -1
  15. package/dist/components/atomic/Waveform.d.ts +0 -1
  16. package/dist/components/atomic/index.d.ts +0 -1
  17. package/dist/components/audio-output/AudioOutput.d.ts +0 -1
  18. package/dist/components/board/Board.d.ts +22 -0
  19. package/dist/components/board/BoardCursors.d.ts +21 -0
  20. package/dist/components/board/BoardIcon.d.ts +46 -0
  21. package/dist/components/board/BoardIsland.d.ts +21 -0
  22. package/dist/components/board/BoardMenu.d.ts +15 -0
  23. package/dist/components/board/BoardStyles.d.ts +2 -0
  24. package/dist/components/board/board-engine.d.ts +31 -0
  25. package/dist/components/board/board-history.d.ts +7 -0
  26. package/dist/components/board/board-model.d.ts +86 -0
  27. package/dist/components/board/board-toasts.d.ts +6 -0
  28. package/dist/components/board/board-view-versions.d.ts +7 -0
  29. package/dist/components/call-card/CallCard.d.ts +0 -1
  30. package/dist/components/call-card/CallCardStyles.d.ts +0 -1
  31. package/dist/components/call-limbo/CallLimbo.d.ts +3 -2
  32. package/dist/components/call-limbo/CallReady.d.ts +0 -1
  33. package/dist/components/call-limbo/qr-code.d.ts +0 -1
  34. package/dist/components/call-limbo/useCallLink.d.ts +0 -1
  35. package/dist/components/chalk/Chalk.d.ts +0 -1
  36. package/dist/components/chalk/StatusSurface.d.ts +3 -2
  37. package/dist/components/chalk-ui/ChalkAlert.d.ts +0 -1
  38. package/dist/components/chalk-ui/ChalkBackdrop.d.ts +0 -1
  39. package/dist/components/chalk-ui/ChalkBadge.d.ts +0 -1
  40. package/dist/components/chalk-ui/ChalkButton.d.ts +0 -1
  41. package/dist/components/chalk-ui/ChalkCopyButton.d.ts +0 -1
  42. package/dist/components/chalk-ui/ChalkDialogPanel.d.ts +0 -1
  43. package/dist/components/chalk-ui/ChalkDivider.d.ts +0 -1
  44. package/dist/components/chalk-ui/ChalkEmptyState.d.ts +0 -1
  45. package/dist/components/chalk-ui/ChalkIconButton.d.ts +0 -1
  46. package/dist/components/chalk-ui/ChalkInput.d.ts +0 -1
  47. package/dist/components/chalk-ui/ChalkMenu.d.ts +2 -2
  48. package/dist/components/chalk-ui/ChalkPanel.d.ts +0 -1
  49. package/dist/components/chalk-ui/ChalkPopover.d.ts +0 -1
  50. package/dist/components/chalk-ui/ChalkSegmentedControl.d.ts +0 -1
  51. package/dist/components/chalk-ui/ChalkSheet.d.ts +0 -1
  52. package/dist/components/chalk-ui/ChalkSpinner.d.ts +0 -1
  53. package/dist/components/chalk-ui/ChalkTextarea.d.ts +0 -1
  54. package/dist/components/chalk-ui/ChalkToast.d.ts +0 -1
  55. package/dist/components/chalk-ui/ChalkToggle.d.ts +0 -1
  56. package/dist/components/chalk-ui/ChalkTooltip.d.ts +0 -1
  57. package/dist/components/chalk-ui/common.d.ts +0 -1
  58. package/dist/components/chalk-ui/index.d.ts +0 -1
  59. package/dist/components/composite/ChatPanel.d.ts +0 -1
  60. package/dist/components/composite/CommandErrorAlert.d.ts +0 -1
  61. package/dist/components/composite/MessageBubble.d.ts +0 -1
  62. package/dist/components/composite/ReactionPicker.d.ts +0 -1
  63. package/dist/components/composite/ReactionTray.d.ts +0 -1
  64. package/dist/components/composite/ReactionsOverlay.d.ts +0 -1
  65. package/dist/components/composite/ScreenShareView.d.ts +0 -1
  66. package/dist/components/composite/chat-file-upload.d.ts +0 -1
  67. package/dist/components/composite/chat-panel-model.d.ts +0 -1
  68. package/dist/components/composite/chat-types.d.ts +0 -1
  69. package/dist/components/composite/chat-upload-file.d.ts +0 -1
  70. package/dist/components/composite/reaction-float.d.ts +0 -1
  71. package/dist/components/composite/use-chat-attachment-previews.d.ts +0 -1
  72. package/dist/components/control-bar/CallLinkIcon.d.ts +0 -1
  73. package/dist/components/control-bar/ControlBar.d.ts +0 -1
  74. package/dist/components/control-bar/DockLayout.d.ts +0 -1
  75. package/dist/components/control-bar/LeaveMenu.d.ts +0 -1
  76. package/dist/components/control-bar/ShareControl.d.ts +3 -2
  77. package/dist/components/control-bar/ShellIcon.d.ts +0 -1
  78. package/dist/components/control-bar/ShellStyles.d.ts +0 -1
  79. package/dist/components/control-bar/command-failure.d.ts +0 -1
  80. package/dist/components/control-bar/control-bar-behavior.d.ts +4 -2
  81. package/dist/components/control-bar/control-bar-button.d.ts +0 -1
  82. package/dist/components/control-bar/next-camera.d.ts +0 -1
  83. package/dist/components/device-popover/DevicePopover.d.ts +4 -2
  84. package/dist/components/index.d.ts +0 -1
  85. package/dist/components/index.js +3 -3
  86. package/dist/components/lazy.d.ts +0 -1
  87. package/dist/components/logo/Logo.d.ts +0 -1
  88. package/dist/components/media-request-dialog/MediaRequestDialog.d.ts +0 -1
  89. package/dist/components/media-request-dialog/media-request-dialog-state.d.ts +0 -1
  90. package/dist/components/offline-actions/OfflineActions.d.ts +0 -1
  91. package/dist/components/participant-grid/ParticipantGrid.d.ts +0 -1
  92. package/dist/components/participant-tile/ParticipantTile.d.ts +4 -2
  93. package/dist/components/participant-tile/TileShell.d.ts +3 -2
  94. package/dist/components/participant-tile/useVideoTrack.d.ts +0 -1
  95. package/dist/components/participants-panel/ParticipantsPanel.d.ts +0 -1
  96. package/dist/components/participants-panel/index.d.ts +0 -1
  97. package/dist/components/participants-panel/participant-options-menu-contract.d.ts +0 -1
  98. package/dist/components/participants-panel/participant-options-menu.d.ts +0 -1
  99. package/dist/components/participants-panel/participant-row.d.ts +0 -1
  100. package/dist/components/participants-panel/participant-volume-context.d.ts +0 -1
  101. package/dist/components/reconnecting-overlay/ReconnectingOverlay.d.ts +0 -1
  102. package/dist/components/recording-history/RecordingHistoryPanel.d.ts +0 -1
  103. package/dist/components/space-header/CallDetails.d.ts +3 -3
  104. package/dist/components/space-header/LayoutMenu.d.ts +0 -1
  105. package/dist/components/space-header/SpaceHeader.d.ts +0 -1
  106. package/dist/components/space-header/call-diagnostics.d.ts +0 -1
  107. package/dist/components/space-header/share-qr.d.ts +0 -1
  108. package/dist/components/space-header/space-header-contract.d.ts +0 -1
  109. package/dist/components/space-header/useCallShortcuts.d.ts +0 -1
  110. package/dist/components/space-view/RecordingSpaceView.d.ts +0 -1
  111. package/dist/components/space-view/SpacePresentation.d.ts +0 -1
  112. package/dist/components/space-view/SpaceView.d.ts +3 -1
  113. package/dist/components/space-view/useDrawerPresence.d.ts +0 -1
  114. package/dist/components/stage/SpaceStage.d.ts +3 -2
  115. package/dist/components/stage/Stage.d.ts +5 -2
  116. package/dist/components/stage/StageContentTile.d.ts +0 -1
  117. package/dist/components/stage/StageIcon.d.ts +0 -1
  118. package/dist/components/stage/StageMotion.d.ts +0 -1
  119. package/dist/components/stage/stage-items.d.ts +0 -1
  120. package/dist/components/stage/stage-layout.d.ts +2 -1
  121. package/dist/components/stage/useStageDimensions.d.ts +0 -1
  122. package/dist/components/toast-stack/CallEventIcon.d.ts +0 -1
  123. package/dist/components/toast-stack/ToastStack.d.ts +0 -1
  124. package/dist/components/toast-stack/call-events.d.ts +0 -1
  125. package/dist/components/toast-stack/useCallToasts.d.ts +0 -1
  126. package/dist/components/ui/index.d.ts +0 -1
  127. package/dist/components/whiteboard-view/RecordingWhiteboardView.d.ts +0 -1
  128. package/dist/components/whiteboard-view/WhiteboardView.d.ts +0 -1
  129. package/dist/components/whiteboard-view/recording-whiteboard-state.d.ts +0 -1
  130. package/dist/headless.d.ts +0 -1
  131. package/dist/headless.js +1 -1
  132. package/dist/index.d.ts +0 -1
  133. package/dist/index.js +3 -3
  134. package/dist/internal/call-motion.d.ts +0 -1
  135. package/dist/internal/sound-cues.d.ts +0 -1
  136. package/dist/internal/useEpisodeDuration.d.ts +0 -1
  137. package/dist/internal/useHaptics.d.ts +0 -1
  138. package/dist/internal/useMediaQuery.d.ts +0 -1
  139. package/dist/internal/useMirrorSetting.d.ts +0 -1
  140. package/dist/internal/useSoundCues.d.ts +0 -1
  141. package/dist/internal/useSoundsSetting.d.ts +0 -1
  142. package/dist/internal/useWhiteboardSceneSubscription.d.ts +0 -1
  143. package/dist/public-surface.contract.d.ts +0 -1
  144. package/dist/selectors/space-selectors.d.ts +0 -1
  145. package/dist/test-support/index.d.ts +0 -1
  146. package/dist/test-support/index.js +25 -3
  147. package/dist/test-support/preview-client.d.ts +0 -1
  148. package/dist/test-support/preview-devices.d.ts +0 -1
  149. package/dist/test-support/preview-fixtures.d.ts +0 -1
  150. package/dist/theme.d.ts +0 -1
  151. package/dist/utils/cn.d.ts +0 -1
  152. package/dist/utils/colorGenerator.d.ts +0 -1
  153. package/dist/utils/icons.d.ts +0 -1
  154. package/dist/utils/index.d.ts +0 -1
  155. package/dist/utils/theme.d.ts +0 -1
  156. package/dist/whiteboard/wire-adapters.d.ts +0 -1
  157. package/package.json +6 -5
  158. package/dist/bindings/context.d.ts.map +0 -1
  159. package/dist/bindings/hooks.d.ts.map +0 -1
  160. package/dist/components/admission-panel/AdmissionPanel.d.ts.map +0 -1
  161. package/dist/components/atomic/AudioIndicator.d.ts.map +0 -1
  162. package/dist/components/atomic/Avatar.d.ts.map +0 -1
  163. package/dist/components/atomic/HandRaiseIndicator.d.ts.map +0 -1
  164. package/dist/components/atomic/NameTag.d.ts.map +0 -1
  165. package/dist/components/atomic/ReactionBubble.d.ts.map +0 -1
  166. package/dist/components/atomic/StatusBadge.d.ts.map +0 -1
  167. package/dist/components/atomic/VolumeSlider.d.ts.map +0 -1
  168. package/dist/components/atomic/Waveform.d.ts.map +0 -1
  169. package/dist/components/atomic/index.d.ts.map +0 -1
  170. package/dist/components/audio-output/AudioOutput.d.ts.map +0 -1
  171. package/dist/components/call-card/CallCard.d.ts.map +0 -1
  172. package/dist/components/call-card/CallCardStyles.d.ts.map +0 -1
  173. package/dist/components/call-limbo/CallLimbo.d.ts.map +0 -1
  174. package/dist/components/call-limbo/CallReady.d.ts.map +0 -1
  175. package/dist/components/call-limbo/qr-code.d.ts.map +0 -1
  176. package/dist/components/call-limbo/useCallLink.d.ts.map +0 -1
  177. package/dist/components/chalk/Chalk.d.ts.map +0 -1
  178. package/dist/components/chalk/StatusSurface.d.ts.map +0 -1
  179. package/dist/components/chalk-ui/ChalkAlert.d.ts.map +0 -1
  180. package/dist/components/chalk-ui/ChalkBackdrop.d.ts.map +0 -1
  181. package/dist/components/chalk-ui/ChalkBadge.d.ts.map +0 -1
  182. package/dist/components/chalk-ui/ChalkButton.d.ts.map +0 -1
  183. package/dist/components/chalk-ui/ChalkCopyButton.d.ts.map +0 -1
  184. package/dist/components/chalk-ui/ChalkDialogPanel.d.ts.map +0 -1
  185. package/dist/components/chalk-ui/ChalkDivider.d.ts.map +0 -1
  186. package/dist/components/chalk-ui/ChalkEmptyState.d.ts.map +0 -1
  187. package/dist/components/chalk-ui/ChalkIconButton.d.ts.map +0 -1
  188. package/dist/components/chalk-ui/ChalkInput.d.ts.map +0 -1
  189. package/dist/components/chalk-ui/ChalkMenu.d.ts.map +0 -1
  190. package/dist/components/chalk-ui/ChalkPanel.d.ts.map +0 -1
  191. package/dist/components/chalk-ui/ChalkPopover.d.ts.map +0 -1
  192. package/dist/components/chalk-ui/ChalkSegmentedControl.d.ts.map +0 -1
  193. package/dist/components/chalk-ui/ChalkSheet.d.ts.map +0 -1
  194. package/dist/components/chalk-ui/ChalkSpinner.d.ts.map +0 -1
  195. package/dist/components/chalk-ui/ChalkTextarea.d.ts.map +0 -1
  196. package/dist/components/chalk-ui/ChalkToast.d.ts.map +0 -1
  197. package/dist/components/chalk-ui/ChalkToggle.d.ts.map +0 -1
  198. package/dist/components/chalk-ui/ChalkTooltip.d.ts.map +0 -1
  199. package/dist/components/chalk-ui/common.d.ts.map +0 -1
  200. package/dist/components/chalk-ui/index.d.ts.map +0 -1
  201. package/dist/components/composite/ChatPanel.d.ts.map +0 -1
  202. package/dist/components/composite/CommandErrorAlert.d.ts.map +0 -1
  203. package/dist/components/composite/MessageBubble.d.ts.map +0 -1
  204. package/dist/components/composite/ReactionPicker.d.ts.map +0 -1
  205. package/dist/components/composite/ReactionTray.d.ts.map +0 -1
  206. package/dist/components/composite/ReactionsOverlay.d.ts.map +0 -1
  207. package/dist/components/composite/ScreenShareView.d.ts.map +0 -1
  208. package/dist/components/composite/chat-file-upload.d.ts.map +0 -1
  209. package/dist/components/composite/chat-panel-model.d.ts.map +0 -1
  210. package/dist/components/composite/chat-types.d.ts.map +0 -1
  211. package/dist/components/composite/chat-upload-file.d.ts.map +0 -1
  212. package/dist/components/composite/reaction-float.d.ts.map +0 -1
  213. package/dist/components/composite/use-chat-attachment-previews.d.ts.map +0 -1
  214. package/dist/components/control-bar/CallLinkIcon.d.ts.map +0 -1
  215. package/dist/components/control-bar/ControlBar.d.ts.map +0 -1
  216. package/dist/components/control-bar/DockLayout.d.ts.map +0 -1
  217. package/dist/components/control-bar/LeaveMenu.d.ts.map +0 -1
  218. package/dist/components/control-bar/ShareControl.d.ts.map +0 -1
  219. package/dist/components/control-bar/ShellIcon.d.ts.map +0 -1
  220. package/dist/components/control-bar/ShellStyles.d.ts.map +0 -1
  221. package/dist/components/control-bar/command-failure.d.ts.map +0 -1
  222. package/dist/components/control-bar/control-bar-behavior.d.ts.map +0 -1
  223. package/dist/components/control-bar/control-bar-button.d.ts.map +0 -1
  224. package/dist/components/control-bar/next-camera.d.ts.map +0 -1
  225. package/dist/components/device-popover/DevicePopover.d.ts.map +0 -1
  226. package/dist/components/index.d.ts.map +0 -1
  227. package/dist/components/lazy.d.ts.map +0 -1
  228. package/dist/components/logo/Logo.d.ts.map +0 -1
  229. package/dist/components/media-request-dialog/MediaRequestDialog.d.ts.map +0 -1
  230. package/dist/components/media-request-dialog/media-request-dialog-state.d.ts.map +0 -1
  231. package/dist/components/offline-actions/OfflineActions.d.ts.map +0 -1
  232. package/dist/components/participant-grid/ParticipantGrid.d.ts.map +0 -1
  233. package/dist/components/participant-tile/ParticipantTile.d.ts.map +0 -1
  234. package/dist/components/participant-tile/TileShell.d.ts.map +0 -1
  235. package/dist/components/participant-tile/useVideoTrack.d.ts.map +0 -1
  236. package/dist/components/participants-panel/ParticipantsPanel.d.ts.map +0 -1
  237. package/dist/components/participants-panel/index.d.ts.map +0 -1
  238. package/dist/components/participants-panel/participant-options-menu-contract.d.ts.map +0 -1
  239. package/dist/components/participants-panel/participant-options-menu.d.ts.map +0 -1
  240. package/dist/components/participants-panel/participant-row.d.ts.map +0 -1
  241. package/dist/components/participants-panel/participant-volume-context.d.ts.map +0 -1
  242. package/dist/components/reconnecting-overlay/ReconnectingOverlay.d.ts.map +0 -1
  243. package/dist/components/recording-history/RecordingHistoryPanel.d.ts.map +0 -1
  244. package/dist/components/space-header/CallDetails.d.ts.map +0 -1
  245. package/dist/components/space-header/LayoutMenu.d.ts.map +0 -1
  246. package/dist/components/space-header/SpaceHeader.d.ts.map +0 -1
  247. package/dist/components/space-header/call-diagnostics.d.ts.map +0 -1
  248. package/dist/components/space-header/share-qr.d.ts.map +0 -1
  249. package/dist/components/space-header/space-header-contract.d.ts.map +0 -1
  250. package/dist/components/space-header/useCallShortcuts.d.ts.map +0 -1
  251. package/dist/components/space-view/RecordingSpaceView.d.ts.map +0 -1
  252. package/dist/components/space-view/SpacePresentation.d.ts.map +0 -1
  253. package/dist/components/space-view/SpaceView.d.ts.map +0 -1
  254. package/dist/components/space-view/useDrawerPresence.d.ts.map +0 -1
  255. package/dist/components/stage/SpaceStage.d.ts.map +0 -1
  256. package/dist/components/stage/Stage.d.ts.map +0 -1
  257. package/dist/components/stage/StageContentTile.d.ts.map +0 -1
  258. package/dist/components/stage/StageIcon.d.ts.map +0 -1
  259. package/dist/components/stage/StageMotion.d.ts.map +0 -1
  260. package/dist/components/stage/stage-items.d.ts.map +0 -1
  261. package/dist/components/stage/stage-layout.d.ts.map +0 -1
  262. package/dist/components/stage/useStageDimensions.d.ts.map +0 -1
  263. package/dist/components/toast-stack/CallEventIcon.d.ts.map +0 -1
  264. package/dist/components/toast-stack/ToastStack.d.ts.map +0 -1
  265. package/dist/components/toast-stack/call-events.d.ts.map +0 -1
  266. package/dist/components/toast-stack/useCallToasts.d.ts.map +0 -1
  267. package/dist/components/ui/index.d.ts.map +0 -1
  268. package/dist/components/whiteboard-view/RecordingWhiteboardView.d.ts.map +0 -1
  269. package/dist/components/whiteboard-view/WhiteboardView.d.ts.map +0 -1
  270. package/dist/components/whiteboard-view/recording-whiteboard-state.d.ts.map +0 -1
  271. package/dist/headless.d.ts.map +0 -1
  272. package/dist/index.d.ts.map +0 -1
  273. package/dist/internal/call-motion.d.ts.map +0 -1
  274. package/dist/internal/sound-cues.d.ts.map +0 -1
  275. package/dist/internal/useEpisodeDuration.d.ts.map +0 -1
  276. package/dist/internal/useHaptics.d.ts.map +0 -1
  277. package/dist/internal/useMediaQuery.d.ts.map +0 -1
  278. package/dist/internal/useMirrorSetting.d.ts.map +0 -1
  279. package/dist/internal/useSoundCues.d.ts.map +0 -1
  280. package/dist/internal/useSoundsSetting.d.ts.map +0 -1
  281. package/dist/internal/useWhiteboardSceneSubscription.d.ts.map +0 -1
  282. package/dist/public-surface.contract.d.ts.map +0 -1
  283. package/dist/selectors/space-selectors.d.ts.map +0 -1
  284. package/dist/test-support/index.d.ts.map +0 -1
  285. package/dist/test-support/preview-client.d.ts.map +0 -1
  286. package/dist/test-support/preview-devices.d.ts.map +0 -1
  287. package/dist/test-support/preview-fixtures.d.ts.map +0 -1
  288. package/dist/theme.d.ts.map +0 -1
  289. package/dist/utils/cn.d.ts.map +0 -1
  290. package/dist/utils/colorGenerator.d.ts.map +0 -1
  291. package/dist/utils/icons.d.ts.map +0 -1
  292. package/dist/utils/index.d.ts.map +0 -1
  293. package/dist/utils/theme.d.ts.map +0 -1
  294. package/dist/whiteboard/wire-adapters.d.ts.map +0 -1
@@ -10,7 +10,7 @@ import {
10
10
  gridOrder,
11
11
  isContentItem,
12
12
  stabilizeOrder
13
- } from "./chunk-WRAEH24P.js";
13
+ } from "./chunk-ZFWIMKUC.js";
14
14
  import {
15
15
  ArrowLeft01Icon,
16
16
  ArrowRight01Icon,
@@ -278,7 +278,7 @@ ChalkBackdrop.displayName = "ChalkBackdrop";
278
278
  import { Menu } from "@base-ui/react/menu";
279
279
  import { useRef } from "react";
280
280
  import { Fragment as Fragment2, jsx as jsx12, jsxs as jsxs2 } from "react/jsx-runtime";
281
- function ChalkMenu({ trigger, children, side = "bottom", className }) {
281
+ function ChalkMenu({ trigger, children, side = "bottom", className, onOpenChange }) {
282
282
  const container = useRef(null);
283
283
  return /* @__PURE__ */ jsx12(
284
284
  "span",
@@ -287,7 +287,7 @@ function ChalkMenu({ trigger, children, side = "bottom", className }) {
287
287
  container.current = element?.closest("[data-chalk]") ?? element;
288
288
  },
289
289
  style: { display: "contents" },
290
- children: /* @__PURE__ */ jsxs2(Menu.Root, { children: [
290
+ children: /* @__PURE__ */ jsxs2(Menu.Root, { onOpenChange, children: [
291
291
  /* @__PURE__ */ jsx12(Menu.Trigger, { render: trigger }),
292
292
  /* @__PURE__ */ jsx12(Menu.Portal, { container, children: /* @__PURE__ */ jsx12(Menu.Positioner, { side, align: side === "top" ? "center" : "end", sideOffset: side === "top" ? 10 : 6, collisionPadding: 8, className: "chalk-menu-positioner", children: /* @__PURE__ */ jsx12(Menu.Popup, { "data-part": "menu", className: cn("chalk-menu", className), children }) }) })
293
293
  ] })
@@ -644,7 +644,7 @@ function CallEventIcon({ kind, error = false }) {
644
644
 
645
645
  // src/components/call-limbo/CallLimbo.tsx
646
646
  import { jsx as jsx23, jsxs as jsxs10 } from "react/jsx-runtime";
647
- function CallLimbo({ spaceName = "Call", message, detail, action, children, overlay = false, active = true, initiallyCovered = false, theme, recorded = false, blocking = true }) {
647
+ function CallLimbo({ spaceName = "Call", message, detail, action, children, overlay = false, active = true, initiallyCovered = false, theme, recorded = false, blocking = true, className }) {
648
648
  const layerRef = useRef6(null);
649
649
  const reducedMotion = usePrefersReducedMotion();
650
650
  const [covered, setCovered] = useState6(active || initiallyCovered);
@@ -698,7 +698,7 @@ function CallLimbo({ spaceName = "Call", message, detail, action, children, over
698
698
  opacity: covered ? 1 : 0,
699
699
  transition: reducedMotion ? "none" : "opacity 220ms cubic-bezier(.2,.8,.2,1)"
700
700
  },
701
- children: /* @__PURE__ */ jsxs10("div", { "data-part": active ? "limbo-message" : void 0, style: { borderRadius: 16 }, className: "flex max-w-full flex-col items-center gap-2 rounded-2xl border border-[var(--chalk-line)] bg-[var(--chalk-surface)] px-7 py-[22px] text-center", children: [
701
+ children: /* @__PURE__ */ jsxs10("div", { "data-part": active ? "limbo-message" : void 0, style: { borderRadius: 16 }, className: cn("flex max-w-full flex-col items-center gap-2 rounded-2xl border border-[var(--chalk-line)] bg-[var(--chalk-surface)] px-7 py-[22px] text-center", className), children: [
702
702
  message ? /* @__PURE__ */ jsxs10("p", { role: active ? "status" : void 0, className: "flex items-center gap-2.5 text-[17px] font-semibold", children: [
703
703
  message.endsWith("\u2026") ? /* @__PURE__ */ jsx23("span", { "aria-hidden": "true", className: "h-[18px] w-[18px] animate-spin rounded-full border-2 border-[var(--chalk-line-strong)] border-t-[var(--chalk-text)] motion-reduce:animate-none" }) : null,
704
704
  message
@@ -1311,7 +1311,7 @@ Avatar.displayName = "Avatar";
1311
1311
  // src/components/participant-tile/TileShell.tsx
1312
1312
  import "react";
1313
1313
  import { jsx as jsx29, jsxs as jsxs14 } from "react/jsx-runtime";
1314
- function TileShell({ label, accentColor, pinned = false, speaking = false, audioLevel = 0.5, lift = true, onClick, onDoubleClick, className, style, dataTour, hidden = false, chip, corner, children }) {
1314
+ function TileShell({ label, accentColor, pinned = false, ring = false, speaking = false, audioLevel = 0.5, lift = true, onClick, onDoubleClick, className, style, dataTour, hidden = false, chip, corner, children }) {
1315
1315
  const vars = { ...style, containerType: "size", "--chalk-participant-color": accentColor, "--chalk-voice-level": audioLevel };
1316
1316
  return /* @__PURE__ */ jsxs14(
1317
1317
  "div",
@@ -1351,6 +1351,7 @@ function TileShell({ label, accentColor, pinned = false, speaking = false, audio
1351
1351
  style: { opacity: speaking ? 0.3 + 0.7 * audioLevel : 0, boxShadow: "inset 0 0 0 3px rgba(255,255,255,.9), inset 0 0 0 4px rgba(0,0,0,.35)" }
1352
1352
  }
1353
1353
  ),
1354
+ ring ? /* @__PURE__ */ jsx29("div", { "aria-hidden": "true", "data-part": "follow-ring", className: "pointer-events-none absolute inset-0 z-30 rounded-[inherit] shadow-[inset_0_0_0_2px_var(--chalk-text)]" }) : null,
1354
1355
  corner,
1355
1356
  chip ? /* @__PURE__ */ jsx29("div", { "data-part": "tile-tag", className: "pointer-events-none absolute bottom-2 left-2 z-30 flex h-6 max-w-[calc(100%_-_16px)] items-center gap-[5px] rounded-[6px] bg-[rgba(12,14,18,.75)] px-2 text-[12px] leading-[1.4] font-medium text-white", children: chip }) : null
1356
1357
  ]
@@ -1425,6 +1426,8 @@ var ParticipantTileSurface = React5.memo(function ParticipantTileSurface2({
1425
1426
  onClick,
1426
1427
  onDoubleClick,
1427
1428
  pinned,
1429
+ clickAction = "pin",
1430
+ following = false,
1428
1431
  primary,
1429
1432
  className,
1430
1433
  style,
@@ -1451,17 +1454,20 @@ var ParticipantTileSurface = React5.memo(function ParticipantTileSurface2({
1451
1454
  const mirrored = mirror ?? (participant.isLocal === true && localMirrored);
1452
1455
  const colors = useMemo6(() => getParticipantColor(participant.displayName || participant.id, gradientPreference), [gradientPreference, participant.displayName, participant.id]);
1453
1456
  const recipe = useMemo6(() => getParticipantAvatarRecipe(participant.displayName || participant.id, gradientPreference), [participant.displayName, participant.id, gradientPreference]);
1457
+ const first2 = participant.displayName.split(" ")[0] ?? participant.displayName;
1458
+ const actionLabel = clickAction === "follow" ? `${following ? "Stop following" : "Follow"} ${first2}` : `${pinned ? "Unpin" : "Pin"} ${participant.displayName}`;
1454
1459
  const hasPoorConnection = participant.connectionQuality !== void 0 && participant.connectionQuality > 0 && participant.connectionQuality <= 2;
1455
1460
  return /* @__PURE__ */ jsxs15(
1456
1461
  TileShell,
1457
1462
  {
1458
- label: `Video tile for ${participant.displayName}`,
1463
+ label: clickAction === "follow" ? actionLabel : `Video tile for ${participant.displayName}`,
1459
1464
  accentColor: colors.primary,
1460
- pinned,
1465
+ pinned: clickAction === "follow" ? following : pinned,
1466
+ ring: following,
1461
1467
  lift: !primary,
1462
1468
  speaking: mediaVisible && participant.isSpeaking && !participant.isMuted,
1463
1469
  audioLevel: participant.audioLevel,
1464
- onClick: onClick && phone ? openOptions : onClick,
1470
+ onClick: onClick && phone && clickAction === "pin" ? openOptions : onClick,
1465
1471
  onDoubleClick,
1466
1472
  className: cn(aspectRatioClasses[aspectRatio], className),
1467
1473
  style,
@@ -1473,11 +1479,7 @@ var ParticipantTileSurface = React5.memo(function ParticipantTileSurface2({
1473
1479
  ChalkMenu,
1474
1480
  {
1475
1481
  trigger: /* @__PURE__ */ jsx30("button", { type: "button", "aria-label": `More options for ${participant.displayName}`, className: "grid h-8 w-8 place-items-center rounded-full bg-[rgba(12,14,18,.75)] text-white outline-none hover:bg-[rgba(40,42,48,.95)] focus-visible:ring-2 focus-visible:ring-[var(--chalk-focus)]", children: /* @__PURE__ */ jsx30(StageIcon, { name: "more", size: 18 }) }),
1476
- children: /* @__PURE__ */ jsxs15(ChalkMenuItem, { onClick, children: [
1477
- pinned ? "Unpin" : "Pin",
1478
- " ",
1479
- participant.displayName
1480
- ] })
1482
+ children: /* @__PURE__ */ jsx30(ChalkMenuItem, { onClick, children: actionLabel })
1481
1483
  }
1482
1484
  ) }) : null
1483
1485
  ] }),
@@ -1487,6 +1489,7 @@ var ParticipantTileSurface = React5.memo(function ParticipantTileSurface2({
1487
1489
  participant.displayName,
1488
1490
  participant.isLocal && participant.displayName !== "You" && /* @__PURE__ */ jsx30("span", { className: "text-white", children: " (You)" })
1489
1491
  ] }),
1492
+ following ? /* @__PURE__ */ jsx30("span", { className: "shrink-0 font-semibold opacity-80", children: "following" }) : null,
1490
1493
  showStatus && participant.isMuted && /* @__PURE__ */ jsx30(StageIcon, { name: "mute", size: 14, className: "shrink-0 text-[#ff6b6b]", "aria-label": "Muted" }),
1491
1494
  showStatus && participant.isHandRaised && /* @__PURE__ */ jsxs15("span", { className: "flex shrink-0 items-center text-[#e2bd3a]", role: "img", "aria-label": `Hand raised${participant.handRaiseOrder ? `, number ${participant.handRaiseOrder}` : ""}`, children: [
1492
1495
  /* @__PURE__ */ jsx30(StageIcon, { name: "hand", size: 14 }),
@@ -1513,7 +1516,7 @@ var ParticipantTileSurface = React5.memo(function ParticipantTileSurface2({
1513
1516
  colors: recipe.facehashColors
1514
1517
  }
1515
1518
  ) : /* @__PURE__ */ jsx30(Avatar, { name: participant.displayName, src: participant.avatarUrl, size: "xl", generated: false, style: { width: "100%", height: "100%" } }) }) }),
1516
- phone && onClick ? /* @__PURE__ */ jsxs15(
1519
+ phone && onClick && clickAction === "pin" ? /* @__PURE__ */ jsxs15(
1517
1520
  ChalkSheet,
1518
1521
  {
1519
1522
  open: optionsOpen,
@@ -3147,11 +3150,9 @@ var ChatPanelSurface = React10.memo(
3147
3150
  const [pickingFiles, setPickingFiles] = useState17(false);
3148
3151
  const [composerError, setComposerError] = useState17(null);
3149
3152
  const scrollRef = useRef12(null);
3150
- const endRef = useRef12(null);
3151
3153
  const textareaRef = useRef12(null);
3152
3154
  const fileInputRef = useRef12(null);
3153
3155
  const isAtBottomRef = useRef12(true);
3154
- const mountedRef = useRef12(false);
3155
3156
  const lastMarkedSequenceRef = useRef12(localReadThroughSequence);
3156
3157
  const restoreScrollRef = useRef12(null);
3157
3158
  const grouped = useMemo8(() => groupChatMessages(messages), [messages]);
@@ -3169,10 +3170,8 @@ var ChatPanelSurface = React10.memo(
3169
3170
  return;
3170
3171
  }
3171
3172
  if (!isAtBottomRef.current) return;
3172
- scroller.scrollTop = scroller.scrollHeight;
3173
- endRef.current?.scrollIntoView?.({ behavior: autoScrollBehavior ?? (mountedRef.current ? "smooth" : "auto"), block: "end" });
3173
+ scroller.scrollTo({ top: scroller.scrollHeight, behavior: autoScrollBehavior ?? "auto" });
3174
3174
  if (latestSequence) markChatSequenceRead(latestSequence, lastMarkedSequenceRef, onMarkRead);
3175
- mountedRef.current = true;
3176
3175
  }, [autoScrollBehavior, latestSequence, messages.length, onMarkRead, pendingMessages.length]);
3177
3176
  useEffect13(() => {
3178
3177
  if (!localReadThroughSequence) return;
@@ -3387,8 +3386,7 @@ var ChatPanelSurface = React10.memo(
3387
3386
  /* @__PURE__ */ jsx36("span", { children: pending.error?.message || "Not sent" }),
3388
3387
  onRetryMessage ? /* @__PURE__ */ jsx36("button", { type: "button", className: "font-medium text-[var(--chalk-app-control-active-line)] hover:underline", onClick: () => void onRetryMessage(pending.clientMessageId), children: "Retry" }) : null
3389
3388
  ] }) : null
3390
- ] }, pending.clientMessageId)),
3391
- /* @__PURE__ */ jsx36("div", { ref: endRef })
3389
+ ] }, pending.clientMessageId))
3392
3390
  ] }),
3393
3391
  (error || composerError) && /* @__PURE__ */ jsx36("p", { role: "alert", className: "mx-4 mb-2 rounded-lg bg-[var(--chalk-app-danger)]/10 px-3 py-2 text-sm text-[var(--chalk-app-danger)]", children: composerError || error }),
3394
3392
  stagedFiles.length > 0 ? /* @__PURE__ */ jsx36("div", { className: "flex flex-wrap gap-2 border-t border-[var(--chalk-app-line)] px-4 pt-3", "aria-label": "Attachments", children: stagedFiles.map((file, index) => /* @__PURE__ */ jsxs20("div", { className: "call-staged-file", children: [
@@ -4241,19 +4239,44 @@ function InputLevel({ track }) {
4241
4239
  }, [track]);
4242
4240
  return /* @__PURE__ */ jsx41("span", { className: "chalk-shell-level", role: "meter", "aria-label": "Input level", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": level, children: /* @__PURE__ */ jsx41("i", { style: { transform: `scaleX(${level / 100})` } }) });
4243
4241
  }
4244
- function CameraPreview({ track, mirrored }) {
4242
+ function CameraPreview({ track, mirrored, open, selectedDeviceId, captureCameraPreview }) {
4245
4243
  const video = useRef14(null);
4244
+ const [unavailable, setUnavailable] = useState19(false);
4245
+ const trackEnabled = track?.enabled;
4246
4246
  useEffect15(() => {
4247
4247
  const element = video.current;
4248
- if (!element || !track) return;
4249
- element.srcObject = new MediaStream([track]);
4248
+ if (!element || !open) return;
4249
+ let cancelled = false;
4250
+ let preview;
4251
+ setUnavailable(false);
4252
+ if (captureCameraPreview) {
4253
+ void captureCameraPreview().then(
4254
+ (captured) => {
4255
+ if (cancelled) {
4256
+ captured.release();
4257
+ return;
4258
+ }
4259
+ preview = captured;
4260
+ element.srcObject = new MediaStream([captured.track]);
4261
+ },
4262
+ () => {
4263
+ if (!cancelled) setUnavailable(true);
4264
+ }
4265
+ );
4266
+ } else if (track?.enabled && track.readyState === "live") {
4267
+ element.srcObject = new MediaStream([track]);
4268
+ } else {
4269
+ setUnavailable(true);
4270
+ }
4250
4271
  return () => {
4272
+ cancelled = true;
4251
4273
  element.srcObject = null;
4274
+ preview?.release();
4252
4275
  };
4253
- }, [track]);
4276
+ }, [track, trackEnabled, open, selectedDeviceId, captureCameraPreview]);
4254
4277
  return /* @__PURE__ */ jsxs23("div", { className: "chalk-shell-preview", children: [
4255
4278
  /* @__PURE__ */ jsx41("video", { ref: video, autoPlay: true, muted: true, playsInline: true, style: { transform: mirrored ? "scaleX(-1)" : void 0 } }),
4256
- /* @__PURE__ */ jsx41("small", { children: track ? "Only you see this preview" : "Camera is off" })
4279
+ /* @__PURE__ */ jsx41("small", { children: unavailable ? "Camera preview unavailable. Check camera access." : "Only you see this preview" })
4257
4280
  ] });
4258
4281
  }
4259
4282
  function DeviceSwitch({ label, checked, onChange }) {
@@ -4273,6 +4296,7 @@ function DevicePopover({
4273
4296
  selectedSecondaryDeviceId,
4274
4297
  onSecondaryDeviceChange,
4275
4298
  track,
4299
+ captureCameraPreview,
4276
4300
  warning,
4277
4301
  speakingWhileMuted,
4278
4302
  noiseSuppression = true,
@@ -4283,8 +4307,10 @@ function DevicePopover({
4283
4307
  orientation = "up",
4284
4308
  className,
4285
4309
  disabled,
4310
+ toggleDisabled,
4286
4311
  busy
4287
4312
  }) {
4313
+ const [open, setOpen] = useState19(false);
4288
4314
  const mic = type === "mic";
4289
4315
  const label = mic ? isActive ? "Mute microphone" : "Unmute microphone" : isActive ? "Turn off camera" : "Turn on camera";
4290
4316
  return /* @__PURE__ */ jsxs23("div", { "data-part": `${type}-split`, className: cn("chalk-shell-split", className), "data-off": !isActive, children: [
@@ -4292,7 +4318,7 @@ function DevicePopover({
4292
4318
  /* @__PURE__ */ jsx41(
4293
4319
  ChalkTooltip,
4294
4320
  {
4295
- trigger: /* @__PURE__ */ jsx41("button", { type: "button", className: "chalk-shell-main", "aria-label": busy ? `Updating ${mic ? "microphone" : "camera"}` : label, "aria-pressed": isActive, disabled: disabled || busy, onClick: onToggle, children: /* @__PURE__ */ jsx41(ShellIcon, { name: mic ? isActive ? "mic" : "micOff" : isActive ? "cam" : "camOff" }) }),
4321
+ trigger: /* @__PURE__ */ jsx41("button", { type: "button", className: "chalk-shell-main", "aria-label": busy ? `Updating ${mic ? "microphone" : "camera"}` : label, "aria-pressed": isActive, disabled: disabled || toggleDisabled || busy, onClick: onToggle, children: /* @__PURE__ */ jsx41(ShellIcon, { name: mic ? isActive ? "mic" : "micOff" : isActive ? "cam" : "camOff" }) }),
4296
4322
  children: label
4297
4323
  }
4298
4324
  ),
@@ -4301,9 +4327,10 @@ function DevicePopover({
4301
4327
  {
4302
4328
  side: orientation === "up" ? "top" : "bottom",
4303
4329
  className: "chalk-device-menu",
4330
+ onOpenChange: setOpen,
4304
4331
  trigger: /* @__PURE__ */ jsx41("button", { type: "button", className: "chalk-shell-caret", "aria-label": `Select ${mic ? "microphone" : "camera"}`, disabled, children: /* @__PURE__ */ jsx41(ShellIcon, { name: "down" }) }),
4305
4332
  children: [
4306
- !mic ? /* @__PURE__ */ jsx41(CameraPreview, { track, mirrored }) : null,
4333
+ !mic ? /* @__PURE__ */ jsx41(CameraPreview, { track, mirrored, open, selectedDeviceId, captureCameraPreview }) : null,
4307
4334
  /* @__PURE__ */ jsx41(ChalkMenuLabel, { children: mic ? "Microphone" : "Camera" }),
4308
4335
  devices.length ? devices.map((device) => /* @__PURE__ */ jsx41(ChalkMenuItem, { checked: selectedDeviceId === device.deviceId, onClick: () => onDeviceChange(device.deviceId), closeOnClick: false, children: /* @__PURE__ */ jsxs23("span", { className: "chalk-shell-device-label", children: [
4309
4336
  device.label || (mic ? "Microphone" : "Camera"),
@@ -4410,11 +4437,12 @@ function SourceIcon({ source }) {
4410
4437
  /* @__PURE__ */ jsx45("path", { d: "M3 11h18" })
4411
4438
  ] }) });
4412
4439
  }
4413
- function ShareControl({ sharing, onToggle, onStart }) {
4440
+ function ShareControl({ sharing, onToggle, onStart, disabled = false, startDisabled = false }) {
4414
4441
  const [surface, setSurface] = useState21("window");
4415
4442
  const [audio, setAudio] = useState21(true);
4416
4443
  const [replace, setReplace] = useState21(false);
4417
4444
  const start = (next) => {
4445
+ if (disabled || startDisabled) return;
4418
4446
  setSurface(next);
4419
4447
  setReplace(false);
4420
4448
  onStart?.({ surface: next, audio: next === "browser" && audio });
@@ -4423,7 +4451,7 @@ function ShareControl({ sharing, onToggle, onStart }) {
4423
4451
  /* @__PURE__ */ jsx45(
4424
4452
  ChalkTooltip,
4425
4453
  {
4426
- trigger: /* @__PURE__ */ jsx45("button", { type: "button", className: "chalk-shell-main", "aria-label": sharing ? "Stop sharing" : "Share screen", "aria-pressed": sharing, onClick: () => sharing || !onStart ? onToggle?.() : start(surface), children: /* @__PURE__ */ jsx45(ShellIcon, { name: sharing ? "shareOff" : "share" }) }),
4454
+ trigger: /* @__PURE__ */ jsx45("button", { type: "button", className: "chalk-shell-main", disabled, "aria-label": sharing ? "Stop sharing" : "Share screen", "aria-pressed": sharing, onClick: () => sharing || !onStart ? onToggle?.() : start(surface), children: /* @__PURE__ */ jsx45(ShellIcon, { name: sharing ? "shareOff" : "share" }) }),
4427
4455
  children: sharing ? "Stop sharing" : "Share screen"
4428
4456
  }
4429
4457
  ),
@@ -4432,17 +4460,17 @@ function ShareControl({ sharing, onToggle, onStart }) {
4432
4460
  {
4433
4461
  side: "top",
4434
4462
  className: "chalk-device-menu",
4435
- trigger: /* @__PURE__ */ jsx45("button", { type: "button", className: "chalk-shell-caret", "aria-label": "Share options", children: /* @__PURE__ */ jsx45(ShellIcon, { name: "down" }) }),
4463
+ trigger: /* @__PURE__ */ jsx45("button", { type: "button", className: "chalk-shell-caret", disabled: disabled || startDisabled, "aria-label": "Share options", children: /* @__PURE__ */ jsx45(ShellIcon, { name: "down" }) }),
4436
4464
  children: sharing && !replace ? /* @__PURE__ */ jsxs27(Fragment14, { children: [
4437
4465
  /* @__PURE__ */ jsx45(ChalkMenuLabel, { children: "Sharing your screen" }),
4438
- /* @__PURE__ */ jsx45(Menu4.Item, { className: "chalk-menu-item", closeOnClick: false, onClick: () => setReplace(true), children: "Share something else" }),
4466
+ /* @__PURE__ */ jsx45(Menu4.Item, { className: "chalk-menu-item", disabled: disabled || startDisabled, closeOnClick: false, onClick: () => setReplace(true), children: "Share something else" }),
4439
4467
  /* @__PURE__ */ jsx45(ChalkMenuItem, { onClick: onToggle, children: "Stop sharing" })
4440
4468
  ] }) : /* @__PURE__ */ jsxs27(Fragment14, { children: [
4441
4469
  [
4442
4470
  { value: "monitor", label: "Entire screen", description: "Everything on one display" },
4443
4471
  { value: "window", label: "Window", description: "One app, like Slides or Figma" },
4444
4472
  { value: "browser", label: "Browser tab", description: "One tab, and its sound if you like" }
4445
- ].map((option) => /* @__PURE__ */ jsxs27(Menu4.Item, { className: "chalk-menu-item chalk-shell-source", role: "menuitemradio", "aria-label": option.label, "aria-checked": surface === option.value, onClick: () => start(option.value), children: [
4473
+ ].map((option) => /* @__PURE__ */ jsxs27(Menu4.Item, { className: "chalk-menu-item chalk-shell-source", disabled: disabled || startDisabled, role: "menuitemradio", "aria-label": option.label, "aria-checked": surface === option.value, onClick: () => start(option.value), children: [
4446
4474
  /* @__PURE__ */ jsx45(SourceIcon, { source: option.value }),
4447
4475
  /* @__PURE__ */ jsxs27("span", { className: "chalk-shell-device-label", children: [
4448
4476
  /* @__PURE__ */ jsx45("span", { children: option.label }),
@@ -4568,6 +4596,7 @@ function useConnectedControlBarBehavior(props) {
4568
4596
  const canRaiseHand = useCan("raiseHand");
4569
4597
  const canDrawWhiteboard = useCan("drawWhiteboard");
4570
4598
  const canManageRecording = useCan("manageRecording");
4599
+ const live = useConnection().status === "live";
4571
4600
  const microphoneEnabled = media.local.microphone.state === "enabled";
4572
4601
  const cameraEnabled = media.local.camera.state === "enabled";
4573
4602
  const screenSharing = media.local.screen.state === "enabled" || media.local.screen.state === "requesting";
@@ -4617,6 +4646,7 @@ function useConnectedControlBarBehavior(props) {
4617
4646
  buttons,
4618
4647
  microphoneTrack: media.local.microphone.track,
4619
4648
  cameraTrack: media.local.camera.track,
4649
+ captureCameraPreview: client.media.captureCameraPreview,
4620
4650
  mirrored: props.mirrored ?? mirrored,
4621
4651
  onMirrorChange: props.onMirrorChange ?? (props.mirrored === void 0 ? setMirrored2 : void 0),
4622
4652
  microphoneWarning: media.local.microphone.state === "failed" || media.local.microphone.state === "unavailable",
@@ -4632,6 +4662,8 @@ function useConnectedControlBarBehavior(props) {
4632
4662
  shareUrl: props.shareUrl,
4633
4663
  isMuted: !microphoneEnabled,
4634
4664
  microphonePending: media.local.microphone.state === "requesting",
4665
+ mediaEnableDisabled: !live,
4666
+ reactionsDisabled: !live,
4635
4667
  cameraPending: media.local.camera.state === "requesting",
4636
4668
  isVideoEnabled: cameraEnabled,
4637
4669
  isScreenSharing: screenSharing,
@@ -4727,10 +4759,10 @@ var ControlBarSurface = React12.memo((props) => {
4727
4759
  return () => document.removeEventListener("fullscreenchange", changed);
4728
4760
  }, []);
4729
4761
  const has = (button) => buttonsToRender.includes(button);
4730
- const iconButton = (label, icon, onClick, active, badge = 0, hand = false) => /* @__PURE__ */ jsx46(
4762
+ const iconButton = (label, icon, onClick, active, badge = 0, hand = false, disabled = false) => /* @__PURE__ */ jsx46(
4731
4763
  ChalkTooltip,
4732
4764
  {
4733
- trigger: /* @__PURE__ */ jsxs28(ChalkIconButton, { size: "lg", "aria-label": label, "aria-pressed": active, onClick, children: [
4765
+ trigger: /* @__PURE__ */ jsxs28(ChalkIconButton, { size: "lg", "aria-label": label, "aria-pressed": active, onClick, disabled, children: [
4734
4766
  icon,
4735
4767
  badge > 0 ? /* @__PURE__ */ jsx46("span", { className: "chalk-shell-dot", "aria-label": `${badge} unread` }) : null,
4736
4768
  hand ? /* @__PURE__ */ jsx46("span", { className: "chalk-shell-dot", "aria-label": "Hand raised" }) : null
@@ -4746,7 +4778,7 @@ var ControlBarSurface = React12.memo((props) => {
4746
4778
  const chat = has("chat") && (props.onToggleChat || props.displayOnly) ? iconButton("Chat", /* @__PURE__ */ jsx46(ShellIcon, { name: "chat" }), props.onToggleChat, props.isChatOpen, props.isChatOpen ? 0 : props.unreadChatCount) : null;
4747
4779
  const copyLink = () => void navigator.clipboard.writeText(props.shareUrl ?? location.href).catch(() => props.onCopyError?.());
4748
4780
  const linkLabel = props.onCallLinkToggle ? props.callLinkOpen ? "Hide call link" : "Show call link" : "Copy call link";
4749
- const reactions = has("reactions") && (props.onOpenReactions || props.displayOnly) ? iconButton("Reactions", /* @__PURE__ */ jsx46(ShellIcon, { name: "smile" }), props.onOpenReactions, void 0, 0, props.isHandRaised) : null;
4781
+ const reactions = has("reactions") && (props.onOpenReactions || props.displayOnly) ? iconButton("Reactions", /* @__PURE__ */ jsx46(ShellIcon, { name: "smile" }), props.onOpenReactions, void 0, 0, props.isHandRaised, props.reactionsDisabled) : null;
4750
4782
  return /* @__PURE__ */ jsxs28("div", { ref: dock, "data-part": "dock", className: cn("chalk-shell-dock", props.className), role: "toolbar", "aria-label": "Space controls", children: [
4751
4783
  /* @__PURE__ */ jsx46(ShellStyles, {}),
4752
4784
  has("mic") ? /* @__PURE__ */ jsx46(
@@ -4755,6 +4787,7 @@ var ControlBarSurface = React12.memo((props) => {
4755
4787
  type: "mic",
4756
4788
  isActive: !props.isMuted,
4757
4789
  busy: props.microphonePending,
4790
+ toggleDisabled: props.mediaEnableDisabled && props.isMuted,
4758
4791
  onToggle: props.onToggleMute ?? (() => {
4759
4792
  }),
4760
4793
  devices: effectiveAudioInputDevices,
@@ -4778,6 +4811,7 @@ var ControlBarSurface = React12.memo((props) => {
4778
4811
  type: "video",
4779
4812
  isActive: props.isVideoEnabled ?? true,
4780
4813
  busy: props.cameraPending,
4814
+ toggleDisabled: props.mediaEnableDisabled && !props.isVideoEnabled,
4781
4815
  onToggle: props.onToggleVideo ?? (() => {
4782
4816
  }),
4783
4817
  devices: effectiveVideoInputDevices,
@@ -4785,15 +4819,16 @@ var ControlBarSurface = React12.memo((props) => {
4785
4819
  onDeviceChange: props.onVideoInputChange ?? (() => {
4786
4820
  }),
4787
4821
  track: props.cameraTrack,
4822
+ captureCameraPreview: props.captureCameraPreview,
4788
4823
  warning: props.cameraWarning,
4789
4824
  mirrored: props.mirrored,
4790
4825
  onMirrorChange: props.onMirrorChange
4791
4826
  }
4792
4827
  ) : null,
4793
4828
  /* @__PURE__ */ jsx46("span", { className: "chalk-shell-gap chalk-shell-desktop" }),
4794
- has("screenshare") ? /* @__PURE__ */ jsx46(ShareControl, { sharing: props.isScreenSharing ?? false, onToggle: props.onToggleScreenShare, onStart: props.onStartScreenShare }) : null,
4829
+ has("screenshare") ? /* @__PURE__ */ jsx46(ShareControl, { disabled: props.mediaEnableDisabled && !props.isScreenSharing, startDisabled: props.mediaEnableDisabled, sharing: props.isScreenSharing ?? false, onToggle: props.onToggleScreenShare, onStart: props.onStartScreenShare }) : null,
4795
4830
  reactions,
4796
- !reactions && has("handraise") ? iconButton(props.isHandRaised ? "Lower hand" : "Raise hand", /* @__PURE__ */ jsx46(HandIcon, {}), props.onToggleHandRaise, props.isHandRaised) : null,
4831
+ (!reactions || props.reactionsDisabled) && has("handraise") ? iconButton(props.isHandRaised ? "Lower hand" : "Raise hand", /* @__PURE__ */ jsx46(HandIcon, {}), props.onToggleHandRaise, props.isHandRaised) : null,
4797
4832
  has("whiteboard") && (props.onToggleWhiteboard || props.displayOnly) ? /* @__PURE__ */ jsx46("span", { className: "chalk-shell-desktop", children: iconButton("Board", /* @__PURE__ */ jsx46(ShellIcon, { name: "pen" }), props.onToggleWhiteboard, props.isWhiteboardOpen) }) : null,
4798
4833
  has("record") && (props.onToggleRecording || props.displayOnly) ? /* @__PURE__ */ jsx46("span", { className: "chalk-shell-desktop", children: iconButton(props.isRecording ? "Stop recording" : "Record", /* @__PURE__ */ jsx46(ShellIcon, { name: "rec" }), props.isRecordingPending ? void 0 : props.onToggleRecording, props.isRecording) }) : null,
4799
4834
  /* @__PURE__ */ jsx46("span", { className: "chalk-shell-desktop", children: iconButton(fullscreen ? "Exit fullscreen" : "Fullscreen", /* @__PURE__ */ jsx46(ShellIcon, { name: fullscreen ? "min" : "max" }), toggleFullscreen, fullscreen) }),
@@ -4820,7 +4855,7 @@ var ControlBarSurface = React12.memo((props) => {
4820
4855
  ]
4821
4856
  }
4822
4857
  ) : null,
4823
- has("screenshare") && canCaptureDisplay ? /* @__PURE__ */ jsxs28("button", { type: "button", className: "chalk-sheet-row", onClick: props.onToggleScreenShare, children: [
4858
+ has("screenshare") && canCaptureDisplay ? /* @__PURE__ */ jsxs28("button", { type: "button", className: "chalk-sheet-row", disabled: props.mediaEnableDisabled && !props.isScreenSharing, onClick: props.onToggleScreenShare, children: [
4824
4859
  /* @__PURE__ */ jsx46(ShellIcon, { name: "share" }),
4825
4860
  /* @__PURE__ */ jsx46("span", { children: props.isScreenSharing ? "Stop sharing" : "Share screen" })
4826
4861
  ] }) : null,
@@ -5339,12 +5374,24 @@ var ToastStack = memo(function ToastStack2({ toasts, onDismiss, maxVisible = 3,
5339
5374
 
5340
5375
  // src/components/chalk/StatusSurface.tsx
5341
5376
  import { Fragment as Fragment18, jsx as jsx51, jsxs as jsxs32 } from "react/jsx-runtime";
5342
- function StatusSurface({ phase, callName = "the call", reason, theme, onRejoin, onHome, onRetry, onReportProblem, rejoinDisabled = false, retryPending = false }) {
5377
+ var DEFAULT_FAILURE_DETAIL = "Something went wrong while joining. Try again.";
5378
+ var FAILURE_DETAILS = {
5379
+ "access.invalid": "Access to this call was rejected. Refresh the page and try again.",
5380
+ "access.unavailable": "Couldn\u2019t get access to the call. Try again.",
5381
+ "connection.sync_start_failed": "Couldn\u2019t reach the call service. Try again.",
5382
+ "connection.sync_hello_timeout": "The call service did not respond in time. Try again.",
5383
+ "connection.sync_recovery_exhausted": "The call connection could not recover. Try again.",
5384
+ "connection.media_start_failed": "Audio and video could not connect. Try again.",
5385
+ "connection.media_recovery_exhausted": "Audio and video could not reconnect. Try again.",
5386
+ "environment.unsupported": "This browser cannot start the call. Use an up-to-date browser over HTTPS.",
5387
+ "client.internal_error": DEFAULT_FAILURE_DETAIL
5388
+ };
5389
+ function StatusSurface({ phase, callName = "the call", reason, errorCode, theme, onRejoin, onHome, onRetry, onReportProblem, rejoinDisabled = false, retryPending = false }) {
5343
5390
  const copy = {
5344
5391
  entering: { title: `Joining ${callName}\u2026`, detail: "" },
5345
5392
  left: { title: `You left ${callName}`, detail: "" },
5346
5393
  "episode-ended": { title: "The call ended", detail: "" },
5347
- failed: { title: `Couldn\u2019t connect to ${callName}`, detail: "Check your internet connection, then try again." },
5394
+ failed: { title: `Couldn\u2019t connect to ${callName}`, detail: (errorCode && FAILURE_DETAILS[errorCode]) ?? DEFAULT_FAILURE_DETAIL },
5348
5395
  removed: { title: `You were removed from ${callName}`, detail: "The host removed you from this call." },
5349
5396
  full: { title: "This call is full", detail: `${callName} has reached 100 people. Ask the host to make room, then try again.` },
5350
5397
  elsewhere: { title: "You joined from another tab", detail: `${callName} is open in another tab or window. Use it here instead?` }
@@ -5362,6 +5409,11 @@ function StatusSurface({ phase, callName = "the call", reason, theme, onRejoin,
5362
5409
  children: [
5363
5410
  /* @__PURE__ */ jsx51("h1", { role: phase === "entering" ? "status" : void 0, style: { fontSize: 22, fontWeight: 600, lineHeight: 1.4, fontFamily: "inherit", letterSpacing: "normal", textWrap: "wrap" }, children: copy.title }),
5364
5411
  reason || copy.detail ? /* @__PURE__ */ jsx51("p", { className: "text-[14px] text-[var(--chalk-muted-text)]", children: reason ?? copy.detail }) : null,
5412
+ phase === "failed" && errorCode ? /* @__PURE__ */ jsxs32("p", { className: "text-[13px] text-[var(--chalk-muted-text)]", children: [
5413
+ "Reference:",
5414
+ " ",
5415
+ /* @__PURE__ */ jsx51("code", { dir: "ltr", className: "break-all", children: errorCode })
5416
+ ] }) : null,
5365
5417
  /* @__PURE__ */ jsxs32("div", { "data-part": "status-actions", className: "mt-5 flex gap-7", children: [
5366
5418
  canRetry && retry ? /* @__PURE__ */ jsx51(StatusAction, { icon: /* @__PURE__ */ jsx51(StatusIcon, { retry: true }), label: phase === "elsewhere" ? "Use here" : phase === "left" ? "Rejoin" : "Try again", onClick: retry, disabled: rejoinDisabled || retryPending }) : null,
5367
5419
  phase !== "entering" && onHome ? /* @__PURE__ */ jsx51(StatusAction, { icon: /* @__PURE__ */ jsx51(StatusIcon, {}), label: "Home", onClick: onHome }) : null
@@ -5880,6 +5932,7 @@ var CALL_SHORTCUTS = [
5880
5932
  ];
5881
5933
  function CallDialog({
5882
5934
  kind,
5935
+ open,
5883
5936
  onClose,
5884
5937
  onReportProblem,
5885
5938
  diagnostics,
@@ -5892,12 +5945,12 @@ function CallDialog({
5892
5945
  const [status, setStatus] = useState28("idle");
5893
5946
  const [error, setError] = useState28("");
5894
5947
  useEffect21(() => {
5895
- if (kind === "report") {
5948
+ if (open && kind === "report") {
5896
5949
  setText("");
5897
5950
  setError("");
5898
5951
  setStatus("idle");
5899
5952
  }
5900
- }, [kind]);
5953
+ }, [kind, open]);
5901
5954
  return /* @__PURE__ */ jsx54(
5902
5955
  "span",
5903
5956
  {
@@ -5908,9 +5961,9 @@ function CallDialog({
5908
5961
  children: /* @__PURE__ */ jsx54(
5909
5962
  Dialog2.Root,
5910
5963
  {
5911
- open: kind !== null,
5912
- onOpenChange: (open) => {
5913
- if (!open) onClose();
5964
+ open,
5965
+ onOpenChange: (open2) => {
5966
+ if (!open2) onClose();
5914
5967
  },
5915
5968
  children: /* @__PURE__ */ jsxs34(Dialog2.Portal, { container, children: [
5916
5969
  /* @__PURE__ */ jsx54(Dialog2.Backdrop, { className: "chalk-shell-modal-backdrop" }),
@@ -6134,8 +6187,8 @@ var SpaceHeader = React14.memo((props) => {
6134
6187
  const [sounds, setSounds] = useSoundsSetting();
6135
6188
  const triggerRef = useRef20(null);
6136
6189
  const [open, setOpen] = useState29(false);
6137
- const [dialog, setDialog] = useState29(null);
6138
- const showShortcuts = useCallback6(() => setDialog("shortcuts"), []);
6190
+ const [dialog, setDialog] = useState29({ kind: "shortcuts", open: false });
6191
+ const showShortcuts = useCallback6(() => setDialog({ kind: "shortcuts", open: true }), []);
6139
6192
  useCallShortcuts(props.shortcuts, showShortcuts);
6140
6193
  const phone = useMediaQuery("(max-width: 640px)");
6141
6194
  const chevron = /* @__PURE__ */ jsx55("svg", { width: phone ? "16" : "14", height: phone ? "16" : "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", "aria-hidden": "true", children: /* @__PURE__ */ jsx55("path", { d: "m6 9 6 6 6-6" }) });
@@ -6153,7 +6206,7 @@ var SpaceHeader = React14.memo((props) => {
6153
6206
  onSoundsChange: setSounds,
6154
6207
  onDialog: (kind) => {
6155
6208
  setOpen(false);
6156
- setDialog(kind);
6209
+ setDialog({ kind, open: true });
6157
6210
  }
6158
6211
  }
6159
6212
  );
@@ -6200,7 +6253,7 @@ var SpaceHeader = React14.memo((props) => {
6200
6253
  ] }) : null,
6201
6254
  onLayoutChange ? /* @__PURE__ */ jsx55(LayoutMenu, { layout, onLayoutChange }) : null
6202
6255
  ] }),
6203
- /* @__PURE__ */ jsx55(CallDialog, { returnFocus: triggerRef, kind: dialog, onClose: () => setDialog(null), onReportProblem: props.onReportProblem, diagnostics: (props.diagnostics ?? []).map(({ label, value }) => `${label}: ${value}`).join("\n") })
6256
+ /* @__PURE__ */ jsx55(CallDialog, { returnFocus: triggerRef, kind: dialog.kind, open: dialog.open, onClose: () => setDialog((current) => ({ ...current, open: false })), onReportProblem: props.onReportProblem, diagnostics: props.diagnostics?.map(({ label, value }) => `${label}: ${value}`).join("\n") ?? "" })
6204
6257
  ] });
6205
6258
  });
6206
6259
  SpaceHeader.displayName = "SpaceHeader";
@@ -6218,7 +6271,7 @@ function ScreenSharePresentation({ sharedByName, media, mediaVisible, state, isL
6218
6271
  const [error, setError] = useState30(null);
6219
6272
  const status = state ?? (mediaVisible ? "live" : "connecting");
6220
6273
  const name = isLocal ? "Your screen" : `${sharedByName}'s screen`;
6221
- const source = capture?.surface === "monitor" ? "your screen" : capture?.surface === "browser" ? "a tab" : "a window";
6274
+ const source = capture?.surface === "window" ? "a window" : capture?.surface === "browser" ? "a tab" : capture?.surface === "monitor" ? "your screen" : null;
6222
6275
  const run = async (command) => {
6223
6276
  if (pending) return;
6224
6277
  setPending(true);
@@ -6319,10 +6372,7 @@ function ScreenSharePresentation({ sharedByName, media, mediaVisible, state, isL
6319
6372
  primary ? switcher : null,
6320
6373
  isLocal && primary ? /* @__PURE__ */ jsxs36("div", { "data-part": "share-local-overlay", role: "group", "aria-label": "Your share", className: "absolute top-2.5 right-2.5 left-2.5 z-30 flex items-center gap-2 rounded-[12px] bg-[rgba(12,14,18,.86)] p-2 pl-3.5 text-white max-[640px]:flex-wrap", children: [
6321
6374
  /* @__PURE__ */ jsxs36("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [
6322
- /* @__PURE__ */ jsxs36("b", { className: "truncate font-semibold", children: [
6323
- "You\u2019re sharing ",
6324
- source
6325
- ] }),
6375
+ /* @__PURE__ */ jsx56("b", { className: "truncate font-semibold", children: source ? `You\u2019re sharing ${source}` : "You\u2019re screen sharing" }),
6326
6376
  capture?.surface === "monitor" || capture?.surface === "browser" ? /* @__PURE__ */ jsxs36("span", { className: "text-[12px] text-[#c9cbd1]", children: [
6327
6377
  "Seeing a mirror effect? That\u2019s normal when you share ",
6328
6378
  capture.surface === "browser" ? "this tab" : "your whole screen",
@@ -6375,6 +6425,7 @@ function ScreenShareViewSurface({ screenShareTrack, ...props }) {
6375
6425
  }
6376
6426
  function ScreenShareView(props) {
6377
6427
  const client = useSpaceClient();
6428
+ const connection = useConnection();
6378
6429
  const self = useSelf();
6379
6430
  const participantsSlice = useParticipants();
6380
6431
  const media = useMedia();
@@ -6392,8 +6443,12 @@ function ScreenShareView(props) {
6392
6443
  isLocal: active.isLocal,
6393
6444
  hasAudio: active.isLocal ? media.screenShare.hasAudio : participantsSlice.roster.find((person) => person.participantId === active.id)?.media.screenShareHasAudio,
6394
6445
  capture: active.isLocal ? media.screenShare.capture : void 0,
6395
- onStopSharing: active.isLocal ? () => client.media.setScreenShareEnabled(false) : void 0,
6396
- onChangeShare: active.isLocal ? (options) => client.media.setScreenShareEnabled(true, options) : void 0
6446
+ onStopSharing: active.isLocal ? async () => {
6447
+ await client.media.setScreenShareEnabled(false);
6448
+ } : void 0,
6449
+ onChangeShare: active.isLocal && connection.status === "live" ? async (options) => {
6450
+ await client.media.setScreenShareEnabled(true, options);
6451
+ } : void 0
6397
6452
  }
6398
6453
  );
6399
6454
  }
@@ -6586,6 +6641,9 @@ function Stage({
6586
6641
  items,
6587
6642
  layout,
6588
6643
  pinnedId,
6644
+ followingId = null,
6645
+ onFollowChange,
6646
+ clickAction = "pin",
6589
6647
  onPinnedChange,
6590
6648
  onOverflowClick,
6591
6649
  onItemClick,
@@ -6662,7 +6720,7 @@ function Stage({
6662
6720
  box,
6663
6721
  primary.id,
6664
6722
  strip.map((item) => item.id),
6665
- { ...DEFAULT_SPOTLIGHT_OPTIONS, primaryAspect: aspect, content: primary.kind !== "participant", wideIds: primary.kind === "screen-share" ? ordered.filter((item) => item.kind === "whiteboard").map((item) => item.id) : wideIds, maxPerPage }
6723
+ { ...DEFAULT_SPOTLIGHT_OPTIONS, primaryAspect: aspect, content: primary.kind !== "participant", slimPhoneStrip: primary.kind === "whiteboard", wideIds: primary.kind === "screen-share" ? ordered.filter((item) => item.kind === "whiteboard").map((item) => item.id) : wideIds, maxPerPage }
6666
6724
  );
6667
6725
  }, [activePinnedId, box, layout, maxPerPage, minTileWidth, ordered, primary, shareAspects]);
6668
6726
  const framesById = useMemo12(() => new Map(geometry.frames.map((frame) => [frame.id, frame])), [geometry.frames]);
@@ -6720,14 +6778,15 @@ function Stage({
6720
6778
  for (const item of ordered) {
6721
6779
  map.set(item.id, {
6722
6780
  onClick: () => {
6723
- togglePin(item);
6781
+ if (clickAction === "follow" && item.kind === "participant" && !item.participant.isLocal) onFollowChange?.(item.id);
6782
+ else togglePin(item);
6724
6783
  onItemClick?.(item);
6725
6784
  },
6726
6785
  onDoubleClick: onItemDoubleClick ? () => onItemDoubleClick(item) : void 0
6727
6786
  });
6728
6787
  }
6729
6788
  return map;
6730
- }, [interactive, onItemDoubleClick, onItemClick, ordered, togglePin]);
6789
+ }, [clickAction, interactive, onFollowChange, onItemDoubleClick, onItemClick, ordered, togglePin]);
6731
6790
  if (ordered.length === 0) {
6732
6791
  return /* @__PURE__ */ jsx59("div", { className: cn("flex h-full w-full items-center justify-center px-6 py-10", className), "data-tour": "video-grid", children: emptyState });
6733
6792
  }
@@ -6752,6 +6811,8 @@ function Stage({
6752
6811
  aspectRatio: "fill",
6753
6812
  primary: frame?.role === "primary",
6754
6813
  pinned,
6814
+ clickAction: item.participant.isLocal ? "pin" : clickAction,
6815
+ following: followingId === item.id,
6755
6816
  onClick: click,
6756
6817
  onDoubleClick: doubleClick,
6757
6818
  style,
@@ -6812,6 +6873,7 @@ function Stage({
6812
6873
  setOverflowOpen(false);
6813
6874
  },
6814
6875
  children: [
6876
+ clickAction === "follow" && !item.participant.isLocal ? `${followingId === item.id ? "Stop following" : "Follow"} ` : "",
6815
6877
  item.participant.displayName,
6816
6878
  item.participant.isMuted ? " \xB7 Muted" : ""
6817
6879
  ]
@@ -7019,7 +7081,7 @@ function SpacePresentation({ colorScheme = "dark", stageBackground, header, stag
7019
7081
  // src/components/space-view/SpaceView.tsx
7020
7082
  import { SpaceClientError as SpaceClientError2 } from "@q9labsai/chalk-client";
7021
7083
  import { DEFAULT_QUICK_REACTIONS as DEFAULT_QUICK_REACTIONS2 } from "@q9labsai/chalk-ui/reactions";
7022
- import { useCallback as useCallback13, useEffect as useEffect35, useMemo as useMemo16, useState as useState42 } from "react";
7084
+ import { useCallback as useCallback15, useEffect as useEffect40, useMemo as useMemo18, useRef as useRef34, useState as useState46 } from "react";
7023
7085
 
7024
7086
  // src/components/space-header/call-diagnostics.ts
7025
7087
  function callDiagnostics(connection, media) {
@@ -7267,6 +7329,8 @@ function ReactionTray({ isHandRaised, onToggleHandRaise, reactions, onSelect, on
7267
7329
  useLayoutEffect3(() => {
7268
7330
  returnFocus.current = document.activeElement;
7269
7331
  const tray = listRef.current;
7332
+ const frame = tray?.closest("[data-part='call-frame']");
7333
+ const notices = frame?.querySelector("[data-part='toast-stack']");
7270
7334
  const anchor = tray?.closest("[data-part='call-frame']")?.querySelector("[data-part='dock'] button[aria-label='Reactions']") ?? returnFocus.current;
7271
7335
  const place = () => {
7272
7336
  if (!tray || !(anchor instanceof HTMLElement) || !(tray.offsetParent instanceof HTMLElement)) return;
@@ -7275,7 +7339,12 @@ function ReactionTray({ isHandRaised, onToggleHandRaise, reactions, onSelect, on
7275
7339
  const width = tray.offsetWidth;
7276
7340
  const height = tray.offsetHeight;
7277
7341
  tray.style.left = `${Math.max(8, Math.min(window.innerWidth - width - 8, trigger2.x + trigger2.width / 2 - width / 2)) - parent.x}px`;
7278
- tray.style.top = `${Math.max(8, Math.min(window.innerHeight - height - 8, position === "top" ? trigger2.y - height - 10 : trigger2.bottom + 10)) - parent.y}px`;
7342
+ const top = Math.max(8, Math.min(window.innerHeight - height - 8, position === "top" ? trigger2.y - height - 10 : trigger2.bottom + 10));
7343
+ tray.style.top = `${top - parent.y}px`;
7344
+ if (notices?.offsetParent instanceof HTMLElement) {
7345
+ const baseline = notices.offsetParent.getBoundingClientRect().bottom - Number.parseFloat(getComputedStyle(notices).bottom);
7346
+ frame?.style.setProperty("--chalk-reaction-toast-lift", `${Math.max(0, baseline - top + 12)}px`);
7347
+ }
7279
7348
  };
7280
7349
  place();
7281
7350
  const resize = new ResizeObserver(place);
@@ -7285,6 +7354,7 @@ function ReactionTray({ isHandRaised, onToggleHandRaise, reactions, onSelect, on
7285
7354
  return () => {
7286
7355
  window.removeEventListener("resize", place);
7287
7356
  resize.disconnect();
7357
+ frame?.style.removeProperty("--chalk-reaction-toast-lift");
7288
7358
  if (returnFocus.current instanceof HTMLElement && returnFocus.current.isConnected) returnFocus.current.focus();
7289
7359
  };
7290
7360
  }, [position]);
@@ -7516,8 +7586,10 @@ function OfflineActions({ reconnecting: requestedReconnecting = false } = {}) {
7516
7586
  const count = offline.pending.length;
7517
7587
  const messagesOnly = offline.pending.every((action) => action.kind === "chat_message");
7518
7588
  const pendingDetail = count ? `${count} ${messagesOnly ? count === 1 ? "message" : "messages" : count === 1 ? "action" : "actions"} will send when you\u2019re back` : void 0;
7519
- if (reconnecting || !offline.decisionNeeded && !offline.notice) return /* @__PURE__ */ jsx66(CallLimbo, { overlay: true, blocking: false, active: reconnectingNoticeDue, message: offlineNoticeText(0, online), detail: pendingDetail });
7589
+ const notice = reconnecting ? reconnectingNoticeDue ? `${offlineNoticeText(0, online)}${pendingDetail ? ` ${pendingDetail}` : ""}` : null : connection.syncHealth === "degraded" ? "Connection is slow" : null;
7520
7590
  if (offline.notice && offline.notice !== dismissedNotice) return /* @__PURE__ */ jsx66(ToastStack, { toasts: [{ id: "offline-notice", message: offline.notice }], onDismiss: () => setDismissedNotice(offline.notice ?? null) });
7591
+ if (notice)
7592
+ return /* @__PURE__ */ jsx66("div", { role: "status", "data-part": "connection-notice", style: { pointerEvents: "none" }, className: "absolute inset-x-4 top-16 z-50 mx-auto w-fit max-w-[calc(100%_-_2rem)] rounded-full bg-[var(--chalk-surface)] px-3 py-1 text-center text-sm text-[var(--chalk-text)]", children: notice });
7521
7593
  return null;
7522
7594
  }
7523
7595
  function useTrueFor(value, milliseconds) {
@@ -7729,7 +7801,9 @@ function useCallToasts() {
7729
7801
  label: "Try again",
7730
7802
  onClick: () => {
7731
7803
  void client.media.setScreenShareEnabled(true).then(
7732
- () => setToasts((current) => current.filter((event) => event.id !== id)),
7804
+ () => {
7805
+ setToasts((current) => current.filter((event) => event.id !== id));
7806
+ },
7733
7807
  () => void 0
7734
7808
  );
7735
7809
  }
@@ -7760,19 +7834,1550 @@ function useCallLink(alone) {
7760
7834
  return { open, toggle: () => setOpen((current) => !current), close: () => setOpen(false) };
7761
7835
  }
7762
7836
 
7837
+ // src/components/board/board-toasts.ts
7838
+ import { createContext as createContext3, useCallback as useCallback12, useContext as useContext3, useEffect as useEffect34, useMemo as useMemo15 } from "react";
7839
+ var BoardToastContext = createContext3(() => void 0);
7840
+ function useBoardToast(lifetime) {
7841
+ const publish = useContext3(BoardToastContext);
7842
+ const scope = useMemo15(() => ({ active: true }), [lifetime]);
7843
+ useEffect34(() => {
7844
+ scope.active = true;
7845
+ return () => {
7846
+ scope.active = false;
7847
+ publish(null);
7848
+ };
7849
+ }, [publish, scope]);
7850
+ return useCallback12(
7851
+ (toast) => {
7852
+ if (scope.active) publish(toast);
7853
+ },
7854
+ [publish, scope]
7855
+ );
7856
+ }
7857
+
7858
+ // src/components/stage/SpaceStage.tsx
7859
+ import { useCallback as useCallback14, useEffect as useEffect38, useMemo as useMemo17, useState as useState44 } from "react";
7860
+
7861
+ // src/components/board/board-history.ts
7862
+ import { useEffect as useEffect35, useState as useState40 } from "react";
7863
+ import { observeEngineHistory } from "@q9labsai/chalk-whiteboard/react";
7864
+ function useEngineHistory(root, engine) {
7865
+ const [history, setHistory] = useState40({ canUndo: false, canRedo: false });
7866
+ useEffect35(() => {
7867
+ const canvas = root.current?.querySelector(".excalidraw");
7868
+ if (!engine || !canvas) return;
7869
+ return observeEngineHistory(engine.api, canvas, setHistory);
7870
+ }, [engine, root]);
7871
+ return history;
7872
+ }
7873
+
7874
+ // src/components/board/board-view-versions.ts
7875
+ var ViewVersions = class {
7876
+ #versions = /* @__PURE__ */ new Map();
7877
+ isNew(view) {
7878
+ if (!view.originParticipantId) return true;
7879
+ return view.originSequence > (this.#versions.get(`${view.originParticipantId}:${view.originEpoch ?? ""}`) ?? 0);
7880
+ }
7881
+ remember(view) {
7882
+ if (!view.originParticipantId || !this.isNew(view)) return;
7883
+ const origin = `${view.originParticipantId}:${view.originEpoch ?? ""}`;
7884
+ this.#versions.delete(origin);
7885
+ this.#versions.set(origin, view.originSequence);
7886
+ if (this.#versions.size > 256) {
7887
+ const oldest = this.#versions.keys().next().value;
7888
+ if (oldest !== void 0) this.#versions.delete(oldest);
7889
+ }
7890
+ }
7891
+ };
7892
+
7893
+ // src/components/board/Board.tsx
7894
+ import { WhiteboardCanvas as WhiteboardCanvas2 } from "@q9labsai/chalk-whiteboard/react";
7895
+ import { useCallback as useCallback13, useEffect as useEffect37, useMemo as useMemo16, useRef as useRef33, useState as useState43 } from "react";
7896
+
7897
+ // src/components/board/board-model.ts
7898
+ import { ChalkWhiteboardViewBounds } from "@q9labsai/chalk-client";
7899
+ var SURFACES = [
7900
+ { id: "chalkboard", label: "Chalkboard" },
7901
+ { id: "paper", label: "Paper" },
7902
+ { id: "dots", label: "Dot grid" }
7903
+ ];
7904
+ var ISLAND_PLACES = [
7905
+ { id: "left", label: "Left" },
7906
+ { id: "bottom", label: "Bottom" },
7907
+ { id: "top", label: "Top" }
7908
+ ];
7909
+ var PEN_COLORS = [
7910
+ { id: "ink", label: "Ink" },
7911
+ { id: "red", label: "Red" },
7912
+ { id: "orange", label: "Orange" },
7913
+ { id: "green", label: "Green" },
7914
+ { id: "blue", label: "Blue" },
7915
+ { id: "violet", label: "Violet" }
7916
+ ];
7917
+ var PEN_HEX = { red: "#e23240", orange: "#c15a10", green: "#288a51", blue: "#1f7ad6", violet: "#935ae2" };
7918
+ var NOTE_FILL = { ink: "#f6e27a", red: "#f2a3a8", orange: "#f4be8c", green: "#a8dbb8", blue: "#a9cdf2", violet: "#cdb8f2" };
7919
+ var NOTE_TEXT = "#1b1c1e";
7920
+ var NOTE_SIZE = { width: 170, height: 150 };
7921
+ var NOTE_ROUNDNESS = { type: 3, value: 4 };
7922
+ var PEN_WIDTHS = [
7923
+ { id: "thin", label: "Thin", units: 2.5 },
7924
+ { id: "medium", label: "Medium", units: 4.5 },
7925
+ { id: "thick", label: "Thick", units: 8 }
7926
+ ];
7927
+ var SHAPES = [
7928
+ { id: "rectangle", label: "Rectangle", key: "R" },
7929
+ { id: "ellipse", label: "Ellipse", key: "O" },
7930
+ { id: "diamond", label: "Diamond", key: "D" },
7931
+ { id: "arrow", label: "Arrow", key: "A" },
7932
+ { id: "line", label: "Line", key: "L" }
7933
+ ];
7934
+ var TOOL_KEYS = { v: "selection", h: "hand", p: "pen", u: "highlighter", r: "rectangle", o: "ellipse", d: "diamond", a: "arrow", l: "line", t: "text", n: "note", e: "eraser", k: "laser" };
7935
+ var PERSON_COLORS = ["#1d70c5", "#8753d0", "#d02e3b", "#257f4b", "#b2530f", "#1b7979", "#c0307a", "#62751d", "#3b6fd4", "#a14fb5", "#b5512c", "#297f65"];
7936
+ function personColor(index) {
7937
+ return PERSON_COLORS[(index % PERSON_COLORS.length + PERSON_COLORS.length) % PERSON_COLORS.length];
7938
+ }
7939
+ var DARK_SURFACES = /* @__PURE__ */ new Set(["chalkboard", "dots-dark"]);
7940
+ function surfaceKey(surface, theme) {
7941
+ return surface === "dots" ? `dots-${theme}` : surface;
7942
+ }
7943
+ function isDarkSurface(surface, theme) {
7944
+ return DARK_SURFACES.has(surfaceKey(surface, theme));
7945
+ }
7946
+ function surfaceInk(surface, theme) {
7947
+ if (surface === "chalkboard") return "#f1f0ea";
7948
+ if (surface === "paper") return "#1b1c1f";
7949
+ return theme === "dark" ? "#ececee" : "#18181b";
7950
+ }
7951
+ function surfaceFill(surface, theme) {
7952
+ if (surface === "chalkboard") return "#1c2622";
7953
+ if (surface === "paper") return theme === "dark" ? "#eae6db" : "#f5f2ea";
7954
+ return theme === "dark" ? "#151619" : "#ffffff";
7955
+ }
7956
+ function selectionColor(surface, theme) {
7957
+ return isDarkSurface(surface, theme) ? "#7db8ff" : "#1f6fd0";
7958
+ }
7959
+ function penHex(color, surface, theme) {
7960
+ return color === "ink" ? surfaceInk(surface, theme) : PEN_HEX[color];
7961
+ }
7962
+ function highlighterStyle(color, width) {
7963
+ return { strokeColor: PEN_HEX[color === "ink" ? "orange" : color], strokeWidth: widthUnits(width) * 4, opacity: 40 };
7964
+ }
7965
+ var FREEHAND_SCALE = 4.25;
7966
+ function engineStrokeWidth(px, freehand) {
7967
+ return freehand ? px / FREEHAND_SCALE : px;
7968
+ }
7969
+ function widthUnits(width) {
7970
+ return PEN_WIDTHS.find((entry) => entry.id === width).units;
7971
+ }
7972
+ var MIN_ZOOM = 0.1;
7973
+ var MAX_ZOOM = 4;
7974
+ function clampZoom(zoom) {
7975
+ return Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, zoom));
7976
+ }
7977
+ function visibleView(camera, viewport) {
7978
+ const { maxCoordinate, maxExtent } = ChalkWhiteboardViewBounds;
7979
+ return {
7980
+ x: Math.max(-maxCoordinate, Math.min(maxCoordinate, -camera.scrollX)),
7981
+ y: Math.max(-maxCoordinate, Math.min(maxCoordinate, -camera.scrollY)),
7982
+ width: Math.min(maxExtent, viewport.width / camera.zoom),
7983
+ height: Math.min(maxExtent, viewport.height / camera.zoom)
7984
+ };
7985
+ }
7986
+ function cameraForView(view, viewport) {
7987
+ const zoom = clampZoom(Math.min(viewport.width / view.width, viewport.height / view.height));
7988
+ return { zoom, scrollX: viewport.width / (2 * zoom) - (view.x + view.width / 2), scrollY: viewport.height / (2 * zoom) - (view.y + view.height / 2) };
7989
+ }
7990
+ function zoomAroundCentre(camera, viewport, zoom) {
7991
+ const next = clampZoom(zoom);
7992
+ const centreX = viewport.width / 2 / camera.zoom - camera.scrollX;
7993
+ const centreY = viewport.height / 2 / camera.zoom - camera.scrollY;
7994
+ return { zoom: next, scrollX: viewport.width / 2 / next - centreX, scrollY: viewport.height / 2 / next - centreY };
7995
+ }
7996
+ function sameCamera(left, right) {
7997
+ return Math.abs(left.zoom - right.zoom) < 1e-4 && Math.abs(left.scrollX - right.scrollX) < 0.5 && Math.abs(left.scrollY - right.scrollY) < 0.5;
7998
+ }
7999
+ function mixCamera(from, to, progress) {
8000
+ const zoom = Math.exp(Math.log(from.zoom) + (Math.log(to.zoom) - Math.log(from.zoom)) * progress);
8001
+ return { zoom, scrollX: from.scrollX + (to.scrollX - from.scrollX) * progress, scrollY: from.scrollY + (to.scrollY - from.scrollY) * progress };
8002
+ }
8003
+ function springStep(offset, velocity, rate, seconds) {
8004
+ const decay = Math.exp(-rate * seconds);
8005
+ const drift = (velocity + rate * offset) * seconds;
8006
+ return [(offset + drift) * decay, (velocity - rate * drift) * decay];
8007
+ }
8008
+ function readStored(key, allowed, fallback3) {
8009
+ try {
8010
+ const value = globalThis.localStorage?.getItem(key);
8011
+ return allowed.find((entry) => entry.id === value)?.id ?? fallback3;
8012
+ } catch {
8013
+ return fallback3;
8014
+ }
8015
+ }
8016
+ function writeStored(key, value) {
8017
+ try {
8018
+ globalThis.localStorage?.setItem(key, value);
8019
+ } catch {
8020
+ }
8021
+ }
8022
+
8023
+ // src/components/board/board-engine.ts
8024
+ var TEXT_FONT = 6;
8025
+ var NOTE_FONT_SIZE = 18;
8026
+ var BOUND_TEXT_PADDING = 5;
8027
+ function engineTool(tool) {
8028
+ if (tool === "pen" || tool === "highlighter") return { type: "freedraw", locked: true };
8029
+ if (tool === "note" || tool === "selection") return { type: "selection" };
8030
+ return { type: tool, locked: true };
8031
+ }
8032
+ function penStyle(tool, pen) {
8033
+ if (tool === "highlighter") return highlighterStyle(pen.color, pen.width);
8034
+ return { strokeColor: penHex(pen.color, pen.surface, pen.theme), strokeWidth: widthUnits(pen.width), opacity: 100 };
8035
+ }
8036
+ function applyTool(engine, tool, pen) {
8037
+ const style = penStyle(tool, pen);
8038
+ engine.api.updateScene({
8039
+ appState: {
8040
+ currentItemStrokeColor: style.strokeColor,
8041
+ currentItemStrokeWidth: engineStrokeWidth(style.strokeWidth, tool === "pen" || tool === "highlighter"),
8042
+ currentItemOpacity: style.opacity,
8043
+ currentItemRoughness: 0,
8044
+ currentItemFontFamily: TEXT_FONT,
8045
+ currentItemBackgroundColor: "transparent"
8046
+ }
8047
+ });
8048
+ engine.api.setActiveTool(engineTool(tool));
8049
+ }
8050
+ function selectionStyle(element, pen, changes) {
8051
+ if (element.type === "rectangle" && element.customData?.chalkNote) {
8052
+ if (!changes.color) return {};
8053
+ return { strokeColor: NOTE_FILL[changes.color], backgroundColor: NOTE_FILL[changes.color] };
8054
+ }
8055
+ const style = penStyle(element.type === "freedraw" && element.opacity === 40 ? "highlighter" : "pen", pen);
8056
+ return {
8057
+ ...changes.color ? { strokeColor: style.strokeColor } : {},
8058
+ ...changes.width ? { strokeWidth: engineStrokeWidth(style.strokeWidth, element.type === "freedraw") } : {}
8059
+ };
8060
+ }
8061
+ function styleChanged(element, updates) {
8062
+ return updates.strokeColor !== void 0 && updates.strokeColor !== element.strokeColor || updates.strokeWidth !== void 0 && updates.strokeWidth !== element.strokeWidth || updates.backgroundColor !== void 0 && updates.backgroundColor !== element.backgroundColor;
8063
+ }
8064
+ function restyleSelection(engine, pen, changes) {
8065
+ const { api, excalidraw } = engine;
8066
+ const selected = api.getAppState().selectedElementIds;
8067
+ let changed = false;
8068
+ const elements = api.getSceneElementsIncludingDeleted().map((element) => {
8069
+ if (!selected[element.id] || element.isDeleted) return element;
8070
+ const updates = selectionStyle(element, pen, changes);
8071
+ if (!styleChanged(element, updates)) return element;
8072
+ changed = true;
8073
+ return excalidraw.newElementWith(element, updates);
8074
+ });
8075
+ if (changed) api.updateScene({ elements, captureUpdate: excalidraw.CaptureUpdateAction.IMMEDIATELY });
8076
+ return changed;
8077
+ }
8078
+ function placeNote(engine, container, point, color) {
8079
+ const { api, excalidraw } = engine;
8080
+ const fill = NOTE_FILL[color];
8081
+ const [note] = excalidraw.convertToExcalidrawElements([
8082
+ { type: "rectangle", x: point.x - NOTE_SIZE.width / 2, y: point.y - NOTE_SIZE.height / 2, width: NOTE_SIZE.width, height: NOTE_SIZE.height, backgroundColor: fill, strokeColor: fill, fillStyle: "solid", strokeWidth: 1, roughness: 0, roundness: NOTE_ROUNDNESS, customData: { chalkNote: true } }
8083
+ ]);
8084
+ if (!note) return;
8085
+ api.updateScene({
8086
+ elements: [...api.getSceneElementsIncludingDeleted(), note],
8087
+ appState: { selectedElementIds: { [note.id]: true }, currentItemStrokeColor: NOTE_TEXT, currentItemFontSize: NOTE_FONT_SIZE, currentItemTextAlign: "left", currentItemFontFamily: TEXT_FONT },
8088
+ captureUpdate: excalidraw.CaptureUpdateAction.IMMEDIATELY
8089
+ });
8090
+ api.setActiveTool({ type: "selection" });
8091
+ requestAnimationFrame(() => container.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", bubbles: true })));
8092
+ }
8093
+ function settleNoteText(engine) {
8094
+ const { api, excalidraw } = engine;
8095
+ const elements = api.getSceneElementsIncludingDeleted();
8096
+ const notes = new Map(elements.filter((element) => element.type === "rectangle" && element.customData?.chalkNote).map((element) => [element.id, element]));
8097
+ let changed = false;
8098
+ const next = elements.map((element) => {
8099
+ if (element.type !== "text" || !element.containerId || element.isDeleted) return element;
8100
+ const note = notes.get(element.containerId);
8101
+ if (!note) return element;
8102
+ const x = note.x + BOUND_TEXT_PADDING;
8103
+ const y = note.y + BOUND_TEXT_PADDING;
8104
+ if (element.verticalAlign === "top" && element.textAlign === "left" && element.strokeColor === NOTE_TEXT && element.x === x && element.y === y) return element;
8105
+ changed = true;
8106
+ return excalidraw.newElementWith(element, { verticalAlign: "top", textAlign: "left", strokeColor: NOTE_TEXT, x, y });
8107
+ });
8108
+ if (changed) api.updateScene({ elements: next, captureUpdate: excalidraw.CaptureUpdateAction.NEVER });
8109
+ }
8110
+ var isApple = () => typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform);
8111
+ function pressHistory(container, redo) {
8112
+ const apple = isApple();
8113
+ container.dispatchEvent(new KeyboardEvent("keydown", { key: redo ? "Z" : "z", code: "KeyZ", shiftKey: redo, metaKey: apple, ctrlKey: !apple, bubbles: true }));
8114
+ }
8115
+ function readCamera(engine) {
8116
+ const state = engine.api.getAppState();
8117
+ return { scrollX: state.scrollX, scrollY: state.scrollY, zoom: state.zoom.value };
8118
+ }
8119
+ function setCamera(engine, camera) {
8120
+ engine.api.updateScene({ appState: { scrollX: camera.scrollX, scrollY: camera.scrollY, zoom: { value: camera.zoom } } });
8121
+ }
8122
+ function hasContent(engine) {
8123
+ return engine.api.getSceneElements().length > 0;
8124
+ }
8125
+ async function boardImage(engine, background) {
8126
+ const { api, excalidraw } = engine;
8127
+ return excalidraw.exportToBlob({ elements: api.getSceneElements(), files: api.getFiles(), appState: { exportBackground: true, viewBackgroundColor: background }, mimeType: "image/png", exportPadding: 24 });
8128
+ }
8129
+ function sceneSignature(engine) {
8130
+ return engine.excalidraw.getSceneVersion(engine.api.getSceneElementsIncludingDeleted());
8131
+ }
8132
+
8133
+ // src/components/board/BoardCursors.tsx
8134
+ import { useEffect as useEffect36, useRef as useRef31, useState as useState41 } from "react";
8135
+
8136
+ // src/components/board/BoardIcon.tsx
8137
+ import { Fragment as Fragment28, jsx as jsx69, jsxs as jsxs44 } from "react/jsx-runtime";
8138
+ var GLYPHS2 = {
8139
+ select: /* @__PURE__ */ jsx69("path", { d: "M5 3l14 7-6.5 2L10 19z" }),
8140
+ hand: /* @__PURE__ */ jsx69("path", { d: "M18 11V6a2 2 0 0 0-4 0M14 10V4a2 2 0 0 0-4 0v2M10 10.5V6a2 2 0 0 0-4 0v8M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.9-6-2.3l-3.6-3.6a2 2 0 0 1 2.8-2.8L7 15" }),
8141
+ pen: /* @__PURE__ */ jsx69("path", { d: "M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z" }),
8142
+ highlighter: /* @__PURE__ */ jsxs44(Fragment28, { children: [
8143
+ /* @__PURE__ */ jsx69("path", { d: "M9 11l-6 6v3h9l3-3" }),
8144
+ /* @__PURE__ */ jsx69("path", { d: "M22 12l-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4" })
8145
+ ] }),
8146
+ rectangle: /* @__PURE__ */ jsx69("rect", { x: "3", y: "5", width: "18", height: "14", rx: "2" }),
8147
+ ellipse: /* @__PURE__ */ jsx69("ellipse", { cx: "12", cy: "12", rx: "9", ry: "6.5" }),
8148
+ diamond: /* @__PURE__ */ jsx69("path", { d: "M12 3l9 9-9 9-9-9z" }),
8149
+ arrow: /* @__PURE__ */ jsx69("path", { d: "M5 19L19 5M9 5h10v10" }),
8150
+ line: /* @__PURE__ */ jsx69("path", { d: "M4 20L20 4" }),
8151
+ text: /* @__PURE__ */ jsx69("path", { d: "M5 6V4h14v2M12 4v16M9 20h6" }),
8152
+ note: /* @__PURE__ */ jsxs44(Fragment28, { children: [
8153
+ /* @__PURE__ */ jsx69("path", { d: "M5 3h14a2 2 0 0 1 2 2v9l-7 7H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z" }),
8154
+ /* @__PURE__ */ jsx69("path", { d: "M14 21v-5a2 2 0 0 1 2-2h5" })
8155
+ ] }),
8156
+ eraser: /* @__PURE__ */ jsx69("path", { d: "M7 21h10M5.5 13.5l7-7a2 2 0 0 1 2.8 0l3.2 3.2a2 2 0 0 1 0 2.8L11 19H8l-2.5-2.5a2 2 0 0 1 0-3z" }),
8157
+ laser: /* @__PURE__ */ jsxs44(Fragment28, { children: [
8158
+ /* @__PURE__ */ jsx69("circle", { cx: "12", cy: "12", r: "2.5", fill: "currentColor" }),
8159
+ /* @__PURE__ */ jsx69("path", { d: "M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M18.4 5.6l-2.1 2.1M7.7 16.3l-2.1 2.1" })
8160
+ ] }),
8161
+ undo: /* @__PURE__ */ jsxs44(Fragment28, { children: [
8162
+ /* @__PURE__ */ jsx69("path", { d: "M9 14L4 9l5-5" }),
8163
+ /* @__PURE__ */ jsx69("path", { d: "M4 9h10.5a5.5 5.5 0 0 1 0 11H11" })
8164
+ ] }),
8165
+ redo: /* @__PURE__ */ jsxs44(Fragment28, { children: [
8166
+ /* @__PURE__ */ jsx69("path", { d: "M15 14l5-5-5-5" }),
8167
+ /* @__PURE__ */ jsx69("path", { d: "M20 9H9.5a5.5 5.5 0 0 0 0 11H13" })
8168
+ ] }),
8169
+ minus: /* @__PURE__ */ jsx69("path", { d: "M5 12h14" }),
8170
+ plus: /* @__PURE__ */ jsx69("path", { d: "M12 5v14M5 12h14" }),
8171
+ fit: /* @__PURE__ */ jsx69("path", { d: "M4 9V5a1 1 0 0 1 1-1h4M20 9V5a1 1 0 0 0-1-1h-4M4 15v4a1 1 0 0 0 1 1h4M20 15v4a1 1 0 0 1-1 1h-4" }),
8172
+ download: /* @__PURE__ */ jsx69("path", { d: "M12 3v12M7 10l5 5 5-5M4 21h16" }),
8173
+ copy: /* @__PURE__ */ jsxs44(Fragment28, { children: [
8174
+ /* @__PURE__ */ jsx69("rect", { x: "9", y: "9", width: "12", height: "12", rx: "2" }),
8175
+ /* @__PURE__ */ jsx69("path", { d: "M5 15V5a2 2 0 0 1 2-2h10" })
8176
+ ] }),
8177
+ trash: /* @__PURE__ */ jsx69("path", { d: "M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3" }),
8178
+ close: /* @__PURE__ */ jsx69("path", { d: "M6 6l12 12M18 6L6 18" }),
8179
+ check: /* @__PURE__ */ jsx69("path", { d: "M5 12l5 5 9-10" }),
8180
+ target: /* @__PURE__ */ jsxs44(Fragment28, { children: [
8181
+ /* @__PURE__ */ jsx69("circle", { cx: "12", cy: "12", r: "9" }),
8182
+ /* @__PURE__ */ jsx69("circle", { cx: "12", cy: "12", r: "4" })
8183
+ ] }),
8184
+ eye: /* @__PURE__ */ jsxs44(Fragment28, { children: [
8185
+ /* @__PURE__ */ jsx69("path", { d: "M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12z" }),
8186
+ /* @__PURE__ */ jsx69("circle", { cx: "12", cy: "12", r: "3" })
8187
+ ] }),
8188
+ wifiOff: /* @__PURE__ */ jsx69("path", { d: "M2 2l20 20M8.5 16.5a5 5 0 0 1 7 0M5 12.9a10 10 0 0 1 5-2.7M10.7 5.1A16 16 0 0 1 22 9M2 8.8a15 15 0 0 1 4.2-2.5M12 20h.01" }),
8189
+ lock: /* @__PURE__ */ jsxs44(Fragment28, { children: [
8190
+ /* @__PURE__ */ jsx69("rect", { x: "5", y: "11", width: "14", height: "9", rx: "2" }),
8191
+ /* @__PURE__ */ jsx69("path", { d: "M8 11V8a4 4 0 0 1 8 0v3" })
8192
+ ] }),
8193
+ more: /* @__PURE__ */ jsxs44(Fragment28, { children: [
8194
+ /* @__PURE__ */ jsx69("circle", { cx: "5", cy: "12", r: "1.2" }),
8195
+ /* @__PURE__ */ jsx69("circle", { cx: "12", cy: "12", r: "1.2" }),
8196
+ /* @__PURE__ */ jsx69("circle", { cx: "19", cy: "12", r: "1.2" })
8197
+ ] }),
8198
+ chevron: /* @__PURE__ */ jsx69("path", { d: "M6 9l6 6 6-6" })
8199
+ };
8200
+ function BoardIcon({ name, size = 20, className }) {
8201
+ return /* @__PURE__ */ jsx69("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", className, "aria-hidden": "true", children: GLYPHS2[name] });
8202
+ }
8203
+ function GripIcon({ className }) {
8204
+ return /* @__PURE__ */ jsx69("svg", { width: "10", height: "18", viewBox: "0 0 10 18", fill: "currentColor", className, "aria-hidden": "true", children: [3, 9, 15].flatMap((y) => [/* @__PURE__ */ jsx69("circle", { cx: "2.5", cy: y, r: "1.5" }, `l${y}`), /* @__PURE__ */ jsx69("circle", { cx: "7.5", cy: y, r: "1.5" }, `r${y}`)]) });
8205
+ }
8206
+ function CursorArrow({ color }) {
8207
+ const path = "M3.2 2.6V20.2L8 15.7L11.3 22.6L14.6 21L11.4 14.2L17.9 13.6Z";
8208
+ return /* @__PURE__ */ jsxs44("svg", { className: "chalk-board-cursor-arrow", width: "22", height: "26", viewBox: "0 0 22 26", fill: "none", "aria-hidden": "true", children: [
8209
+ /* @__PURE__ */ jsx69("path", { d: path, fill: "#fff", stroke: "#fff", strokeWidth: "5", strokeLinejoin: "round" }),
8210
+ /* @__PURE__ */ jsx69("path", { d: path, fill: color, stroke: color, strokeWidth: "2.2", strokeLinejoin: "round" })
8211
+ ] });
8212
+ }
8213
+
8214
+ // src/components/board/BoardCursors.tsx
8215
+ import { jsx as jsx70, jsxs as jsxs45 } from "react/jsx-runtime";
8216
+ var REST_RATE = 16;
8217
+ var STALE_MS = 1e4;
8218
+ var NAME_HOLD_MS = 700;
8219
+ var MOVING_PX_PER_SECOND = 25;
8220
+ var HOVER_PX = 26;
8221
+ function rememberCursor(cursors, cursor) {
8222
+ const now = Date.now();
8223
+ for (const [id, previous] of cursors) if (now - Date.parse(previous.occurredAt) > STALE_MS) cursors.delete(id);
8224
+ cursors.delete(cursor.participantId);
8225
+ if (now - Date.parse(cursor.occurredAt) <= STALE_MS) cursors.set(cursor.participantId, cursor);
8226
+ if (cursors.size > 256) {
8227
+ const oldest = cursors.keys().next().value;
8228
+ if (oldest !== void 0) cursors.delete(oldest);
8229
+ }
8230
+ }
8231
+ function BoardCursors({ cursorsRef, cameraRef, colorFor, hidden }) {
8232
+ const layer = useRef31(null);
8233
+ const [people, setPeople] = useState41([]);
8234
+ const tracked = useRef31(/* @__PURE__ */ new Map());
8235
+ const pointer = useRef31(null);
8236
+ useEffect36(() => {
8237
+ let frame = 0;
8238
+ let last = performance.now();
8239
+ const step = (now) => {
8240
+ const seconds = Math.min(0.1, (now - last) / 1e3);
8241
+ last = now;
8242
+ const camera = cameraRef.current;
8243
+ const roster = cursorsRef.current;
8244
+ let rosterChanged = false;
8245
+ for (const [id, cursor] of roster) {
8246
+ if (Date.now() - Date.parse(cursor.occurredAt) > STALE_MS) {
8247
+ roster.delete(id);
8248
+ continue;
8249
+ }
8250
+ let state = tracked.current.get(id);
8251
+ if (!state) {
8252
+ state = { name: cursor.displayName, x: cursor.x, y: cursor.y, vx: 0, vy: 0, tx: cursor.x, ty: cursor.y, movingUntil: 0, lastX: cursor.x, lastY: cursor.y };
8253
+ tracked.current.set(id, state);
8254
+ rosterChanged = true;
8255
+ }
8256
+ if (state.name !== cursor.displayName) {
8257
+ state.name = cursor.displayName;
8258
+ rosterChanged = true;
8259
+ }
8260
+ if (cursor.x !== state.tx || cursor.y !== state.ty) {
8261
+ state.tx = cursor.x;
8262
+ state.ty = cursor.y;
8263
+ }
8264
+ }
8265
+ for (const [id, state] of tracked.current) {
8266
+ if (!roster.has(id)) {
8267
+ tracked.current.delete(id);
8268
+ roster.delete(id);
8269
+ rosterChanged = true;
8270
+ continue;
8271
+ }
8272
+ const [dx, vx] = springStep(state.x - state.tx, state.vx, REST_RATE, seconds);
8273
+ const [dy, vy] = springStep(state.y - state.ty, state.vy, REST_RATE, seconds);
8274
+ state.x = state.tx + dx;
8275
+ state.y = state.ty + dy;
8276
+ state.vx = vx;
8277
+ state.vy = vy;
8278
+ const screenX = (state.x + camera.scrollX) * camera.zoom;
8279
+ const screenY = (state.y + camera.scrollY) * camera.zoom;
8280
+ const speed = Math.hypot(screenX - state.lastX, screenY - state.lastY) / Math.max(seconds, 1e-3);
8281
+ state.lastX = screenX;
8282
+ state.lastY = screenY;
8283
+ if (speed > MOVING_PX_PER_SECOND) state.movingUntil = now + NAME_HOLD_MS;
8284
+ const element = layer.current?.querySelector(`[data-cursor="${CSS.escape(id)}"]`);
8285
+ if (!element) continue;
8286
+ element.style.transform = `translate3d(${screenX}px, ${screenY}px, 0)`;
8287
+ const near = pointer.current !== null && Math.hypot(pointer.current.x - screenX, pointer.current.y - screenY) < HOVER_PX;
8288
+ element.dataset.named = String(near || now < state.movingUntil);
8289
+ }
8290
+ if (rosterChanged) setPeople([...roster.values()].filter((cursor) => tracked.current.has(cursor.participantId)).map((cursor) => ({ id: cursor.participantId, name: cursor.displayName })));
8291
+ frame = requestAnimationFrame(step);
8292
+ };
8293
+ frame = requestAnimationFrame(step);
8294
+ return () => cancelAnimationFrame(frame);
8295
+ }, [cameraRef, cursorsRef]);
8296
+ useEffect36(() => {
8297
+ const board = layer.current?.parentElement;
8298
+ if (!board) return;
8299
+ const move = (event) => {
8300
+ const rect = board.getBoundingClientRect();
8301
+ pointer.current = { x: event.clientX - rect.left, y: event.clientY - rect.top };
8302
+ };
8303
+ const leave = () => pointer.current = null;
8304
+ board.addEventListener("pointermove", move);
8305
+ board.addEventListener("pointerleave", leave);
8306
+ return () => {
8307
+ board.removeEventListener("pointermove", move);
8308
+ board.removeEventListener("pointerleave", leave);
8309
+ };
8310
+ }, []);
8311
+ return /* @__PURE__ */ jsx70("div", { ref: layer, className: "chalk-board-cursors", "aria-hidden": "true", "data-hidden": hidden || void 0, children: people.map((person) => /* @__PURE__ */ jsxs45("div", { className: "chalk-board-cursor", "data-cursor": person.id, children: [
8312
+ /* @__PURE__ */ jsx70(CursorArrow, { color: colorFor(person.id) }),
8313
+ /* @__PURE__ */ jsxs45("span", { className: "chalk-board-cursor-tag", children: [
8314
+ /* @__PURE__ */ jsx70("span", { className: "chalk-board-cursor-name", style: { background: colorFor(person.id) }, children: person.name.split(" ")[0] }),
8315
+ /* @__PURE__ */ jsx70(Avatar, { name: person.name, size: "xs", animated: false, className: "chalk-board-cursor-avatar" })
8316
+ ] })
8317
+ ] }, person.id)) });
8318
+ }
8319
+
8320
+ // src/components/board/BoardIsland.tsx
8321
+ import { useLayoutEffect as useLayoutEffect4, useRef as useRef32, useState as useState42 } from "react";
8322
+ import { Fragment as Fragment29, jsx as jsx71, jsxs as jsxs46 } from "react/jsx-runtime";
8323
+ var TOOLS = [
8324
+ [
8325
+ { tool: "selection", label: "Select", key: "V", glyph: "select" },
8326
+ { tool: "hand", label: "Hand", key: "H", glyph: "hand" }
8327
+ ],
8328
+ [
8329
+ { tool: "pen", label: "Pen", key: "P", glyph: "pen" },
8330
+ { tool: "highlighter", label: "Highlighter", key: "U", glyph: "highlighter" }
8331
+ ],
8332
+ [
8333
+ { tool: "text", label: "Text", key: "T", glyph: "text" },
8334
+ { tool: "note", label: "Sticky note", key: "N", glyph: "note" }
8335
+ ],
8336
+ [
8337
+ { tool: "eraser", label: "Eraser", key: "E", glyph: "eraser" },
8338
+ { tool: "laser", label: "Laser pointer", key: "K", glyph: "laser" }
8339
+ ]
8340
+ ];
8341
+ var LONG_PRESS_MS = 500;
8342
+ var DRAG_START_PX = 4;
8343
+ function Tip({ label, keyName, children }) {
8344
+ return /* @__PURE__ */ jsxs46(ChalkTooltip, { trigger: children, children: [
8345
+ label,
8346
+ keyName ? /* @__PURE__ */ jsx71("kbd", { className: "chalk-board-kbd", children: keyName }) : null
8347
+ ] });
8348
+ }
8349
+ function ToolButton({ entry, active, onTool }) {
8350
+ return /* @__PURE__ */ jsx71(Tip, { label: entry.label, keyName: entry.key, children: /* @__PURE__ */ jsx71("button", { type: "button", className: "chalk-board-tool", "aria-label": entry.label, "aria-keyshortcuts": entry.key, "aria-pressed": active, onClick: () => onTool(entry.tool), children: /* @__PURE__ */ jsx71(BoardIcon, { name: entry.glyph }) }) });
8351
+ }
8352
+ function ShapesButton({ tool, shape, onTool, side }) {
8353
+ const active = SHAPES.some((entry) => entry.id === tool);
8354
+ return /* @__PURE__ */ jsx71(
8355
+ ChalkMenu,
8356
+ {
8357
+ side,
8358
+ trigger: /* @__PURE__ */ jsxs46("button", { type: "button", className: "chalk-board-tool", "aria-label": "Shapes", "aria-keyshortcuts": "R", "aria-pressed": active, children: [
8359
+ /* @__PURE__ */ jsx71(BoardIcon, { name: shape }),
8360
+ /* @__PURE__ */ jsx71(BoardIcon, { name: "chevron", size: 9, className: "chalk-board-tool-caret" })
8361
+ ] }),
8362
+ children: SHAPES.map((entry) => /* @__PURE__ */ jsxs46(ChalkMenuItem, { checked: tool === entry.id, onClick: () => onTool(entry.id), children: [
8363
+ /* @__PURE__ */ jsx71(BoardIcon, { name: entry.id, size: 18 }),
8364
+ entry.label,
8365
+ /* @__PURE__ */ jsx71("kbd", { className: "chalk-board-menu-kbd", children: entry.key })
8366
+ ] }, entry.id))
8367
+ }
8368
+ );
8369
+ }
8370
+ function ColorPanel({ color, width, surface, theme, onColor, onWidth }) {
8371
+ return /* @__PURE__ */ jsxs46("div", { className: "chalk-board-colors", children: [
8372
+ /* @__PURE__ */ jsx71("div", { role: "radiogroup", "aria-label": "Colour", className: "chalk-board-swatches", children: PEN_COLORS.map((entry) => /* @__PURE__ */ jsx71("button", { type: "button", role: "radio", "aria-checked": color === entry.id, "aria-label": entry.label, className: "chalk-board-swatch", onClick: () => onColor(entry.id), children: /* @__PURE__ */ jsx71("i", { style: { background: penHex(entry.id, surface, theme) } }) }, entry.id)) }),
8373
+ /* @__PURE__ */ jsx71("div", { role: "radiogroup", "aria-label": "Stroke width", className: "chalk-board-swatches", children: PEN_WIDTHS.map((entry) => /* @__PURE__ */ jsx71("button", { type: "button", role: "radio", "aria-checked": width === entry.id, "aria-label": entry.label, className: "chalk-board-width", onClick: () => onWidth(entry.id), children: /* @__PURE__ */ jsx71("i", { style: { height: entry.units } }) }, entry.id)) })
8374
+ ] });
8375
+ }
8376
+ function ColorButton(props) {
8377
+ const swatch = penHex(props.color, props.surface, props.theme);
8378
+ return /* @__PURE__ */ jsx71(
8379
+ ChalkPopover,
8380
+ {
8381
+ label: "Colour and width",
8382
+ tooltip: "Colour and width",
8383
+ open: props.open,
8384
+ onOpenChange: props.onOpenChange,
8385
+ className: "chalk-board-color-popover",
8386
+ trigger: props.trigger ?? /* @__PURE__ */ jsx71("button", { type: "button", className: "chalk-board-tool", "aria-label": `Colour: ${PEN_COLORS.find((entry) => entry.id === props.color).label}`, children: /* @__PURE__ */ jsx71("span", { className: "chalk-board-tool-swatch", style: { background: swatch } }) }),
8387
+ children: /* @__PURE__ */ jsx71(ColorPanel, { ...props })
8388
+ }
8389
+ );
8390
+ }
8391
+ function roveFocus(event, vertical) {
8392
+ const back = vertical ? "ArrowUp" : "ArrowLeft";
8393
+ const forward = vertical ? "ArrowDown" : "ArrowRight";
8394
+ if (event.key !== back && event.key !== forward && event.key !== "Home" && event.key !== "End") return;
8395
+ const buttons = [...event.currentTarget.querySelectorAll("button:not(:disabled)")];
8396
+ const index = buttons.findIndex((button) => button === document.activeElement);
8397
+ if (index < 0) return;
8398
+ event.preventDefault();
8399
+ event.stopPropagation();
8400
+ const next = event.key === "Home" ? 0 : event.key === "End" ? buttons.length - 1 : (index + (event.key === forward ? 1 : -1) + buttons.length) % buttons.length;
8401
+ buttons[next]?.focus();
8402
+ }
8403
+ function Separator() {
8404
+ return /* @__PURE__ */ jsx71("span", { className: "chalk-board-separator", "aria-hidden": "true" });
8405
+ }
8406
+ function ToolRow({ tool, shape, onTool, place, canUndo, canRedo, onUndo, onRedo, ...colors }) {
8407
+ const groups = TOOLS.map((group, index) => /* @__PURE__ */ jsxs46("span", { className: "chalk-board-group", children: [
8408
+ group.map((entry) => /* @__PURE__ */ jsx71(ToolButton, { entry, active: tool === entry.tool, onTool }, entry.tool)),
8409
+ index === 1 ? /* @__PURE__ */ jsx71(ShapesButton, { tool, shape, onTool, side: place === "top" ? "bottom" : "top" }) : null
8410
+ ] }, index));
8411
+ return /* @__PURE__ */ jsxs46(Fragment29, { children: [
8412
+ groups.flatMap((group, index) => [index > 0 ? /* @__PURE__ */ jsx71(Separator, {}, `s${index}`) : null, group]),
8413
+ /* @__PURE__ */ jsx71(Separator, {}),
8414
+ /* @__PURE__ */ jsx71(Tip, { label: "Undo", keyName: "\u2318Z", children: /* @__PURE__ */ jsx71("button", { type: "button", className: "chalk-board-tool", "aria-label": "Undo", disabled: !canUndo, onClick: onUndo, children: /* @__PURE__ */ jsx71(BoardIcon, { name: "undo" }) }) }),
8415
+ /* @__PURE__ */ jsx71(Tip, { label: "Redo", keyName: "\u21E7\u2318Z", children: /* @__PURE__ */ jsx71("button", { type: "button", className: "chalk-board-tool", "aria-label": "Redo", disabled: !canRedo, onClick: onRedo, children: /* @__PURE__ */ jsx71(BoardIcon, { name: "redo" }) }) }),
8416
+ /* @__PURE__ */ jsx71(Separator, {}),
8417
+ /* @__PURE__ */ jsx71(ColorButton, { ...colors })
8418
+ ] });
8419
+ }
8420
+ function nearestPlace(board, x, y) {
8421
+ const distances = { left: x - board.left, bottom: board.bottom - y, top: y - board.top };
8422
+ return ISLAND_PLACES.map((entry) => entry.id).reduce((best, place) => distances[place] < distances[best] ? place : best);
8423
+ }
8424
+ function BoardIsland(props) {
8425
+ const { place, onPlace, compact } = props;
8426
+ const island = useRef32(null);
8427
+ const tabStop = useRef32(null);
8428
+ const reduced = usePrefersReducedMotion();
8429
+ const [drag, setDrag] = useState42(null);
8430
+ const dropRect = useRef32(null);
8431
+ const [expanded, setExpanded] = useState42(false);
8432
+ const [colorsOpen, setColorsOpen] = useState42(false);
8433
+ const press = useRef32({ long: false });
8434
+ useLayoutEffect4(() => {
8435
+ const buttons = [...island.current?.querySelectorAll("button") ?? []];
8436
+ const stop = buttons.find((button) => button === tabStop.current && !button.disabled) ?? buttons.find((button) => !button.disabled);
8437
+ tabStop.current = stop ?? null;
8438
+ for (const button of buttons) button.tabIndex = button === stop ? 0 : -1;
8439
+ });
8440
+ useLayoutEffect4(() => {
8441
+ const element = island.current;
8442
+ const from = dropRect.current;
8443
+ dropRect.current = null;
8444
+ if (!element || !from || reduced) return;
8445
+ const to = element.getBoundingClientRect();
8446
+ element.animate(
8447
+ [
8448
+ { transform: `translate(${from.left - to.left}px, ${from.top - to.top}px)`, opacity: 0.85 },
8449
+ { transform: "none", opacity: 1 }
8450
+ ],
8451
+ { duration: 300, easing: "cubic-bezier(0.16, 1, 0.3, 1)" }
8452
+ );
8453
+ }, [place, reduced]);
8454
+ const startDrag = (event) => {
8455
+ const board = island.current?.parentElement;
8456
+ if (!board || event.button !== 0) return;
8457
+ const start = { x: event.clientX, y: event.clientY };
8458
+ let moved = false;
8459
+ const grip = event.currentTarget;
8460
+ grip.setPointerCapture(event.pointerId);
8461
+ const move = (next) => {
8462
+ const dx = next.clientX - start.x;
8463
+ const dy = next.clientY - start.y;
8464
+ if (!moved && Math.hypot(dx, dy) < DRAG_START_PX) return;
8465
+ moved = true;
8466
+ setDrag({ x: dx, y: dy, near: nearestPlace(board.getBoundingClientRect(), next.clientX, next.clientY) });
8467
+ };
8468
+ const end = (next) => {
8469
+ grip.removeEventListener("pointermove", move);
8470
+ grip.removeEventListener("pointerup", end);
8471
+ grip.removeEventListener("pointercancel", end);
8472
+ if (moved && next.type === "pointerup") {
8473
+ dropRect.current = island.current?.getBoundingClientRect() ?? null;
8474
+ onPlace(nearestPlace(board.getBoundingClientRect(), next.clientX, next.clientY), true);
8475
+ }
8476
+ setDrag(null);
8477
+ };
8478
+ grip.addEventListener("pointermove", move);
8479
+ grip.addEventListener("pointerup", end);
8480
+ grip.addEventListener("pointercancel", end);
8481
+ };
8482
+ const gripKeys = (event) => {
8483
+ if (!event.altKey) return;
8484
+ const target = { ArrowLeft: "left", ArrowDown: "bottom", ArrowUp: "top" };
8485
+ const next = target[event.key];
8486
+ if (!next) return;
8487
+ event.preventDefault();
8488
+ event.stopPropagation();
8489
+ if (next !== place) {
8490
+ dropRect.current = island.current?.getBoundingClientRect() ?? null;
8491
+ onPlace(next, true);
8492
+ }
8493
+ };
8494
+ if (compact && !expanded) {
8495
+ const cancelPress = () => clearTimeout(press.current.timer);
8496
+ return /* @__PURE__ */ jsx71("div", { ref: island, className: "chalk-board-island", "data-place": "compact", children: /* @__PURE__ */ jsx71(
8497
+ ColorButton,
8498
+ {
8499
+ ...props,
8500
+ open: colorsOpen,
8501
+ onOpenChange: (open) => {
8502
+ if (!open) setColorsOpen(false);
8503
+ },
8504
+ trigger: /* @__PURE__ */ jsxs46(
8505
+ "button",
8506
+ {
8507
+ type: "button",
8508
+ className: "chalk-board-tool",
8509
+ "aria-label": "Drawing tools",
8510
+ "aria-expanded": false,
8511
+ onPointerDown: () => {
8512
+ press.current.long = false;
8513
+ press.current.timer = setTimeout(() => {
8514
+ press.current.long = true;
8515
+ setColorsOpen(true);
8516
+ }, LONG_PRESS_MS);
8517
+ },
8518
+ onPointerUp: cancelPress,
8519
+ onPointerLeave: cancelPress,
8520
+ onContextMenu: (event) => event.preventDefault(),
8521
+ onClick: () => {
8522
+ if (!press.current.long) setExpanded(true);
8523
+ },
8524
+ children: [
8525
+ /* @__PURE__ */ jsx71(BoardIcon, { name: "pen" }),
8526
+ /* @__PURE__ */ jsx71("span", { className: "chalk-board-tool-dot", style: { background: penHex(props.color, props.surface, props.theme) } })
8527
+ ]
8528
+ }
8529
+ )
8530
+ }
8531
+ ) });
8532
+ }
8533
+ const vertical = place === "left" && !compact;
8534
+ return /* @__PURE__ */ jsxs46(Fragment29, { children: [
8535
+ drag ? ISLAND_PLACES.map((entry) => /* @__PURE__ */ jsx71("div", { className: "chalk-board-dropzone", "data-zone": entry.id, "data-near": drag.near === entry.id || void 0, "aria-hidden": "true" }, entry.id)) : null,
8536
+ /* @__PURE__ */ jsxs46(
8537
+ "div",
8538
+ {
8539
+ ref: island,
8540
+ role: "toolbar",
8541
+ "aria-label": "Drawing tools",
8542
+ "aria-orientation": vertical ? "vertical" : "horizontal",
8543
+ className: "chalk-board-island",
8544
+ "data-place": compact ? "expanded" : place,
8545
+ "data-dragging": drag ? true : void 0,
8546
+ style: drag ? { transform: `translate(${drag.x}px, ${drag.y}px)` } : void 0,
8547
+ onKeyDownCapture: (event) => {
8548
+ if (!event.altKey) roveFocus(event, vertical);
8549
+ },
8550
+ onFocusCapture: (event) => {
8551
+ if (!(event.target instanceof HTMLButtonElement) || !event.currentTarget.contains(event.target)) return;
8552
+ tabStop.current = event.target;
8553
+ for (const button of event.currentTarget.querySelectorAll("button")) button.tabIndex = button === event.target ? 0 : -1;
8554
+ },
8555
+ children: [
8556
+ compact ? /* @__PURE__ */ jsxs46(Fragment29, { children: [
8557
+ /* @__PURE__ */ jsx71("button", { type: "button", className: "chalk-board-tool", "aria-label": "Close drawing tools", onClick: () => setExpanded(false), children: /* @__PURE__ */ jsx71(BoardIcon, { name: "close" }) }),
8558
+ /* @__PURE__ */ jsx71(Separator, {})
8559
+ ] }) : /* @__PURE__ */ jsx71(Tip, { label: "Move the tools", children: /* @__PURE__ */ jsx71("button", { type: "button", className: "chalk-board-grip", "aria-label": "Move the tools. Use Alt and the arrow keys to dock them left, bottom or top.", onPointerDown: startDrag, onKeyDown: gripKeys, children: /* @__PURE__ */ jsx71(GripIcon, {}) }) }),
8560
+ /* @__PURE__ */ jsx71(ToolRow, { ...props, place: compact ? "bottom" : place })
8561
+ ]
8562
+ }
8563
+ )
8564
+ ] });
8565
+ }
8566
+
8567
+ // src/components/board/BoardMenu.tsx
8568
+ import { Menu as Menu5 } from "@base-ui/react/menu";
8569
+ import { Fragment as Fragment30, jsx as jsx72, jsxs as jsxs47 } from "react/jsx-runtime";
8570
+ function SurfacePreview({ surface }) {
8571
+ return /* @__PURE__ */ jsx72("span", { className: "chalk-board-tile-preview", "data-surface": surface, "aria-hidden": "true", children: surface === "dots" ? null : /* @__PURE__ */ jsx72("svg", { viewBox: "0 0 60 38", width: "100%", height: "100%", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: /* @__PURE__ */ jsx72("path", { d: "M14 22c5-8 9-8 12 0s8 6 12-2 6-6 8 0" }) }) });
8572
+ }
8573
+ function IslandPreview({ place }) {
8574
+ return /* @__PURE__ */ jsx72("span", { className: "chalk-board-tile-preview", "data-island": place, "aria-hidden": "true", children: /* @__PURE__ */ jsx72("i", {}) });
8575
+ }
8576
+ function Tiles({ label, value, options, onChange, preview }) {
8577
+ return /* @__PURE__ */ jsxs47(Fragment30, { children: [
8578
+ /* @__PURE__ */ jsx72("div", { className: "chalk-board-menu-sublabel", children: label }),
8579
+ /* @__PURE__ */ jsx72(Menu5.RadioGroup, { value, onValueChange: (next) => onChange(next), className: "chalk-board-tiles", "aria-label": label, children: options.map((option) => /* @__PURE__ */ jsxs47(Menu5.RadioItem, { value: option.id, closeOnClick: false, className: "chalk-board-tile", children: [
8580
+ preview(option.id),
8581
+ option.label
8582
+ ] }, option.id)) })
8583
+ ] });
8584
+ }
8585
+ function BoardMenu({ surface, onSurface, place, onPlace, onDownload, onCopy, onBring, onClear, onClose }) {
8586
+ return /* @__PURE__ */ jsxs47(
8587
+ ChalkMenu,
8588
+ {
8589
+ className: "chalk-board-menu",
8590
+ trigger: /* @__PURE__ */ jsx72("button", { type: "button", className: "chalk-board-menu-button", "aria-label": "Board menu", children: /* @__PURE__ */ jsx72(BoardIcon, { name: "more" }) }),
8591
+ children: [
8592
+ /* @__PURE__ */ jsxs47(ChalkMenuItem, { onClick: onDownload, children: [
8593
+ /* @__PURE__ */ jsx72(BoardIcon, { name: "download", size: 18 }),
8594
+ "Download as PNG"
8595
+ ] }),
8596
+ /* @__PURE__ */ jsxs47(ChalkMenuItem, { onClick: onCopy, children: [
8597
+ /* @__PURE__ */ jsx72(BoardIcon, { name: "copy", size: 18 }),
8598
+ "Copy as image"
8599
+ ] }),
8600
+ /* @__PURE__ */ jsx72(Menu5.Separator, { className: "chalk-board-menu-rule" }),
8601
+ /* @__PURE__ */ jsx72(ChalkMenuLabel, { children: "Board appearance" }),
8602
+ /* @__PURE__ */ jsx72(Tiles, { label: "Surface", value: surface, options: SURFACES, onChange: onSurface, preview: (id) => /* @__PURE__ */ jsx72(SurfacePreview, { surface: id }) }),
8603
+ place ? /* @__PURE__ */ jsx72(Tiles, { label: "Tool island", value: place, options: ISLAND_PLACES, onChange: onPlace, preview: (id) => /* @__PURE__ */ jsx72(IslandPreview, { place: id }) }) : null,
8604
+ onBring || onClear ? /* @__PURE__ */ jsx72(Menu5.Separator, { className: "chalk-board-menu-rule" }) : null,
8605
+ onBring ? /* @__PURE__ */ jsxs47(ChalkMenuItem, { onClick: onBring, children: [
8606
+ /* @__PURE__ */ jsx72(BoardIcon, { name: "target", size: 18 }),
8607
+ "Bring everyone here"
8608
+ ] }) : null,
8609
+ onClear ? /* @__PURE__ */ jsx72(ChalkMenuItem, { onClick: onClear, children: /* @__PURE__ */ jsxs47("span", { className: "chalk-board-danger", children: [
8610
+ /* @__PURE__ */ jsx72(BoardIcon, { name: "trash", size: 18 }),
8611
+ "Clear board"
8612
+ ] }) }) : null,
8613
+ onClose ? /* @__PURE__ */ jsxs47(Fragment30, { children: [
8614
+ /* @__PURE__ */ jsx72(Menu5.Separator, { className: "chalk-board-menu-rule" }),
8615
+ /* @__PURE__ */ jsxs47(ChalkMenuItem, { onClick: onClose, children: [
8616
+ /* @__PURE__ */ jsx72(BoardIcon, { name: "close", size: 18 }),
8617
+ "Close board"
8618
+ ] })
8619
+ ] }) : null
8620
+ ]
8621
+ }
8622
+ );
8623
+ }
8624
+
8625
+ // src/components/board/BoardStyles.tsx
8626
+ import { jsx as jsx73 } from "react/jsx-runtime";
8627
+ function BoardStyles() {
8628
+ return /* @__PURE__ */ jsx73("style", { href: "chalk-board", precedence: "default", children: BOARD_CSS });
8629
+ }
8630
+ var BOARD_CSS = `.chalk-board {
8631
+ --board-shadow: 0 0 0 1px var(--chalk-line-strong), 0 8px 24px rgba(0,0,0,.35);
8632
+ --board-dot-gap: 24px;
8633
+ --board-dot-x: 0px;
8634
+ --board-dot-y: 0px;
8635
+ position: relative;
8636
+ height: 100%;
8637
+ overflow: clip;
8638
+ border-radius: inherit;
8639
+ background-color: var(--board-fill);
8640
+ color: var(--board-ink);
8641
+ font-family: var(--chalk-font-family, Figtree, system-ui, sans-serif);
8642
+ isolation: isolate;
8643
+ }
8644
+ .chalk-board[hidden] {display:none;}
8645
+ .chalk-board[data-theme="light"] {--board-shadow: 0 0 0 1px var(--chalk-line-strong), 0 8px 24px rgba(0,0,0,.12);}
8646
+ .chalk-board[data-surface="chalkboard"] {
8647
+ --board-fill: #1c2622;
8648
+ --board-ink: #f1f0ea;
8649
+ --board-muted: #aeb9b4;
8650
+ background-image: radial-gradient(120% 90% at 50% 35%, rgba(255,255,255,.05), transparent 70%), url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 .05 0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
8651
+ }
8652
+ .chalk-board[data-surface="paper"] {--board-fill: #eae6db; --board-ink: #1b1c1f; --board-muted: #5d5a52;}
8653
+ .chalk-board[data-surface="paper"][data-theme="light"] {--board-fill: #f5f2ea;}
8654
+ .chalk-board[data-surface="dots"] {
8655
+ --board-fill: var(--chalk-surface);
8656
+ --board-ink: var(--chalk-text);
8657
+ --board-muted: var(--chalk-muted-text);
8658
+ --board-dot: rgba(255,255,255,.11);
8659
+ background-image: radial-gradient(var(--board-dot) 1.3px, transparent 1.7px);
8660
+ background-size: var(--board-dot-gap) var(--board-dot-gap);
8661
+ background-position: var(--board-dot-x) var(--board-dot-y);
8662
+ }
8663
+ .chalk-board[data-surface="dots"][data-theme="light"] {--board-dot: rgba(0,0,0,.16);}
8664
+
8665
+ /* Excalidraw draws; none of its own interface shows. */
8666
+ .chalk-board-engine {position:absolute; inset:0; transition:opacity .2s ease-out;}
8667
+ .chalk-board:not([data-ready]) .chalk-board-engine:not(:has([role="alert"])) {opacity:0; pointer-events:none;}
8668
+ .chalk-board-engine > [role="alert"] {background:var(--board-fill); color:var(--board-ink);}
8669
+ .chalk-board:has(.chalk-board-engine > [role="alert"]) .chalk-board-loading {display:none;}
8670
+ .chalk-board .excalidraw {--color-selection: var(--board-selection); background: transparent;}
8671
+ .chalk-board .excalidraw :is(.layer-ui__wrapper, .context-menu, .HintViewer, .Modal, .welcome-screen-center, .library-menu, .sidebar, .App-toolbar, .App-bottom-bar, .FixedSideContainer, .popover, .scroll-back-to-content, .excalidraw-tooltip, .ToolIcon__hidden, .App-menu, .layer-ui__wrapper__footer) {display:none !important;}
8672
+ body:has(.chalk-board) :is(.excalidraw-modal-container, .excalidraw-tooltip) {display:none !important;}
8673
+ .chalk-board .excalidraw-wysiwyg {font-family: Nunito, var(--chalk-font-family, system-ui), sans-serif;}
8674
+
8675
+ .chalk-board-loading {position:absolute; inset:0; display:grid; place-content:center; justify-items:center; gap:12px; color:var(--board-muted); font-size:14px;}
8676
+ .chalk-board-spinner {width:22px; height:22px; border-radius:50%; border:2px solid var(--board-muted); border-top-color:transparent; animation:chalk-board-spin .8s linear infinite; opacity:.8;}
8677
+ @keyframes chalk-board-spin {to {transform:rotate(360deg);}}
8678
+
8679
+ .chalk-board-hint {position:absolute; inset:0; display:grid; place-content:center; justify-items:center; gap:14px; color:var(--board-muted); text-align:center; pointer-events:none; z-index:1;}
8680
+ .chalk-board-hint p {margin:0; font-size:15px; max-width:280px; line-height:1.4; text-wrap:balance;}
8681
+ .chalk-board-hint ul {list-style:none; margin:0; padding:0; display:flex; gap:18px; flex-wrap:wrap; justify-content:center; font-size:13px;}
8682
+ .chalk-board-hint li {display:flex; align-items:center; gap:6px;}
8683
+ .chalk-board-hint kbd {font:600 11px ui-monospace, SFMono-Regular, Menlo, monospace; padding:2px 6px; border-radius:5px; border:1px solid var(--board-muted); color:var(--board-muted);}
8684
+
8685
+ .chalk-board-note-layer {position:absolute; inset:0; z-index:2; cursor:crosshair; touch-action:none;}
8686
+
8687
+ .chalk-board-cursors {position:absolute; inset:0; z-index:3; pointer-events:none; overflow:hidden;}
8688
+ .chalk-board-cursors[data-hidden] {display:none;}
8689
+ .chalk-board-cursor {position:absolute; left:0; top:0; will-change:transform;}
8690
+ .chalk-board-cursor-arrow {position:absolute; left:-3px; top:-3px; width:22px; height:26px; max-width:none; display:block; filter:drop-shadow(0 2px 3px rgba(0,0,0,.35));}
8691
+ .chalk-board-cursor-tag {position:absolute; left:12px; top:18px; display:flex; align-items:center; height:28px;}
8692
+ .chalk-board-cursor-avatar {position:relative; z-index:1; box-shadow:0 0 0 2px rgba(255,255,255,.92); border-radius:50%;}
8693
+ .chalk-board-cursor-name {position:absolute; left:14px; top:0; height:28px; display:flex; align-items:center; padding:0 11px 0 18px; border-radius:14px; white-space:nowrap; color:#fff; font:600 13px/1 var(--chalk-font-family, Figtree, system-ui, sans-serif); box-shadow:0 0 0 1.5px rgba(255,255,255,.92), 0 3px 10px rgba(0,0,0,.28); opacity:0; transform:translateX(-10px); transition:opacity .3s ease-out, transform .3s ease-out;}
8694
+ .chalk-board-cursor[data-named="true"] .chalk-board-cursor-name {opacity:1; transform:none;}
8695
+
8696
+ .chalk-board :focus-visible {outline:2px solid var(--chalk-focus); outline-offset:2px;}
8697
+ .chalk-board button {font:inherit; color:inherit; background:none; border:0; padding:0; cursor:pointer;}
8698
+
8699
+ .chalk-board-island, .chalk-board-zoom, .chalk-board-menu-button, .chalk-board-chip, .chalk-board-view-only {background:var(--chalk-surface); color:var(--chalk-text); box-shadow:var(--board-shadow);}
8700
+ .chalk-board-chips {position:absolute; left:0; right:0; top:12px; z-index:5; display:flex; flex-direction:column; align-items:center; gap:8px; pointer-events:none;}
8701
+ .chalk-board-chips[data-low] {top:76px;}
8702
+ .chalk-board-chip {display:flex; align-items:center; gap:8px; height:36px; padding:0 6px 0 14px; border-radius:999px; font-size:13px; font-weight:600; pointer-events:auto;}
8703
+ .chalk-board-chip[data-quiet] {padding:0 14px; font-weight:500;}
8704
+ .chalk-board-chip svg {color:var(--chalk-muted-text);}
8705
+ .chalk-board-chip-button {height:28px !important; padding:0 12px !important; border-radius:999px; background:var(--chalk-hover) !important; font-weight:600; font-size:13px;}
8706
+ .chalk-board-view-only {position:absolute; left:12px; top:12px; z-index:5; height:30px; padding:0 12px; border-radius:999px; color:var(--chalk-soft-text); font-size:12px; font-weight:600; display:flex; align-items:center; gap:6px; box-shadow:0 0 0 1px var(--chalk-line-strong);}
8707
+
8708
+ .chalk-board-corner {position:absolute; right:12px; top:12px; z-index:5;}
8709
+ .chalk-board-menu-button {width:40px; height:40px; border-radius:50%; display:grid; place-items:center; transition:transform .15s ease-out;}
8710
+ .chalk-board-menu-button:hover {background:var(--chalk-raised);}
8711
+ .chalk-board-menu-button:active {transform:scale(.94);}
8712
+ .chalk-board-menu-button[aria-expanded="true"], .chalk-board-menu-button[data-popup-open] {background:var(--chalk-text); color:var(--chalk-canvas);}
8713
+ .chalk-menu.chalk-board-menu {width:304px;}
8714
+ .chalk-board-menu-rule {border:0; border-top:1px solid var(--chalk-line); margin:4px 0;}
8715
+ .chalk-board-menu-sublabel {padding:6px 10px 4px; font-size:12px; font-weight:600; color:var(--chalk-muted-text);}
8716
+ .chalk-board-tiles {display:grid; grid-template-columns:repeat(3, 1fr); gap:6px; padding:2px 4px 8px;}
8717
+ .chalk-board-tile {display:grid; gap:6px; justify-items:center; align-content:start; min-height:44px; padding:6px 4px 8px; border-radius:10px; border:2px solid transparent; font-size:12px; font-weight:600; color:var(--chalk-text); text-align:center; cursor:pointer; outline:none;}
8718
+ .chalk-board-tile[data-highlighted], .chalk-board-tile:hover {background:var(--chalk-hover);}
8719
+ .chalk-board-tile[data-checked] {border-color:var(--chalk-text);}
8720
+ .chalk-board-tile-preview {display:block; position:relative; width:100%; height:38px; border-radius:6px; overflow:hidden; box-shadow:0 0 0 1px var(--chalk-line-strong);}
8721
+ .chalk-board-tile-preview[data-surface="chalkboard"] {background:#1c2622; color:#f1f0ea;}
8722
+ .chalk-board-tile-preview[data-surface="paper"] {background:#f5f2ea; color:#1b1c1f;}
8723
+ .chalk-board-tile-preview[data-surface="dots"] {background:var(--chalk-surface) radial-gradient(var(--chalk-muted-text) 1px, transparent 1.5px) 0 0/8px 8px;}
8724
+ .chalk-board-tile-preview[data-island] {background:var(--chalk-raised);}
8725
+ .chalk-board-tile-preview[data-island] i {position:absolute; background:var(--chalk-text); border-radius:3px;}
8726
+ .chalk-board-tile-preview[data-island="bottom"] i {left:30%; right:30%; bottom:5px; height:5px;}
8727
+ .chalk-board-tile-preview[data-island="top"] i {left:30%; right:30%; top:5px; height:5px;}
8728
+ .chalk-board-tile-preview[data-island="left"] i {left:5px; top:20%; bottom:20%; width:5px;}
8729
+ .chalk-board-menu-kbd {margin-left:auto; padding-left:18px; font:600 11px ui-monospace, SFMono-Regular, Menlo, monospace; color:var(--chalk-muted-text);}
8730
+ .chalk-board-danger {display:contents; color:var(--chalk-danger-text);}
8731
+
8732
+ .chalk-board-zoom {position:absolute; right:12px; bottom:12px; z-index:5; height:40px; display:flex; align-items:center; border-radius:999px; padding:0 4px; transition:translate .2s ease-out;}
8733
+ .chalk-board-zoom button {height:32px; min-width:32px; border-radius:999px; display:grid; place-items:center; padding:0 6px !important; font-size:13px; font-weight:600; font-variant-numeric:tabular-nums;}
8734
+ .chalk-board-zoom button:hover {background:var(--chalk-hover);}
8735
+ .chalk-board-zoom-value {min-width:52px !important;}
8736
+ .chalk-board-zoom-rule {width:1px; height:18px; background:var(--chalk-line-strong); margin:0 2px;}
8737
+
8738
+ .chalk-board-island {position:absolute; z-index:5; display:flex; align-items:center; gap:2px; padding:6px; border-radius:26px; max-width:calc(100% - 24px);}
8739
+ .chalk-board-island[data-place="bottom"] {left:50%; bottom:12px; translate:-50% 0;}
8740
+ .chalk-board-island[data-place="top"] {left:50%; top:12px; translate:-50% 0;}
8741
+ .chalk-board-island[data-place="left"] {left:12px; top:50%; translate:0 -50%; flex-direction:column; max-height:calc(100% - 24px); overflow-y:auto; scrollbar-width:none;}
8742
+ .chalk-board-island[data-place="compact"] {left:12px; bottom:12px; padding:4px;}
8743
+ .chalk-board-island[data-place="expanded"] {left:12px; right:12px; bottom:12px; padding:4px; border-radius:28px; overflow-x:auto; scrollbar-width:none;}
8744
+ .chalk-board-island[data-dragging] {z-index:6; cursor:grabbing; box-shadow:0 0 0 1px var(--chalk-line-strong), 0 14px 36px rgba(0,0,0,.45);}
8745
+ .chalk-board:has(.chalk-board-island[data-place="expanded"]) .chalk-board-zoom {translate:0 -60px;}
8746
+ .chalk-board-group {display:contents;}
8747
+ .chalk-board-separator {flex:none; width:1px; height:24px; margin:0 4px; background:var(--chalk-line-strong);}
8748
+ .chalk-board-island[data-place="left"] .chalk-board-separator {width:24px; height:1px; margin:4px 0;}
8749
+ .chalk-board-tool {width:40px; height:40px; flex:none; border-radius:50%; display:grid !important; place-items:center; position:relative; transition:transform .15s ease-out;}
8750
+ .chalk-board-tool:hover {background:var(--chalk-hover);}
8751
+ .chalk-board-tool:active {transform:scale(.92);}
8752
+ .chalk-board-tool[aria-pressed="true"] {background:var(--chalk-text); color:var(--chalk-canvas);}
8753
+ .chalk-board-tool[aria-pressed="true"]:hover {background:var(--chalk-text-strong);}
8754
+ .chalk-board-tool:disabled {opacity:.35; cursor:default; transform:none; background:none;}
8755
+ .chalk-board[data-compact] .chalk-board-tool {width:44px; height:44px;}
8756
+ @media (pointer:coarse) {.chalk-board-tool {width:44px; height:44px;}}
8757
+ .chalk-board-tool-caret {position:absolute; right:3px; bottom:3px;}
8758
+ .chalk-board-tool-swatch {width:22px; height:22px; border-radius:50%; box-shadow:0 0 0 2px var(--chalk-surface), 0 0 0 3.5px var(--chalk-muted-text);}
8759
+ .chalk-board-tool-dot {position:absolute; right:5px; bottom:5px; width:11px; height:11px; border-radius:50%; box-shadow:0 0 0 2px var(--chalk-surface);}
8760
+ .chalk-board-grip {width:22px; height:40px; flex:none; display:grid !important; place-items:center; border-radius:12px; color:var(--chalk-muted-text) !important; cursor:grab !important; touch-action:none;}
8761
+ .chalk-board-grip:hover {background:var(--chalk-hover); color:var(--chalk-text) !important;}
8762
+ .chalk-board-island[data-place="left"] .chalk-board-grip {width:40px; height:22px;}
8763
+ .chalk-board-island[data-place="left"] .chalk-board-grip svg {transform:rotate(90deg);}
8764
+ @media (pointer:coarse) {.chalk-board-grip {width:26px; height:44px;} .chalk-board-island[data-place="left"] .chalk-board-grip {width:44px; height:26px;}}
8765
+ .chalk-board-kbd {font:600 11px ui-monospace, monospace; margin-left:6px; padding:1px 5px; border-radius:4px; background:rgba(127,127,127,.28);}
8766
+
8767
+ .chalk-board-dropzone {position:absolute; z-index:4; border:2px dashed var(--board-selection); border-radius:28px; background:rgba(125,184,255,.08); opacity:.45; pointer-events:none; transition:opacity .15s ease-out;}
8768
+ .chalk-board-dropzone[data-near] {opacity:1;}
8769
+ .chalk-board-dropzone[data-zone="left"] {left:6px; top:12px; bottom:12px; width:70px;}
8770
+ .chalk-board-dropzone[data-zone="bottom"] {left:12px; right:12px; bottom:6px; height:70px;}
8771
+ .chalk-board-dropzone[data-zone="top"] {left:12px; right:12px; top:6px; height:70px;}
8772
+
8773
+ .chalk-board-colors {display:grid; gap:10px; padding:4px;}
8774
+ .chalk-board-swatches {display:flex; gap:2px;}
8775
+ .chalk-board-swatch, .chalk-board-width {width:40px; height:40px; border-radius:50%; display:grid; place-items:center; flex:none; color:var(--chalk-text); background:none; border:0; cursor:pointer;}
8776
+ @media (pointer:coarse) {.chalk-board-swatch, .chalk-board-width {width:44px; height:44px;}}
8777
+ .chalk-board-swatch:hover, .chalk-board-width:hover {background:var(--chalk-hover);}
8778
+ .chalk-board-swatch i {width:24px; height:24px; border-radius:50%; box-shadow:0 0 0 1px var(--chalk-muted-text); display:grid; place-items:center;}
8779
+ .chalk-board-swatch[aria-checked="true"] i {box-shadow:0 0 0 2px var(--chalk-popover), 0 0 0 4px var(--chalk-text);}
8780
+ .chalk-board-width i {display:block; width:22px; border-radius:99px; background:var(--chalk-text);}
8781
+ .chalk-board-width[aria-checked="true"] {background:var(--chalk-text);}
8782
+ .chalk-board-width[aria-checked="true"] i {background:var(--chalk-canvas);}
8783
+
8784
+ .chalk-board-announcer {position:absolute; width:1px; height:1px; overflow:hidden; clip-path:inset(50%); white-space:nowrap;}
8785
+
8786
+ @media (max-width: 640px) {
8787
+ .chalk-board-zoom {height:52px;}
8788
+ .chalk-board-zoom button {height:44px; min-width:44px;}
8789
+ .chalk-board-menu-button {width:44px; height:44px;}
8790
+ }
8791
+ @media (prefers-reduced-motion: reduce) {
8792
+ .chalk-board-spinner {animation:none;}
8793
+ .chalk-board-engine, .chalk-board-cursor-name, .chalk-board-zoom {transition:none;}
8794
+ .chalk-board-cursor-name {transform:none !important;}
8795
+ }
8796
+ `;
8797
+
8798
+ // src/components/board/Board.tsx
8799
+ import { Fragment as Fragment31, jsx as jsx74, jsxs as jsxs48 } from "react/jsx-runtime";
8800
+ var SURFACE_KEY = "chalk.board.surface";
8801
+ var ISLAND_KEY = "chalk.board.island";
8802
+ var COMPACT_BELOW_PX = 1060;
8803
+ var VIEW_SEND_MS = 150;
8804
+ var VIEW_HEARTBEAT_MS = 2e3;
8805
+ var GLIDE_MS = 300;
8806
+ var ZOOM_STEP = 1.25;
8807
+ var NOTE_FONT_PX = 18;
8808
+ var EDITING_KEYS = /* @__PURE__ */ new Set(["delete", "backspace", "arrowup", "arrowdown", "arrowleft", "arrowright", "escape", " ", "enter", "shift", "alt", "control", "meta", "tab"]);
8809
+ var MODIFIED_ENGINE_KEYS = /* @__PURE__ */ new Set(["c", "v", "x", "a", "d", "=", "+", "-", "0"]);
8810
+ var first = (name) => name.split(" ")[0] ?? name;
8811
+ function fitContent(engine, animate) {
8812
+ engine.api.scrollToContent(void 0, { fitToViewport: true, viewportZoomFactor: 0.85, maxZoom: 1, animate });
8813
+ }
8814
+ function isTyping(target) {
8815
+ return target instanceof HTMLElement && (target.isContentEditable || target.tagName === "TEXTAREA" || target.tagName === "INPUT");
8816
+ }
8817
+ function useCamera(engine, root, onManualMove, onMoved, onViewportReady) {
8818
+ const cameraRef = useRef33({ scrollX: 0, scrollY: 0, zoom: 1 });
8819
+ const viewportRef = useRef33({ width: 1, height: 1 });
8820
+ const expected = useRef33(null);
8821
+ const glide = useRef33(0);
8822
+ const [zoom, setZoom] = useState43(1);
8823
+ const reduced = usePrefersReducedMotion();
8824
+ const manual = useRef33(onManualMove);
8825
+ manual.current = onManualMove;
8826
+ const moved = useRef33(onMoved);
8827
+ moved.current = onMoved;
8828
+ const resized = useRef33(onViewportReady);
8829
+ resized.current = onViewportReady;
8830
+ useEffect37(() => {
8831
+ if (!engine) return;
8832
+ const paint = (camera) => {
8833
+ cameraRef.current = camera;
8834
+ const element = root.current;
8835
+ if (element) {
8836
+ let gap = 24 * camera.zoom;
8837
+ while (gap < 14) gap *= 2;
8838
+ element.style.setProperty("--board-dot-gap", `${gap}px`);
8839
+ element.style.setProperty("--board-dot-x", `${camera.scrollX * camera.zoom % gap}px`);
8840
+ element.style.setProperty("--board-dot-y", `${camera.scrollY * camera.zoom % gap}px`);
8841
+ }
8842
+ setZoom(camera.zoom);
8843
+ };
8844
+ paint(readCamera(engine));
8845
+ return engine.api.onScrollChange((scrollX, scrollY, next) => {
8846
+ const camera = { scrollX, scrollY, zoom: next.value };
8847
+ if (camera.zoom !== clampZoom(camera.zoom)) {
8848
+ expected.current = { ...camera, zoom: clampZoom(camera.zoom) };
8849
+ setCamera(engine, expected.current);
8850
+ return;
8851
+ }
8852
+ paint(camera);
8853
+ if (expected.current && sameCamera(camera, expected.current)) return;
8854
+ expected.current = null;
8855
+ cancelAnimationFrame(glide.current);
8856
+ manual.current();
8857
+ moved.current();
8858
+ });
8859
+ }, [engine, root]);
8860
+ useEffect37(() => {
8861
+ const element = root.current;
8862
+ if (!element) return;
8863
+ viewportRef.current = { width: element.clientWidth || 1, height: element.clientHeight || 1 };
8864
+ const observer = new ResizeObserver(([entry]) => {
8865
+ if (!entry) return;
8866
+ viewportRef.current = { width: entry.contentRect.width, height: entry.contentRect.height };
8867
+ if (entry.contentRect.width > 0 && entry.contentRect.height > 0) resized.current();
8868
+ });
8869
+ observer.observe(element);
8870
+ return () => observer.disconnect();
8871
+ }, [root]);
8872
+ useEffect37(() => () => cancelAnimationFrame(glide.current), []);
8873
+ const move = useCallback13(
8874
+ (target, animate) => {
8875
+ if (!engine) return;
8876
+ cancelAnimationFrame(glide.current);
8877
+ const set = (camera) => {
8878
+ expected.current = camera;
8879
+ setCamera(engine, camera);
8880
+ };
8881
+ if (!animate || reduced) {
8882
+ set(target);
8883
+ moved.current();
8884
+ return;
8885
+ }
8886
+ const from = cameraRef.current;
8887
+ const started = performance.now();
8888
+ const step = (now) => {
8889
+ const progress = Math.min(1, (now - started) / GLIDE_MS);
8890
+ set(mixCamera(from, target, 1 - (1 - progress) ** 3));
8891
+ if (progress < 1) glide.current = requestAnimationFrame(step);
8892
+ else moved.current();
8893
+ };
8894
+ glide.current = requestAnimationFrame(step);
8895
+ },
8896
+ [engine, reduced]
8897
+ );
8898
+ return useMemo16(() => ({ cameraRef, viewportRef, zoom, move }), [move, zoom]);
8899
+ }
8900
+ function useBoardKeys(root, handlers) {
8901
+ const latest = useRef33(handlers);
8902
+ latest.current = handlers;
8903
+ useEffect37(() => {
8904
+ const element = root.current;
8905
+ if (!element) return;
8906
+ const keydown = (event) => {
8907
+ if (isTyping(event.target)) return;
8908
+ const key = event.key.toLowerCase();
8909
+ if (event.altKey && key.startsWith("arrow") && event.target instanceof Element && event.target.closest(".chalk-board-grip")) return;
8910
+ const modified = event.metaKey || event.ctrlKey;
8911
+ const { onTool, onZoom, canDraw } = latest.current;
8912
+ if (modified && !event.altKey && key === "z") return;
8913
+ if (modified && !event.altKey && key === "y") return;
8914
+ if (modified && !event.altKey && !event.shiftKey && MODIFIED_ENGINE_KEYS.has(key)) return;
8915
+ if (!modified && !event.altKey) {
8916
+ const tool = TOOL_KEYS[key];
8917
+ if (tool && canDraw) {
8918
+ event.preventDefault();
8919
+ event.stopPropagation();
8920
+ onTool(tool);
8921
+ return;
8922
+ }
8923
+ if (key === "+" || key === "=" || key === "-") {
8924
+ event.preventDefault();
8925
+ event.stopPropagation();
8926
+ onZoom(key === "-" ? 1 / ZOOM_STEP : ZOOM_STEP);
8927
+ return;
8928
+ }
8929
+ if (key === "escape" && canDraw) onTool("selection");
8930
+ if (EDITING_KEYS.has(key)) return;
8931
+ }
8932
+ event.stopPropagation();
8933
+ };
8934
+ const blockMenu = (event) => {
8935
+ event.preventDefault();
8936
+ event.stopPropagation();
8937
+ };
8938
+ element.addEventListener("keydown", keydown, true);
8939
+ element.addEventListener("contextmenu", blockMenu, true);
8940
+ return () => {
8941
+ element.removeEventListener("keydown", keydown, true);
8942
+ element.removeEventListener("contextmenu", blockMenu, true);
8943
+ };
8944
+ }, [root]);
8945
+ }
8946
+ function useImageRefusal(root, toast) {
8947
+ const latest = useRef33(toast);
8948
+ latest.current = toast;
8949
+ useEffect37(() => {
8950
+ const element = root.current;
8951
+ if (!element) return;
8952
+ const refuse = (files, event) => {
8953
+ if (!files || ![...files].some((file) => file.type.startsWith("image/"))) return;
8954
+ event.preventDefault();
8955
+ event.stopPropagation();
8956
+ latest.current({ type: "error", message: "The board can't take images yet.", duration: 5e3 });
8957
+ };
8958
+ const paste = (event) => refuse(event.clipboardData?.files, event);
8959
+ const drop = (event) => refuse(event.dataTransfer?.files, event);
8960
+ element.addEventListener("paste", paste, true);
8961
+ element.addEventListener("drop", drop, true);
8962
+ return () => {
8963
+ element.removeEventListener("paste", paste, true);
8964
+ element.removeEventListener("drop", drop, true);
8965
+ };
8966
+ }, [root]);
8967
+ }
8968
+ function Board({ board, primary, visible, colorScheme, people, following, onFollow, sendView, onClose }) {
8969
+ const root = useRef33(null);
8970
+ const isHost = useCan("manageWhiteboard");
8971
+ const connection = useConnection();
8972
+ const boardSession = useWhiteboard().engine;
8973
+ const offline = connection.status === "reconnecting";
8974
+ const phone = useMediaQuery("(max-width: 640px)");
8975
+ const [engine, setEngine] = useState43(null);
8976
+ const toast = useBoardToast(engine);
8977
+ const [surface, setSurface] = useState43(() => readStored(SURFACE_KEY, SURFACES, "chalkboard"));
8978
+ const [place, setPlace] = useState43(() => readStored(ISLAND_KEY, ISLAND_PLACES, "bottom"));
8979
+ const [tool, setTool] = useState43("pen");
8980
+ const [shape, setShape] = useState43("rectangle");
8981
+ const [color, setColor] = useState43("ink");
8982
+ const [width, setWidth] = useState43("medium");
8983
+ const [elementCount, setElementCount] = useState43(0);
8984
+ const empty = elementCount === 0;
8985
+ const [narrow, setNarrow] = useState43(false);
8986
+ const [announcement, setAnnouncement] = useState43("");
8987
+ const canDraw = primary && (board.collab?.canDraw ?? board.canDraw ?? true);
8988
+ const compact = phone || narrow;
8989
+ const history = useEngineHistory(root, engine);
8990
+ const touched = useRef33(false);
8991
+ const views = useRef33(/* @__PURE__ */ new Map());
8992
+ const cursors = useRef33(/* @__PURE__ */ new Map());
8993
+ const pen = useMemo16(() => ({ color, width, surface, theme: colorScheme }), [color, width, surface, colorScheme]);
8994
+ const followingRef = useRef33(following);
8995
+ followingRef.current = following;
8996
+ const nameOf = useCallback13((id) => first(people.find((person) => person.id === id)?.displayName ?? "Someone"), [people]);
8997
+ const colorFor = useCallback13(
8998
+ (id) => personColor(
8999
+ Math.max(
9000
+ 0,
9001
+ people.findIndex((person) => person.id === id)
9002
+ )
9003
+ ),
9004
+ [people]
9005
+ );
9006
+ const stopFollowing = useCallback13(() => {
9007
+ const current = followingRef.current;
9008
+ if (!current) return;
9009
+ followingRef.current = null;
9010
+ onFollow(null);
9011
+ toast({ message: `Stopped following ${nameOf(current)}` });
9012
+ }, [nameOf, onFollow, toast]);
9013
+ const shareView = useRef33(() => false);
9014
+ const viewSender = useRef33(sendView);
9015
+ viewSender.current = sendView;
9016
+ const ownParticipantId = useSelf().participantId;
9017
+ const appliedVersions = useRef33(new ViewVersions());
9018
+ const viewsAvailable = sendView !== void 0;
9019
+ const reapplyView = useRef33(() => void 0);
9020
+ const camera = useCamera(
9021
+ engine,
9022
+ root,
9023
+ stopFollowing,
9024
+ () => shareView.current(false),
9025
+ () => reapplyView.current()
9026
+ );
9027
+ useEffect37(() => {
9028
+ if (!visible || !viewsAvailable || !engine) return;
9029
+ let last = 0;
9030
+ let pending;
9031
+ const send = (bring2) => {
9032
+ const followedView = followingRef.current ? views.current.get(followingRef.current) : void 0;
9033
+ if (followedView && !followedView.originParticipantId && !bring2) return true;
9034
+ const view = followedView ?? visibleView(camera.cameraRef.current, camera.viewportRef.current);
9035
+ last = performance.now();
9036
+ const rectangle = { x: view.x, y: view.y, width: view.width, height: view.height, bring: bring2 };
9037
+ const outgoing = followedView?.originParticipantId ? { ...rectangle, originParticipantId: followedView.originParticipantId, originSequence: followedView.originSequence, ...followedView.originEpoch ? { originEpoch: followedView.originEpoch } : {} } : rectangle;
9038
+ try {
9039
+ return viewSender.current?.(outgoing) ?? false;
9040
+ } catch (error) {
9041
+ console.warn("Board view dropped.", { operation: "sendView", error });
9042
+ return false;
9043
+ }
9044
+ };
9045
+ shareView.current = (bring2) => {
9046
+ if (bring2) return send(true);
9047
+ clearTimeout(pending);
9048
+ pending = setTimeout(() => send(false), Math.max(0, VIEW_SEND_MS - (performance.now() - last)));
9049
+ return true;
9050
+ };
9051
+ const heartbeat = setInterval(() => {
9052
+ if (!followingRef.current) send(false);
9053
+ }, VIEW_HEARTBEAT_MS);
9054
+ send(false);
9055
+ return () => {
9056
+ clearInterval(heartbeat);
9057
+ clearTimeout(pending);
9058
+ shareView.current = () => false;
9059
+ };
9060
+ }, [boardSession.sceneId, boardSession.status, camera.cameraRef, camera.viewportRef, connection.status, engine, visible, viewsAvailable]);
9061
+ const { move, viewportRef } = camera;
9062
+ const goTo = useCallback13(
9063
+ (participantId, animate, selected = false) => {
9064
+ const view = views.current.get(participantId);
9065
+ if (!view || viewportRef.current.width <= 0 || viewportRef.current.height <= 0) return;
9066
+ if (!selected && !appliedVersions.current.isNew(view)) return;
9067
+ appliedVersions.current.remember(view);
9068
+ move(cameraForView(view, viewportRef.current), animate);
9069
+ },
9070
+ [move, viewportRef]
9071
+ );
9072
+ reapplyView.current = () => {
9073
+ if (followingRef.current) goTo(followingRef.current, false, true);
9074
+ };
9075
+ const subscribe3 = board.collab?.subscribe;
9076
+ const latestView = useRef33(() => void 0);
9077
+ latestView.current = (event) => {
9078
+ const previous = views.current.get(event.participantId);
9079
+ if (!event.bring) {
9080
+ if (event.originParticipantId === ownParticipantId && ownParticipantId) return;
9081
+ if (!appliedVersions.current.isNew(event)) return;
9082
+ if (event.originParticipantId && previous?.originParticipantId === event.originParticipantId && event.originEpoch === previous.originEpoch && event.originSequence <= previous.originSequence) return;
9083
+ }
9084
+ const newPeer = !previous;
9085
+ views.current.delete(event.participantId);
9086
+ views.current.set(event.participantId, event.bring ? { x: event.x, y: event.y, width: event.width, height: event.height, bring: false } : event);
9087
+ if (views.current.size > 256) {
9088
+ const oldest = views.current.keys().next().value;
9089
+ if (oldest !== void 0) views.current.delete(oldest);
9090
+ }
9091
+ if (newPeer) {
9092
+ shareView.current(false);
9093
+ }
9094
+ if (event.bring) {
9095
+ followingRef.current = event.participantId;
9096
+ onFollow(event.participantId);
9097
+ toast({ message: `${nameOf(event.participantId)} brought everyone to this view` });
9098
+ goTo(event.participantId, true, true);
9099
+ return;
9100
+ }
9101
+ if (followingRef.current === event.participantId) goTo(event.participantId, true);
9102
+ };
9103
+ useEffect37(() => {
9104
+ if (!subscribe3) return;
9105
+ return subscribe3((event) => {
9106
+ if (event.type === "cursor") rememberCursor(cursors.current, event);
9107
+ else if (event.type === "view") latestView.current(event);
9108
+ });
9109
+ }, [subscribe3]);
9110
+ useEffect37(() => {
9111
+ if (following) goTo(following, true, true);
9112
+ }, [following, goTo]);
9113
+ useEffect37(() => {
9114
+ if (engine && canDraw) applyTool(engine, tool, pen);
9115
+ }, [canDraw, engine, pen, tool]);
9116
+ useEffect37(() => {
9117
+ if (engine) void document.fonts.load(`${NOTE_FONT_PX}px Nunito`);
9118
+ }, [engine]);
9119
+ useEffect37(() => {
9120
+ if (!engine) return;
9121
+ let beforeEditing = sceneSignature(engine);
9122
+ let wasEditing = false;
9123
+ setElementCount(engine.api.getSceneElements().length);
9124
+ if (hasContent(engine)) {
9125
+ touched.current = true;
9126
+ if (!followingRef.current) fitContent(engine, false);
9127
+ }
9128
+ const offDown = engine.api.onPointerDown(() => {
9129
+ touched.current = true;
9130
+ });
9131
+ const offChange = engine.api.onChange((elements, appState) => {
9132
+ const count = elements.filter((element) => !element.isDeleted).length;
9133
+ setElementCount(count);
9134
+ if (!touched.current && count > 0) {
9135
+ touched.current = true;
9136
+ if (!followingRef.current) requestAnimationFrame(() => fitContent(engine, false));
9137
+ }
9138
+ const editing = appState.editingTextElement !== null;
9139
+ if (!wasEditing && editing) beforeEditing = sceneSignature(engine);
9140
+ const finishedEditing = wasEditing && !editing;
9141
+ wasEditing = editing;
9142
+ if (finishedEditing && sceneSignature(engine) !== beforeEditing) {
9143
+ settleNoteText(engine);
9144
+ }
9145
+ });
9146
+ return () => {
9147
+ offDown();
9148
+ offChange();
9149
+ };
9150
+ }, [engine]);
9151
+ useEffect37(() => {
9152
+ const element = root.current;
9153
+ if (!element) return;
9154
+ const observer = new ResizeObserver(([entry]) => setNarrow((entry?.contentRect.width ?? COMPACT_BELOW_PX) < COMPACT_BELOW_PX));
9155
+ observer.observe(element);
9156
+ return () => observer.disconnect();
9157
+ }, []);
9158
+ const chooseTool = useCallback13((next) => {
9159
+ if (next === "rectangle" || next === "ellipse" || next === "diamond" || next === "arrow" || next === "line") setShape(next);
9160
+ setTool(next);
9161
+ }, []);
9162
+ const undo = useCallback13(() => {
9163
+ const container = root.current?.querySelector(".excalidraw");
9164
+ if (container) pressHistory(container, false);
9165
+ }, []);
9166
+ const redo = useCallback13(() => {
9167
+ const container = root.current?.querySelector(".excalidraw");
9168
+ if (container) pressHistory(container, true);
9169
+ }, []);
9170
+ const zoomBy = useCallback13(
9171
+ (factor) => {
9172
+ stopFollowing();
9173
+ camera.move(zoomAroundCentre(camera.cameraRef.current, camera.viewportRef.current, camera.cameraRef.current.zoom * factor), true);
9174
+ },
9175
+ [camera, stopFollowing]
9176
+ );
9177
+ useBoardKeys(root, { onTool: chooseTool, onZoom: zoomBy, canDraw });
9178
+ useImageRefusal(root, toast);
9179
+ const restyle = (next) => {
9180
+ if (next.color) setColor(next.color);
9181
+ if (next.width) setWidth(next.width);
9182
+ if (engine) restyleSelection(engine, { ...pen, ...next }, next);
9183
+ };
9184
+ const choosePlace = (next, announce) => {
9185
+ setPlace(next);
9186
+ writeStored(ISLAND_KEY, next);
9187
+ if (announce) setAnnouncement(`Tool island moved to ${next}`);
9188
+ };
9189
+ const chooseSurface = (next) => {
9190
+ setSurface(next);
9191
+ writeStored(SURFACE_KEY, next);
9192
+ };
9193
+ const fit = () => {
9194
+ stopFollowing();
9195
+ if (engine) fitContent(engine, true);
9196
+ };
9197
+ const download = async () => {
9198
+ if (!engine) return;
9199
+ try {
9200
+ const url = URL.createObjectURL(await boardImage(engine, surfaceFill(surface, colorScheme)));
9201
+ const link = document.createElement("a");
9202
+ link.href = url;
9203
+ link.download = "chalk-board.png";
9204
+ link.click();
9205
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
9206
+ toast({ message: "Saved chalk-board.png" });
9207
+ } catch {
9208
+ toast({ type: "error", message: "Could not save the image. Try again." });
9209
+ }
9210
+ };
9211
+ const copy = async () => {
9212
+ if (!engine) return;
9213
+ try {
9214
+ const image = await boardImage(engine, surfaceFill(surface, colorScheme));
9215
+ await navigator.clipboard.write([new ClipboardItem({ "image/png": image })]);
9216
+ toast({ message: "Copied the board as an image" });
9217
+ } catch {
9218
+ toast({ type: "error", message: "The browser blocked copying. Use Download as PNG instead." });
9219
+ }
9220
+ };
9221
+ const bring = () => {
9222
+ if (followingRef.current) {
9223
+ followingRef.current = null;
9224
+ onFollow(null);
9225
+ }
9226
+ if (shareView.current(true)) toast({ message: "Sharing your view with everyone" });
9227
+ else toast({ type: "error", message: "Could not share your view. Try again." });
9228
+ };
9229
+ const clear = async () => {
9230
+ if (!engine) return;
9231
+ try {
9232
+ const restore = await engine.clearWithUndo();
9233
+ toast({ message: "Board cleared", ...canDraw ? { action: { label: "Undo", onClick: () => void restore() } } : {} });
9234
+ } catch {
9235
+ toast({ type: "error", message: "Could not clear the board. Try again." });
9236
+ }
9237
+ };
9238
+ const placeNoteAt = (event) => {
9239
+ if (event.button !== 0) return;
9240
+ const container = root.current?.querySelector(".excalidraw");
9241
+ if (!engine || !container || !root.current) return;
9242
+ const rect = root.current.getBoundingClientRect();
9243
+ const { scrollX, scrollY, zoom } = camera.cameraRef.current;
9244
+ touched.current = true;
9245
+ placeNote(engine, container, { x: (event.clientX - rect.left) / zoom - scrollX, y: (event.clientY - rect.top) / zoom - scrollY }, color);
9246
+ setTool("selection");
9247
+ };
9248
+ const ink = surfaceInk(surface, colorScheme);
9249
+ const surfaceVars = { "--board-selection": selectionColor(surface, colorScheme) };
9250
+ const followed = following ? nameOf(following) : null;
9251
+ const ready = engine !== null;
9252
+ return /* @__PURE__ */ jsxs48("div", { ref: root, className: "chalk-board", "data-surface": surface, "data-theme": colorScheme, "data-ready": ready || void 0, "data-compact": compact || void 0, "data-primary": primary || void 0, "data-elements": elementCount, style: surfaceVars, hidden: !visible, children: [
9253
+ /* @__PURE__ */ jsx74(BoardStyles, {}),
9254
+ /* @__PURE__ */ jsx74(WhiteboardCanvas2, { ...board, headless: true, canDraw, collab: board.collab ? { ...board.collab, canDraw, canManage: isHost } : void 0, inkColor: ink, theme: "light", isVisible: visible, className: "chalk-board-engine", onEngineReady: setEngine }),
9255
+ !ready ? /* @__PURE__ */ jsxs48("div", { className: "chalk-board-loading", role: "status", children: [
9256
+ /* @__PURE__ */ jsx74("span", { className: "chalk-board-spinner", "aria-hidden": "true" }),
9257
+ "Opening board"
9258
+ ] }) : null,
9259
+ primary && ready && empty ? /* @__PURE__ */ jsx74(BoardHint, { canDraw }) : null,
9260
+ primary && tool === "note" && canDraw ? /* @__PURE__ */ jsx74("div", { className: "chalk-board-note-layer", "aria-hidden": "true", onPointerDown: placeNoteAt }) : null,
9261
+ primary ? /* @__PURE__ */ jsx74(BoardCursors, { cursorsRef: cursors, cameraRef: camera.cameraRef, colorFor, hidden: offline }) : null,
9262
+ primary && ready ? /* @__PURE__ */ jsxs48(Fragment31, { children: [
9263
+ /* @__PURE__ */ jsxs48("div", { className: "chalk-board-chips", "data-low": !compact && place === "top" && canDraw || void 0, children: [
9264
+ followed ? /* @__PURE__ */ jsxs48("div", { className: "chalk-board-chip", role: "status", children: [
9265
+ /* @__PURE__ */ jsx74(BoardIcon, { name: "eye", size: 16 }),
9266
+ "Following ",
9267
+ followed,
9268
+ /* @__PURE__ */ jsx74("button", { type: "button", className: "chalk-board-chip-button", onClick: stopFollowing, children: "Stop" })
9269
+ ] }) : null,
9270
+ offline ? /* @__PURE__ */ jsxs48("div", { className: "chalk-board-chip", "data-quiet": true, role: "status", children: [
9271
+ /* @__PURE__ */ jsx74(BoardIcon, { name: "wifiOff", size: 16 }),
9272
+ "Offline. Your drawing will sync."
9273
+ ] }) : null
9274
+ ] }),
9275
+ !canDraw ? /* @__PURE__ */ jsxs48("div", { className: "chalk-board-view-only", children: [
9276
+ /* @__PURE__ */ jsx74(BoardIcon, { name: "lock", size: 14 }),
9277
+ "View only"
9278
+ ] }) : null,
9279
+ /* @__PURE__ */ jsx74("div", { className: "chalk-board-corner", children: /* @__PURE__ */ jsx74(
9280
+ BoardMenu,
9281
+ {
9282
+ surface,
9283
+ onSurface: chooseSurface,
9284
+ place: !compact && canDraw ? place : void 0,
9285
+ onPlace: (next) => choosePlace(next, true),
9286
+ onDownload: () => void download(),
9287
+ onCopy: () => void copy(),
9288
+ onBring: isHost && sendView ? bring : void 0,
9289
+ onClear: isHost ? () => void clear() : void 0,
9290
+ onClose
9291
+ }
9292
+ ) }),
9293
+ /* @__PURE__ */ jsxs48("div", { className: "chalk-board-zoom", role: "group", "aria-label": "Zoom", children: [
9294
+ /* @__PURE__ */ jsx74("button", { type: "button", "aria-label": "Zoom out", onClick: () => zoomBy(1 / ZOOM_STEP), children: /* @__PURE__ */ jsx74(BoardIcon, { name: "minus", size: 16 }) }),
9295
+ /* @__PURE__ */ jsxs48("button", { type: "button", "aria-label": `Zoom ${Math.round(camera.zoom * 100)}%, reset to 100%`, className: "chalk-board-zoom-value", onClick: () => zoomBy(1 / camera.cameraRef.current.zoom), children: [
9296
+ Math.round(camera.zoom * 100),
9297
+ "%"
9298
+ ] }),
9299
+ /* @__PURE__ */ jsx74("button", { type: "button", "aria-label": "Zoom in", onClick: () => zoomBy(ZOOM_STEP), children: /* @__PURE__ */ jsx74(BoardIcon, { name: "plus", size: 16 }) }),
9300
+ /* @__PURE__ */ jsx74("span", { className: "chalk-board-zoom-rule", "aria-hidden": "true" }),
9301
+ /* @__PURE__ */ jsx74("button", { type: "button", "aria-label": "Fit to content", onClick: fit, children: /* @__PURE__ */ jsx74(BoardIcon, { name: "fit", size: 16 }) })
9302
+ ] }),
9303
+ canDraw ? /* @__PURE__ */ jsx74(
9304
+ BoardIsland,
9305
+ {
9306
+ tool,
9307
+ shape,
9308
+ onTool: chooseTool,
9309
+ color,
9310
+ width,
9311
+ onColor: (next) => restyle({ color: next }),
9312
+ onWidth: (next) => restyle({ width: next }),
9313
+ surface,
9314
+ theme: colorScheme,
9315
+ canUndo: history.canUndo,
9316
+ canRedo: history.canRedo,
9317
+ onUndo: undo,
9318
+ onRedo: redo,
9319
+ place,
9320
+ onPlace: choosePlace,
9321
+ compact
9322
+ }
9323
+ ) : null,
9324
+ /* @__PURE__ */ jsx74("div", { className: "chalk-board-announcer", "aria-live": "polite", children: announcement })
9325
+ ] }) : null
9326
+ ] });
9327
+ }
9328
+ function BoardHint({ canDraw }) {
9329
+ if (!canDraw)
9330
+ return /* @__PURE__ */ jsx74("div", { className: "chalk-board-hint", children: /* @__PURE__ */ jsx74("p", { children: "Nothing here yet. When someone draws, it shows up here." }) });
9331
+ return /* @__PURE__ */ jsxs48("div", { className: "chalk-board-hint", children: [
9332
+ /* @__PURE__ */ jsx74("p", { children: "An empty board. Draw, type or add a sticky note." }),
9333
+ /* @__PURE__ */ jsxs48("ul", { children: [
9334
+ /* @__PURE__ */ jsxs48("li", { children: [
9335
+ /* @__PURE__ */ jsx74("kbd", { children: "P" }),
9336
+ " Pen"
9337
+ ] }),
9338
+ /* @__PURE__ */ jsxs48("li", { children: [
9339
+ /* @__PURE__ */ jsx74("kbd", { children: "T" }),
9340
+ " Text"
9341
+ ] }),
9342
+ /* @__PURE__ */ jsxs48("li", { children: [
9343
+ /* @__PURE__ */ jsx74("kbd", { children: "N" }),
9344
+ " Sticky note"
9345
+ ] })
9346
+ ] })
9347
+ ] });
9348
+ }
9349
+
7763
9350
  // src/components/stage/SpaceStage.tsx
7764
- import { useCallback as useCallback12, useMemo as useMemo15, useState as useState40 } from "react";
7765
- import { jsx as jsx69 } from "react/jsx-runtime";
7766
- function SpaceStage({ tiles, layout, onLayoutChange, generatedAvatars = true, whiteboard, onOverflowClick, shareCapture, onStopSharing, onChangeShare, className }) {
7767
- const items = useMemo15(() => buildStageItems(tiles, whiteboard?.isOpen === true), [tiles, whiteboard?.isOpen]);
7768
- const [selectedShare, setSelectedShare] = useState40(null);
7769
- const [pin, setPin] = useState40(null);
9351
+ import { jsx as jsx75 } from "react/jsx-runtime";
9352
+ function SpaceStage({ tiles, layout, onLayoutChange, generatedAvatars = true, whiteboard, onOverflowClick, shareCapture, onStopSharing, onChangeShare, colorScheme = "dark", onCloseBoard, className }) {
9353
+ const items = useMemo17(() => buildStageItems(tiles, whiteboard?.isOpen === true), [tiles, whiteboard?.isOpen]);
9354
+ const [selectedShare, setSelectedShare] = useState44(null);
9355
+ const [pin, setPin] = useState44(null);
9356
+ const [following, setFollowing] = useState44(null);
9357
+ const boardOpen = whiteboard?.isOpen === true && Boolean(whiteboard.props);
9358
+ useEffect38(() => {
9359
+ if (!boardOpen) setFollowing(null);
9360
+ }, [boardOpen]);
9361
+ const canFollow = boardOpen && Boolean(whiteboard?.sendView);
9362
+ const followed = canFollow && tiles.some((tile) => tile.id === following) ? following : null;
9363
+ const activePin = items.some((item) => item.id === pin) ? pin : null;
7770
9364
  const shares = items.filter((item) => item.kind === "screen-share");
7771
9365
  const activeShare = selectedShare && shares.some((item) => item.id === selectedShare) ? selectedShare : null;
7772
- const renderContentTile = useCallback12(
9366
+ useEffect38(() => {
9367
+ if (pin !== activePin) setPin(activePin);
9368
+ if (selectedShare !== activeShare) setSelectedShare(activeShare);
9369
+ }, [pin, activePin, selectedShare, activeShare]);
9370
+ const follow = useCallback14((id) => {
9371
+ if (id) {
9372
+ setPin(null);
9373
+ setSelectedShare(null);
9374
+ }
9375
+ setFollowing(id);
9376
+ }, []);
9377
+ const renderContentTile = useCallback14(
7773
9378
  (item, context) => {
7774
9379
  if (item.kind === "screen-share")
7775
- return /* @__PURE__ */ jsx69(
9380
+ return /* @__PURE__ */ jsx75(
7776
9381
  StageContentTile,
7777
9382
  {
7778
9383
  item,
@@ -7780,7 +9385,7 @@ function SpaceStage({ tiles, layout, onLayoutChange, generatedAvatars = true, wh
7780
9385
  capture: item.participant.isLocal ? shareCapture : void 0,
7781
9386
  onStopSharing: item.participant.isLocal ? onStopSharing : void 0,
7782
9387
  onChangeShare: item.participant.isLocal ? onChangeShare : void 0,
7783
- switcher: shares.length > 1 ? /* @__PURE__ */ jsx69("div", { role: "group", "aria-label": "Whose screen is big", className: cn("absolute left-2 z-30 flex rounded-[999px] bg-[rgba(12,14,18,.82)] p-[3px]", item.participant.screenShareState === "low-quality" ? "top-11" : "top-2"), children: shares.map((share) => /* @__PURE__ */ jsx69(
9388
+ switcher: shares.length > 1 ? /* @__PURE__ */ jsx75("div", { role: "group", "aria-label": "Whose screen is big", className: cn("absolute left-2 z-30 flex rounded-[999px] bg-[rgba(12,14,18,.82)] p-[3px]", item.participant.screenShareState === "low-quality" ? "top-11" : "top-2"), children: shares.map((share) => /* @__PURE__ */ jsx75(
7784
9389
  "button",
7785
9390
  {
7786
9391
  type: "button",
@@ -7797,44 +9402,52 @@ function SpaceStage({ tiles, layout, onLayoutChange, generatedAvatars = true, wh
7797
9402
  }
7798
9403
  );
7799
9404
  const boardProps = whiteboard?.props;
7800
- return /* @__PURE__ */ jsx69(
9405
+ return /* @__PURE__ */ jsx75(
7801
9406
  StageContentTileSurface,
7802
9407
  {
7803
9408
  item,
7804
9409
  ...context,
7805
9410
  mediaVisible: Boolean(boardProps),
7806
- media: boardProps ? /* @__PURE__ */ jsx69("div", { className: cn("h-full", !context.primary && "pointer-events-none"), inert: !context.primary || void 0, children: /* @__PURE__ */ jsx69(
7807
- WhiteboardView,
7808
- {
7809
- ...boardProps,
7810
- canDraw: context.primary ? boardProps.canDraw : false,
7811
- collab: boardProps.collab ? { ...boardProps.collab, canDraw: context.primary && boardProps.collab.canDraw } : void 0,
7812
- isVisible: context.active,
7813
- className: cn("h-full !min-h-0", boardProps.className)
7814
- }
7815
- ) }) : null
9411
+ media: boardProps ? /* @__PURE__ */ jsx75("div", { className: cn("h-full", !context.primary && "pointer-events-none"), inert: !context.primary || void 0, children: /* @__PURE__ */ jsx75(Board, { board: boardProps, primary: context.primary, visible: context.active, colorScheme, people: tiles, following: followed, onFollow: follow, sendView: canFollow ? whiteboard?.sendView : void 0, onClose: onCloseBoard }) }) : null
7816
9412
  }
7817
9413
  );
7818
9414
  },
7819
- [whiteboard, shares, shareCapture, onStopSharing, onChangeShare]
9415
+ [follow, whiteboard, shares, shareCapture, onStopSharing, onChangeShare, colorScheme, tiles, followed, canFollow, onCloseBoard]
7820
9416
  );
7821
- const focusContent = useCallback12(
9417
+ const focusContent = useCallback14(
7822
9418
  (item) => {
7823
9419
  if (item.kind !== "participant") onLayoutChange?.("focus");
7824
9420
  },
7825
9421
  [onLayoutChange]
7826
9422
  );
7827
- return /* @__PURE__ */ jsx69(Stage, { items, layout, pinnedId: pin ?? activeShare, onPinnedChange: setPin, renderContentTile, onItemClick: focusContent, onOverflowClick, generatedAvatars, emptyState: /* @__PURE__ */ jsx69(QuietSpace, {}), className });
9423
+ return /* @__PURE__ */ jsx75(
9424
+ Stage,
9425
+ {
9426
+ items,
9427
+ layout,
9428
+ pinnedId: activePin ?? activeShare ?? (followed ? "whiteboard" : null),
9429
+ onPinnedChange: setPin,
9430
+ onFollowChange: (id) => follow(followed === id ? null : id),
9431
+ followingId: followed,
9432
+ clickAction: canFollow ? "follow" : "pin",
9433
+ renderContentTile,
9434
+ onItemClick: focusContent,
9435
+ onOverflowClick: canFollow ? void 0 : onOverflowClick,
9436
+ generatedAvatars,
9437
+ emptyState: /* @__PURE__ */ jsx75(QuietSpace, {}),
9438
+ className
9439
+ }
9440
+ );
7828
9441
  }
7829
9442
  SpaceStage.displayName = "SpaceStage";
7830
9443
 
7831
9444
  // src/components/space-view/useDrawerPresence.ts
7832
- import { useEffect as useEffect34, useState as useState41 } from "react";
9445
+ import { useEffect as useEffect39, useState as useState45 } from "react";
7833
9446
  var DRAWER_EXIT_MS = 200;
7834
9447
  function useDrawerPresence(panel, exitMs) {
7835
- const [retained, setRetained] = useState41(panel);
7836
- const [state, setState] = useState41(panel === null ? "closed" : "open");
7837
- useEffect34(() => {
9448
+ const [retained, setRetained] = useState45(panel);
9449
+ const [state, setState] = useState45(panel === null ? "closed" : "open");
9450
+ useEffect39(() => {
7838
9451
  if (panel !== null) {
7839
9452
  setRetained(panel);
7840
9453
  setState("open");
@@ -7851,7 +9464,7 @@ function useDrawerPresence(panel, exitMs) {
7851
9464
  }
7852
9465
 
7853
9466
  // src/components/space-view/SpaceView.tsx
7854
- import { Fragment as Fragment28, jsx as jsx70, jsxs as jsxs44 } from "react/jsx-runtime";
9467
+ import { Fragment as Fragment32, jsx as jsx76, jsxs as jsxs49 } from "react/jsx-runtime";
7855
9468
  function SpaceView({
7856
9469
  colorScheme = "dark",
7857
9470
  onColorSchemeChange,
@@ -7895,11 +9508,11 @@ function SpaceView({
7895
9508
  const canManageAdmission = useCan("manageAdmission");
7896
9509
  const canManageRecording = useCan("manageRecording");
7897
9510
  const episodeDuration = useEpisodeDuration();
7898
- const [uncontrolledLayout, setUncontrolledLayout] = useState42(controlledLayout ?? "focus");
7899
- const [activePanel, setActivePanel] = useState42(initialPanel);
7900
- const [isReactionPickerOpen, setReactionPickerOpen] = useState42(false);
7901
- const [commandError, setCommandError] = useState42(null);
7902
- const [commandSource, setCommandSource] = useState42();
9511
+ const [uncontrolledLayout, setUncontrolledLayout] = useState46(controlledLayout ?? "focus");
9512
+ const [activePanel, setActivePanel] = useState46(initialPanel);
9513
+ const [isReactionPickerOpen, setReactionPickerOpen] = useState46(false);
9514
+ const [commandError, setCommandError] = useState46(null);
9515
+ const [commandSource, setCommandSource] = useState46();
7903
9516
  const onCommandError = (failure, source) => {
7904
9517
  if (!failure && commandError && typeof commandError === "object") callToasts.dismissFailure(commandError.code);
7905
9518
  setCommandError(failure);
@@ -7909,15 +9522,15 @@ function SpaceView({
7909
9522
  const layout = onLayoutChange ? controlledLayout ?? uncontrolledLayout : uncontrolledLayout;
7910
9523
  const feature = (name) => features?.[name] !== false;
7911
9524
  const localId = self.participantId ?? "local";
7912
- const tiles = useMemo16(
9525
+ const tiles = useMemo18(
7913
9526
  () => toVideoParticipants(participantsSlice.roster, media.remote, localId, self.displayName ?? "You", media.local).map((tile) => ({
7914
9527
  ...tile,
7915
9528
  screenShareHasAudio: tile.isLocal ? media.screenShare.hasAudio : participantsSlice.roster.find((person) => person.participantId === tile.id)?.media.screenShareHasAudio
7916
9529
  })),
7917
9530
  [localId, media.local, media.remote, media.screenShare.hasAudio, participantsSlice.roster, self.displayName]
7918
9531
  );
7919
- useEffect35(() => setActivePanel(initialPanel), [initialPanel]);
7920
- useEffect35(() => {
9532
+ useEffect40(() => setActivePanel(initialPanel), [initialPanel]);
9533
+ useEffect40(() => {
7921
9534
  if (feature("admission") && feature("participants") && canManageAdmission && participantsSlice.admissionQueue.length > 0) setActivePanel("participants");
7922
9535
  }, [canManageAdmission, participantsSlice.admissionQueue.length, features?.admission, features?.participants]);
7923
9536
  const buttons = [
@@ -7939,7 +9552,7 @@ function SpaceView({
7939
9552
  const playSound = useSoundCues(feature("sounds") && soundsEnabled);
7940
9553
  const prefersReducedMotion = usePrefersReducedMotion();
7941
9554
  const drawer = useDrawerPresence(activePanel, prefersReducedMotion ? 0 : DRAWER_EXIT_MS);
7942
- const runCommand = useCallback13(async (command) => {
9555
+ const runCommand = useCallback15(async (command) => {
7943
9556
  try {
7944
9557
  await command();
7945
9558
  setCommandError(null);
@@ -7958,17 +9571,32 @@ function SpaceView({
7958
9571
  else if (commandSource === "microphone") await client.media.setMicrophoneEnabled(true);
7959
9572
  onCommandError(null);
7960
9573
  } catch (cause) {
9574
+ if (cause instanceof SpaceClientError2 && cause.code === "connection.invalid_state") return;
7961
9575
  onCommandError(cause instanceof SpaceClientError2 ? cause : { code: "client.internal_error", recoverable: true, message: "Your device couldn\u2019t start. Try again." }, commandSource);
7962
9576
  }
7963
9577
  };
7964
9578
  const retryableMedia = commandSource && typeof displayedCommandError === "object" && displayedCommandError?.recoverable && (commandSource === "screen" || displayedCommandError.code === "media.device_busy");
7965
9579
  const commandToasts = mappedFailure ? [{ id: "space-command-error", ...mappedFailure, action: retryableMedia ? { label: "Try again", onClick: () => void retryMedia() } : void 0 }] : typeof displayedCommandError === "string" ? [{ id: "space-command-error", message: displayedCommandError, type: "error" }] : [];
9580
+ const [boardToasts, setBoardToasts] = useState46([]);
9581
+ const boardToastCount = useRef34(0);
9582
+ useEffect40(() => {
9583
+ if (!whiteboard?.isOpen) setBoardToasts([]);
9584
+ }, [whiteboard?.isOpen]);
9585
+ const showBoardToast = useCallback15((toast) => {
9586
+ if (!toast) {
9587
+ setBoardToasts([]);
9588
+ return;
9589
+ }
9590
+ boardToastCount.current += 1;
9591
+ const id = `board-${boardToastCount.current}`;
9592
+ setBoardToasts((current) => [...current.slice(-2), { ...toast, id }]);
9593
+ }, []);
7966
9594
  const dismissCommandError = () => {
7967
9595
  if (commandError && typeof commandError === "object") callToasts.dismissFailure(commandError.code);
7968
9596
  setCommandError(null);
7969
9597
  onDismissCommandError?.();
7970
9598
  };
7971
- return /* @__PURE__ */ jsx70(ParticipantVolumeProvider, { children: /* @__PURE__ */ jsx70(
9599
+ return /* @__PURE__ */ jsx76(ParticipantVolumeProvider, { children: /* @__PURE__ */ jsx76(BoardToastContext.Provider, { value: showBoardToast, children: /* @__PURE__ */ jsx76(
7972
9600
  SpacePresentation,
7973
9601
  {
7974
9602
  colorScheme,
@@ -7987,21 +9615,24 @@ function SpaceView({
7987
9615
  diagnostics: callDiagnostics(connection, media),
7988
9616
  shortcuts: {
7989
9617
  isMuted: media.local.microphone.state !== "enabled",
7990
- onToggleMute: () => void runCommand(() => client.media.setMicrophoneEnabled(media.local.microphone.state !== "enabled")),
7991
- onToggleVideo: () => void runCommand(() => client.media.setCameraEnabled(media.local.camera.state !== "enabled")),
9618
+ onToggleMute: connection.status === "live" || media.local.microphone.state === "enabled" ? () => void runCommand(() => client.media.setMicrophoneEnabled(media.local.microphone.state !== "enabled")) : void 0,
9619
+ onToggleVideo: connection.status === "live" || media.local.camera.state === "enabled" ? () => void runCommand(() => client.media.setCameraEnabled(media.local.camera.state !== "enabled")) : void 0,
7992
9620
  onToggleHand: feature("handRaise") && canRaiseHand ? () => void runCommand(() => self.handRaised ? client.participants.lowerHand() : client.participants.raiseHand()) : void 0,
7993
9621
  onToggleChat: feature("chat") ? () => setActivePanel((current) => current === "chat" ? null : "chat") : void 0,
7994
9622
  onTogglePeople: feature("participants") ? () => setActivePanel((current) => current === "participants" ? null : "participants") : void 0,
7995
9623
  onToggleLayout: () => updateLayout(layout === "grid" ? "focus" : "grid"),
7996
9624
  onClosePanel: () => setActivePanel(null),
7997
- onPushToTalk: (enabled) => void runCommand(() => client.media.setMicrophoneEnabled(enabled))
9625
+ onPushToTalk: (enabled) => {
9626
+ if (enabled && connection.status !== "live") return;
9627
+ void runCommand(() => client.media.setMicrophoneEnabled(enabled));
9628
+ }
7998
9629
  },
7999
9630
  duration: episodeDuration,
8000
9631
  isRecording: recording.current?.status === "recording",
8001
9632
  layout,
8002
9633
  onLayoutChange: updateLayout
8003
9634
  },
8004
- stage: /* @__PURE__ */ jsx70(
9635
+ stage: /* @__PURE__ */ jsx76(
8005
9636
  SpaceStage,
8006
9637
  {
8007
9638
  tiles,
@@ -8009,16 +9640,20 @@ function SpaceView({
8009
9640
  onLayoutChange: updateLayout,
8010
9641
  generatedAvatars,
8011
9642
  whiteboard,
9643
+ onCloseBoard: whiteboard?.isOpen ? onToggleWhiteboard : void 0,
9644
+ colorScheme,
8012
9645
  onOverflowClick: feature("participants") ? () => setActivePanel("participants") : void 0,
8013
9646
  shareCapture: media.screenShare.capture,
8014
- onStopSharing: () => client.media.setScreenShareEnabled(false),
8015
- onChangeShare: async (options) => {
8016
- await client.media.setScreenShareEnabled(true, options);
9647
+ onStopSharing: async () => {
9648
+ await client.media.setScreenShareEnabled(false);
8017
9649
  },
9650
+ onChangeShare: connection.status === "live" ? async (options) => {
9651
+ await client.media.setScreenShareEnabled(true, options);
9652
+ } : void 0,
8018
9653
  className: "h-full"
8019
9654
  }
8020
9655
  ),
8021
- controls: /* @__PURE__ */ jsx70(
9656
+ controls: /* @__PURE__ */ jsx76(
8022
9657
  ControlBar,
8023
9658
  {
8024
9659
  density: "comfortable",
@@ -8038,9 +9673,9 @@ function SpaceView({
8038
9673
  onEndForEveryone: endCallForEveryone
8039
9674
  }
8040
9675
  ),
8041
- stageOverlay: /* @__PURE__ */ jsxs44(Fragment28, { children: [
8042
- feature("reactions") ? /* @__PURE__ */ jsx70(ReactionsOverlay, {}) : null,
8043
- feature("reactions") && canSendReaction ? /* @__PURE__ */ jsx70("div", { className: "absolute bottom-24 left-1/2 z-50 -translate-x-1/2", children: /* @__PURE__ */ jsx70(
9676
+ stageOverlay: /* @__PURE__ */ jsxs49(Fragment32, { children: [
9677
+ feature("reactions") ? /* @__PURE__ */ jsx76(ReactionsOverlay, {}) : null,
9678
+ feature("reactions") && canSendReaction && connection.status === "live" ? /* @__PURE__ */ jsx76("div", { className: "absolute bottom-24 left-1/2 z-50 -translate-x-1/2", children: /* @__PURE__ */ jsx76(
8044
9679
  ReactionPicker,
8045
9680
  {
8046
9681
  isHandRaised: self.handRaised,
@@ -8053,28 +9688,32 @@ function SpaceView({
8053
9688
  }
8054
9689
  ) }) : null
8055
9690
  ] }),
8056
- sidebar: /* @__PURE__ */ jsxs44(CallCard, { state: drawer.state, panel: drawer.panel, onPanelChange: setActivePanel, peopleCount: participantsSlice.roster.length, participantsEnabled: feature("participants"), chatEnabled: feature("chat"), onClose: () => setActivePanel(null), children: [
8057
- drawer.panel === "chat" && feature("chat") ? /* @__PURE__ */ jsx70(ChatPanel, { variant: "mobile", generatedAvatars, onClose: () => setActivePanel(null), pickChatFiles }) : null,
8058
- drawer.panel === "participants" && feature("participants") ? /* @__PURE__ */ jsx70(ParticipantsPanel, { variant: "mobile", admissionEnabled: feature("admission"), generatedAvatars, onCommandError, onClose: () => setActivePanel(null) }) : null,
8059
- drawer.panel === "admission" && feature("admission") ? /* @__PURE__ */ jsx70(AdmissionPanel, { className: "h-full w-full rounded-none shadow-none", onClose: () => setActivePanel(null) }) : null
9691
+ sidebar: /* @__PURE__ */ jsxs49(CallCard, { state: drawer.state, panel: drawer.panel, onPanelChange: setActivePanel, peopleCount: participantsSlice.roster.length, participantsEnabled: feature("participants"), chatEnabled: feature("chat"), onClose: () => setActivePanel(null), children: [
9692
+ drawer.panel === "chat" && feature("chat") ? /* @__PURE__ */ jsx76(ChatPanel, { variant: "mobile", generatedAvatars, onClose: () => setActivePanel(null), pickChatFiles }) : null,
9693
+ drawer.panel === "participants" && feature("participants") ? /* @__PURE__ */ jsx76(ParticipantsPanel, { variant: "mobile", admissionEnabled: feature("admission"), generatedAvatars, onCommandError, onClose: () => setActivePanel(null) }) : null,
9694
+ drawer.panel === "admission" && feature("admission") ? /* @__PURE__ */ jsx76(AdmissionPanel, { className: "h-full w-full rounded-none shadow-none", onClose: () => setActivePanel(null) }) : null
8060
9695
  ] }),
8061
- toasts: /* @__PURE__ */ jsx70(
9696
+ toasts: /* @__PURE__ */ jsx76(
8062
9697
  ToastStack,
8063
9698
  {
8064
- toasts: [...callToasts.toasts.filter((toast) => !(typeof displayedCommandError === "object" && displayedCommandError && toast.failureCode === displayedCommandError.code)), ...commandToasts],
8065
- onDismiss: (id) => id === "space-command-error" ? dismissCommandError() : callToasts.dismiss(id)
9699
+ toasts: [...callToasts.toasts.filter((toast) => !(typeof displayedCommandError === "object" && displayedCommandError && toast.failureCode === displayedCommandError.code)), ...whiteboard?.isOpen ? boardToasts : [], ...commandToasts],
9700
+ onDismiss: (id) => {
9701
+ if (id === "space-command-error") dismissCommandError();
9702
+ else if (id.startsWith("board-")) setBoardToasts((current) => current.filter((toast) => toast.id !== id));
9703
+ else callToasts.dismiss(id);
9704
+ }
8066
9705
  }
8067
9706
  ),
8068
- mediaOutput: /* @__PURE__ */ jsx70("div", { className: "[&>div]:top-[68px] [&>div>button]:h-11 [&>div>button]:shadow-[0_8px_24px_rgba(0,0,0,.4)]", children: /* @__PURE__ */ jsx70(AudioOutput, { resumeAudioLabel: "Your browser paused the sound. Click to hear the call" }) }),
8069
- overlays: /* @__PURE__ */ jsxs44(Fragment28, { children: [
8070
- /* @__PURE__ */ jsx70(CallReady, { open: callLinkOpen ?? callLink.open, onClose: onCallLinkToggle ?? callLink.close, link: effectiveShareUrl }),
9707
+ mediaOutput: /* @__PURE__ */ jsx76("div", { className: "[&>div]:top-[68px] [&>div>button]:h-11 [&>div>button]:shadow-[0_8px_24px_rgba(0,0,0,.4)]", children: /* @__PURE__ */ jsx76(AudioOutput, { resumeAudioLabel: "Your browser paused the sound. Click to hear the call" }) }),
9708
+ overlays: /* @__PURE__ */ jsxs49(Fragment32, { children: [
9709
+ /* @__PURE__ */ jsx76(CallReady, { open: callLinkOpen ?? callLink.open, onClose: onCallLinkToggle ?? callLink.close, link: effectiveShareUrl }),
8071
9710
  overlay,
8072
- reconnecting && reconnecting.status !== "reconnecting" ? /* @__PURE__ */ jsx70(ReconnectingOverlay, { ...reconnecting }) : null,
8073
- /* @__PURE__ */ jsx70(OfflineActions, { reconnecting: reconnecting?.status === "reconnecting" && reconnecting.isVisible })
9711
+ reconnecting && reconnecting.status !== "reconnecting" ? /* @__PURE__ */ jsx76(ReconnectingOverlay, { ...reconnecting }) : null,
9712
+ /* @__PURE__ */ jsx76(OfflineActions, { reconnecting: reconnecting?.status === "reconnecting" && reconnecting.isVisible })
8074
9713
  ] }),
8075
9714
  className
8076
9715
  }
8077
- ) });
9716
+ ) }) });
8078
9717
  }
8079
9718
 
8080
9719
  export {
@@ -8141,6 +9780,6 @@ export {
8141
9780
  Stage,
8142
9781
  ParticipantGrid,
8143
9782
  QuietSpace,
8144
- WhiteboardView,
8145
- SpaceView
9783
+ SpaceView,
9784
+ WhiteboardView
8146
9785
  };