@q9labsai/chalk-react 4.1.15 → 4.1.16

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 (235) hide show
  1. package/README.md +23 -1
  2. package/THIRD_PARTY_NOTICES.md +29 -0
  3. package/dist/bindings/hooks.d.ts +4 -1
  4. package/dist/bindings/hooks.d.ts.map +1 -1
  5. package/dist/{chunk-4X3NFZGL.js → chunk-C7PNVYDZ.js} +9679 -10020
  6. package/dist/chunk-FFPNO2MY.js +131 -0
  7. package/dist/chunk-HZ43I4OS.js +138 -0
  8. package/dist/chunk-RMOJ5DLX.js +1421 -0
  9. package/dist/chunk-S6OVGZD5.js +3164 -0
  10. package/dist/chunk-VLAZKVNN.js +182 -0
  11. package/dist/components/atomic/Avatar.d.ts +4 -0
  12. package/dist/components/atomic/Avatar.d.ts.map +1 -1
  13. package/dist/components/atomic/ClassicAvatar.d.ts +1 -1
  14. package/dist/components/atomic/ClassicAvatar.d.ts.map +1 -1
  15. package/dist/components/atomic/ClassicReactionBubble.d.ts +2 -1
  16. package/dist/components/atomic/ClassicReactionBubble.d.ts.map +1 -1
  17. package/dist/components/atomic/ReactionBubble.d.ts +5 -2
  18. package/dist/components/atomic/ReactionBubble.d.ts.map +1 -1
  19. package/dist/components/audio-output/AudioOutput.d.ts +3 -1
  20. package/dist/components/audio-output/AudioOutput.d.ts.map +1 -1
  21. package/dist/components/chalk/Chalk.d.ts +3 -1
  22. package/dist/components/chalk/Chalk.d.ts.map +1 -1
  23. package/dist/components/composite/ChatPanel.d.ts +24 -1
  24. package/dist/components/composite/ChatPanel.d.ts.map +1 -1
  25. package/dist/components/composite/ClassicChatPanel.d.ts +23 -0
  26. package/dist/components/composite/ClassicChatPanel.d.ts.map +1 -1
  27. package/dist/components/composite/ClassicMessageBubble.d.ts.map +1 -1
  28. package/dist/components/composite/ClassicSettingsDialog.d.ts +1 -35
  29. package/dist/components/composite/ClassicSettingsDialog.d.ts.map +1 -1
  30. package/dist/components/composite/MessageBubble.d.ts +7 -1
  31. package/dist/components/composite/MessageBubble.d.ts.map +1 -1
  32. package/dist/components/composite/ReactionsOverlay.d.ts +13 -0
  33. package/dist/components/composite/ReactionsOverlay.d.ts.map +1 -1
  34. package/dist/components/composite/ScreenShareView.d.ts +8 -0
  35. package/dist/components/composite/ScreenShareView.d.ts.map +1 -1
  36. package/dist/components/composite/SettingsDialog.d.ts +2 -84
  37. package/dist/components/composite/SettingsDialog.d.ts.map +1 -1
  38. package/dist/components/composite/chat-panel-model.d.ts +17 -0
  39. package/dist/components/composite/chat-panel-model.d.ts.map +1 -1
  40. package/dist/components/composite/chat-upload-file.d.ts +8 -0
  41. package/dist/components/composite/chat-upload-file.d.ts.map +1 -0
  42. package/dist/components/composite/reaction-float.d.ts +2 -0
  43. package/dist/components/composite/reaction-float.d.ts.map +1 -1
  44. package/dist/components/composite/settings-dialog-behavior.d.ts +645 -0
  45. package/dist/components/composite/settings-dialog-behavior.d.ts.map +1 -0
  46. package/dist/components/composite/use-chat-attachment-previews.d.ts +10 -0
  47. package/dist/components/composite/use-chat-attachment-previews.d.ts.map +1 -0
  48. package/dist/components/control-bar/ClassicControlBar.d.ts +2 -1
  49. package/dist/components/control-bar/ClassicControlBar.d.ts.map +1 -1
  50. package/dist/components/control-bar/ControlBar.d.ts +8 -26
  51. package/dist/components/control-bar/ControlBar.d.ts.map +1 -1
  52. package/dist/components/control-bar/control-bar-behavior.d.ts +123 -0
  53. package/dist/components/control-bar/control-bar-behavior.d.ts.map +1 -0
  54. package/dist/components/device-popover/ClassicDevicePopover.d.ts +1 -1
  55. package/dist/components/device-popover/ClassicDevicePopover.d.ts.map +1 -1
  56. package/dist/components/device-popover/DevicePopover.d.ts +1 -0
  57. package/dist/components/device-popover/DevicePopover.d.ts.map +1 -1
  58. package/dist/components/entrance/Entrance.d.ts +2 -1
  59. package/dist/components/entrance/Entrance.d.ts.map +1 -1
  60. package/dist/components/entrance/EntranceDeviceControls.d.ts.map +1 -1
  61. package/dist/components/feedback/FeedbackDialog.d.ts +12 -0
  62. package/dist/components/feedback/FeedbackDialog.d.ts.map +1 -0
  63. package/dist/components/index.d.ts +8 -0
  64. package/dist/components/index.d.ts.map +1 -1
  65. package/dist/components/index.js +31 -6
  66. package/dist/components/invite-dialog/ClassicInviteDialog.d.ts.map +1 -1
  67. package/dist/components/invite-dialog/InviteDialog.d.ts.map +1 -1
  68. package/dist/components/lazy.d.ts +2 -2
  69. package/dist/components/lazy.d.ts.map +1 -1
  70. package/dist/components/offline-actions/OfflineActions.d.ts +17 -0
  71. package/dist/components/offline-actions/OfflineActions.d.ts.map +1 -0
  72. package/dist/components/offline-actions/OfflineActionsSetting.d.ts +4 -0
  73. package/dist/components/offline-actions/OfflineActionsSetting.d.ts.map +1 -0
  74. package/dist/components/offline-actions/offline-actions-policy.d.ts +4 -0
  75. package/dist/components/offline-actions/offline-actions-policy.d.ts.map +1 -0
  76. package/dist/components/participant-tile/ParticipantTile.d.ts +7 -1
  77. package/dist/components/participant-tile/ParticipantTile.d.ts.map +1 -1
  78. package/dist/components/participant-tile/TileShell.d.ts +3 -1
  79. package/dist/components/participant-tile/TileShell.d.ts.map +1 -1
  80. package/dist/components/participants-panel/ParticipantsPanel.d.ts.map +1 -1
  81. package/dist/components/reconnecting-overlay/ClassicReconnectingOverlay.d.ts.map +1 -1
  82. package/dist/components/reconnecting-overlay/ReconnectingNotice.d.ts +6 -0
  83. package/dist/components/reconnecting-overlay/ReconnectingNotice.d.ts.map +1 -0
  84. package/dist/components/reconnecting-overlay/ReconnectingOverlay.d.ts +3 -0
  85. package/dist/components/reconnecting-overlay/ReconnectingOverlay.d.ts.map +1 -1
  86. package/dist/components/recording-history/RecordingHistoryPanel.d.ts +62 -0
  87. package/dist/components/recording-history/RecordingHistoryPanel.d.ts.map +1 -0
  88. package/dist/components/space-header/LayoutMenu.d.ts +1 -499
  89. package/dist/components/space-header/LayoutMenu.d.ts.map +1 -1
  90. package/dist/components/space-info-dialog/SpaceInfoContent.d.ts.map +1 -1
  91. package/dist/components/space-view/ClassicSpaceView.d.ts +1 -1
  92. package/dist/components/space-view/ClassicSpaceView.d.ts.map +1 -1
  93. package/dist/components/space-view/RecordingSpaceView.d.ts +26 -0
  94. package/dist/components/space-view/RecordingSpaceView.d.ts.map +1 -0
  95. package/dist/components/space-view/SpaceDrawer.d.ts +3 -2
  96. package/dist/components/space-view/SpaceDrawer.d.ts.map +1 -1
  97. package/dist/components/space-view/SpacePresentation.d.ts +27 -0
  98. package/dist/components/space-view/SpacePresentation.d.ts.map +1 -0
  99. package/dist/components/space-view/SpaceStage.d.ts.map +1 -1
  100. package/dist/components/space-view/SpaceView.d.ts +2 -0
  101. package/dist/components/space-view/SpaceView.d.ts.map +1 -1
  102. package/dist/components/stage/Stage.d.ts +22 -2
  103. package/dist/components/stage/Stage.d.ts.map +1 -1
  104. package/dist/components/stage/StageContentTile.d.ts +5 -0
  105. package/dist/components/stage/StageContentTile.d.ts.map +1 -1
  106. package/dist/components/stage/StagePager.d.ts +2 -1
  107. package/dist/components/stage/StagePager.d.ts.map +1 -1
  108. package/dist/components/stage/stage-items.d.ts +0 -1
  109. package/dist/components/stage/stage-items.d.ts.map +1 -1
  110. package/dist/components/transcript-panel/ClassicTranscriptPanel.d.ts +1 -1
  111. package/dist/components/transcript-panel/ClassicTranscriptPanel.d.ts.map +1 -1
  112. package/dist/components/transcript-panel/TranscriptPanel.d.ts +2 -25
  113. package/dist/components/transcript-panel/TranscriptPanel.d.ts.map +1 -1
  114. package/dist/components/transcript-panel/transcript-panel-behavior.d.ts +84 -0
  115. package/dist/components/transcript-panel/transcript-panel-behavior.d.ts.map +1 -0
  116. package/dist/components/whiteboard-view/RecordingWhiteboardView.d.ts +23 -0
  117. package/dist/components/whiteboard-view/RecordingWhiteboardView.d.ts.map +1 -0
  118. package/dist/components/whiteboard-view/recording-whiteboard-state.d.ts +31 -0
  119. package/dist/components/whiteboard-view/recording-whiteboard-state.d.ts.map +1 -0
  120. package/dist/headless.d.ts +8 -0
  121. package/dist/headless.d.ts.map +1 -0
  122. package/dist/headless.js +28 -0
  123. package/dist/index.d.ts +9 -1
  124. package/dist/index.d.ts.map +1 -1
  125. package/dist/index.js +32 -4
  126. package/dist/internal/useWhiteboardSceneSubscription.d.ts.map +1 -1
  127. package/dist/public-surface.contract.d.ts +2 -2
  128. package/dist/public-surface.contract.d.ts.map +1 -1
  129. package/dist/selectors/space-selectors.d.ts.map +1 -1
  130. package/dist/test-support/index.d.ts +20 -0
  131. package/dist/test-support/index.d.ts.map +1 -0
  132. package/dist/test-support/index.js +508 -0
  133. package/dist/test-support/preview-client.d.ts +5 -0
  134. package/dist/test-support/preview-client.d.ts.map +1 -1
  135. package/dist/test-support/preview-feedback.d.ts +3 -0
  136. package/dist/test-support/preview-feedback.d.ts.map +1 -0
  137. package/dist/utils/animated-icon-renderer.d.ts +6 -0
  138. package/dist/utils/animated-icon-renderer.d.ts.map +1 -0
  139. package/dist/utils/animated-icon-runtime.d.ts +19 -0
  140. package/dist/utils/animated-icon-runtime.d.ts.map +1 -0
  141. package/dist/utils/animated-icons/arrow-left-02.d.ts +11 -0
  142. package/dist/utils/animated-icons/arrow-left-02.d.ts.map +1 -0
  143. package/dist/utils/animated-icons/cancel-01.d.ts +11 -0
  144. package/dist/utils/animated-icons/cancel-01.d.ts.map +1 -0
  145. package/dist/utils/animated-icons/clock-01.d.ts +11 -0
  146. package/dist/utils/animated-icons/clock-01.d.ts.map +1 -0
  147. package/dist/utils/animated-icons/copy-01.d.ts +13 -0
  148. package/dist/utils/animated-icons/copy-01.d.ts.map +1 -0
  149. package/dist/utils/animated-icons/crown.d.ts +11 -0
  150. package/dist/utils/animated-icons/crown.d.ts.map +1 -0
  151. package/dist/utils/animated-icons/download-01.d.ts +11 -0
  152. package/dist/utils/animated-icons/download-01.d.ts.map +1 -0
  153. package/dist/utils/animated-icons/edit-02.d.ts +11 -0
  154. package/dist/utils/animated-icons/edit-02.d.ts.map +1 -0
  155. package/dist/utils/animated-icons/home-01.d.ts +11 -0
  156. package/dist/utils/animated-icons/home-01.d.ts.map +1 -0
  157. package/dist/utils/animated-icons/image-01.d.ts +13 -0
  158. package/dist/utils/animated-icons/image-01.d.ts.map +1 -0
  159. package/dist/utils/animated-icons/index.d.ts +85 -0
  160. package/dist/utils/animated-icons/index.d.ts.map +1 -0
  161. package/dist/utils/animated-icons/information-circle.d.ts +11 -0
  162. package/dist/utils/animated-icons/information-circle.d.ts.map +1 -0
  163. package/dist/utils/animated-icons/link-01.d.ts +11 -0
  164. package/dist/utils/animated-icons/link-01.d.ts.map +1 -0
  165. package/dist/utils/animated-icons/mail-01.d.ts +11 -0
  166. package/dist/utils/animated-icons/mail-01.d.ts.map +1 -0
  167. package/dist/utils/animated-icons/maximize-screen.d.ts +11 -0
  168. package/dist/utils/animated-icons/maximize-screen.d.ts.map +1 -0
  169. package/dist/utils/animated-icons/message-01.d.ts +11 -0
  170. package/dist/utils/animated-icons/message-01.d.ts.map +1 -0
  171. package/dist/utils/animated-icons/mic-01.d.ts +11 -0
  172. package/dist/utils/animated-icons/mic-01.d.ts.map +1 -0
  173. package/dist/utils/animated-icons/moon-02.d.ts +11 -0
  174. package/dist/utils/animated-icons/moon-02.d.ts.map +1 -0
  175. package/dist/utils/animated-icons/more-horizontal.d.ts +11 -0
  176. package/dist/utils/animated-icons/more-horizontal.d.ts.map +1 -0
  177. package/dist/utils/animated-icons/more-vertical.d.ts +11 -0
  178. package/dist/utils/animated-icons/more-vertical.d.ts.map +1 -0
  179. package/dist/utils/animated-icons/pause.d.ts +11 -0
  180. package/dist/utils/animated-icons/pause.d.ts.map +1 -0
  181. package/dist/utils/animated-icons/pin.d.ts +11 -0
  182. package/dist/utils/animated-icons/pin.d.ts.map +1 -0
  183. package/dist/utils/animated-icons/play.d.ts +11 -0
  184. package/dist/utils/animated-icons/play.d.ts.map +1 -0
  185. package/dist/utils/animated-icons/plus-sign.d.ts +11 -0
  186. package/dist/utils/animated-icons/plus-sign.d.ts.map +1 -0
  187. package/dist/utils/animated-icons/qr-code-01.d.ts +11 -0
  188. package/dist/utils/animated-icons/qr-code-01.d.ts.map +1 -0
  189. package/dist/utils/animated-icons/refresh.d.ts +11 -0
  190. package/dist/utils/animated-icons/refresh.d.ts.map +1 -0
  191. package/dist/utils/animated-icons/search-01.d.ts +11 -0
  192. package/dist/utils/animated-icons/search-01.d.ts.map +1 -0
  193. package/dist/utils/animated-icons/sent.d.ts +11 -0
  194. package/dist/utils/animated-icons/sent.d.ts.map +1 -0
  195. package/dist/utils/animated-icons/settings-01.d.ts +11 -0
  196. package/dist/utils/animated-icons/settings-01.d.ts.map +1 -0
  197. package/dist/utils/animated-icons/share-08.d.ts +11 -0
  198. package/dist/utils/animated-icons/share-08.d.ts.map +1 -0
  199. package/dist/utils/animated-icons/shield-02.d.ts +11 -0
  200. package/dist/utils/animated-icons/shield-02.d.ts.map +1 -0
  201. package/dist/utils/animated-icons/smile.d.ts +11 -0
  202. package/dist/utils/animated-icons/smile.d.ts.map +1 -0
  203. package/dist/utils/animated-icons/sparkles.d.ts +11 -0
  204. package/dist/utils/animated-icons/sparkles.d.ts.map +1 -0
  205. package/dist/utils/animated-icons/star.d.ts +11 -0
  206. package/dist/utils/animated-icons/star.d.ts.map +1 -0
  207. package/dist/utils/animated-icons/thumbs-up.d.ts +11 -0
  208. package/dist/utils/animated-icons/thumbs-up.d.ts.map +1 -0
  209. package/dist/utils/animated-icons/upload-01.d.ts +11 -0
  210. package/dist/utils/animated-icons/upload-01.d.ts.map +1 -0
  211. package/dist/utils/animated-icons/user-add-01.d.ts +11 -0
  212. package/dist/utils/animated-icons/user-add-01.d.ts.map +1 -0
  213. package/dist/utils/animated-icons/user-group.d.ts +11 -0
  214. package/dist/utils/animated-icons/user-group.d.ts.map +1 -0
  215. package/dist/utils/animated-icons/user-remove-01.d.ts +11 -0
  216. package/dist/utils/animated-icons/user-remove-01.d.ts.map +1 -0
  217. package/dist/utils/animated-icons/video-01.d.ts +11 -0
  218. package/dist/utils/animated-icons/video-01.d.ts.map +1 -0
  219. package/dist/utils/animated-icons/volume-high.d.ts +11 -0
  220. package/dist/utils/animated-icons/volume-high.d.ts.map +1 -0
  221. package/dist/utils/animated-icons/volume-mute-01.d.ts +11 -0
  222. package/dist/utils/animated-icons/volume-mute-01.d.ts.map +1 -0
  223. package/dist/utils/animated-icons/zoom-in-area.d.ts +11 -0
  224. package/dist/utils/animated-icons/zoom-in-area.d.ts.map +1 -0
  225. package/dist/utils/animated-icons/zoom-out-area.d.ts +11 -0
  226. package/dist/utils/animated-icons/zoom-out-area.d.ts.map +1 -0
  227. package/dist/utils/feedback.d.ts +5 -0
  228. package/dist/utils/feedback.d.ts.map +1 -0
  229. package/dist/utils/icons.d.ts +42 -42
  230. package/dist/utils/icons.d.ts.map +1 -1
  231. package/dist/utils/index.d.ts +5 -0
  232. package/dist/utils/index.d.ts.map +1 -1
  233. package/dist/utils/index.js +297 -1
  234. package/package.json +25 -8
  235. package/dist/chunk-A3L7BOCE.js +0 -210
@@ -0,0 +1,1421 @@
1
+ import {
2
+ parseRecordingWhiteboardStateV1,
3
+ recordingWhiteboardFileAssetId,
4
+ recordingWhiteboardFileIds,
5
+ recordingWhiteboardSceneAppState
6
+ } from "./chunk-HZ43I4OS.js";
7
+ import {
8
+ ChalkBackdrop,
9
+ ChalkBadge,
10
+ ChalkButton,
11
+ ChalkDialogPanel,
12
+ ChalkEmptyState,
13
+ ChalkIconButton,
14
+ ChalkPanel,
15
+ ChalkProvider,
16
+ ChalkTextarea,
17
+ ChatPanelPresentation,
18
+ ControlBarPresentation,
19
+ EntranceSurface,
20
+ MediaRequestDialog,
21
+ ParticipantTileSurface,
22
+ QuietSpace,
23
+ ReactionsOverlayPresentation,
24
+ ScreenSharePresentation,
25
+ SettingsDialog,
26
+ SkinProvider,
27
+ SpaceDrawer,
28
+ SpacePresentation,
29
+ SpaceView,
30
+ Stage,
31
+ StageContentTileSurface,
32
+ StatusSurface,
33
+ WhiteboardView,
34
+ chalkThemeStyle,
35
+ getThemeMode,
36
+ useCan,
37
+ useConnection,
38
+ useMedia,
39
+ useOffline,
40
+ useParticipants,
41
+ useSelf,
42
+ useSkin,
43
+ useSpaceClient,
44
+ useWhiteboard
45
+ } from "./chunk-C7PNVYDZ.js";
46
+ import {
47
+ WHITEBOARD_ITEM_ID,
48
+ screenShareItemId
49
+ } from "./chunk-VLAZKVNN.js";
50
+ import {
51
+ captureFeedbackScreenshot,
52
+ collectBrowserFeedbackEvidence
53
+ } from "./chunk-FFPNO2MY.js";
54
+ import {
55
+ Cancel01Icon2 as Cancel01Icon,
56
+ Download01Icon2 as Download01Icon,
57
+ FileTextIcon,
58
+ Loading01Icon,
59
+ PlayIcon2 as PlayIcon,
60
+ RefreshIcon2 as RefreshIcon,
61
+ Video01Icon2 as Video01Icon,
62
+ cn
63
+ } from "./chunk-S6OVGZD5.js";
64
+
65
+ // src/components/entrance/Entrance.tsx
66
+ import { useEffect, useState } from "react";
67
+ import { describeMediaCaptureError } from "@q9labsai/chalk-client";
68
+ import { jsx } from "react/jsx-runtime";
69
+ function Entrance({
70
+ spaceName,
71
+ logoUrl,
72
+ defaultDisplayName = "",
73
+ defaults,
74
+ joining = false,
75
+ error,
76
+ theme,
77
+ generatedAvatars = true,
78
+ onJoin,
79
+ onCancel,
80
+ onPrepare,
81
+ audioInputDevices,
82
+ videoInputDevices,
83
+ audioOutputDevices,
84
+ selectedAudioInput: selectedAudioInputProp,
85
+ selectedVideoInput: selectedVideoInputProp,
86
+ selectedAudioOutput: selectedAudioOutputProp,
87
+ onAudioInputChange,
88
+ onVideoInputChange,
89
+ onAudioOutputChange
90
+ }) {
91
+ useEffect(() => {
92
+ void onPrepare?.().catch(() => void 0);
93
+ }, [onPrepare]);
94
+ const [displayName, setDisplayName] = useState(defaultDisplayName);
95
+ const [microphone, setMicrophone] = useState(defaults?.microphone ?? true);
96
+ const [camera, setCamera] = useState(defaults?.camera ?? true);
97
+ const [selectedAudioInput, setSelectedAudioInput] = useState(selectedAudioInputProp ?? "");
98
+ const [selectedVideoInput, setSelectedVideoInput] = useState(selectedVideoInputProp ?? "");
99
+ const [selectedAudioOutput, setSelectedAudioOutput] = useState(selectedAudioOutputProp ?? "");
100
+ const [enumeratedDevices, setEnumeratedDevices] = useState({ audioInputDevices: [], videoInputDevices: [], audioOutputDevices: [] });
101
+ const [previewError, setPreviewError] = useState(null);
102
+ const [previewStream, setPreviewStream] = useState(null);
103
+ useEffect(() => {
104
+ if (selectedAudioInputProp !== void 0) setSelectedAudioInput(selectedAudioInputProp ?? "");
105
+ }, [selectedAudioInputProp]);
106
+ useEffect(() => {
107
+ if (selectedVideoInputProp !== void 0) setSelectedVideoInput(selectedVideoInputProp ?? "");
108
+ }, [selectedVideoInputProp]);
109
+ useEffect(() => {
110
+ if (selectedAudioOutputProp !== void 0) setSelectedAudioOutput(selectedAudioOutputProp ?? "");
111
+ }, [selectedAudioOutputProp]);
112
+ const hasMissingDeviceList = audioInputDevices === void 0 || videoInputDevices === void 0 || audioOutputDevices === void 0;
113
+ useEffect(() => {
114
+ if (!hasMissingDeviceList || !navigator.mediaDevices?.enumerateDevices) return;
115
+ let cancelled = false;
116
+ void navigator.mediaDevices.enumerateDevices().then((devices) => {
117
+ if (cancelled) return;
118
+ setEnumeratedDevices({
119
+ audioInputDevices: devicesOfKind(devices, "audioinput"),
120
+ videoInputDevices: devicesOfKind(devices, "videoinput"),
121
+ audioOutputDevices: devicesOfKind(devices, "audiooutput")
122
+ });
123
+ }).catch(() => void 0);
124
+ return () => {
125
+ cancelled = true;
126
+ };
127
+ }, [hasMissingDeviceList]);
128
+ const effectiveAudioInputDevices = audioInputDevices ?? enumeratedDevices.audioInputDevices;
129
+ const effectiveVideoInputDevices = videoInputDevices ?? enumeratedDevices.videoInputDevices;
130
+ const effectiveAudioOutputDevices = audioOutputDevices ?? enumeratedDevices.audioOutputDevices;
131
+ useEffect(() => {
132
+ if (!navigator.mediaDevices?.getUserMedia || !microphone && !camera) {
133
+ setPreviewStream((current) => {
134
+ stopTracks(current);
135
+ return null;
136
+ });
137
+ return;
138
+ }
139
+ let cancelled = false;
140
+ let requestedStream = null;
141
+ void navigator.mediaDevices.getUserMedia({ audio: captureConstraint(microphone, selectedAudioInput), video: captureConstraint(camera, selectedVideoInput) }).then((stream) => {
142
+ requestedStream = stream;
143
+ if (cancelled) {
144
+ stopTracks(stream);
145
+ return;
146
+ }
147
+ setPreviewError(null);
148
+ setPreviewStream((current) => {
149
+ stopTracks(current);
150
+ return stream;
151
+ });
152
+ }).catch((cause) => {
153
+ if (!cancelled) setPreviewError(describeMediaCaptureError(cause).message);
154
+ });
155
+ return () => {
156
+ cancelled = true;
157
+ stopTracks(requestedStream);
158
+ };
159
+ }, [camera, microphone, selectedAudioInput, selectedVideoInput]);
160
+ useEffect(() => () => stopTracks(previewStream), [previewStream]);
161
+ const submit = () => {
162
+ const normalizedDisplayName = displayName.trim();
163
+ if (!normalizedDisplayName || joining) return;
164
+ void onJoin({
165
+ displayName: normalizedDisplayName,
166
+ microphone,
167
+ camera,
168
+ ...selectedAudioInput ? { audioInputDeviceId: selectedAudioInput } : {},
169
+ ...selectedVideoInput ? { videoInputDeviceId: selectedVideoInput } : {},
170
+ ...selectedAudioOutput ? { audioOutputDeviceId: selectedAudioOutput } : {}
171
+ });
172
+ };
173
+ const selectAudioInput = (deviceId) => {
174
+ setSelectedAudioInput(deviceId);
175
+ void onAudioInputChange?.(deviceId);
176
+ };
177
+ const selectVideoInput = (deviceId) => {
178
+ setSelectedVideoInput(deviceId);
179
+ void onVideoInputChange?.(deviceId);
180
+ };
181
+ const selectAudioOutput = (deviceId) => {
182
+ setSelectedAudioOutput(deviceId);
183
+ void onAudioOutputChange?.(deviceId);
184
+ };
185
+ return /* @__PURE__ */ jsx(
186
+ EntranceSurface,
187
+ {
188
+ spaceName,
189
+ logoUrl,
190
+ displayName,
191
+ microphone,
192
+ camera,
193
+ joining,
194
+ error,
195
+ theme,
196
+ generatedAvatars,
197
+ previewError,
198
+ previewStream,
199
+ onDisplayNameChange: setDisplayName,
200
+ onMicrophoneChange: setMicrophone,
201
+ onCameraChange: setCamera,
202
+ audioInputDevices: effectiveAudioInputDevices,
203
+ videoInputDevices: effectiveVideoInputDevices,
204
+ audioOutputDevices: effectiveAudioOutputDevices,
205
+ selectedAudioInput,
206
+ selectedVideoInput,
207
+ selectedAudioOutput,
208
+ onAudioInputChange: selectAudioInput,
209
+ onVideoInputChange: selectVideoInput,
210
+ onAudioOutputChange: selectAudioOutput,
211
+ onSubmit: submit,
212
+ onCancel
213
+ }
214
+ );
215
+ }
216
+ function devicesOfKind(devices, kind) {
217
+ return devices.filter((device) => device.kind === kind).map(({ deviceId, label }) => ({ deviceId, label }));
218
+ }
219
+ function captureConstraint(enabled, deviceId) {
220
+ if (!enabled) return false;
221
+ return deviceId ? { deviceId: { exact: deviceId } } : true;
222
+ }
223
+ function stopTracks(stream) {
224
+ stream?.getTracks().forEach((track) => track.stop());
225
+ }
226
+
227
+ // src/components/feedback/FeedbackDialog.tsx
228
+ import { useCallback, useEffect as useEffect2, useRef, useState as useState2 } from "react";
229
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
230
+ var CATEGORIES = [
231
+ { value: "bug", label: "Bug" },
232
+ { value: "feature_request", label: "Feature request" },
233
+ { value: "other", label: "Other" }
234
+ ];
235
+ function FeedbackDialog({ isOpen, onClose, client, source = "embedded", captureRootRef }) {
236
+ const skin = useSkin();
237
+ const [category, setCategory] = useState2("bug");
238
+ const [message, setMessage] = useState2("");
239
+ const [prepared, setPrepared] = useState2(null);
240
+ const [screenshotUrl, setScreenshotUrl] = useState2();
241
+ const [screenshotFailure, setScreenshotFailure] = useState2();
242
+ const [preparing, setPreparing] = useState2(false);
243
+ const [submitting, setSubmitting] = useState2(false);
244
+ const [prepareError, setPrepareError] = useState2();
245
+ const [submitError, setSubmitError] = useState2();
246
+ const [submitted, setSubmitted] = useState2(false);
247
+ const openedRef = useRef(false);
248
+ const preparationRef = useRef(0);
249
+ const primaryText = skin === "classic" ? "!text-white" : "!text-[var(--chalk-accent-text)]";
250
+ const prepareFeedback = useCallback(
251
+ async (includeScreenshot) => {
252
+ const generation = ++preparationRef.current;
253
+ setPreparing(true);
254
+ setPrepared(null);
255
+ setScreenshotUrl(void 0);
256
+ setScreenshotFailure(void 0);
257
+ setPrepareError(void 0);
258
+ try {
259
+ const evidence = collectBrowserFeedbackEvidence();
260
+ const next = await client.feedback.prepare({
261
+ source,
262
+ evidence,
263
+ screenshot_provider: () => includeScreenshot ? captureFeedbackScreenshot(captureRootRef.current) : { state: "removed" }
264
+ });
265
+ if (generation !== preparationRef.current) return;
266
+ setPrepared(next);
267
+ setScreenshotUrl(next.screenshot ? `data:${next.screenshot.mime_type};base64,${next.screenshot.data_base64}` : void 0);
268
+ setScreenshotFailure(next.evidence.screenshot.state === "captured" || next.evidence.screenshot.state === "partial" ? void 0 : next.evidence.screenshot.failure_code);
269
+ } catch (cause) {
270
+ if (generation !== preparationRef.current) return;
271
+ setPrepared(null);
272
+ setScreenshotUrl(void 0);
273
+ setPrepareError(cause instanceof Error ? cause.message : "Feedback evidence is unavailable. You can still send this message.");
274
+ } finally {
275
+ if (generation === preparationRef.current) setPreparing(false);
276
+ }
277
+ },
278
+ [captureRootRef, client, source]
279
+ );
280
+ useEffect2(() => {
281
+ if (!isOpen) {
282
+ openedRef.current = false;
283
+ preparationRef.current += 1;
284
+ return;
285
+ }
286
+ if (openedRef.current) return;
287
+ openedRef.current = true;
288
+ setCategory("bug");
289
+ setMessage("");
290
+ setPrepared(null);
291
+ setScreenshotUrl(void 0);
292
+ setScreenshotFailure(void 0);
293
+ setPrepareError(void 0);
294
+ setSubmitError(void 0);
295
+ setSubmitted(false);
296
+ void prepareFeedback(true);
297
+ }, [isOpen, prepareFeedback]);
298
+ useEffect2(() => {
299
+ if (!isOpen) return;
300
+ const onKeyDown = (event) => {
301
+ if (event.key === "Escape") onClose();
302
+ };
303
+ window.addEventListener("keydown", onKeyDown);
304
+ return () => window.removeEventListener("keydown", onKeyDown);
305
+ }, [isOpen, onClose]);
306
+ if (!isOpen) return null;
307
+ const reviseFeedback = () => {
308
+ if (!submitError) return;
309
+ setSubmitError(void 0);
310
+ setPrepared(null);
311
+ void prepareFeedback(screenshotUrl !== void 0);
312
+ };
313
+ const submit = (event) => {
314
+ event.preventDefault();
315
+ const normalizedMessage = message.trim();
316
+ if (!normalizedMessage || submitting) return;
317
+ setSubmitting(true);
318
+ setSubmitError(void 0);
319
+ const send = async () => {
320
+ try {
321
+ if (prepared) {
322
+ await prepared.send({ category, message: normalizedMessage });
323
+ } else {
324
+ const evidence = collectBrowserFeedbackEvidence();
325
+ await client.feedback.send({ category, message: normalizedMessage, source, evidence: { ...evidence, screenshot: { state: "unavailable", failure_code: "capture_failed" } } });
326
+ }
327
+ setSubmitted(true);
328
+ } catch (cause) {
329
+ setSubmitError(cause instanceof Error ? cause.message : "Feedback could not be sent. Please try again.");
330
+ } finally {
331
+ setSubmitting(false);
332
+ }
333
+ };
334
+ void send();
335
+ };
336
+ return /* @__PURE__ */ jsxs("div", { "data-chalk-feedback-private": true, "data-chalk-skin": skin, className: "fixed inset-0 z-[70]", onMouseDown: onClose, children: [
337
+ /* @__PURE__ */ jsx2(ChalkBackdrop, { className: "z-0 !bg-[color-mix(in_srgb,var(--chalk-app-canvas)_90%,transparent)] !backdrop-blur-[1px]" }),
338
+ /* @__PURE__ */ jsx2("div", { className: "relative z-10 grid h-full place-items-center p-4", children: /* @__PURE__ */ jsxs(
339
+ ChalkDialogPanel,
340
+ {
341
+ className: cn("chalk-textured-surface w-full max-w-lg overflow-hidden !rounded-[14px] !border border-[var(--chalk-app-line-strong)] bg-[var(--chalk-app-panel)] text-[var(--chalk-app-text)] !p-0 shadow-[var(--chalk-app-shadow-sm)]"),
342
+ "aria-modal": "true",
343
+ "aria-labelledby": "chalk-feedback-title",
344
+ onMouseDown: (event) => event.stopPropagation(),
345
+ children: [
346
+ /* @__PURE__ */ jsxs("header", { className: "flex items-start justify-between gap-5 border-b border-[var(--chalk-app-line)] px-6 py-5", children: [
347
+ /* @__PURE__ */ jsxs("div", { children: [
348
+ /* @__PURE__ */ jsx2("h2", { id: "chalk-feedback-title", className: "text-xl font-semibold tracking-[-0.025em]", children: "Send feedback" }),
349
+ /* @__PURE__ */ jsx2("p", { className: "mt-1 text-sm text-[var(--chalk-app-text-muted)]", children: "Tell Chalk what happened. We\u2019ll include safe technical context to help us understand it." })
350
+ ] }),
351
+ /* @__PURE__ */ jsx2(ChalkIconButton, { type: "button", onClick: onClose, size: "sm", "aria-label": "Close feedback dialog", className: "!h-9 !w-9 !rounded-[8px] text-[var(--chalk-app-text-muted)]", children: /* @__PURE__ */ jsx2(Cancel01Icon, { size: 19 }) })
352
+ ] }),
353
+ submitted ? /* @__PURE__ */ jsxs("div", { className: "space-y-5 p-6", children: [
354
+ /* @__PURE__ */ jsx2("p", { role: "status", className: "text-sm leading-6 text-[var(--chalk-app-text)]", children: "Thanks. Chalk received your feedback." }),
355
+ /* @__PURE__ */ jsx2(ChalkButton, { type: "button", variant: "solid", tone: "accent", className: cn("w-full", primaryText), onClick: onClose, children: "Done" })
356
+ ] }) : /* @__PURE__ */ jsxs("form", { className: "space-y-5 p-6", onSubmit: submit, children: [
357
+ /* @__PURE__ */ jsxs("fieldset", { className: "space-y-2", children: [
358
+ /* @__PURE__ */ jsx2("legend", { className: "text-sm font-semibold", children: "What kind of feedback is this?" }),
359
+ /* @__PURE__ */ jsx2("div", { className: "grid gap-2 sm:grid-cols-3", children: CATEGORIES.map((option) => /* @__PURE__ */ jsxs("label", { className: cn("flex cursor-pointer items-center gap-2 rounded-md border border-[var(--chalk-app-line)] px-3 py-2 text-sm", category === option.value && "border-[var(--chalk-app-control-active-line)] bg-[var(--chalk-app-control-active)]/10"), children: [
360
+ /* @__PURE__ */ jsx2(
361
+ "input",
362
+ {
363
+ type: "radio",
364
+ name: "feedback-category",
365
+ value: option.value,
366
+ checked: category === option.value,
367
+ onChange: () => {
368
+ setCategory(option.value);
369
+ reviseFeedback();
370
+ }
371
+ }
372
+ ),
373
+ /* @__PURE__ */ jsx2("span", { children: option.label })
374
+ ] }, option.value)) })
375
+ ] }),
376
+ /* @__PURE__ */ jsxs("label", { className: "block text-sm font-semibold", htmlFor: "chalk-feedback-message", children: [
377
+ "Message",
378
+ /* @__PURE__ */ jsx2(
379
+ ChalkTextarea,
380
+ {
381
+ id: "chalk-feedback-message",
382
+ className: "mt-2",
383
+ required: true,
384
+ maxLength: 8e3,
385
+ value: message,
386
+ onChange: (event) => {
387
+ setMessage(event.target.value);
388
+ reviseFeedback();
389
+ },
390
+ placeholder: "What should we know?",
391
+ "aria-describedby": "chalk-feedback-message-help"
392
+ }
393
+ )
394
+ ] }),
395
+ /* @__PURE__ */ jsx2("p", { id: "chalk-feedback-message-help", className: "text-xs text-[var(--chalk-app-text-muted)]", children: "One message is enough. Please don\u2019t include secrets or private content." }),
396
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", "aria-live": "polite", children: [
397
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
398
+ /* @__PURE__ */ jsx2("p", { className: "text-sm font-semibold", children: "Screenshot" }),
399
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
400
+ /* @__PURE__ */ jsxs(ChalkButton, { type: "button", variant: "outline", disabled: preparing, onClick: () => void prepareFeedback(true), className: "!h-8 !rounded-[7px] !px-2.5 !text-xs", children: [
401
+ /* @__PURE__ */ jsx2(RefreshIcon, { size: 14 }),
402
+ "Refresh"
403
+ ] }),
404
+ /* @__PURE__ */ jsx2(ChalkButton, { type: "button", variant: "outline", disabled: submitting || !preparing && !screenshotUrl, onClick: () => void prepareFeedback(false), className: "!h-8 !rounded-[7px] !px-2.5 !text-xs", children: "Remove" })
405
+ ] })
406
+ ] }),
407
+ screenshotUrl ? /* @__PURE__ */ jsx2("img", { src: screenshotUrl, alt: "Screenshot preview", className: "max-h-36 w-full rounded-md border border-[var(--chalk-app-line)] object-cover" }) : /* @__PURE__ */ jsx2("p", { className: "rounded-md border border-dashed border-[var(--chalk-app-line)] px-3 py-3 text-xs text-[var(--chalk-app-text-muted)]", children: preparing ? "Preparing screenshot\u2026" : screenshotFailure ? "Screenshot unavailable. You can still send feedback." : "No screenshot attached." })
408
+ ] }),
409
+ prepareError ? /* @__PURE__ */ jsx2("p", { className: "text-xs text-[var(--chalk-app-text-muted)]", children: prepareError }) : null,
410
+ submitError ? /* @__PURE__ */ jsx2("p", { role: "alert", className: "text-sm text-[var(--chalk-app-danger)]", children: submitError }) : null,
411
+ /* @__PURE__ */ jsx2(ChalkButton, { type: "submit", variant: "solid", tone: "accent", loading: submitting, disabled: !message.trim(), className: cn("w-full", primaryText), children: "Send feedback" })
412
+ ] })
413
+ ]
414
+ }
415
+ ) })
416
+ ] });
417
+ }
418
+
419
+ // src/components/chalk/Chalk.tsx
420
+ import { createSpaceClient } from "@q9labsai/chalk-client";
421
+ import { useCallback as useCallback2, useEffect as useEffect4, useMemo, useRef as useRef2, useState as useState4 } from "react";
422
+
423
+ // src/internal/useWhiteboardSceneSubscription.ts
424
+ import { useEffect as useEffect3, useState as useState3 } from "react";
425
+ var errorMessage = (cause) => cause instanceof Error ? cause : new Error("Whiteboard could not connect.");
426
+ var summaryError = (summary) => summary.error ? new Error(summary.error.message) : errorMessage(void 0);
427
+ var MAX_RECOVERY_ATTEMPTS = 3;
428
+ var recoveryDelayMs = (attempt) => 250 * 2 ** attempt;
429
+ function useWhiteboardSceneSubscription(transport, active) {
430
+ const [subscription, setSubscription] = useState3({ status: "closed" });
431
+ useEffect3(() => {
432
+ if (!active || !transport) {
433
+ setSubscription({ status: "closed" });
434
+ return;
435
+ }
436
+ let disposed = false;
437
+ let starting = false;
438
+ let startupSettled = false;
439
+ let ready = false;
440
+ let recoveryAttempts = 0;
441
+ let recoveryTimer;
442
+ let latestSummary;
443
+ const observesSummary = transport.subscribeSummary !== void 0;
444
+ const clearRecoveryTimer = () => {
445
+ if (recoveryTimer === void 0) return;
446
+ globalThis.clearTimeout(recoveryTimer);
447
+ recoveryTimer = void 0;
448
+ };
449
+ const recover = (cause) => {
450
+ if (disposed || recoveryTimer !== void 0 || starting) return;
451
+ if (recoveryAttempts >= MAX_RECOVERY_ATTEMPTS) {
452
+ setSubscription({ status: "failed", error: errorMessage(cause) });
453
+ return;
454
+ }
455
+ const attempt = recoveryAttempts++;
456
+ setSubscription({ status: "loading" });
457
+ recoveryTimer = globalThis.setTimeout(() => {
458
+ recoveryTimer = void 0;
459
+ start();
460
+ }, recoveryDelayMs(attempt));
461
+ };
462
+ const handleSummary = (summary) => {
463
+ latestSummary = summary;
464
+ if (disposed) return;
465
+ if (summary.status === "unsubscribed") {
466
+ setSubscription({ status: "loading" });
467
+ return;
468
+ }
469
+ if (summary.status === "ready") {
470
+ if (!starting || startupSettled) {
471
+ recoveryAttempts = 0;
472
+ ready = true;
473
+ setSubscription({ status: "ready", transport });
474
+ }
475
+ return;
476
+ }
477
+ if (summary.status === "loading" || summary.status === "recovering") {
478
+ if (!ready) setSubscription({ status: "loading" });
479
+ return;
480
+ }
481
+ if (summary.status === "failed") {
482
+ starting = false;
483
+ setSubscription({ status: "loading" });
484
+ recover(summaryError(summary));
485
+ }
486
+ };
487
+ const start = () => {
488
+ if (disposed || starting) return;
489
+ clearRecoveryTimer();
490
+ starting = true;
491
+ startupSettled = false;
492
+ setSubscription({ status: "loading" });
493
+ void Promise.resolve().then(() => transport.startSceneSubscription()).then(
494
+ () => {
495
+ starting = false;
496
+ startupSettled = true;
497
+ if (disposed) return;
498
+ if (!latestSummary || latestSummary.status === "ready") {
499
+ recoveryAttempts = 0;
500
+ ready = true;
501
+ setSubscription({ status: "ready", transport });
502
+ }
503
+ },
504
+ (cause) => {
505
+ starting = false;
506
+ if (!disposed && observesSummary) recover(cause);
507
+ else if (!disposed) setSubscription({ status: "failed", error: errorMessage(cause) });
508
+ }
509
+ );
510
+ };
511
+ setSubscription({ status: "loading" });
512
+ const unsubscribeSummary = transport.subscribeSummary?.(handleSummary);
513
+ start();
514
+ return () => {
515
+ disposed = true;
516
+ clearRecoveryTimer();
517
+ unsubscribeSummary?.();
518
+ void Promise.resolve(transport.stopSceneSubscription());
519
+ };
520
+ }, [active, transport]);
521
+ return subscription;
522
+ }
523
+
524
+ // src/whiteboard/wire-adapters.ts
525
+ function fromWhiteboardWireElement(element) {
526
+ return {
527
+ id: element.id,
528
+ type: element.type,
529
+ version: element.version,
530
+ versionNonce: element.version_nonce,
531
+ index: element.index,
532
+ isDeleted: element.is_deleted,
533
+ payload: element.payload
534
+ };
535
+ }
536
+ function toWhiteboardWireElement(element) {
537
+ return {
538
+ id: element.id,
539
+ type: element.type,
540
+ version: element.version,
541
+ version_nonce: element.versionNonce,
542
+ index: element.index,
543
+ is_deleted: element.isDeleted,
544
+ payload: element.payload
545
+ };
546
+ }
547
+ function toWhiteboardCollaborationEvent(event) {
548
+ if (event.type === "snapshot" || event.type === "update") {
549
+ return {
550
+ ...event,
551
+ elements: event.elements.map(toWhiteboardWireElement)
552
+ };
553
+ }
554
+ if (event.type === "cursor") {
555
+ return {
556
+ type: "cursor",
557
+ participantId: event.participantId,
558
+ displayName: event.displayName,
559
+ x: event.x,
560
+ y: event.y,
561
+ occurredAt: event.occurredAt
562
+ };
563
+ }
564
+ return event;
565
+ }
566
+
567
+ // src/components/chalk/Chalk.tsx
568
+ import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
569
+ function Chalk(props) {
570
+ const colorScheme = useResolvedColorScheme(props.theme?.palette ? getThemeMode(props.theme.palette) : props.theme?.colorScheme);
571
+ const skin = props.theme?.skin ?? "classic";
572
+ const suppliedClient = props.client;
573
+ const getAccessRef = useRef2(props.getAccess);
574
+ getAccessRef.current = props.getAccess;
575
+ const getLatestAccess = useCallback2((context) => {
576
+ const latestGetAccess = getAccessRef.current;
577
+ if (!latestGetAccess) return Promise.reject(new Error("Chalk cannot refresh access after getAccess was removed."));
578
+ return latestGetAccess(context);
579
+ }, []);
580
+ const ownedClient = useMemo(() => {
581
+ if (suppliedClient) return null;
582
+ if (!props.space || !getAccessRef.current) throw new Error("Chalk requires either client or both space and getAccess.");
583
+ return createSpaceClient({ space: props.space, getAccess: getLatestAccess });
584
+ }, [getLatestAccess, props.space, suppliedClient]);
585
+ const client = suppliedClient ?? ownedClient;
586
+ const feedbackRootRef = useRef2(null);
587
+ useEffect4(() => {
588
+ if (!ownedClient) return;
589
+ return () => {
590
+ void ownedClient.leave().catch(() => void 0).finally(() => ownedClient.dispose());
591
+ };
592
+ }, [ownedClient]);
593
+ return /* @__PURE__ */ jsx3(SkinProvider, { skin, children: /* @__PURE__ */ jsx3(
594
+ "div",
595
+ {
596
+ ref: feedbackRootRef,
597
+ "data-chalk": true,
598
+ "data-chalk-feedback-root": true,
599
+ "data-chalk-theme": colorScheme,
600
+ "data-chalk-palette": props.theme?.palette,
601
+ "data-chalk-texture": props.theme?.texture,
602
+ "data-chalk-skin": skin,
603
+ className: "chalk-root h-full min-h-0 w-full",
604
+ style: chalkThemeStyle(props.theme, colorScheme),
605
+ children: /* @__PURE__ */ jsx3(ChalkProvider, { client, children: /* @__PURE__ */ jsx3(SpaceExperience, { ...props, feedbackRootRef, resolvedColorScheme: colorScheme }) })
606
+ }
607
+ ) });
608
+ }
609
+ function SpaceExperience(props) {
610
+ const client = useSpaceClient();
611
+ const connection = useConnection();
612
+ const offline = useOffline();
613
+ const participants = useParticipants();
614
+ const previousStatus = useRef2(connection.status);
615
+ const hasObservedStatus = useRef2(false);
616
+ const hasBeenLive = useRef2(connection.status === "live" || connection.status === "reconnecting");
617
+ const autoJoinAttempted = useRef2(false);
618
+ const previousClient = useRef2(client);
619
+ const [joinError, setJoinError] = useState4(null);
620
+ const lastEpisode = useRef2(connection.episode);
621
+ const lastParticipantCount = useRef2(participants.roster.length);
622
+ const [retryPending, setRetryPending] = useState4(false);
623
+ const [episodeEnded, setEpisodeEnded] = useState4(false);
624
+ const [endedAt, setEndedAt] = useState4(null);
625
+ const entrance = props.entrance ?? true;
626
+ const spaceName = props.spaceName ?? props.space ?? "Space";
627
+ if (connection.episode) lastEpisode.current = connection.episode;
628
+ if (participants.roster.length > 0) lastParticipantCount.current = participants.roster.length;
629
+ if (previousClient.current !== client) {
630
+ previousClient.current = client;
631
+ previousStatus.current = connection.status;
632
+ hasObservedStatus.current = false;
633
+ hasBeenLive.current = connection.status === "live" || connection.status === "reconnecting";
634
+ autoJoinAttempted.current = false;
635
+ }
636
+ useClientEvents(client, props);
637
+ const join = useCallback2(
638
+ async (settings) => {
639
+ try {
640
+ setJoinError(null);
641
+ await client.join(settings);
642
+ } catch (cause) {
643
+ setJoinError(cause instanceof Error ? cause.message : "Could not enter this Space.");
644
+ }
645
+ },
646
+ [client]
647
+ );
648
+ const retryAutomaticJoin = useCallback2(() => {
649
+ autoJoinAttempted.current = true;
650
+ setRetryPending(true);
651
+ void join(defaultJoinOptions(props)).finally(() => setRetryPending(false));
652
+ }, [join, props]);
653
+ useEffect4(() => setJoinError(null), [client]);
654
+ useEffect4(() => {
655
+ setEpisodeEnded(false);
656
+ setEndedAt(null);
657
+ }, [client]);
658
+ useEffect4(
659
+ () => client.on("episodeEnded", () => {
660
+ setEpisodeEnded(true);
661
+ setEndedAt((/* @__PURE__ */ new Date()).toISOString());
662
+ }),
663
+ [client]
664
+ );
665
+ useEffect4(() => {
666
+ if (entrance || connection.status !== "idle" || autoJoinAttempted.current) return;
667
+ autoJoinAttempted.current = true;
668
+ void join(defaultJoinOptions(props));
669
+ }, [connection.status, entrance, join, props.defaults?.camera, props.defaults?.microphone, props.displayName]);
670
+ useEffect4(() => {
671
+ const previous = previousStatus.current;
672
+ if (hasObservedStatus.current) {
673
+ if (connection.status === "live" && previous !== "live" && previous !== "reconnecting") props.onJoined?.();
674
+ if (connection.status === "left" && previous !== "left") props.onLeft?.();
675
+ }
676
+ previousStatus.current = connection.status;
677
+ hasObservedStatus.current = true;
678
+ if (connection.status === "live" || connection.status === "reconnecting") hasBeenLive.current = true;
679
+ if (connection.status === "left" && previous !== "left") setEndedAt((current) => current ?? (/* @__PURE__ */ new Date()).toISOString());
680
+ }, [connection.status, props.onJoined, props.onLeft]);
681
+ if (connection.status === "idle") {
682
+ if (!entrance) return /* @__PURE__ */ jsx3(StatusSurface, { message: joinError ?? `Entering ${spaceName}\u2026`, onRetry: joinError ? retryAutomaticJoin : void 0, retryPending, retryError: joinError });
683
+ return /* @__PURE__ */ jsx3(Entrance, { spaceName, logoUrl: props.logoUrl, defaultDisplayName: props.displayName, defaults: props.defaults, error: joinError ?? void 0, theme: props.theme, onJoin: join });
684
+ }
685
+ if (connection.status === "joining")
686
+ return entrance ? /* @__PURE__ */ jsx3(Entrance, { spaceName, logoUrl: props.logoUrl, defaultDisplayName: props.displayName, defaults: props.defaults, joining: true, error: joinError ?? void 0, theme: props.theme, onJoin: join }) : /* @__PURE__ */ jsx3(StatusSurface, { message: `Entering ${spaceName}\u2026` });
687
+ if (connection.status === "failed") {
688
+ const failureMessage = [connection.lastError?.message ?? joinError ?? "Unable to enter this Space.", offline.notice].filter(Boolean).join(" ");
689
+ if (!hasBeenLive.current && entrance) return /* @__PURE__ */ jsx3(Entrance, { spaceName, logoUrl: props.logoUrl, defaultDisplayName: props.displayName, defaults: props.defaults, error: failureMessage, theme: props.theme, onJoin: join });
690
+ return /* @__PURE__ */ jsx3(StatusSurface, { phase: "failed", message: failureMessage, onRetry: retryAutomaticJoin, retryPending, retryError: joinError });
691
+ }
692
+ if (connection.status === "leaving") return /* @__PURE__ */ jsx3(StatusSurface, { phase: "leaving", message: `Leaving ${spaceName}\u2026`, spaceName });
693
+ if (connection.status === "left")
694
+ return /* @__PURE__ */ jsx3(
695
+ StatusSurface,
696
+ {
697
+ phase: episodeEnded ? "episode-ended" : "left",
698
+ message: [episodeEnded ? "This Episode has ended for everyone." : "Your connection is closed. You can re-enter from here.", offline.notice].filter(Boolean).join(" "),
699
+ spaceName,
700
+ episode: lastEpisode.current,
701
+ endedAt,
702
+ participantCount: lastParticipantCount.current,
703
+ onRetry: episodeEnded ? void 0 : retryAutomaticJoin,
704
+ retryPending,
705
+ retryError: joinError
706
+ }
707
+ );
708
+ return /* @__PURE__ */ jsx3(SpaceSurface, { ...props, spaceName });
709
+ }
710
+ function SpaceSurface(props) {
711
+ const client = useSpaceClient();
712
+ const connection = useConnection();
713
+ const self = useSelf();
714
+ const media = useMedia();
715
+ const whiteboardState = useWhiteboard();
716
+ const canEndEpisode = useCan("endEpisode");
717
+ const canDrawWhiteboard = useCan("drawWhiteboard");
718
+ const [settingsOpen, setSettingsOpen] = useState4(false);
719
+ const [settingsInitialSection, setSettingsInitialSection] = useState4();
720
+ const [infoOpen, setInfoOpen] = useState4(false);
721
+ const [feedbackOpen, setFeedbackOpen] = useState4(false);
722
+ const [commandError, setCommandError] = useState4(null);
723
+ const [settings, setSettings] = useState4(() => createSettings(self.displayName ?? "", props.layout ?? "focus", props.theme?.skin ?? "classic", props.theme?.palette ?? (props.resolvedColorScheme === "dark" ? "warm-charcoal" : "light"), props.theme?.texture ?? "none"));
724
+ const resolvedSkin = props.theme?.skin ?? "classic";
725
+ const resolvedPalette = props.theme?.palette ?? (props.resolvedColorScheme === "dark" ? "warm-charcoal" : "light");
726
+ const resolvedTexture = props.theme?.texture ?? "none";
727
+ const appearancePaletteExplicitRef = useRef2(props.theme?.palette !== void 0);
728
+ const settingsRef = useRef2(settings);
729
+ settingsRef.current = settings;
730
+ useEffect4(() => {
731
+ setSettings((current) => ({
732
+ ...current,
733
+ identity: { displayName: self.displayName ?? current.identity.displayName },
734
+ join: { audioEnabled: media.local.microphone.state === "enabled" || media.local.microphone.state === "requesting", videoEnabled: media.local.camera.state === "enabled" || media.local.camera.state === "requesting" },
735
+ audio: { ...current.audio, selectedInput: media.selection.microphone ?? void 0, selectedOutput: media.selection.speaker ?? void 0 },
736
+ video: { ...current.video, selectedInput: media.selection.camera ?? void 0 }
737
+ }));
738
+ }, [media.local.camera.state, media.local.microphone.state, media.selection.camera, media.selection.microphone, media.selection.speaker, self.displayName]);
739
+ useEffect4(() => {
740
+ if (!props.theme?.skin && !props.theme?.palette && !props.theme?.texture) return;
741
+ setSettings((current) => ({
742
+ ...current,
743
+ appearance: {
744
+ ...current.appearance,
745
+ skin: props.theme?.skin ?? current.appearance.skin,
746
+ palette: props.theme?.palette ?? current.appearance.palette,
747
+ texture: props.theme?.texture ?? current.appearance.texture,
748
+ theme: (props.theme?.palette ?? current.appearance.palette) === "light" ? "light" : "dark"
749
+ }
750
+ }));
751
+ }, [props.theme?.palette, props.theme?.skin, props.theme?.texture]);
752
+ useEffect4(() => {
753
+ if (props.theme?.palette || appearancePaletteExplicitRef.current) return;
754
+ const palette = props.resolvedColorScheme === "dark" ? "warm-charcoal" : "light";
755
+ setSettings((current) => current.appearance.palette === palette ? current : { ...current, appearance: { ...current.appearance, palette, theme: palette === "light" ? "light" : "dark" } });
756
+ }, [props.resolvedColorScheme, props.theme?.palette, props.theme?.texture]);
757
+ const runCommand = useCallback2(async (command) => {
758
+ try {
759
+ await command();
760
+ setCommandError(null);
761
+ } catch (cause) {
762
+ setCommandError(cause instanceof Error ? cause.message : "This command could not be completed.");
763
+ }
764
+ }, []);
765
+ const whiteboardTransport = client.whiteboard.transport();
766
+ const whiteboardAvailable = props.features?.whiteboard !== false;
767
+ const whiteboardSubscription = useWhiteboardSceneSubscription(whiteboardTransport, whiteboardAvailable);
768
+ const setWhiteboardPresentation = whiteboardSubscription.status === "ready" ? bindWhiteboardPresentation(whiteboardSubscription.transport) : void 0;
769
+ useEffect4(() => {
770
+ if (whiteboardSubscription.status === "failed") setCommandError(whiteboardSubscription.error.message);
771
+ else if (whiteboardSubscription.status === "loading" || whiteboardSubscription.status === "ready") setCommandError(null);
772
+ }, [whiteboardSubscription]);
773
+ const whiteboard = whiteboardAvailable && whiteboardState.engine.presenting && whiteboardSubscription.status === "ready" ? {
774
+ isOpen: true,
775
+ props: {
776
+ canDraw: canDrawWhiteboard,
777
+ collab: {
778
+ canDraw: canDrawWhiteboard,
779
+ subscribe: (listener) => whiteboardSubscription.transport.subscribe((event) => listener(toWhiteboardCollaborationEvent(event))),
780
+ submitUpdate: async (input) => whiteboardSubscription.transport.submitUpdate({ sceneId: input.sceneId, syncAll: input.syncAll, elements: input.elements.map(fromWhiteboardWireElement) }),
781
+ sendCursor: (input) => whiteboardSubscription.transport.sendCursor(input),
782
+ requestSnapshot: () => whiteboardSubscription.transport.requestSnapshot(),
783
+ onSubmissionError: (cause) => setCommandError(cause instanceof Error ? cause.message : "Whiteboard could not sync."),
784
+ clear: () => whiteboardSubscription.transport.clear(),
785
+ initiateUpload: (input) => whiteboardSubscription.transport.files.initiateUpload(input),
786
+ finalizeUpload: (uploadId) => whiteboardSubscription.transport.files.finalizeUpload(uploadId),
787
+ presignDownload: (fileId) => whiteboardSubscription.transport.files.getDownloadUrl(fileId)
788
+ }
789
+ }
790
+ } : void 0;
791
+ return /* @__PURE__ */ jsx3(
792
+ SpaceView,
793
+ {
794
+ spaceName: props.spaceName,
795
+ logoUrl: props.logoUrl,
796
+ inviteLink: props.inviteLink,
797
+ pickChatFiles: props.pickChatFiles,
798
+ skin: settings.appearance.skin ?? resolvedSkin,
799
+ palette: settings.appearance.palette ?? resolvedPalette,
800
+ texture: settings.appearance.texture ?? resolvedTexture,
801
+ generatedAvatars: settings.appearance.generatedAvatars,
802
+ commandError: commandError ?? void 0,
803
+ onDismissCommandError: () => setCommandError(null),
804
+ layout: settings.appearance.layout === "focus" || settings.appearance.layout === "grid" || settings.appearance.layout === "presentation" ? settings.appearance.layout : props.layout ?? "focus",
805
+ onLayoutChange: (nextLayout) => {
806
+ setSettings((current) => ({ ...current, appearance: { ...current.appearance, layout: nextLayout } }));
807
+ props.onLayoutChange?.(nextLayout);
808
+ },
809
+ features: { ...props.features, sounds: props.features?.sounds !== false && settings.experience.sounds },
810
+ onOpenDiagnostics: props.onOpenDiagnostics,
811
+ onOpenFeedback: () => setFeedbackOpen(true),
812
+ whiteboard,
813
+ onToggleWhiteboard: canDrawWhiteboard && setWhiteboardPresentation ? () => void runCommand(() => setWhiteboardPresentation(!whiteboardState.engine.presenting)) : void 0,
814
+ infoDialog: props.features?.info !== false && (props.inviteLink || props.diagnosticReference || props.spaceDescription) ? {
815
+ isOpen: infoOpen,
816
+ onOpenChange: setInfoOpen,
817
+ spaceName: props.spaceName,
818
+ spaceDescription: props.spaceDescription,
819
+ inviteLink: props.inviteLink,
820
+ onCopyLink: props.inviteLink ? () => void navigator.clipboard?.writeText(props.inviteLink) : void 0,
821
+ diagnosticReference: props.diagnosticReference,
822
+ onCopyDiagnosticReference: (reference) => void navigator.clipboard?.writeText(reference),
823
+ onSendFeedback: props.onSendFeedback
824
+ } : void 0,
825
+ onOpenSettings: props.features?.settings !== false ? () => {
826
+ setSettingsInitialSection(void 0);
827
+ setSettingsOpen(true);
828
+ } : void 0,
829
+ settingsDialog: props.features?.settings !== false ? /* @__PURE__ */ jsx3(
830
+ SettingsDialog,
831
+ {
832
+ isOpen: settingsOpen,
833
+ onClose: () => {
834
+ setSettingsOpen(false);
835
+ setSettingsInitialSection(void 0);
836
+ },
837
+ initialSection: settingsInitialSection,
838
+ settings,
839
+ onUpdateIdentity: (updates) => {
840
+ setSettings((current) => ({ ...current, identity: { ...current.identity, ...updates } }));
841
+ const nextDisplayName = updates.displayName?.trim();
842
+ if (nextDisplayName) void runCommand(() => client.participants.renameSelf(nextDisplayName));
843
+ },
844
+ onUpdateJoin: (updates) => {
845
+ setSettings((current) => ({ ...current, join: { ...current.join, ...updates } }));
846
+ if (updates.audioEnabled !== void 0) runCommand(() => client.media.setMicrophoneEnabled(updates.audioEnabled));
847
+ if (updates.videoEnabled !== void 0) runCommand(() => client.media.setCameraEnabled(updates.videoEnabled));
848
+ },
849
+ onUpdateAudio: (updates) => {
850
+ setSettings((current) => ({ ...current, audio: { ...current.audio, ...updates } }));
851
+ if (updates.selectedInput) runCommand(() => client.media.selectMicrophone(updates.selectedInput));
852
+ if (updates.selectedOutput) runCommand(() => client.media.selectSpeaker(updates.selectedOutput));
853
+ },
854
+ onUpdateVideo: (updates) => {
855
+ setSettings((current) => ({ ...current, video: { ...current.video, ...updates } }));
856
+ if (updates.selectedInput) runCommand(() => client.media.selectCamera(updates.selectedInput));
857
+ },
858
+ onUpdateAppearance: (updates) => {
859
+ if (updates.palette !== void 0) appearancePaletteExplicitRef.current = true;
860
+ const current = settingsRef.current;
861
+ if (updates.skin !== void 0 && updates.skin !== current.appearance.skin) setSettingsInitialSection("appearance");
862
+ const next = { ...current, appearance: { ...current.appearance, ...updates } };
863
+ settingsRef.current = next;
864
+ setSettings(next);
865
+ if (updates.layout === "focus" || updates.layout === "grid" || updates.layout === "presentation") props.onLayoutChange?.(updates.layout);
866
+ },
867
+ onUpdateExperience: (updates) => setSettings((current) => ({ ...current, experience: { ...current.experience, ...updates } })),
868
+ audioInputDevices: media.devices.microphones.map((device) => ({ ...device, kind: "audioinput" })),
869
+ audioOutputDevices: media.devices.speakers.map((device) => ({ ...device, kind: "audiooutput" })),
870
+ videoInputDevices: media.devices.cameras.map((device) => ({ ...device, kind: "videoinput" })),
871
+ videoTrack: media.local.camera.track,
872
+ participantColorSeed: self.displayName ?? void 0
873
+ }
874
+ ) : void 0,
875
+ onLeft: () => runCommand(() => client.leave()),
876
+ onEndEpisode: canEndEpisode ? () => runCommand(() => client.endEpisode()) : void 0,
877
+ reconnecting: { isVisible: connection.status === "reconnecting", status: "reconnecting", showNotice: false },
878
+ overlay: /* @__PURE__ */ jsxs2(Fragment, { children: [
879
+ media.incomingRequests[0] ? /* @__PURE__ */ jsx3(MediaRequestDialog, { request: media.incomingRequests[0], onDecline: () => void runCommand(() => client.media.declineRequest(media.incomingRequests[0].requestId)), onAllow: () => void runCommand(() => client.media.acceptRequest(media.incomingRequests[0].requestId)) }) : null,
880
+ /* @__PURE__ */ jsx3(FeedbackDialog, { isOpen: feedbackOpen, onClose: () => setFeedbackOpen(false), client, source: props.feedbackSource, captureRootRef: props.feedbackRootRef })
881
+ ] })
882
+ }
883
+ );
884
+ }
885
+ function bindWhiteboardPresentation(transport) {
886
+ const setPresentation = transport.setPresentation;
887
+ if (!setPresentation) return void 0;
888
+ return (presenting) => setPresentation.call(transport, presenting);
889
+ }
890
+ function defaultJoinOptions(props) {
891
+ const displayName = props.displayName?.trim();
892
+ return { ...displayName ? { displayName } : {}, microphone: props.defaults?.microphone ?? true, camera: props.defaults?.camera ?? true };
893
+ }
894
+ function useResolvedColorScheme(requested = "light") {
895
+ const [resolved, setResolved] = useState4(() => requested === "system" ? readSystemColorScheme() : requested);
896
+ useEffect4(() => {
897
+ if (requested !== "system") return void setResolved(requested);
898
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return void setResolved("light");
899
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
900
+ const update = () => setResolved(mediaQuery.matches ? "dark" : "light");
901
+ update();
902
+ mediaQuery.addEventListener?.("change", update);
903
+ return () => mediaQuery.removeEventListener?.("change", update);
904
+ }, [requested]);
905
+ return requested === "system" ? resolved : requested;
906
+ }
907
+ function readSystemColorScheme() {
908
+ return typeof window !== "undefined" && window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
909
+ }
910
+ function useClientEvents(client, callbacks) {
911
+ useEffect4(() => client.on("participantJoined", (event) => callbacks.onParticipantJoined?.(event)), [callbacks.onParticipantJoined, client]);
912
+ useEffect4(() => client.on("participantLeft", (event) => callbacks.onParticipantLeft?.(event)), [callbacks.onParticipantLeft, client]);
913
+ useEffect4(() => client.on("episodeEnded", (event) => callbacks.onEpisodeEnded?.(event)), [callbacks.onEpisodeEnded, client]);
914
+ useEffect4(() => client.on("screenShareStarted", (event) => callbacks.onScreenShareStarted?.(event)), [callbacks.onScreenShareStarted, client]);
915
+ useEffect4(() => client.on("screenShareStopped", (event) => callbacks.onScreenShareStopped?.(event)), [callbacks.onScreenShareStopped, client]);
916
+ useEffect4(() => client.on("error", (event) => callbacks.onError?.(event)), [callbacks.onError, client]);
917
+ }
918
+ function createSettings(displayName, layout, skin, palette, texture) {
919
+ return {
920
+ identity: { displayName },
921
+ join: { videoEnabled: true, audioEnabled: true },
922
+ audio: { outputVolume: 100, noiseSuppression: false, echoCancellation: true, autoGainControl: true },
923
+ video: { quality: "auto" },
924
+ appearance: { layout, theme: palette === "light" ? "light" : "dark", skin, palette, texture, gradient: "default", showFilmstrip: true, reducedMotion: false, generatedAvatars: true, profileGradient: { mode: "auto" }, ambientBackground: true },
925
+ experience: { captions: false, compactMode: false, showInviteToast: true, defaultOpenChat: false, defaultOpenParticipants: false, defaultOpenTranscription: false, autoOpenPictureInPicture: false, sounds: true }
926
+ };
927
+ }
928
+
929
+ // src/components/space-view/RecordingSpaceView.tsx
930
+ import { CHALK_CHAT_ATTACHMENT_MIME_TYPES } from "@q9labsai/chalk-client";
931
+ import { jsx as jsx4 } from "react/jsx-runtime";
932
+ var RECORDING_TOOLBAR_BUTTONS = ["record", "participants", "chat", "reactions", "whiteboard"];
933
+ var CHAT_ATTACHMENT_MIME_TYPES = new Set(CHALK_CHAT_ATTACHMENT_MIME_TYPES);
934
+ function hasRenderableMedia(media) {
935
+ return media !== null && media !== void 0 && media !== false;
936
+ }
937
+ function isChatAttachmentMimeType(value) {
938
+ return CHAT_ATTACHMENT_MIME_TYPES.has(value);
939
+ }
940
+ function toChatMessages(frame) {
941
+ const displayTimes = /* @__PURE__ */ new Map();
942
+ const attachmentAssets = /* @__PURE__ */ new Map();
943
+ const messages = frame.chat.messages.map((message) => {
944
+ displayTimes.set(message.id, message.displayTime);
945
+ const attachments = message.attachments.map((attachment) => {
946
+ if (!isChatAttachmentMimeType(attachment.contentType)) throw new TypeError(`unsupported recording chat attachment content type: ${attachment.contentType}`);
947
+ attachmentAssets.set(attachment.id, attachment.assetId);
948
+ return {
949
+ attachmentId: attachment.id,
950
+ fileName: attachment.fileName,
951
+ mimeType: attachment.contentType,
952
+ byteLength: attachment.byteSize
953
+ };
954
+ });
955
+ return {
956
+ messageId: message.id,
957
+ clientMessageId: message.id,
958
+ sequence: String(message.sequence),
959
+ participantId: message.participantId,
960
+ displayName: message.displayName,
961
+ text: message.text,
962
+ // The supplied displayTime is authoritative. This stable ISO value only
963
+ // satisfies the shared chat model and is never formatted for presentation.
964
+ createdAt: new Date(message.createdAtMs).toISOString(),
965
+ attachments
966
+ };
967
+ });
968
+ return { messages, displayTimes, attachmentAssets };
969
+ }
970
+ function toParticipant(participant, resolveAssetUrl) {
971
+ return {
972
+ id: participant.id,
973
+ displayName: participant.displayName,
974
+ isSpeaking: participant.speaking,
975
+ isActiveSpeaker: participant.activeSpeaker,
976
+ isMuted: participant.microphoneMuted,
977
+ isVideoEnabled: participant.cameraEnabled,
978
+ isScreenSharing: participant.screenShareEnabled,
979
+ isHandRaised: participant.handRaised,
980
+ avatarUrl: participant.avatarAssetId ? resolveAssetUrl(participant.avatarAssetId) : void 0
981
+ };
982
+ }
983
+ function RecordingSpaceView({ frame, resolveMedia, resolveAssetUrl, renderWhiteboard, renderToken, className }) {
984
+ const sharedContent = frame.sharedContent;
985
+ const joinedParticipants = [...frame.participants.filter((participant) => participant.joined)].sort((left, right) => left.joinOrdinal - right.joinOrdinal || left.id.localeCompare(right.id));
986
+ const recordingParticipantsById = new Map(joinedParticipants.map((participant) => [participant.id, participant]));
987
+ const participantsById = new Map(joinedParticipants.map((participant) => [participant.id, toParticipant(participant, resolveAssetUrl)]));
988
+ const visibleMedia = frame.media.filter((source) => source.visible);
989
+ const mediaNodes = /* @__PURE__ */ new Map();
990
+ for (const source of visibleMedia) {
991
+ const participant = recordingParticipantsById.get(source.participantId);
992
+ if (!participant) continue;
993
+ mediaNodes.set(source.sourceId, resolveMedia(source, { participant, placement: source.kind === "camera" ? "camera" : "shared_content" }));
994
+ }
995
+ const cameraByParticipant = new Map(visibleMedia.filter((source) => source.kind === "camera").map((source) => [source.participantId, source]));
996
+ const items = joinedParticipants.map((participant) => ({ kind: "participant", id: participant.id, participant: participantsById.get(participant.id) }));
997
+ if (sharedContent.kind === "screen_share") {
998
+ const participant = participantsById.get(sharedContent.participantId);
999
+ const source = visibleMedia.find((candidate) => candidate.sourceId === sharedContent.sourceId && candidate.kind === "screen_share");
1000
+ if (!participant || !source) throw new TypeError("recording screen share does not resolve to a joined participant and visible source");
1001
+ items.push({ kind: "screen-share", id: screenShareItemId(participant.id), participant });
1002
+ } else if (sharedContent.kind === "whiteboard") {
1003
+ items.push({ kind: "whiteboard", id: WHITEBOARD_ITEM_ID });
1004
+ }
1005
+ const { messages, displayTimes, attachmentAssets } = toChatMessages(frame);
1006
+ const participantNames = Object.fromEntries(frame.participants.map((participant) => [participant.id, participant.displayName]));
1007
+ const durationSeconds = Math.floor(frame.elapsedMs / 1e3);
1008
+ const renderContent = (item) => {
1009
+ if (item.kind === "whiteboard") {
1010
+ const content = sharedContent.kind === "whiteboard" ? renderWhiteboard?.(sharedContent) : null;
1011
+ return /* @__PURE__ */ jsx4(StageContentTileSurface, { item, media: content, mediaVisible: hasRenderableMedia(content), className: "h-full w-full" });
1012
+ }
1013
+ if (sharedContent.kind !== "screen_share") throw new TypeError("recording stage contains a screen share without shared-content state");
1014
+ const source = visibleMedia.find((candidate) => candidate.sourceId === sharedContent.sourceId);
1015
+ if (!source) throw new TypeError("recording screen share source is not visible");
1016
+ const media = mediaNodes.get(source.sourceId);
1017
+ return /* @__PURE__ */ jsx4(ScreenSharePresentation, { sharedByName: item.participant.displayName, media, mediaVisible: hasRenderableMedia(media) });
1018
+ };
1019
+ return /* @__PURE__ */ jsx4(SkinProvider, { skin: frame.profile.theme.skin, children: /* @__PURE__ */ jsx4(
1020
+ SpacePresentation,
1021
+ {
1022
+ skin: frame.profile.theme.skin,
1023
+ palette: frame.profile.theme.palette,
1024
+ texture: frame.profile.theme.texture,
1025
+ stageBackground: frame.profile.theme.stageBackground,
1026
+ header: {
1027
+ spaceName: frame.space.name,
1028
+ logoUrl: frame.space.logoAssetId ? resolveAssetUrl(frame.space.logoAssetId) : void 0,
1029
+ duration: durationSeconds,
1030
+ isRecording: true,
1031
+ layout: frame.view.layout
1032
+ },
1033
+ stage: /* @__PURE__ */ jsx4(
1034
+ Stage,
1035
+ {
1036
+ items,
1037
+ layout: frame.view.layout,
1038
+ interactive: false,
1039
+ animate: false,
1040
+ generatedAvatars: frame.profile.theme.generatedAvatars,
1041
+ emptyState: /* @__PURE__ */ jsx4(QuietSpace, {}),
1042
+ renderParticipant: (item, context) => {
1043
+ const source = cameraByParticipant.get(item.participant.id);
1044
+ const media = source ? mediaNodes.get(source.sourceId) : null;
1045
+ return /* @__PURE__ */ jsx4(
1046
+ ParticipantTileSurface,
1047
+ {
1048
+ participant: item.participant,
1049
+ media,
1050
+ mediaVisible: item.participant.isVideoEnabled === true && hasRenderableMedia(media),
1051
+ aspectRatio: "fill",
1052
+ pinned: context.pinned,
1053
+ style: context.style,
1054
+ hidden: context.hidden,
1055
+ generatedAvatars: frame.profile.theme.generatedAvatars,
1056
+ animatedAvatars: false
1057
+ }
1058
+ );
1059
+ },
1060
+ renderContentTile: (item, context) => {
1061
+ const media = item.kind === "whiteboard" && frame.sharedContent.kind === "whiteboard" ? renderWhiteboard?.(frame.sharedContent) : frame.sharedContent.kind === "screen_share" ? mediaNodes.get(frame.sharedContent.sourceId) : null;
1062
+ return /* @__PURE__ */ jsx4(StageContentTileSurface, { item, media, mediaVisible: hasRenderableMedia(media), pinned: context.pinned, style: context.style, hidden: context.hidden });
1063
+ },
1064
+ renderPrimaryContent: renderContent,
1065
+ className: "h-full"
1066
+ }
1067
+ ),
1068
+ controls: /* @__PURE__ */ jsx4(ControlBarPresentation, { placement: "floating", density: "comfortable", duration: durationSeconds, buttons: RECORDING_TOOLBAR_BUTTONS, isRecording: true, isChatOpen: true, isWhiteboardOpen: sharedContent.kind === "whiteboard", expanded: true, detectDevices: false, displayOnly: true }),
1069
+ stageOverlay: /* @__PURE__ */ jsx4(ReactionsOverlayPresentation, { reactions: frame.reactions, elapsedMs: frame.elapsedMs }),
1070
+ sidebar: /* @__PURE__ */ jsx4(SpaceDrawer, { state: "open", interactive: false, children: /* @__PURE__ */ jsx4(
1071
+ ChatPanelPresentation,
1072
+ {
1073
+ messages,
1074
+ participantNames,
1075
+ onResolveAttachmentUrl: async (attachmentId) => {
1076
+ const assetId = attachmentAssets.get(attachmentId);
1077
+ if (!assetId) throw new TypeError(`recording chat attachment is unknown: ${attachmentId}`);
1078
+ return resolveAssetUrl(assetId);
1079
+ },
1080
+ messageDisplayTime: (message) => displayTimes.get(message.messageId) ?? "",
1081
+ autoScrollBehavior: "auto",
1082
+ generatedAvatars: frame.profile.theme.generatedAvatars,
1083
+ stableTexture: true,
1084
+ disabled: true
1085
+ }
1086
+ ) }),
1087
+ renderToken,
1088
+ interactive: false,
1089
+ className
1090
+ }
1091
+ ) });
1092
+ }
1093
+ RecordingSpaceView.displayName = "RecordingSpaceView";
1094
+
1095
+ // src/components/recording-history/RecordingHistoryPanel.tsx
1096
+ import "react";
1097
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
1098
+ function RecordingHistoryPanel({
1099
+ recordings,
1100
+ isLoading = false,
1101
+ error,
1102
+ hasMore = false,
1103
+ downloadingRecordingId,
1104
+ readingTranscriptRecordingId,
1105
+ refreshingExportRecordingId,
1106
+ requestingExportRecordingId,
1107
+ requestingTranscriptRecordingId,
1108
+ onDownload,
1109
+ onLoadMore,
1110
+ onReadTranscript,
1111
+ onRefreshExport,
1112
+ onRequestExport,
1113
+ onRequestTranscript,
1114
+ onRetry,
1115
+ onWatch,
1116
+ className
1117
+ }) {
1118
+ return /* @__PURE__ */ jsxs3(ChalkPanel, { className: cn("h-full min-h-0 w-full overflow-hidden bg-[var(--chalk-app-panel)] p-0", className), contentClassName: "flex h-full min-h-0 flex-col", role: "region", "aria-label": "Recording history", seed: "recording-history-panel", children: [
1119
+ /* @__PURE__ */ jsxs3("header", { className: "flex items-center justify-between gap-3 border-b border-[var(--chalk-app-line)] px-4 py-3", children: [
1120
+ /* @__PURE__ */ jsxs3("div", { children: [
1121
+ /* @__PURE__ */ jsx5("h2", { className: "text-base font-semibold", children: "Recordings" }),
1122
+ /* @__PURE__ */ jsx5("p", { className: "mt-0.5 text-xs text-[var(--chalk-app-text-muted)]", children: "Captured Episode activity" })
1123
+ ] }),
1124
+ isLoading ? /* @__PURE__ */ jsx5(Loading01Icon, { className: "size-4 animate-spin text-[var(--chalk-app-text-muted)]", "aria-label": "Loading recordings" }) : null
1125
+ ] }),
1126
+ /* @__PURE__ */ jsx5("div", { className: "min-h-0 flex-1 overflow-y-auto p-3", children: error ? /* @__PURE__ */ jsx5(ChalkEmptyState, { title: "Recordings could not be loaded", description: error, tone: "danger", seed: "recording-history-error", children: onRetry ? /* @__PURE__ */ jsxs3(ChalkButton, { className: "mt-4", onClick: onRetry, tone: "danger", children: [
1127
+ /* @__PURE__ */ jsx5(RefreshIcon, { className: "size-4" }),
1128
+ "Try again"
1129
+ ] }) : null }) : recordings.length === 0 && !isLoading ? /* @__PURE__ */ jsx5(ChalkEmptyState, { title: "No recordings yet", description: "Captured Episode activity will appear here.", seed: "recording-history-empty", children: /* @__PURE__ */ jsx5(Video01Icon, { className: "mx-auto mt-4 size-8 text-[var(--chalk-app-text-muted)]", "aria-hidden": "true" }) }) : /* @__PURE__ */ jsx5("ul", { className: "grid gap-2", "aria-label": "Recordings", children: recordings.map((recording) => /* @__PURE__ */ jsx5(
1130
+ RecordingHistoryRow,
1131
+ {
1132
+ recording,
1133
+ downloading: downloadingRecordingId === recording.id,
1134
+ readingTranscript: readingTranscriptRecordingId === recording.id,
1135
+ refreshingExport: refreshingExportRecordingId === recording.id,
1136
+ requestingExport: requestingExportRecordingId === recording.id,
1137
+ requestingTranscript: requestingTranscriptRecordingId === recording.id,
1138
+ onDownload,
1139
+ onReadTranscript,
1140
+ onRefreshExport,
1141
+ onRequestExport,
1142
+ onRequestTranscript,
1143
+ onWatch
1144
+ },
1145
+ recording.id
1146
+ )) }) }),
1147
+ hasMore && !error ? /* @__PURE__ */ jsx5("footer", { className: "border-t border-[var(--chalk-app-line)] p-3", children: /* @__PURE__ */ jsx5(ChalkButton, { className: "w-full", disabled: isLoading || !onLoadMore, loading: isLoading, onClick: onLoadMore, children: "Load more" }) }) : null
1148
+ ] });
1149
+ }
1150
+ function RecordingHistoryRow({ downloading, onDownload, onReadTranscript, onRefreshExport, onRequestExport, onRequestTranscript, onWatch, readingTranscript, recording, refreshingExport, requestingExport, requestingTranscript }) {
1151
+ return /* @__PURE__ */ jsx5("li", { children: /* @__PURE__ */ jsxs3(ChalkPanel, { className: "p-3", contentClassName: "flex items-start gap-3", seed: `recording-${recording.id}`, children: [
1152
+ /* @__PURE__ */ jsx5("span", { className: "grid size-10 shrink-0 place-items-center rounded-full bg-[var(--chalk-app-control)] text-[var(--chalk-app-text-muted)]", "aria-hidden": "true", children: /* @__PURE__ */ jsx5(Video01Icon, { className: "size-5" }) }),
1153
+ /* @__PURE__ */ jsxs3("div", { className: "min-w-0 flex-1", children: [
1154
+ /* @__PURE__ */ jsxs3("div", { className: "flex flex-wrap items-center gap-2", children: [
1155
+ /* @__PURE__ */ jsxs3("span", { className: "truncate text-sm font-medium", children: [
1156
+ "Episode ",
1157
+ shortId(recording.episode_id)
1158
+ ] }),
1159
+ recording.export ? null : /* @__PURE__ */ jsx5(StatusBadge, { label: legacyStatusLabel(recording.status), tone: legacyStatusTone(recording.status), seed: `recording-status-${recording.id}` })
1160
+ ] }),
1161
+ /* @__PURE__ */ jsx5("p", { className: "mt-1 text-xs text-[var(--chalk-app-text-muted)]", children: /* @__PURE__ */ jsx5("time", { dateTime: recording.created_at, children: formatTimestamp(recording.created_at) }) }),
1162
+ recording.export ? /* @__PURE__ */ jsx5(
1163
+ RecordingArtifactStatus,
1164
+ {
1165
+ downloading,
1166
+ onDownload,
1167
+ onReadTranscript,
1168
+ onRefreshExport,
1169
+ onRequestExport,
1170
+ onRequestTranscript,
1171
+ onWatch,
1172
+ readingTranscript,
1173
+ recording,
1174
+ refreshingExport,
1175
+ requestingExport,
1176
+ requestingTranscript
1177
+ }
1178
+ ) : /* @__PURE__ */ jsx5(LegacyRecordingActions, { downloading, onDownload, recording })
1179
+ ] })
1180
+ ] }) });
1181
+ }
1182
+ function RecordingArtifactStatus({ downloading, onDownload, onReadTranscript, onRefreshExport, onRequestExport, onRequestTranscript, onWatch, readingTranscript, recording, refreshingExport, requestingExport, requestingTranscript }) {
1183
+ const video = recording.export;
1184
+ if (!video) throw new Error("Recording export is required when rendering artifact status");
1185
+ const transcriptStatus = recording.transcript?.status === "none" && recording.transcript.requestable ? "requestable" : recording.transcript?.status;
1186
+ return /* @__PURE__ */ jsxs3("div", { className: "mt-3 grid gap-2 border-t border-[var(--chalk-app-line)] pt-3", children: [
1187
+ recording.source ? /* @__PURE__ */ jsx5(ArtifactStatus, { icon: /* @__PURE__ */ jsx5(Video01Icon, { className: "size-4" }), label: "Capture", detail: sourceDetail(recording.source), status: sourceLabel(recording.source.status), tone: sourceTone(recording.source.status), seed: `capture-${recording.id}` }) : null,
1188
+ transcriptStatus ? /* @__PURE__ */ jsx5(ArtifactStatus, { icon: /* @__PURE__ */ jsx5(FileTextIcon, { className: "size-4" }), label: "Transcript", detail: transcriptDetail(transcriptStatus), status: transcriptLabel(transcriptStatus), tone: transcriptTone(transcriptStatus), seed: `transcript-${recording.id}` }) : null,
1189
+ recording.transcript ? /* @__PURE__ */ jsx5(TranscriptContent, { onReadTranscript, onRequestTranscript, reading: readingTranscript, recording, requestingTranscript }) : null,
1190
+ /* @__PURE__ */ jsx5(ArtifactStatus, { icon: /* @__PURE__ */ jsx5(Video01Icon, { className: "size-4" }), label: "Video", detail: exportDetail(video), status: exportLabel(video.status), tone: exportTone(video.status), seed: `video-${recording.id}` }),
1191
+ video.status === "ready" ? /* @__PURE__ */ jsx5(ReadyVideoActions, { downloading, onDownload, onWatch, recording }) : null,
1192
+ video.status === "pending" ? /* @__PURE__ */ jsx5(PendingVideoActions, { onRefreshExport, recording, refreshingExport, requestingExport }) : null,
1193
+ canRequestExport(video) ? /* @__PURE__ */ jsx5(RequestedVideoActions, { onRequestExport, recording, requestingExport }) : null
1194
+ ] });
1195
+ }
1196
+ function TranscriptContent({ onReadTranscript, onRequestTranscript, reading, recording, requestingTranscript }) {
1197
+ const transcript = recording.transcript;
1198
+ if (!transcript) return null;
1199
+ if ((transcript.status === "none" || transcript.status === "requestable") && transcript.requestable && onRequestTranscript) {
1200
+ return /* @__PURE__ */ jsx5("div", { className: "pt-1", children: /* @__PURE__ */ jsxs3(ChalkButton, { "aria-label": `Request transcript ${shortId(recording.id)}`, disabled: requestingTranscript, loading: requestingTranscript, onClick: () => onRequestTranscript(recording), variant: "ghost", className: "shrink-0 px-3", children: [
1201
+ /* @__PURE__ */ jsx5(FileTextIcon, { className: "size-4" }),
1202
+ "Request transcript"
1203
+ ] }) });
1204
+ }
1205
+ if (transcript.status !== "completed") return null;
1206
+ if (transcript.text !== void 0) {
1207
+ return /* @__PURE__ */ jsxs3("details", { className: "rounded border border-[var(--chalk-app-line)] p-2", children: [
1208
+ /* @__PURE__ */ jsx5("summary", { className: "cursor-pointer text-xs font-medium", children: "Transcript" }),
1209
+ /* @__PURE__ */ jsx5("p", { className: "mt-2 whitespace-pre-wrap text-xs leading-5 text-[var(--chalk-app-text-muted)]", children: transcript.text })
1210
+ ] });
1211
+ }
1212
+ if (!onReadTranscript) return null;
1213
+ return /* @__PURE__ */ jsx5("div", { className: "pt-1", children: /* @__PURE__ */ jsxs3(ChalkButton, { "aria-label": `Read transcript ${shortId(recording.id)}`, disabled: reading, loading: reading, onClick: () => onReadTranscript(recording), variant: "ghost", className: "shrink-0 px-3", children: [
1214
+ /* @__PURE__ */ jsx5(FileTextIcon, { className: "size-4" }),
1215
+ "Read transcript"
1216
+ ] }) });
1217
+ }
1218
+ function ArtifactStatus({ detail, icon, label, seed, status, tone }) {
1219
+ return /* @__PURE__ */ jsxs3("div", { className: "flex items-start gap-2 text-xs", children: [
1220
+ /* @__PURE__ */ jsx5("span", { className: "mt-0.5 text-[var(--chalk-app-text-muted)]", "aria-hidden": "true", children: icon }),
1221
+ /* @__PURE__ */ jsxs3("div", { className: "min-w-0 flex-1", children: [
1222
+ /* @__PURE__ */ jsxs3("div", { className: "flex flex-wrap items-center gap-2", children: [
1223
+ /* @__PURE__ */ jsx5("span", { className: "font-medium", children: label }),
1224
+ /* @__PURE__ */ jsx5(StatusBadge, { label: status, tone, seed })
1225
+ ] }),
1226
+ /* @__PURE__ */ jsx5("p", { className: "mt-0.5 text-[var(--chalk-app-text-muted)]", children: detail })
1227
+ ] })
1228
+ ] });
1229
+ }
1230
+ function StatusBadge({ label, seed, tone }) {
1231
+ return /* @__PURE__ */ jsx5(ChalkBadge, { tone, seed, children: label });
1232
+ }
1233
+ function ReadyVideoActions({ downloading, onDownload, onWatch, recording }) {
1234
+ if (!onDownload && !onWatch) return null;
1235
+ return /* @__PURE__ */ jsxs3("div", { className: "flex flex-wrap gap-2 pt-1", children: [
1236
+ onWatch ? /* @__PURE__ */ jsxs3(ChalkButton, { "aria-label": `Watch recording ${shortId(recording.id)}`, onClick: () => onWatch(recording), variant: "ghost", className: "shrink-0 px-3", children: [
1237
+ /* @__PURE__ */ jsx5(PlayIcon, { className: "size-4" }),
1238
+ "Watch"
1239
+ ] }) : null,
1240
+ onDownload ? /* @__PURE__ */ jsxs3(ChalkButton, { "aria-label": `Download recording ${shortId(recording.id)}`, disabled: downloading, loading: downloading, onClick: () => onDownload(recording), variant: "ghost", className: "shrink-0 px-3", children: [
1241
+ /* @__PURE__ */ jsx5(Download01Icon, { className: "size-4" }),
1242
+ "Download"
1243
+ ] }) : null
1244
+ ] });
1245
+ }
1246
+ function PendingVideoActions({ onRefreshExport, recording, refreshingExport, requestingExport }) {
1247
+ if (!onRefreshExport) return null;
1248
+ return /* @__PURE__ */ jsx5("div", { className: "pt-1", children: /* @__PURE__ */ jsxs3(ChalkButton, { "aria-label": `Check video status ${shortId(recording.id)}`, disabled: refreshingExport || requestingExport, loading: refreshingExport || requestingExport, onClick: () => onRefreshExport(recording), variant: "ghost", className: "shrink-0 px-3", children: [
1249
+ /* @__PURE__ */ jsx5(RefreshIcon, { className: "size-4" }),
1250
+ "Check video status"
1251
+ ] }) });
1252
+ }
1253
+ function RequestedVideoActions({ onRequestExport, recording, requestingExport }) {
1254
+ if (!onRequestExport) return null;
1255
+ if (recording.export?.status === "failed") {
1256
+ return /* @__PURE__ */ jsx5("div", { className: "pt-1", children: /* @__PURE__ */ jsxs3(ChalkButton, { "aria-label": `Retry Export ${shortId(recording.id)}`, disabled: requestingExport, loading: requestingExport, onClick: () => onRequestExport(recording, "retry"), variant: "ghost", className: "shrink-0 px-3", children: [
1257
+ /* @__PURE__ */ jsx5(RefreshIcon, { className: "size-4" }),
1258
+ "Retry Export"
1259
+ ] }) });
1260
+ }
1261
+ return /* @__PURE__ */ jsxs3("div", { className: "flex flex-wrap gap-2 pt-1", children: [
1262
+ /* @__PURE__ */ jsxs3(ChalkButton, { "aria-label": `Watch recording ${shortId(recording.id)}`, disabled: requestingExport, loading: requestingExport, onClick: () => onRequestExport(recording, "watch"), variant: "ghost", className: "shrink-0 px-3", children: [
1263
+ /* @__PURE__ */ jsx5(PlayIcon, { className: "size-4" }),
1264
+ "Watch"
1265
+ ] }),
1266
+ /* @__PURE__ */ jsxs3(ChalkButton, { "aria-label": `Download recording ${shortId(recording.id)}`, disabled: requestingExport, loading: requestingExport, onClick: () => onRequestExport(recording, "download"), variant: "ghost", className: "shrink-0 px-3", children: [
1267
+ /* @__PURE__ */ jsx5(Download01Icon, { className: "size-4" }),
1268
+ "Download"
1269
+ ] })
1270
+ ] });
1271
+ }
1272
+ function LegacyRecordingActions({ downloading, onDownload, recording }) {
1273
+ if (recording.status !== "completed" || !onDownload) return null;
1274
+ return /* @__PURE__ */ jsx5("div", { className: "pt-3", children: /* @__PURE__ */ jsxs3(ChalkButton, { "aria-label": `Download recording ${shortId(recording.id)}`, disabled: downloading, loading: downloading, onClick: () => onDownload(recording), variant: "ghost", className: "shrink-0 px-3", children: [
1275
+ /* @__PURE__ */ jsx5(Download01Icon, { className: "size-4" }),
1276
+ /* @__PURE__ */ jsx5("span", { className: "hidden sm:inline", children: "Download" })
1277
+ ] }) });
1278
+ }
1279
+ function canRequestExport(value) {
1280
+ return value.status === "none" || value.status === "failed" && value.retryable;
1281
+ }
1282
+ function legacyStatusLabel(status) {
1283
+ if (status === "pending") return "Pending";
1284
+ if (status === "processing") return "Processing";
1285
+ if (status === "completed") return "Ready";
1286
+ return "Failed";
1287
+ }
1288
+ function legacyStatusTone(status) {
1289
+ if (status === "pending") return "neutral";
1290
+ if (status === "processing") return "accent";
1291
+ if (status === "completed") return "success";
1292
+ return "danger";
1293
+ }
1294
+ function sourceDetail(source) {
1295
+ if (source.status === "available") return source.expires_at ? `Available until ${formatTimestamp(source.expires_at)}.` : "Available for transcript processing and a video request.";
1296
+ if (source.status === "pending") return "Capture is still being stored.";
1297
+ if (source.status === "failed") return "Capture failed. A transcript and video cannot be created.";
1298
+ return "Capture is no longer available.";
1299
+ }
1300
+ function transcriptDetail(status) {
1301
+ if (status === "none") return "Transcript was not enabled for this Episode.";
1302
+ if (status === "requestable") return "Transcript can be requested from this Capture.";
1303
+ if (status === "unavailable") return "Transcript cannot be requested from this Capture.";
1304
+ if (status === "pending") return "Transcript is waiting to be prepared.";
1305
+ if (status === "processing") return "Transcript is being prepared.";
1306
+ if (status === "completed") return "Transcript is ready.";
1307
+ return "Transcript could not be created.";
1308
+ }
1309
+ function exportDetail(value) {
1310
+ if (value.status === "none") return "A video will be prepared when you watch or download.";
1311
+ if (value.status === "pending") return "Video is being prepared.";
1312
+ if (value.status === "ready") return "Video is ready to watch or download.";
1313
+ if (value.status === "failed") return value.failure_message ?? "Video could not be prepared.";
1314
+ return "Video cannot be created because the capture is unavailable.";
1315
+ }
1316
+ function sourceTone(status) {
1317
+ if (status === "available") return "success";
1318
+ if (status === "pending") return "accent";
1319
+ return "danger";
1320
+ }
1321
+ function sourceLabel(status) {
1322
+ if (status === "pending") return "Saving";
1323
+ if (status === "available") return "Available";
1324
+ if (status === "failed") return "Failed";
1325
+ return "Expired";
1326
+ }
1327
+ function transcriptTone(status) {
1328
+ if (status === "none" || status === "requestable") return "neutral";
1329
+ if (status === "unavailable") return "danger";
1330
+ if (status === "completed") return "success";
1331
+ if (status === "failed") return "danger";
1332
+ return "accent";
1333
+ }
1334
+ function transcriptLabel(status) {
1335
+ if (status === "none") return "Not enabled";
1336
+ if (status === "requestable") return "Can request";
1337
+ if (status === "unavailable") return "Unavailable";
1338
+ if (status === "pending") return "Waiting";
1339
+ if (status === "processing") return "Preparing";
1340
+ if (status === "completed") return "Ready";
1341
+ return "Failed";
1342
+ }
1343
+ function exportTone(status) {
1344
+ if (status === "ready") return "success";
1345
+ if (status === "none") return "neutral";
1346
+ if (status === "pending") return "accent";
1347
+ return "danger";
1348
+ }
1349
+ function exportLabel(status) {
1350
+ if (status === "none") return "Not requested";
1351
+ if (status === "pending") return "Preparing";
1352
+ if (status === "ready") return "Ready";
1353
+ if (status === "failed") return "Failed";
1354
+ return "Unavailable";
1355
+ }
1356
+ function shortId(value) {
1357
+ return value.slice(0, 8);
1358
+ }
1359
+ function formatTimestamp(value) {
1360
+ const date = new Date(value);
1361
+ if (Number.isNaN(date.getTime())) return value;
1362
+ const datePart = date.toISOString().slice(0, 10);
1363
+ const timePart = date.toISOString().slice(11, 16);
1364
+ return `${datePart} ${timePart} UTC`;
1365
+ }
1366
+ RecordingHistoryPanel.displayName = "RecordingHistoryPanel";
1367
+
1368
+ // src/components/whiteboard-view/RecordingWhiteboardView.tsx
1369
+ import { fromWireElement } from "@q9labsai/chalk-whiteboard";
1370
+ import { useCallback as useCallback3, useEffect as useEffect5, useMemo as useMemo2, useState as useState5 } from "react";
1371
+ import { jsx as jsx6 } from "react/jsx-runtime";
1372
+ function RecordingWhiteboardView({ state: stateValue, files, expectedSceneId, expectedRevision, theme, excalidrawCssPath, onLoadError, onPresented, className }) {
1373
+ const state = useMemo2(() => parseRecordingWhiteboardStateV1(stateValue), [stateValue]);
1374
+ const elements = useMemo2(() => state.elements.map(fromWireElement), [state.elements]);
1375
+ const resolvedTheme = theme ?? "light";
1376
+ const [ready, setReady] = useState5(null);
1377
+ const [presentedState, setPresentedState] = useState5(null);
1378
+ const handleReady = useCallback3((api) => setReady(Object.freeze({ api, defaultViewBackgroundColor: api.getAppState().viewBackgroundColor })), []);
1379
+ const requiredFileIds = recordingWhiteboardFileIds(state);
1380
+ if (expectedSceneId !== void 0 && state.sceneId !== expectedSceneId) throw new TypeError("recording whiteboard scene does not match the presentation frame");
1381
+ if (expectedRevision !== void 0 && state.revision !== expectedRevision) throw new TypeError("recording whiteboard revision does not match the presentation frame");
1382
+ for (const fileId of requiredFileIds) {
1383
+ if (!Object.hasOwn(files ?? {}, fileId)) throw new TypeError(`recording whiteboard file is missing: ${recordingWhiteboardFileAssetId(fileId)}`);
1384
+ }
1385
+ useEffect5(() => {
1386
+ if (!ready) return;
1387
+ setPresentedState(null);
1388
+ const installScene = () => {
1389
+ if (files) ready.api.addFiles(Object.values(files));
1390
+ ready.api.updateScene({ elements, appState: recordingWhiteboardSceneAppState(state, ready.defaultViewBackgroundColor), captureUpdate: "NEVER" });
1391
+ };
1392
+ installScene();
1393
+ const visibleElements = elements.filter((element) => !element.isDeleted);
1394
+ let presentedFrameRef;
1395
+ const reconcileFrame = requestAnimationFrame(() => {
1396
+ installScene();
1397
+ if (visibleElements.length > 0) ready.api.scrollToContent(visibleElements, { fitToContent: true, animate: false, viewportZoomFactor: 0.9 });
1398
+ const presentedFrame = requestAnimationFrame(() => setPresentedState(state));
1399
+ presentedFrameRef = presentedFrame;
1400
+ });
1401
+ return () => {
1402
+ cancelAnimationFrame(reconcileFrame);
1403
+ if (presentedFrameRef !== void 0) cancelAnimationFrame(presentedFrameRef);
1404
+ };
1405
+ }, [elements, files, ready, state]);
1406
+ useEffect5(() => {
1407
+ if (presentedState !== state || onPresented === void 0) return;
1408
+ onPresented(state);
1409
+ }, [onPresented, presentedState, state]);
1410
+ return /* @__PURE__ */ jsx6(WhiteboardView, { canDraw: false, className: cn("h-full w-full !min-h-0 !border-0", className), excalidrawCssPath, onExcalidrawApiReady: handleReady, onLoadError, theme: resolvedTheme });
1411
+ }
1412
+ RecordingWhiteboardView.displayName = "RecordingWhiteboardView";
1413
+
1414
+ export {
1415
+ Entrance,
1416
+ FeedbackDialog,
1417
+ Chalk,
1418
+ RecordingSpaceView,
1419
+ RecordingHistoryPanel,
1420
+ RecordingWhiteboardView
1421
+ };