@q9labsai/chalk-react 5.1.0 → 5.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/dist/bindings/hooks.d.ts +2 -0
  3. package/dist/{chunk-QDEKMRH2.js → chunk-53ZA776J.js} +72 -60
  4. package/dist/{chunk-ZFWIMKUC.js → chunk-HWQLW4JR.js} +55 -7
  5. package/dist/{chunk-XOHNIYSR.js → chunk-RQXFFDOS.js} +1865 -2870
  6. package/dist/components/board/Board.d.ts +3 -22
  7. package/dist/components/board/board-draw-policy.d.ts +15 -0
  8. package/dist/components/chalk/Chalk.d.ts +12 -0
  9. package/dist/components/chalk/ReportProblemDialog.d.ts +12 -0
  10. package/dist/components/chalk/StatusSurface.d.ts +8 -2
  11. package/dist/components/chalk-ui/ChalkSheet.d.ts +1 -1
  12. package/dist/components/chalk-ui/ChalkToast.d.ts +7 -1
  13. package/dist/components/chalk-ui/index.d.ts +6 -5
  14. package/dist/components/control-bar/command-failure.d.ts +105 -1
  15. package/dist/components/control-bar/control-bar-behavior.d.ts +5 -1
  16. package/dist/components/index.js +7 -3
  17. package/dist/components/offline-actions/OfflineActions.d.ts +2 -1
  18. package/dist/components/space-header/CallDetails.d.ts +4 -2
  19. package/dist/components/space-header/space-header-contract.d.ts +6 -0
  20. package/dist/components/space-view/SpaceView.d.ts +15 -3
  21. package/dist/components/space-view/countdown.d.ts +5 -0
  22. package/dist/components/stage/Stage.d.ts +1 -1
  23. package/dist/components/stage/stage-items.d.ts +4 -0
  24. package/dist/components/stage/stage-layout.d.ts +6 -2
  25. package/dist/components/toast-stack/CallEventIcon.d.ts +1 -1
  26. package/dist/components/toast-stack/NoticeButton.d.ts +7 -0
  27. package/dist/components/toast-stack/ToastStack.d.ts +13 -2
  28. package/dist/components/toast-stack/call-events.d.ts +2 -1
  29. package/dist/components/toast-stack/notice-announcement.d.ts +4 -0
  30. package/dist/components/toast-stack/toast-layout-snapshot.d.ts +4 -0
  31. package/dist/components/toast-stack/useToastCapacity.d.ts +7 -0
  32. package/dist/headless.js +1 -1
  33. package/dist/index.d.ts +1 -0
  34. package/dist/index.js +24 -3
  35. package/dist/public-surface.contract.d.ts +1 -1
  36. package/dist/test-support/index.js +3 -3
  37. package/package.json +5 -5
  38. package/dist/components/board/BoardCursors.d.ts +0 -21
  39. package/dist/components/board/BoardIcon.d.ts +0 -46
  40. package/dist/components/board/BoardIsland.d.ts +0 -21
  41. package/dist/components/board/BoardMenu.d.ts +0 -15
  42. package/dist/components/board/BoardStyles.d.ts +0 -2
  43. package/dist/components/board/board-engine.d.ts +0 -31
  44. package/dist/components/board/board-history.d.ts +0 -7
  45. package/dist/components/board/board-model.d.ts +0 -86
  46. package/dist/components/board/board-toasts.d.ts +0 -6
  47. package/dist/components/board/board-view-versions.d.ts +0 -7
  48. package/dist/components/chalk-ui/ChalkMenu.d.ts +0 -20
  49. package/dist/components/chalk-ui/ChalkPopover.d.ts +0 -11
  50. package/dist/components/chalk-ui/ChalkTooltip.d.ts +0 -5
package/CHANGELOG.md CHANGED
@@ -1,5 +1,107 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.2.0
4
+
5
+ ### board-draw-policy
6
+
7
+ Add a call-wide drawing policy for the board: everyone, only hosts, or picked people.
8
+ Hosts set it with `setDrawPolicy` on the whiteboard transport and pick people with the
9
+ existing `setDrawPermission`. Sync computes each person's permission and sends it with
10
+ the policy over the new `draw_policy_v1` extension, so the summary and
11
+ `useWhiteboard().access` now carry `canDraw`, `drawPolicy` and the host's per-person
12
+ choices. The React board follows that permission instead of the role capability, and
13
+ hosts get a "Who can draw" section with a person picker in the board menu.
14
+
15
+ The whiteboard client now steps its hello down only when Sync can't decode it (close
16
+ 1009). A rejected token or a late hello (close 1008) is retried at the same level up to
17
+ four times, so a brief token problem no longer hides the drawing policy for the rest of
18
+ the connection. On a Sync without the drawing policy, a host's waiting policy change is
19
+ refused as `unavailable`, and also reported on the summary, instead of being replayed.
20
+
21
+ ### board-frame-clock
22
+
23
+ Keep Follow active when Bring everyone moves to a wider board view, without a brief zoom overshoot at the start of the animation.
24
+
25
+ ### board-stage-motion
26
+
27
+ Keep the live whiteboard free of stage translation and scaling animations so
28
+ switching between a participant and the board preserves the full canvas and
29
+ accurate drawing coordinates. Other stage tiles keep their existing motion.
30
+
31
+ ### brand-single-chalk-mark
32
+
33
+ Replace the retired colourful four-stick logo with the monochrome single chalk stick and dust in the distributed SVG assets and React Logo variants. Preserve existing asset filenames and component props. The React Logo now takes its default colour from the surrounding text colour.
34
+
35
+ ### follow-view-owner
36
+
37
+ Converge Follow views on one Episode owner, recover from its canonical snapshot,
38
+ and preserve real camera moves during recovery and tab switches. Zoom controls
39
+ and keyboard moves now consistently leave Follow and lead the person's view.
40
+
41
+ Keep all 500 active people's views using internal schema-generated limits in the client and React caches. The client's public bounds, event unions and decoded-frame unions are unchanged. The client now exports `ChalkWhiteboardViewVersions`, which the React SDK uses to order view moves, and whiteboard view events now carry `ownerId`, `ownerSequence` and `originEpoch`, so both releases are minor.
42
+
43
+ ### phone-board-notices-next-release
44
+
45
+ Float faces over a phone board, and keep call notices off the call's controls.
46
+ On a phone, an open board now fills the stage and up to three faces float at
47
+ its right edge, with short name tags. Notices sit between the header and the
48
+ controls under them. A call shorter than 620px, such as a landscape phone or a
49
+ short embedded call, puts every notice in a notice button in the header, which
50
+ lists each one whole and announces it once to screen readers; a taller call
51
+ shows the newest three and puts older ones in the button. A notice in the
52
+ button starts its timer only once it shows. The header stays visible while
53
+ the button holds notices. Long names, links and error tokens wrap inside
54
+ their notice, and a long call name shortens to make room for the timer and
55
+ the notice button. A screen share beside a short board always keeps its own
56
+ tile when there is no explicit cap. A capped strip or floating column counts
57
+ shared content and its overflow button against that cap; folded shares stay
58
+ readable in the shared-content overflow. One text-only announcer handles
59
+ notice speech independently of its stage or header presentation.
60
+
61
+ New optional props: `ToastStackProps.onFold` reports the ids of the notices
62
+ the stage has no room for, `ChalkToastProps.announce` lets a host that already
63
+ announced a notice keep its toast quiet, and `SpaceHeaderProps.notices` places
64
+ a control before the people button.
65
+
66
+ ### problem-reporting
67
+
68
+ Add the problem-report API contract and reusable report dialog, with opt-in diagnostics,
69
+ editable delivery failures and bounded Unicode input. Call details and failed or ended
70
+ call screens can expose reporting when an integration supplies a delivery callback.
71
+ Use `reportUnavailableReason` to show expired access inside an editable report.
72
+ Live report drafts survive call termination. Integrations with their own terminal
73
+ screens can use `onOpenReportProblem` to keep one app-owned dialog across those screens.
74
+ Give `ReportProblemDialog` a `focusScope` containing the current call or terminal screen,
75
+ so closing it returns focus to its usable opener, or to a visible current control
76
+ when the opener has disappeared. Hidden, inert and disabled targets are skipped.
77
+
78
+ ### recording-auto-render
79
+
80
+ Expose recording expiry reasons through additive failure fields while preserving the existing export status enum.
81
+
82
+ Add the Tenant recording_render_mode setting, defaulting to on_demand, with opt-in automatic MP4 rendering.
83
+
84
+ ### safe-media-command-guidance
85
+
86
+ Show safe, source-aware guidance for media command failures across call controls,
87
+ keyboard shortcuts, screen sharing, and media request dialogs. Preserve the
88
+ existing onCommandError MediaSource argument and provide extended media context
89
+ in an optional third argument. Keep disable-failure wording neutral and explain
90
+ unsupported speaker selection without suggesting a retry. Preserve start/stop
91
+ intent in failure guidance and retries; show terminal guidance for withdrawn
92
+ media requests and other non-recoverable failures.
93
+
94
+ Distinguish device selection, speaker tests, and incoming requests in media error
95
+ guidance. Cover every client failure code with an operation-aware policy and only
96
+ offer retry when the exact failed media command can be replayed.
97
+
98
+ Preserve request context in the integrated Chalk dialog and show safe terminal
99
+ guidance without retry for unknown runtime failure codes.
100
+
101
+ ### sync-connect-diagnostics
102
+
103
+ Report Sync connection failures with the existing `connection.sync_hello_timeout` code without adding values to the public `ErrorCode` union. Add the current attempt's safe diagnostics to client errors, telemetry, and the call failure reference: opening/hello stage, close code and bounded reason, whether the socket opened, and elapsed time. Earlier attempts remain in the diagnostic history. Browsers do not expose DNS, TCP, or TLS progress. Unrecognized peer reasons are redacted. This is a compatible patch for the next client and React releases; released 5.1.0 files are unchanged.
104
+
3
105
  ## 5.1.0
4
106
 
5
107
  - Add optional `CallLimbo.className` for the message card, so a host can size the card around a form.
@@ -21,6 +21,8 @@ export declare function useReactions(): ReactionsSlice;
21
21
  export declare function useRecording(): RecordingSlice;
22
22
  export declare function useWhiteboard(): WhiteboardSlice;
23
23
  export declare function useCan(capability: Capability): boolean;
24
+ /** Whether this person can draw on the board. Sync decides from the call's policy, their role and the host's choice for them; until the board subscription is live, the role default stands in. A Sync without the drawing policy doesn't report role changes to the board, so there the live role still limits drawing. */
25
+ export declare function useCanDrawWhiteboard(): boolean;
24
26
  /** Local capture state and the action that opens the browser's share picker. */
25
27
  export declare function useScreenShare(): {
26
28
  source: import("@q9labsai/chalk-client").MediaSource;
@@ -28,17 +28,19 @@ import {
28
28
  StatusSurface,
29
29
  WhiteboardView,
30
30
  chalkThemeStyle,
31
+ commandFailure,
31
32
  useCan,
33
+ useCanDrawWhiteboard,
32
34
  useConnection,
33
35
  useMedia,
34
36
  useRecording,
35
37
  useSpaceClient,
36
38
  useWhiteboard
37
- } from "./chunk-XOHNIYSR.js";
39
+ } from "./chunk-RQXFFDOS.js";
38
40
  import {
39
41
  WHITEBOARD_ITEM_ID,
40
42
  screenShareItemId
41
- } from "./chunk-ZFWIMKUC.js";
43
+ } from "./chunk-HWQLW4JR.js";
42
44
  import {
43
45
  Download01Icon,
44
46
  FileTextIcon,
@@ -208,10 +210,10 @@ function Chalk(props) {
208
210
  props.onColorSchemeChange?.(next);
209
211
  };
210
212
  const [reportOpen, setReportOpen] = useState2(false);
211
- const reportTrigger = useRef(null);
212
- const openReport = props.onReportProblem ? () => {
213
- reportTrigger.current = document.activeElement instanceof HTMLButtonElement ? document.activeElement : null;
214
- setReportOpen(true);
213
+ const container = useRef(null);
214
+ const openReport = props.onReportProblem && !props.reportUnavailableReason ? () => {
215
+ if (props.onOpenReportProblem) props.onOpenReportProblem();
216
+ else setReportOpen(true);
215
217
  } : void 0;
216
218
  const suppliedClient = props.client;
217
219
  const getAccessRef = useRef(props.getAccess);
@@ -233,12 +235,23 @@ function Chalk(props) {
233
235
  void ownedClient.leave().catch(() => void 0).finally(() => ownedClient.dispose());
234
236
  };
235
237
  }, [ownedClient]);
236
- return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx("div", { "data-chalk": true, "data-chalk-theme": colorScheme, className: "chalk-root h-full min-h-0 w-full", style: chalkThemeStyle(props.theme, colorScheme), children: /* @__PURE__ */ jsxs(ChalkProvider, { client, children: [
238
+ return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx("div", { ref: container, "data-chalk": true, "data-chalk-theme": colorScheme, className: "chalk-root h-full min-h-0 w-full", style: chalkThemeStyle(props.theme, colorScheme), children: /* @__PURE__ */ jsxs(ChalkProvider, { client, children: [
237
239
  /* @__PURE__ */ jsx(SpaceExperience, { ...props, theme: { ...props.theme, colorScheme }, onColorSchemeChange: changeColorScheme, resolvedColorScheme: colorScheme, onOpenReport: openReport }),
238
- props.onReportProblem ? /* @__PURE__ */ jsxs(Fragment, { children: [
240
+ props.onReportProblem && !props.onOpenReportProblem ? /* @__PURE__ */ jsxs(Fragment, { children: [
239
241
  /* @__PURE__ */ jsx(ShellStyles, {}),
240
- /* @__PURE__ */ jsx(CallDialog, { kind: "report", open: reportOpen, onClose: () => setReportOpen(false), onReportProblem: props.onReportProblem, diagnostics: `Call: ${props.spaceName ?? props.space ?? "Call"}
241
- Status: ${client.getSnapshot().connection.status}`, returnFocus: reportTrigger })
242
+ /* @__PURE__ */ jsx(
243
+ CallDialog,
244
+ {
245
+ kind: "report",
246
+ open: reportOpen,
247
+ onClose: () => setReportOpen(false),
248
+ onReportProblem: props.onReportProblem,
249
+ unavailableReason: props.reportUnavailableReason,
250
+ diagnostics: `Call: ${props.spaceName ?? props.space ?? "Call"}
251
+ Status: ${client.getSnapshot().connection.status}`,
252
+ focusScope: container
253
+ }
254
+ )
242
255
  ] }) : null
243
256
  ] }) }) });
244
257
  }
@@ -252,7 +265,7 @@ function SpaceExperience(props) {
252
265
  const previousClient = useRef(client);
253
266
  const [joinError, setJoinError] = useState2(null);
254
267
  const [retryPending, setRetryPending] = useState2(false);
255
- const [episodeEnded, setEpisodeEnded] = useState2(false);
268
+ const [episodeEnded, setEpisodeEnded] = useState2(connection.episodeEnded);
256
269
  const [handledTerminal, setHandledTerminal] = useState2(void 0);
257
270
  useEffect2(() => setHandledTerminal(void 0), [props.terminalState]);
258
271
  const spaceName = props.spaceName ?? props.space ?? "Space";
@@ -310,12 +323,26 @@ function SpaceExperience(props) {
310
323
  if (connection.status === "idle")
311
324
  return /* @__PURE__ */ jsx(CallLimbo, { theme: { ...props.theme, colorScheme: props.resolvedColorScheme }, recorded, spaceName, message: joinError ?? `Joining ${spaceName}\u2026`, action: joinError ? { label: "Try again", onClick: retryAutomaticJoin, disabled: retryPending } : void 0 });
312
325
  if (connection.status === "joining") return /* @__PURE__ */ jsx(CallLimbo, { theme: { ...props.theme, colorScheme: props.resolvedColorScheme }, recorded, spaceName, message: `Joining ${spaceName}\u2026` });
313
- if (connection.status === "failed" && connection.lastError?.code === "episode.ended") return /* @__PURE__ */ jsx(StatusSurface, { phase: "episode-ended", callName, theme: statusTheme, onHome: props.onHome, onReportProblem: props.onOpenReport });
326
+ if (connection.status === "failed" && connection.lastError?.code === "episode.ended")
327
+ return /* @__PURE__ */ jsx(StatusSurface, { phase: "episode-ended", callName, episodeEndReason: connection.episodeEndReason, endAction: props.timeLimit?.endAction, theme: statusTheme, onHome: props.onHome, onReportProblem: props.onOpenReport });
314
328
  if (connection.status === "failed") {
315
- return /* @__PURE__ */ jsx(StatusSurface, { phase: "failed", callName, errorCode: connection.lastError?.code, theme: statusTheme, onHome: props.onHome, onReportProblem: props.onOpenReport, onRetry: retryAutomaticJoin, retryPending });
329
+ return /* @__PURE__ */ jsx(StatusSurface, { phase: "failed", callName, errorCode: connection.lastError?.code, sync: connection.lastError?.sync, theme: statusTheme, onHome: props.onHome, onReportProblem: props.onOpenReport, onRetry: retryAutomaticJoin, retryPending });
316
330
  }
317
331
  if (connection.status === "leaving" || connection.status === "left")
318
- return /* @__PURE__ */ jsx(StatusSurface, { phase: episodeEnded ? "episode-ended" : "left", callName, theme: statusTheme, onReportProblem: props.onOpenReport, onRejoin: props.onRejoin ?? retryAutomaticJoin, rejoinDisabled: connection.status === "leaving" || retryPending, onHome: props.onHome });
332
+ return /* @__PURE__ */ jsx(
333
+ StatusSurface,
334
+ {
335
+ phase: episodeEnded || connection.episodeEnded ? "episode-ended" : "left",
336
+ episodeEndReason: connection.episodeEndReason,
337
+ endAction: props.timeLimit?.endAction,
338
+ callName,
339
+ theme: statusTheme,
340
+ onReportProblem: connection.status === "leaving" ? void 0 : props.onOpenReport,
341
+ onRejoin: props.onRejoin ?? retryAutomaticJoin,
342
+ rejoinDisabled: connection.status === "leaving" || retryPending,
343
+ onHome: props.onHome
344
+ }
345
+ );
319
346
  return /* @__PURE__ */ jsxs("div", { className: "relative h-full min-h-0", children: [
320
347
  /* @__PURE__ */ jsx(SpaceSurface, { ...props, spaceName }),
321
348
  /* @__PURE__ */ jsx(CallLimbo, { overlay: true, active: false, initiallyCovered: true, message: "" })
@@ -327,14 +354,20 @@ function SpaceSurface(props) {
327
354
  const media = useMedia();
328
355
  const whiteboardState = useWhiteboard();
329
356
  const canEndEpisode = useCan("endEpisode");
330
- const canDrawWhiteboard = useCan("drawWhiteboard");
331
- const [commandError, setCommandError] = useState2(() => connection.lastError?.recoverable && connection.lastError.code.startsWith("media.") ? connection.lastError.message : null);
332
- const runCommand = useCallback(async (command) => {
357
+ const canDrawWhiteboard = useCanDrawWhiteboard();
358
+ const [commandError, setCommandError] = useState2(() => connection.lastError?.recoverable && connection.lastError.code.startsWith("media.") ? connection.lastError : null);
359
+ const [commandSource, setCommandSource] = useState2();
360
+ const [commandAction, setCommandAction] = useState2();
361
+ const runCommand = useCallback(async (command, source, action) => {
333
362
  try {
334
363
  await command();
335
364
  setCommandError(null);
365
+ setCommandSource(void 0);
366
+ setCommandAction(void 0);
336
367
  } catch (cause) {
337
- setCommandError(cause instanceof Error ? cause.message : "This command could not be completed.");
368
+ setCommandError(commandFailure(cause));
369
+ setCommandSource(source);
370
+ setCommandAction(action);
338
371
  }
339
372
  }, []);
340
373
  const whiteboardTransport = client.whiteboard.transport();
@@ -368,14 +401,19 @@ function SpaceSurface(props) {
368
401
  return /* @__PURE__ */ jsx(
369
402
  SpaceView,
370
403
  {
404
+ timeLimit: props.timeLimit,
371
405
  colorScheme: props.resolvedColorScheme,
372
406
  shareUrl: props.shareUrl,
373
407
  onReportProblem: props.onReportProblem,
408
+ onOpenReportProblem: props.onOpenReport,
409
+ reportUnavailableReason: props.reportUnavailableReason,
374
410
  onColorSchemeChange: props.onColorSchemeChange,
375
411
  spaceName: props.spaceName,
376
412
  inviteLink: props.inviteLink,
377
413
  pickChatFiles: props.pickChatFiles,
378
414
  commandError: commandError ?? void 0,
415
+ commandSource,
416
+ commandAction,
379
417
  onDismissCommandError: () => setCommandError(null),
380
418
  layout: props.layout,
381
419
  onLayoutChange: props.onLayoutChange,
@@ -385,7 +423,14 @@ function SpaceSurface(props) {
385
423
  onLeft: () => runCommand(() => client.leave()),
386
424
  onEndEpisode: canEndEpisode ? () => runCommand(() => client.endEpisode()) : void 0,
387
425
  reconnecting: { isVisible: connection.status === "reconnecting", status: "reconnecting", showNotice: false },
388
- overlay: /* @__PURE__ */ jsx(Fragment, { children: media.incomingRequests[0] ? /* @__PURE__ */ jsx(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 })
426
+ overlay: /* @__PURE__ */ jsx(Fragment, { children: media.incomingRequests[0] ? /* @__PURE__ */ jsx(
427
+ MediaRequestDialog,
428
+ {
429
+ request: media.incomingRequests[0],
430
+ onDecline: () => void runCommand(() => client.media.declineRequest(media.incomingRequests[0].requestId), media.incomingRequests[0].kind === "start_camera" ? "camera" : "microphone", "request"),
431
+ onAllow: () => void runCommand(() => client.media.acceptRequest(media.incomingRequests[0].requestId), media.incomingRequests[0].kind === "start_camera" ? "camera" : "microphone", "request")
432
+ }
433
+ ) : null })
389
434
  }
390
435
  );
391
436
  }
@@ -891,19 +936,13 @@ function RecordingWhiteboardView({ state: stateValue, files, expectedSceneId, ex
891
936
  RecordingWhiteboardView.displayName = "RecordingWhiteboardView";
892
937
 
893
938
  // src/components/logo/Logo.tsx
894
- import { useId } from "react";
895
939
  import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
896
- var DEFAULT_COLOR = "#1A332B";
940
+ var DEFAULT_COLOR = "currentColor";
897
941
  var DEFAULT_HEIGHT = 32;
898
942
  var DEFAULT_VARIANT = "wordmark";
899
943
  var WORDMARK_VIEW_BOX = "0 0 200 80";
900
944
  var MARK_VIEW_BOX = "0 0 68 80";
901
- function gradientId(prefix, name) {
902
- return `${prefix}-${name}`;
903
- }
904
945
  function Logo({ accessibilityLabel = "Chalk", color = DEFAULT_COLOR, height = DEFAULT_HEIGHT, variant = DEFAULT_VARIANT }) {
905
- const generatedId = useId().replace(/[^a-zA-Z0-9_-]/gu, "");
906
- const idPrefix = `chalk-logo-${generatedId}`;
907
946
  const isWordmark = variant === "wordmark";
908
947
  const viewBox = isWordmark ? WORDMARK_VIEW_BOX : MARK_VIEW_BOX;
909
948
  const width = isWordmark ? height * 2.5 : height * (68 / 80);
@@ -924,41 +963,14 @@ function Logo({ accessibilityLabel = "Chalk", color = DEFAULT_COLOR, height = DE
924
963
  xmlns: "http://www.w3.org/2000/svg",
925
964
  fill: "none",
926
965
  children: [
927
- /* @__PURE__ */ jsxs3("defs", { children: [
928
- /* @__PURE__ */ jsxs3("linearGradient", { id: gradientId(idPrefix, "green"), x1: "0%", y1: "0%", x2: "100%", y2: "0%", children: [
929
- /* @__PURE__ */ jsx5("stop", { offset: "0%", stopColor: "#C5E5C0" }),
930
- /* @__PURE__ */ jsx5("stop", { offset: "100%", stopColor: "#80B879" })
931
- ] }),
932
- /* @__PURE__ */ jsxs3("linearGradient", { id: gradientId(idPrefix, "yellow"), x1: "0%", y1: "0%", x2: "100%", y2: "0%", children: [
933
- /* @__PURE__ */ jsx5("stop", { offset: "0%", stopColor: "#FCEAB3" }),
934
- /* @__PURE__ */ jsx5("stop", { offset: "100%", stopColor: "#D9B641" })
966
+ /* @__PURE__ */ jsxs3("svg", { x: "0", y: "6", width: "68", height: "68", viewBox: "0 0 48 48", children: [
967
+ /* @__PURE__ */ jsxs3("g", { transform: "rotate(28 24 24)", fill: "currentColor", children: [
968
+ /* @__PURE__ */ jsx5("path", { d: "M18 8h12v28a6 6 0 0 1-12 0z" }),
969
+ /* @__PURE__ */ jsx5("path", { d: "M18 8h12l-1.5 4.5h-9z", opacity: "0.45" })
935
970
  ] }),
936
- /* @__PURE__ */ jsxs3("linearGradient", { id: gradientId(idPrefix, "blue"), x1: "0%", y1: "0%", x2: "100%", y2: "0%", children: [
937
- /* @__PURE__ */ jsx5("stop", { offset: "0%", stopColor: "#B2E0F0" }),
938
- /* @__PURE__ */ jsx5("stop", { offset: "100%", stopColor: "#55AAC9" })
939
- ] }),
940
- /* @__PURE__ */ jsxs3("linearGradient", { id: gradientId(idPrefix, "pink"), x1: "0%", y1: "0%", x2: "100%", y2: "0%", children: [
941
- /* @__PURE__ */ jsx5("stop", { offset: "0%", stopColor: "#F8CACA" }),
942
- /* @__PURE__ */ jsx5("stop", { offset: "100%", stopColor: "#D67B7B" })
943
- ] })
944
- ] }),
945
- /* @__PURE__ */ jsxs3("g", { transform: "translate(2, 2) scale(0.95)", children: [
946
- /* @__PURE__ */ jsx5("g", { className: "chalk-logo__stick chalk-logo__stick--green", children: /* @__PURE__ */ jsxs3("g", { transform: "rotate(-15 20 60)", children: [
947
- /* @__PURE__ */ jsx5("path", { d: "M9,20 L21,20 L22,63 A7,7 0 0 1 8,63 Z", fill: `url(#${gradientId(idPrefix, "green")})` }),
948
- /* @__PURE__ */ jsx5("ellipse", { cx: "15", cy: "20", rx: "6", ry: "2.5", fill: "#D8ECD4" })
949
- ] }) }),
950
- /* @__PURE__ */ jsx5("g", { className: "chalk-logo__stick chalk-logo__stick--yellow", children: /* @__PURE__ */ jsxs3("g", { transform: "rotate(-8 32 55)", children: [
951
- /* @__PURE__ */ jsx5("path", { d: "M23,15 L35,15 L36,60 A7,7 0 0 1 22,60 Z", fill: `url(#${gradientId(idPrefix, "yellow")})` }),
952
- /* @__PURE__ */ jsx5("ellipse", { cx: "29", cy: "15", rx: "6", ry: "2.5", fill: "#FEF3D1" })
953
- ] }) }),
954
- /* @__PURE__ */ jsx5("g", { className: "chalk-logo__stick chalk-logo__stick--blue", children: /* @__PURE__ */ jsxs3("g", { transform: "rotate(12 60 30)", children: [
955
- /* @__PURE__ */ jsx5("path", { d: "M36,8 L48,8 L49,56 A7,7 0 0 1 35,56 Z", fill: `url(#${gradientId(idPrefix, "blue")})` }),
956
- /* @__PURE__ */ jsx5("ellipse", { cx: "42", cy: "8", rx: "6", ry: "2.5", fill: "#D0EDF8" })
957
- ] }) }),
958
- /* @__PURE__ */ jsx5("g", { className: "chalk-logo__stick chalk-logo__stick--pink", children: /* @__PURE__ */ jsxs3("g", { transform: "rotate(5 55 50)", children: [
959
- /* @__PURE__ */ jsx5("path", { d: "M49,70 L61,70 L62,29 A7,7 0 0 0 48,29 Z", fill: `url(#${gradientId(idPrefix, "pink")})` }),
960
- /* @__PURE__ */ jsx5("ellipse", { cx: "55", cy: "70", rx: "6", ry: "2.5", fill: "#FBE4E4" })
961
- ] }) })
971
+ /* @__PURE__ */ jsx5("circle", { cx: "10.5", cy: "41.5", r: "1.7", fill: "currentColor", opacity: "0.55" }),
972
+ /* @__PURE__ */ jsx5("circle", { cx: "6.5", cy: "37", r: "1.2", fill: "currentColor", opacity: "0.4" }),
973
+ /* @__PURE__ */ jsx5("circle", { cx: "4", cy: "43.5", r: "0.85", fill: "currentColor", opacity: "0.28" })
962
974
  ] }),
963
975
  isWordmark ? /* @__PURE__ */ jsx5("text", { className: "chalk-logo__wordmark", x: "90", y: "52", fontFamily: "system-ui, -apple-system, BlinkMacSystemFont, sans-serif", fontSize: "38", fontWeight: "600", letterSpacing: "-0.02em", fill: "currentColor", children: "chalk" }) : null
964
976
  ]
@@ -1,6 +1,5 @@
1
1
  // src/components/stage/stage-layout.ts
2
2
  var WIDESCREEN = 16 / 9;
3
- var SLIM_STRIP_PX = 52;
4
3
  var MORE_TILE_ID = "stage:more";
5
4
  var DEFAULT_GRID_OPTIONS = { gap: 8, minTileWidth: 150, minTileHeight: 96, maxPerPage: Number.POSITIVE_INFINITY, minAspect: 0.8, maxAspect: 2 };
6
5
  var DEFAULT_SPOTLIGHT_OPTIONS = { gap: 8, stripRatio: 0.17, minStripHeight: 104, maxStripHeight: 150, stripAspect: WIDESCREEN };
@@ -56,11 +55,13 @@ function fitGrid(box, ids, options, wideIds = []) {
56
55
  let plan = null;
57
56
  let overflowCount = 0;
58
57
  const weight = (id) => wideIds.includes(id) ? 2 : 1;
59
- for (let count = Math.min(ids.length, Math.max(1, options.maxPerPage)); count >= 1; count -= 1) {
58
+ const lastContent = ids.reduce((last, id, index2) => wideIds.includes(id) ? index2 : last, -1);
59
+ const fewest = Math.min(ids.length, Math.max(wideIds.includes(ids[0] ?? "") ? 2 : 1, lastContent + 2));
60
+ for (let count = Math.min(ids.length, Math.floor(options.maxPerPage)); count >= 1; count -= 1) {
60
61
  overflowCount = count < ids.length ? ids.length - count + 1 : 0;
61
62
  keys = overflowCount ? [...ids.slice(0, count - 1), MORE_TILE_ID] : ids;
62
63
  plan = rowPlan(box, keys.map(weight), options);
63
- if (plan && (plan.minWidth >= options.minTileWidth && plan.height >= options.minTileHeight || count <= (wideIds.includes(ids[0] ?? "") ? 2 : 1))) break;
64
+ if (plan && (plan.minWidth >= options.minTileWidth && plan.height >= options.minTileHeight || count <= fewest)) break;
64
65
  }
65
66
  if (!plan) return EMPTY_GEOMETRY;
66
67
  let height = plan.height;
@@ -90,15 +91,52 @@ function contain(box, aspect) {
90
91
  const width = Math.min(box.width, box.height * aspect);
91
92
  return { width, height: width / aspect };
92
93
  }
94
+ var FLOAT_SPEAKER = { width: 120, height: 68 };
95
+ var FLOAT_PERSON = { width: 76, height: 44 };
96
+ var FLOAT_MAX_PEOPLE = 3;
97
+ var FLOAT_TOP = 56;
98
+ var FLOAT_BOTTOM = 8 + 64 + 60;
99
+ function floatPeople(box, primaryId, stripIds, options) {
100
+ const content = stripIds.filter((id) => options.wideIds?.includes(id));
101
+ const people = stripIds.filter((id) => !content.includes(id));
102
+ const speaker = options.speakerId && options.speakerId !== options.localId && people.includes(options.speakerId) ? options.speakerId : null;
103
+ const local = options.localId && people.includes(options.localId) ? options.localId : null;
104
+ const others = people.filter((id) => id !== speaker && id !== local);
105
+ const ordered = [...speaker ? [speaker] : [], ...others, ...local ? [local] : []];
106
+ const fold = (max2) => {
107
+ if (content.length + ordered.length <= max2) return [...content, ...ordered];
108
+ const shares = content.slice(0, max2 - 1);
109
+ const slots = max2 - 1 - shares.length;
110
+ const faces = ordered.length <= slots ? ordered : slots > 0 ? [...ordered.slice(0, slots - 1), ...ordered.slice(-1)] : [];
111
+ return [...shares, ...faces, MORE_TILE_ID];
112
+ };
113
+ const sizeOf = (id) => id === speaker || content.includes(id) ? FLOAT_SPEAKER : FLOAT_PERSON;
114
+ const bottomOf = (ids) => ids.reduce((y2, id) => y2 + sizeOf(id).height + options.gap, FLOAT_TOP) - options.gap;
115
+ let max = Math.max(1, Math.min(content.length + FLOAT_MAX_PEOPLE, options.maxPerPage ?? Infinity));
116
+ const minimum = Number.isFinite(options.maxPerPage) ? 1 : content.length + 1;
117
+ while (max > minimum && bottomOf(fold(max)) > box.height - FLOAT_BOTTOM) max -= 1;
118
+ const column = fold(max);
119
+ if (bottomOf(column) > box.height - FLOAT_BOTTOM) return null;
120
+ let y = FLOAT_TOP;
121
+ const floats = column.map((id) => {
122
+ const size = sizeOf(id);
123
+ const frame = { id, role: "float", page: 0, x: box.width - 12 - size.width, y, ...size };
124
+ y += size.height + options.gap;
125
+ return frame;
126
+ });
127
+ const shown = floats.filter((frame) => frame.id !== MORE_TILE_ID).length;
128
+ return { frames: [{ id: primaryId, role: "primary", page: 0, x: 0, y: 0, ...box }, ...floats], pageCount: 1, perPage: floats.length, overflowCount: stripIds.length - shown };
129
+ }
93
130
  function fitSpotlight(box, primaryId, stripIds, options) {
94
131
  const phone = box.width <= 640;
132
+ const maxPerPage = Math.floor(options.maxPerPage ?? Infinity);
95
133
  const aspect = options.primaryAspect === void 0 ? WIDESCREEN : options.primaryAspect;
96
- if (!stripIds.length) {
134
+ if (!stripIds.length || maxPerPage <= 0) {
97
135
  const aspect2 = clamp(box.width / box.height, 4 / 3, WIDESCREEN);
98
136
  const fitted = contain(box, aspect2);
99
137
  const width = options.content || phone ? box.width : Math.min(box.width * 0.56, fitted.width);
100
138
  const height = options.content || phone ? box.height : width / aspect2;
101
- return { frames: [{ id: primaryId, role: "primary", page: 0, x: (box.width - width) / 2, y: (box.height - height) / 2, width, height }], pageCount: 1, perPage: 0 };
139
+ return { frames: [{ id: primaryId, role: "primary", page: 0, x: (box.width - width) / 2, y: (box.height - height) / 2, width, height }], pageCount: 1, perPage: 0, overflowCount: stripIds.length };
102
140
  }
103
141
  if (phone && stripIds.length === 1 && !options.content) {
104
142
  return {
@@ -110,15 +148,17 @@ function fitSpotlight(box, primaryId, stripIds, options) {
110
148
  perPage: 1
111
149
  };
112
150
  }
151
+ const floating = phone && options.floatingPhonePeople ? floatPeople(box, primaryId, stripIds, { ...options, maxPerPage }) : null;
152
+ if (floating) return floating;
113
153
  const slim = phone && options.slimPhoneStrip === true;
114
- const strip = slim ? SLIM_STRIP_PX : phone ? Math.round(box.height * 0.3) : clamp(box.height * options.stripRatio, options.minStripHeight, options.maxStripHeight);
154
+ const strip = slim ? 52 : phone ? Math.round(box.height * 0.3) : clamp(box.height * options.stripRatio, options.minStripHeight, options.maxStripHeight);
115
155
  const column = phone && options.content ? 120 : clamp(box.width * 0.15, 180, 260);
116
156
  const belowMain = options.content ? contain({ width: box.width, height: box.height - strip - options.gap }, aspect) : { width: box.width, height: box.height - strip - options.gap };
117
157
  const besideMain = options.content ? contain({ width: box.width - column - options.gap, height: box.height }, aspect) : { width: box.width - column - options.gap, height: box.height };
118
158
  const beside = options.content ? besideMain.width * besideMain.height > belowMain.width * belowMain.height * 1.005 : !phone && Math.abs(Math.log(besideMain.width / besideMain.height / WIDESCREEN)) < Math.abs(Math.log(belowMain.width / belowMain.height / WIDESCREEN));
119
159
  const main = { width: beside ? besideMain.width : box.width, height: beside ? box.height : belowMain.height };
120
160
  const remaining = { width: beside ? box.width - main.width - options.gap : box.width, height: beside ? box.height : box.height - main.height - options.gap };
121
- const people = fitGrid(remaining, stripIds, { ...DEFAULT_GRID_OPTIONS, minTileWidth: phone ? 88 : 150, minTileHeight: slim ? SLIM_STRIP_PX : phone ? 72 : 96, maxPerPage: options.maxPerPage ?? DEFAULT_GRID_OPTIONS.maxPerPage }, options.wideIds);
161
+ const people = fitGrid(remaining, stripIds, { ...DEFAULT_GRID_OPTIONS, minTileWidth: phone ? 88 : 150, minTileHeight: slim ? 52 : phone ? 72 : 96, maxPerPage }, options.wideIds);
122
162
  return {
123
163
  ...people,
124
164
  frames: [{ id: primaryId, role: "primary", page: 0, x: 0, y: 0, ...main }, ...people.frames.map((frame) => ({ ...frame, role: "strip", x: frame.x + (beside ? main.width + options.gap : 0), y: frame.y + (beside ? 0 : main.height + options.gap) }))]
@@ -182,6 +222,12 @@ function choosePrimary(items, context) {
182
222
  if (content) return content;
183
223
  return participantWhere(items, (participant) => participant.isActiveSpeaker === true) ?? participantWhere(items, (participant) => participant.isSpeaking === true) ?? (context.lastSpeakerId ? items.find((item) => item.kind === "participant" && item.id === context.lastSpeakerId) : void 0) ?? participantWhere(items, (participant) => !participant.isLocal) ?? participantWhere(items, () => true) ?? items[0] ?? null;
184
224
  }
225
+ function speakingParticipantId(items, accept = () => true) {
226
+ return (participantWhere(items, (participant) => accept(participant) && participant.isActiveSpeaker === true) ?? participantWhere(items, (participant) => accept(participant) && participant.isSpeaking === true))?.id;
227
+ }
228
+ function floatingSpeakerId(items, lastRemoteSpeakerId) {
229
+ return speakingParticipantId(items, (participant) => !participant.isLocal) ?? lastRemoteSpeakerId;
230
+ }
185
231
  function gridOrder(items, pinnedId) {
186
232
  if (!pinnedId) return [...items];
187
233
  const pinned = items.find((item) => item.id === pinnedId);
@@ -201,5 +247,7 @@ export {
201
247
  isContentItem,
202
248
  stabilizeOrder,
203
249
  choosePrimary,
250
+ speakingParticipantId,
251
+ floatingSpeakerId,
204
252
  gridOrder
205
253
  };