@q9labsai/chalk-react 5.1.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/dist/bindings/hooks.d.ts +2 -0
  3. package/dist/{chunk-QDEKMRH2.js → chunk-CYC2G42K.js} +66 -58
  4. package/dist/{chunk-XOHNIYSR.js → chunk-G62BKRNX.js} +1534 -948
  5. package/dist/{chunk-ZFWIMKUC.js → chunk-HWQLW4JR.js} +55 -7
  6. package/dist/components/board/BoardMenu.d.ts +4 -1
  7. package/dist/components/board/board-draw-policy.d.ts +15 -0
  8. package/dist/components/board/board-model.d.ts +5 -1
  9. package/dist/components/board/board-view-versions.d.ts +1 -7
  10. package/dist/components/chalk/Chalk.d.ts +4 -0
  11. package/dist/components/chalk/ReportProblemDialog.d.ts +12 -0
  12. package/dist/components/chalk/StatusSurface.d.ts +3 -2
  13. package/dist/components/chalk-ui/ChalkSheet.d.ts +1 -1
  14. package/dist/components/chalk-ui/ChalkToast.d.ts +7 -1
  15. package/dist/components/control-bar/command-failure.d.ts +105 -1
  16. package/dist/components/control-bar/control-bar-behavior.d.ts +5 -1
  17. package/dist/components/index.js +3 -3
  18. package/dist/components/offline-actions/OfflineActions.d.ts +2 -1
  19. package/dist/components/space-header/CallDetails.d.ts +4 -2
  20. package/dist/components/space-header/space-header-contract.d.ts +5 -0
  21. package/dist/components/space-view/SpaceView.d.ts +8 -3
  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/internal/whiteboard-view-limits.d.ts +1 -0
  36. package/dist/public-surface.contract.d.ts +1 -1
  37. package/dist/test-support/index.js +3 -3
  38. package/package.json +5 -5
@@ -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
  };
@@ -1,3 +1,4 @@
1
+ import type { WhoCanDraw } from "./board-draw-policy";
1
2
  import { type BoardSurface, type IslandPlace } from "./board-model";
2
3
  export type BoardMenuProps = {
3
4
  readonly surface: BoardSurface;
@@ -7,9 +8,11 @@ export type BoardMenuProps = {
7
8
  readonly onPlace: (place: IslandPlace) => void;
8
9
  readonly onDownload: () => void;
9
10
  readonly onCopy: () => void;
11
+ /** Offered to hosts. */
12
+ readonly whoCanDraw?: WhoCanDraw;
10
13
  readonly onBring?: () => void;
11
14
  readonly onClear?: () => void;
12
15
  readonly onClose?: () => void;
13
16
  };
14
17
  /** The board's own menu: export, the person's own appearance, and the host's board actions. */
15
- export declare function BoardMenu({ surface, onSurface, place, onPlace, onDownload, onCopy, onBring, onClear, onClose }: BoardMenuProps): import("react").JSX.Element;
18
+ export declare function BoardMenu({ surface, onSurface, place, onPlace, onDownload, onCopy, whoCanDraw, onBring, onClear, onClose }: BoardMenuProps): import("react").JSX.Element;
@@ -0,0 +1,15 @@
1
+ import type { ChalkWhiteboardDrawPolicy } from "@q9labsai/chalk-client";
2
+ import type { BoardToast } from "./board-toasts";
3
+ export type WhoCanDraw = {
4
+ readonly policy: ChalkWhiteboardDrawPolicy;
5
+ /** Everyone in the call who is not a host, because hosts can always draw under hosts only and picked people. */
6
+ readonly people: readonly {
7
+ readonly id: string;
8
+ readonly name: string;
9
+ readonly picked: boolean;
10
+ }[];
11
+ readonly onPolicy: (policy: ChalkWhiteboardDrawPolicy) => void;
12
+ readonly onPick: (id: string, picked: boolean) => void;
13
+ };
14
+ /** The host's call-wide drawing control. Undefined for people who aren't hosts and on Sync servers without draw_policy_v1. */
15
+ export declare function useWhoCanDraw(toast: (toast: BoardToast) => void): WhoCanDraw | undefined;
@@ -1,4 +1,4 @@
1
- import { type ChalkWhiteboardView } from "@q9labsai/chalk-client";
1
+ import { type ChalkWhiteboardDrawPolicy, type ChalkWhiteboardView } from "@q9labsai/chalk-client";
2
2
  /** design.md, "The board": every colour, size and key the board's own controls use. */
3
3
  export type BoardSurface = "chalkboard" | "paper" | "dots";
4
4
  export type IslandPlace = "bottom" | "top" | "left";
@@ -15,6 +15,10 @@ export declare const ISLAND_PLACES: readonly {
15
15
  readonly id: IslandPlace;
16
16
  readonly label: string;
17
17
  }[];
18
+ export declare const DRAW_POLICIES: readonly {
19
+ readonly id: ChalkWhiteboardDrawPolicy;
20
+ readonly label: string;
21
+ }[];
18
22
  export declare const PEN_COLORS: readonly {
19
23
  readonly id: PenColor;
20
24
  readonly label: string;
@@ -1,7 +1 @@
1
- import type { ChalkWhiteboardView } from "@q9labsai/chalk-client";
2
- /** A bounded record of the latest applied version, not a history of received frames. */
3
- export declare class ViewVersions {
4
- #private;
5
- isNew(view: ChalkWhiteboardView): boolean;
6
- remember(view: ChalkWhiteboardView): void;
7
- }
1
+ export { ChalkWhiteboardViewVersions as ViewVersions } from "@q9labsai/chalk-client";
@@ -43,6 +43,10 @@ export type ChalkProps = SpaceIntegration & SpaceEventCallbacks & {
43
43
  readonly theme?: ChalkTheme;
44
44
  readonly shareUrl?: string;
45
45
  readonly onReportProblem?: import("../space-header/SpaceHeader").SpaceHeaderProps["onReportProblem"];
46
+ /** Opens an app-owned dialog that can outlive this call view. */
47
+ readonly onOpenReportProblem?: () => void;
48
+ /** Hides new report actions and disables Send without discarding an open report. */
49
+ readonly reportUnavailableReason?: string;
46
50
  readonly onColorSchemeChange?: (colorScheme: ChalkColorScheme) => void;
47
51
  readonly pickChatFiles?: () => Promise<readonly ChatUploadFile[]>;
48
52
  readonly spaceName?: string;
@@ -0,0 +1,12 @@
1
+ import type { RefObject } from "react";
2
+ import type { ChalkProps } from "./Chalk";
3
+ /** The call report form for app-owned terminal screens outside Chalk. */
4
+ export declare function ReportProblemDialog({ open, onClose, onReportProblem, colorScheme, focusScope, diagnostics, unavailableReason, }: {
5
+ readonly diagnostics?: string;
6
+ readonly unavailableReason?: string;
7
+ readonly open: boolean;
8
+ readonly onClose: () => void;
9
+ readonly onReportProblem: NonNullable<ChalkProps["onReportProblem"]>;
10
+ readonly colorScheme: "light" | "dark";
11
+ readonly focusScope: RefObject<HTMLElement | null>;
12
+ }): import("react").JSX.Element;
@@ -1,5 +1,5 @@
1
1
  import type React from "react";
2
- import type { ErrorCode } from "@q9labsai/chalk-client";
2
+ import { type ClientFailure, type ErrorCode } from "@q9labsai/chalk-client";
3
3
  import { type ChalkTheme } from "../../theme";
4
4
  export type StatusSurfacePhase = "entering" | "left" | "episode-ended" | "failed" | "removed" | "full" | "elsewhere";
5
5
  export interface StatusSurfaceProps {
@@ -7,6 +7,7 @@ export interface StatusSurfaceProps {
7
7
  readonly callName?: string;
8
8
  readonly reason?: string;
9
9
  readonly errorCode?: ErrorCode;
10
+ readonly sync?: ClientFailure["sync"];
10
11
  readonly theme?: ChalkTheme;
11
12
  readonly onRejoin?: () => void;
12
13
  readonly onHome?: () => void;
@@ -16,4 +17,4 @@ export interface StatusSurfaceProps {
16
17
  readonly retryPending?: boolean;
17
18
  }
18
19
  /** All terminal call states share the same quiet page and host-owned navigation. */
19
- export declare function StatusSurface({ phase, callName, reason, errorCode, theme, onRejoin, onHome, onRetry, onReportProblem, rejoinDisabled, retryPending }: StatusSurfaceProps): React.JSX.Element;
20
+ export declare function StatusSurface({ phase, callName, reason, errorCode, sync, theme, onRejoin, onHome, onRetry, onReportProblem, rejoinDisabled, retryPending }: StatusSurfaceProps): React.JSX.Element;
@@ -1,6 +1,6 @@
1
1
  import { type ReactNode } from "react";
2
2
  export interface ChalkSheetProps {
3
- readonly variant?: "default" | "details" | "panel" | "ready";
3
+ readonly variant?: "default" | "details" | "panel" | "ready" | "stage";
4
4
  readonly open: boolean;
5
5
  readonly onOpenChange: (open: boolean) => void;
6
6
  readonly title: string;
@@ -1,6 +1,10 @@
1
1
  import { type ReactNode } from "react";
2
2
  export interface ChalkToastProps {
3
3
  readonly message: ReactNode;
4
+ /** Stable semantic identity for rich message content. Change it when the message changes. */
5
+ readonly announcementKey?: string;
6
+ /** Plain speech text for custom message components. Literal text and markup are read without mounting them. */
7
+ readonly announcementText?: string;
4
8
  readonly icon?: ReactNode;
5
9
  readonly type?: "info" | "success" | "warning" | "error";
6
10
  readonly duration?: number;
@@ -8,6 +12,8 @@ export interface ChalkToastProps {
8
12
  readonly label: string;
9
13
  readonly onClick: () => void;
10
14
  };
15
+ /** Enables live announcements. Pass false for an already-announced notice; a replacement message can enable the same mounted toast later. */
16
+ readonly announce?: boolean;
11
17
  readonly onDismiss: () => void;
12
18
  }
13
- export declare function ChalkToast({ message, type, duration, action, icon, onDismiss }: ChalkToastProps): import("react").JSX.Element;
19
+ export declare function ChalkToast({ message, announcementKey, announcementText, type, duration, action, icon, announce, onDismiss }: ChalkToastProps): import("react").JSX.Element;
@@ -1,2 +1,106 @@
1
- import { type ClientFailure } from "@q9labsai/chalk-client";
1
+ import { type ClientFailure, type MediaSource } from "@q9labsai/chalk-client";
2
+ export type MediaCommandSource = MediaSource | "speaker";
3
+ export type MediaCommandAction = "start" | "stop" | "select" | "test" | "request";
4
+ type Operation = MediaCommandAction | "change";
5
+ type Context = {
6
+ readonly source?: MediaCommandSource;
7
+ readonly recoverable: boolean;
8
+ };
9
+ type Copy = (context: Context) => string;
10
+ type Rule = {
11
+ readonly copy: Readonly<Record<Operation, Copy>>;
12
+ readonly retry: boolean;
13
+ };
2
14
  export declare function commandFailure(cause: unknown): ClientFailure;
15
+ /** Every client code has a media-context policy. Non-media commands are preserved at the boundary below. */
16
+ export declare const MEDIA_FAILURE_POLICY: {
17
+ "access.invalid": Rule;
18
+ "access.unavailable": Rule;
19
+ "chat.cursor_reset_required": Rule;
20
+ "chat.payload_invalid": Rule;
21
+ "client.internal_error": Rule;
22
+ "collaboration.unavailable": Rule;
23
+ "command.rate_limited": Rule;
24
+ "command.rejected": Rule;
25
+ "connection.invalid_state": {
26
+ copy: {
27
+ test: (context: Context) => string;
28
+ request: (context: Context) => string;
29
+ start: (context: Context) => string;
30
+ stop: (context: Context) => string;
31
+ select: (context: Context) => string;
32
+ change: (context: Context) => string;
33
+ };
34
+ retry: true;
35
+ };
36
+ "connection.join_cleanup_unconfirmed": Rule;
37
+ "connection.leave_unconfirmed": Rule;
38
+ "connection.media_recovery_exhausted": Rule;
39
+ "connection.media_start_failed": Rule;
40
+ "connection.state_digest_recovery_failed": Rule;
41
+ "connection.sync_recovery_exhausted": Rule;
42
+ "connection.sync_start_failed": Rule;
43
+ "connection.sync_hello_timeout": Rule;
44
+ "environment.unsupported": {
45
+ copy: {
46
+ stop: (context: Context) => string;
47
+ start: () => string;
48
+ request: () => string;
49
+ select: (context: Context) => "Media capture is unavailable in this environment. Use a supported browser over HTTPS or the native app." | "This browser can’t change your speaker. Use your system sound settings to choose an output device.";
50
+ test: () => string;
51
+ change: (context: Context) => "Media capture is unavailable in this environment. Use a supported browser over HTTPS or the native app." | "This browser can’t change your speaker. Use your system sound settings to choose an output device.";
52
+ };
53
+ retry: false;
54
+ };
55
+ "episode.ended": {
56
+ copy: {
57
+ start: () => string;
58
+ stop: () => string;
59
+ select: () => string;
60
+ test: () => string;
61
+ request: () => string;
62
+ change: () => string;
63
+ };
64
+ retry: false;
65
+ };
66
+ "media.permission_denied": Rule;
67
+ "media.device_not_found": Rule;
68
+ "media.device_busy": {
69
+ copy: Readonly<Record<Operation, Copy>>;
70
+ retry: true;
71
+ };
72
+ "media.device_constraint_invalid": Rule;
73
+ "media.capture_failed": {
74
+ copy: {
75
+ stop: (context: Context) => string;
76
+ select: (context: Context) => string;
77
+ test: (context: Context) => string;
78
+ start: Copy;
79
+ request: Copy;
80
+ change: (context: Context) => string;
81
+ };
82
+ retry: true;
83
+ };
84
+ "media.request_invalid": {
85
+ copy: {
86
+ test: () => string;
87
+ request: () => string;
88
+ select: (context: Context) => string;
89
+ start: () => string;
90
+ stop: () => string;
91
+ change: () => string;
92
+ };
93
+ retry: false;
94
+ };
95
+ "media.timeout": Rule;
96
+ "offline.discarded": Rule;
97
+ "participant.invalid": Rule;
98
+ "reaction.invalid": Rule;
99
+ "whiteboard.unavailable": Rule;
100
+ };
101
+ export declare function commandFailurePresentation(failure: ClientFailure, source?: MediaCommandSource, operation?: Operation, hasExactRetry?: boolean): {
102
+ readonly message: string;
103
+ readonly retry: boolean;
104
+ };
105
+ export declare function commandFailureMessage(failure: ClientFailure, source?: MediaCommandSource, operation?: MediaCommandAction): string;
106
+ export {};
@@ -1,4 +1,5 @@
1
1
  import type React from "react";
2
+ import { type MediaCommandSource, type MediaCommandAction } from "./command-failure";
2
3
  import type { CameraPreview, ClientFailure, MediaSource, ScreenShareOptions } from "@q9labsai/chalk-client";
3
4
  import { type ParticipantGradientPreference } from "../../utils/colorGenerator";
4
5
  export interface MediaDevice {
@@ -93,7 +94,8 @@ export interface ControlBarProps {
93
94
  /** Set for hosts: the leave button opens a menu with Leave call and End call for everyone. */
94
95
  readonly onEndForEveryone?: () => void;
95
96
  readonly onLeft?: () => void;
96
- readonly onCommandError?: (failure: ClientFailure | null, source?: MediaSource) => void;
97
+ /** The second argument retains the published MediaSource contract; the third also identifies speaker commands. */
98
+ readonly onCommandError?: (failure: ClientFailure | null, source?: MediaSource, commandSource?: MediaCommandSource, action?: MediaCommandAction, retryCommand?: () => Promise<unknown>) => void;
97
99
  readonly participantColorSeed?: string;
98
100
  readonly participantGradientPreference?: ParticipantGradientPreference;
99
101
  readonly className?: string;
@@ -124,6 +126,8 @@ export declare function useControlBarSurfaceBehavior({ density, buttons, audioIn
124
126
  interface ConnectedControlBarBehavior {
125
127
  readonly surfaceProps: ControlBarSurfaceProps;
126
128
  readonly commandError: ClientFailure | null;
129
+ readonly commandSource?: MediaCommandSource;
130
+ readonly commandAction?: MediaCommandAction;
127
131
  }
128
132
  export declare function useConnectedControlBarBehavior(props: ControlBarProps): ConnectedControlBarBehavior;
129
133
  export {};
@@ -5,7 +5,7 @@ import {
5
5
  RecordingHistoryPanel,
6
6
  RecordingSpaceView,
7
7
  RecordingWhiteboardView
8
- } from "../chunk-QDEKMRH2.js";
8
+ } from "../chunk-CYC2G42K.js";
9
9
  import {
10
10
  loadRecordingWhiteboardFiles,
11
11
  parseRecordingWhiteboardStateV1,
@@ -45,10 +45,10 @@ import {
45
45
  ReactionsOverlay,
46
46
  ScreenShareView,
47
47
  Stage
48
- } from "../chunk-XOHNIYSR.js";
48
+ } from "../chunk-G62BKRNX.js";
49
49
  import {
50
50
  buildStageItems
51
- } from "../chunk-ZFWIMKUC.js";
51
+ } from "../chunk-HWQLW4JR.js";
52
52
  import "../chunk-4U7QZC72.js";
53
53
  export {
54
54
  AdmissionPanel,
@@ -3,6 +3,7 @@ import React from "react";
3
3
  export declare const RECONNECTING_GRACE_MS = 2000;
4
4
  export declare function offlineNoticeText(pendingCount: number, online: boolean): string;
5
5
  /** Keep queued actions automatic while preserving the reconnecting notice. */
6
- export declare function OfflineActions({ reconnecting: requestedReconnecting }?: {
6
+ export declare function OfflineActions({ reconnecting: requestedReconnecting, hasNotice }?: {
7
7
  readonly reconnecting?: boolean;
8
+ readonly hasNotice?: boolean;
8
9
  }): React.JSX.Element | null;
@@ -1,13 +1,15 @@
1
1
  import { type RefObject } from "react";
2
2
  import type { SpaceHeaderProps } from "./space-header-contract";
3
3
  export declare const CALL_SHORTCUTS: readonly [readonly ["Mute or unmute", "⌘ D"], readonly ["Turn camera on or off", "⌘ E"], readonly ["Raise or lower hand", "⌃ ⌘ H"], readonly ["Open or close chat", "⌃ ⌘ C"], readonly ["Open or close people", "⌃ ⌘ P"], readonly ["Switch layout", "⌃ ⌘ L"], readonly ["Talk while muted", "Hold Space"], readonly ["Show shortcuts", "?"], readonly ["Close a menu or panel", "Esc"]];
4
- export declare function CallDialog({ kind, open, onClose, onReportProblem, diagnostics, returnFocus, }: {
4
+ export declare function CallDialog({ kind, open, onClose, onReportProblem, diagnostics, returnFocus, focusScope, unavailableReason, }: {
5
5
  readonly kind: "shortcuts" | "report";
6
6
  readonly open: boolean;
7
7
  readonly onClose: () => void;
8
8
  readonly onReportProblem?: SpaceHeaderProps["onReportProblem"];
9
9
  readonly diagnostics: string;
10
- readonly returnFocus: RefObject<HTMLButtonElement | null>;
10
+ readonly returnFocus?: RefObject<HTMLButtonElement | null>;
11
+ readonly focusScope: RefObject<HTMLElement | null>;
12
+ readonly unavailableReason?: string;
11
13
  }): import("react").JSX.Element;
12
14
  export declare function CallDetails({ shareUrl, spaceName, onClose, diagnostics, colorScheme, onColorSchemeChange, sounds, onSoundsChange, onDialog, onReportProblem, }: Pick<SpaceHeaderProps, "spaceName" | "shareUrl" | "diagnostics" | "colorScheme" | "onColorSchemeChange" | "onReportProblem"> & {
13
15
  readonly onClose: () => void;
@@ -1,3 +1,4 @@
1
+ import type { ReactNode } from "react";
1
2
  export interface SpaceHeaderProps {
2
3
  spaceName: string;
3
4
  people?: readonly {
@@ -18,6 +19,8 @@ export interface SpaceHeaderProps {
18
19
  readonly description: string;
19
20
  readonly diagnostics?: string;
20
21
  }) => void | Promise<void>;
22
+ reportUnavailableReason?: string;
23
+ onOpenReportProblem?: () => void;
21
24
  shortcuts?: {
22
25
  readonly isMuted?: boolean;
23
26
  readonly onToggleMute?: () => void;
@@ -36,6 +39,8 @@ export interface SpaceHeaderProps {
36
39
  isTranscribing?: boolean;
37
40
  layout?: "grid" | "focus";
38
41
  onLayoutChange?: (layout: "grid" | "focus") => void;
42
+ /** A control for notices that have no room on the stage, placed at the header's end, before the people button. */
43
+ notices?: ReactNode;
39
44
  className?: string;
40
45
  }
41
46
  export declare function formatDuration(seconds: number): string;
@@ -1,4 +1,5 @@
1
- import { type ChalkWhiteboardView } from "@q9labsai/chalk-client";
1
+ import { type MediaCommandSource, type MediaCommandAction } from "../control-bar/command-failure";
2
+ import { type ChalkWhiteboardView, type ClientFailure } from "@q9labsai/chalk-client";
2
3
  import React from "react";
3
4
  import type { ChatUploadFile } from "@q9labsai/chalk-client";
4
5
  import { type ReconnectingOverlayProps } from "../reconnecting-overlay/ReconnectingOverlay";
@@ -30,6 +31,8 @@ export interface SpaceViewProps {
30
31
  readonly callLinkOpen?: boolean;
31
32
  readonly onCallLinkToggle?: () => void;
32
33
  readonly onReportProblem?: import("../space-header/SpaceHeader").SpaceHeaderProps["onReportProblem"];
34
+ readonly reportUnavailableReason?: string;
35
+ readonly onOpenReportProblem?: () => void;
33
36
  readonly inviteLink?: string;
34
37
  readonly pickChatFiles?: () => Promise<readonly ChatUploadFile[]>;
35
38
  readonly stageBackground?: boolean;
@@ -39,7 +42,9 @@ export interface SpaceViewProps {
39
42
  readonly initialPanel?: SpacePanel | null;
40
43
  readonly features?: SpaceViewFeatures;
41
44
  readonly generatedAvatars?: boolean;
42
- readonly commandError?: string;
45
+ readonly commandError?: string | ClientFailure;
46
+ readonly commandSource?: MediaCommandSource;
47
+ readonly commandAction?: MediaCommandAction;
43
48
  readonly onDismissCommandError?: () => void;
44
49
  readonly onToggleWhiteboard?: () => void;
45
50
  readonly whiteboard?: SpaceViewWhiteboard;
@@ -51,4 +56,4 @@ export interface SpaceViewProps {
51
56
  readonly onEndEpisode?: () => void | Promise<void>;
52
57
  readonly className?: string;
53
58
  }
54
- export declare function SpaceView({ colorScheme, onColorSchemeChange, spaceName, shareUrl, callLinkOpen, onCallLinkToggle, onReportProblem, inviteLink, pickChatFiles, stageBackground, layout: controlledLayout, onLayoutChange, initialPanel, features, generatedAvatars, commandError: externalCommandError, onDismissCommandError, onToggleWhiteboard, whiteboard, reconnecting, overlay, onLeft, onEndEpisode, className, }: SpaceViewProps): React.JSX.Element;
59
+ export declare function SpaceView({ colorScheme, onColorSchemeChange, spaceName, shareUrl, callLinkOpen, onCallLinkToggle, onReportProblem, reportUnavailableReason, onOpenReportProblem, inviteLink, pickChatFiles, stageBackground, layout: controlledLayout, onLayoutChange, initialPanel, features, generatedAvatars, commandError: externalCommandError, commandSource: externalCommandSource, commandAction: externalCommandAction, onDismissCommandError, onToggleWhiteboard, whiteboard, reconnecting, overlay, onLeft, onEndEpisode, className, }: SpaceViewProps): React.JSX.Element;
@@ -24,7 +24,7 @@ export interface StageProps {
24
24
  readonly followingId?: string | null;
25
25
  readonly onFollowChange?: (id: string) => void;
26
26
  readonly clickAction?: "pin" | "follow";
27
- /** Upper bound of visible tiles; the rest fold into a +N People tile. */
27
+ /** Upper bound of grid tiles, or secondary tiles in spotlight; overflow includes shared content. */
28
28
  readonly maxPerPage?: number;
29
29
  /** Tiles narrower than this fold the rest into overflow. */
30
30
  readonly minTileWidth?: number;
@@ -28,5 +28,9 @@ export interface PrimaryContext {
28
28
  }
29
29
  /** A pin wins; otherwise the first live share, board, then active speaker leads Spotlight. */
30
30
  export declare function choosePrimary(items: readonly StageItem[], context: PrimaryContext): StageItem | null;
31
+ /** Who is speaking now: the active speaker before anyone else speaking. */
32
+ export declare function speakingParticipantId(items: readonly StageItem[], accept?: (participant: Participant) => boolean): string | undefined;
33
+ /** The remote person whose face leads the floating column: whoever is speaking now, else the last remote speaker. */
34
+ export declare function floatingSpeakerId(items: readonly StageItem[], lastRemoteSpeakerId: string | null): string | null;
31
35
  /** Grid order: pinned item first, everything else in stable order. */
32
36
  export declare function gridOrder(items: readonly StageItem[], pinnedId: string | null): StageItem[];
@@ -3,7 +3,7 @@ export interface StageBox {
3
3
  readonly width: number;
4
4
  readonly height: number;
5
5
  }
6
- export type StageRole = "primary" | "strip" | "grid";
6
+ export type StageRole = "primary" | "strip" | "grid" | "float";
7
7
  export interface StageFrame {
8
8
  readonly id: string;
9
9
  readonly role: StageRole;
@@ -37,8 +37,12 @@ export interface SpotlightFitOptions {
37
37
  readonly content?: boolean;
38
38
  readonly wideIds?: readonly string[];
39
39
  readonly maxPerPage?: number;
40
- /** On a phone the board keeps the people to one slim row, so the drawing gets the screen. */
40
+ /** @deprecated Prefer floatingPhonePeople for boards. Preserves the 52px phone strip. */
41
41
  readonly slimPhoneStrip?: boolean;
42
+ /** On a phone the board keeps the whole stage, and people float in a short column at its right edge. */
43
+ readonly floatingPhonePeople?: boolean;
44
+ readonly speakerId?: string | null;
45
+ readonly localId?: string | null;
42
46
  }
43
47
  export declare const WIDESCREEN: number;
44
48
  export declare const MORE_TILE_ID = "stage:more";
@@ -1,4 +1,4 @@
1
1
  export declare function CallEventIcon({ kind, error }: {
2
- readonly kind: "microphone" | "microphone-on" | "camera" | "screen" | "recording" | "hand" | "host";
2
+ readonly kind: "speaker" | "microphone" | "microphone-on" | "camera" | "screen" | "recording" | "hand" | "host";
3
3
  readonly error?: boolean;
4
4
  }): import("react").JSX.Element;
@@ -0,0 +1,7 @@
1
+ import type { Toast } from "./ToastStack";
2
+ export interface NoticeButtonProps {
3
+ readonly toasts: readonly Toast[];
4
+ readonly onDismiss: (id: string) => void;
5
+ }
6
+ /** The header's way into notices that have no room on the stage, such as on a short screen with the board's tools open. */
7
+ export declare function NoticeButton({ toasts, onDismiss }: NoticeButtonProps): import("react").JSX.Element | null;
@@ -1,5 +1,5 @@
1
1
  import { type ChalkToastProps } from "../chalk-ui/ChalkToast";
2
- export interface Toast extends Omit<ChalkToastProps, "onDismiss"> {
2
+ export interface Toast extends Omit<ChalkToastProps, "onDismiss" | "announce"> {
3
3
  readonly id: string;
4
4
  }
5
5
  export interface ToastStackProps {
@@ -7,5 +7,16 @@ export interface ToastStackProps {
7
7
  readonly onDismiss: (id: string) => void;
8
8
  readonly maxVisible?: number;
9
9
  readonly className?: string;
10
+ /**
11
+ * Called with the ids of the notices the stage has no room for, so the host can offer them elsewhere, such as from the header.
12
+ * With it, a call shorter than `FOLD_BELOW_PX` folds every notice. Taller calls fold notices past `maxVisible` or outside the available band, announcing each folded message once.
13
+ */
14
+ readonly onFold?: (folded: readonly string[]) => void;
10
15
  }
11
- export declare const ToastStack: import("react").MemoExoticComponent<({ toasts, onDismiss, maxVisible, className }: ToastStackProps) => import("react").JSX.Element>;
16
+ /**
17
+ * Calls shorter than this fold every notice. It is the height the mockup's three-notice stack needs at its tallest, on a phone with
18
+ * the board open: 108px for the header and the board's menu, an 8px gap, three camera errors wrapped to three lines (79px each, with
19
+ * 8px gaps), and the 242px the open drawing tools lift notices by, 611px in all.
20
+ */
21
+ export declare const FOLD_BELOW_PX = 620;
22
+ export declare const ToastStack: import("react").MemoExoticComponent<({ toasts, onDismiss, maxVisible, className, onFold }: ToastStackProps) => import("react").JSX.Element>;
@@ -1,5 +1,6 @@
1
+ import { type MediaCommandSource, type MediaCommandAction } from "../control-bar/command-failure";
1
2
  import type { ClientFailure, SpaceSnapshot } from "@q9labsai/chalk-client";
2
3
  import type { Toast } from "./ToastStack";
3
- export declare function mediaFailureToast(failure: ClientFailure | null, source?: "microphone" | "camera" | "screen"): Omit<Toast, "id"> | null;
4
+ export declare function mediaFailureToast(failure: ClientFailure | null, source?: MediaCommandSource, action?: MediaCommandAction, onRetry?: () => void): Omit<Toast, "id"> | null;
4
5
  /** Snapshot changes, not initial history, are events for the person already in the call. */
5
6
  export declare function callEventToasts(previous: SpaceSnapshot, next: SpaceSnapshot): Omit<Toast, "id">[];
@@ -0,0 +1,4 @@
1
+ import type { ChalkToastProps } from "../chalk-ui/ChalkToast";
2
+ export declare function noticeAnnouncement(toast: Pick<ChalkToastProps, "message" | "announcementText">): string;
3
+ /** Semantic speech/lifetime revision, independent of JSX and layout chrome. */
4
+ export declare function noticeIdentity(toast: Pick<ChalkToastProps, "message" | "announcementText" | "announcementKey" | "type">): string;
@@ -0,0 +1,4 @@
1
+ /** A layout-only DOM snapshot: never instantiate React callers, custom elements, or media resources. */
2
+ export declare function captureToastLayout(toast: HTMLElement): HTMLDivElement;
3
+ /** Reflow cached markup at the current band's width without mounting the folded caller tree. */
4
+ export declare function measureToastLayout(stack: HTMLElement, snapshot: HTMLDivElement): number;
@@ -0,0 +1,7 @@
1
+ import { type RefObject } from "react";
2
+ import type { Toast } from "./ToastStack";
3
+ /** Remember measured sizes when notices fold: resizing must not remount them to measure again. */
4
+ export declare function useToastCapacity(stack: RefObject<HTMLDivElement | null>, candidates: readonly Toast[], enabled: boolean, ownedIds: readonly string[]): {
5
+ readonly count: number;
6
+ readonly ready: boolean;
7
+ };
package/dist/headless.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  fitSpotlight,
14
14
  gridOrder,
15
15
  screenShareItemId
16
- } from "./chunk-ZFWIMKUC.js";
16
+ } from "./chunk-HWQLW4JR.js";
17
17
  export {
18
18
  DEFAULT_GRID_OPTIONS,
19
19
  DEFAULT_SPOTLIGHT_OPTIONS,