@sansavision/yousaid-react 0.0.0-stage → 0.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.
package/dist/index.js ADDED
@@ -0,0 +1,441 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useMemo, useState, } from "react";
4
+ import { collectDiagnostics, SIGNAL_ATTACHMENT_LIMITS, mentionAtCaret, } from "@sansavision/yousaid";
5
+ import { useYouSaid } from "./context.js";
6
+ import { AgentMentionMenu, DEFAULT_MENTION_AGENTS, filterAgents, mentionMenuKey, RequestHumanButton, RoomCanvas, roomAgentStyles } from "./rooms.js";
7
+ export { YouSaidProvider, useYouSaid } from "./context.js";
8
+ export { AgentMentionMenu, canvasEntries, DEFAULT_CANVAS_LABELS, DEFAULT_MENTION_AGENTS, filterAgents, mentionMenuKey, RequestHumanButton, RoomCanvas, roomAgentStyles, useAgentRun, usePendingApprovals, useRequestHuman, useRoomCanvas, useRoomEvents, useRoomMembers, } from "./rooms.js";
9
+ export function YouSaidLauncher({ label = "Talk to us", defaultView = "feedback", appVersion, featureFlags, breadcrumbs, verifyInstall = true, }) {
10
+ const client = useYouSaid();
11
+ const [open, setOpen] = useState(false);
12
+ const [pending, setPending] = useState(false);
13
+ const [receipt, setReceipt] = useState(null);
14
+ const [error, setError] = useState("");
15
+ const [files, setFiles] = useState([]);
16
+ const [shareDiagnostics, setShareDiagnostics] = useState(false);
17
+ const preview = useMemo(() => shareDiagnostics ? collectDiagnostics({ consent: true, ...(appVersion ? { appVersion } : {}), ...(featureFlags ? { featureFlags } : {}), ...(breadcrumbs ? { breadcrumbs } : {}) }) : null, [shareDiagnostics, appVersion, featureFlags, breadcrumbs]);
18
+ useEffect(() => {
19
+ if (!verifyInstall || installVerified.has(client))
20
+ return;
21
+ installVerified.add(client);
22
+ // Health reporting must never disturb the host app.
23
+ void client.verifyInstallation().catch(() => undefined);
24
+ }, [client, verifyInstall]);
25
+ function chooseFiles(list) {
26
+ const chosen = Array.from(list ?? []);
27
+ if (chosen.length > SIGNAL_ATTACHMENT_LIMITS.maxFiles) {
28
+ setError(`Attach at most ${SIGNAL_ATTACHMENT_LIMITS.maxFiles} files.`);
29
+ return;
30
+ }
31
+ if (chosen.some((file) => file.size > SIGNAL_ATTACHMENT_LIMITS.maxFileBytes)) {
32
+ setError("Each file must be 5 MB or smaller.");
33
+ return;
34
+ }
35
+ if (chosen.some((file) => !SIGNAL_ATTACHMENT_LIMITS.types.includes(file.type))) {
36
+ setError("Attach images, PDF, text, CSV, or JSON files.");
37
+ return;
38
+ }
39
+ setError("");
40
+ setFiles(chosen);
41
+ }
42
+ async function submit(event) {
43
+ event.preventDefault();
44
+ const form = event.currentTarget;
45
+ const data = new FormData(form);
46
+ const input = {
47
+ kind: data.get("kind"),
48
+ title: String(data.get("title") ?? ""),
49
+ body: String(data.get("body") ?? ""),
50
+ context: { route: location.pathname, ...(appVersion ? { appVersion } : {}) },
51
+ ...(preview ? { diagnostics: preview } : {}),
52
+ ...(files.length ? { attachments: files } : {}),
53
+ };
54
+ setPending(true);
55
+ setError("");
56
+ try {
57
+ setReceipt(await client.sendSignal(input));
58
+ form.reset();
59
+ setFiles([]);
60
+ setShareDiagnostics(false);
61
+ }
62
+ catch (reason) {
63
+ setError(reason instanceof Error
64
+ ? reason.message
65
+ : "We could not send your message.");
66
+ }
67
+ finally {
68
+ setPending(false);
69
+ }
70
+ }
71
+ return (_jsxs("div", { "data-yousaid-root": "true", children: [_jsx("button", { type: "button", onClick: () => setOpen((value) => !value), "aria-expanded": open, children: label }), open ? (_jsx("section", { role: "dialog", "aria-label": defaultView === "rooms" ? "Customer Rooms" : "Send product feedback", children: defaultView === "rooms" ? (_jsx(YouSaidRooms, { embedded: true })) : receipt ? (_jsx("p", { role: "status", children: receipt.receipt })) : (_jsxs("form", { onSubmit: submit, children: [_jsxs("label", { children: ["Type", _jsxs("select", { name: "kind", defaultValue: "support", children: [_jsx("option", { value: "support", children: "Support" }), _jsx("option", { value: "bug", children: "Bug" }), _jsx("option", { value: "feature", children: "Feature" }), _jsx("option", { value: "ux", children: "Experience" }), _jsx("option", { value: "review", children: "Review" })] })] }), _jsxs("label", { children: ["Title", _jsx("input", { name: "title", required: true, minLength: 3, maxLength: 180 })] }), _jsxs("label", { children: ["Message", _jsx("textarea", { name: "body", required: true, minLength: 3, maxLength: 10000 })] }), _jsxs("label", { children: ["Attach files (optional)", _jsx("input", { type: "file", multiple: true, accept: SIGNAL_ATTACHMENT_LIMITS.types.join(","), onChange: (event) => chooseFiles(event.currentTarget.files) })] }), _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: shareDiagnostics, onChange: (event) => setShareDiagnostics(event.currentTarget.checked) }), "Share technical details to help fix this"] }), preview ? (_jsxs("details", { open: true, children: [_jsx("summary", { children: "What will be shared" }), _jsx("pre", { "data-yousaid-diagnostics-preview": "true", children: JSON.stringify({ ...preview, consent: undefined }, null, 2) }), _jsx("small", { children: "Page addresses are sent without query strings. Personal data and secrets are removed before storage." })] })) : null, error ? _jsx("p", { role: "alert", children: error }) : null, _jsx("button", { type: "submit", disabled: pending, children: pending ? "Sending" : "Send" })] })) })) : null] }));
72
+ }
73
+ const installVerified = new WeakSet();
74
+ /**
75
+ * Live in-app inbox for the signed-in end user: loads the latest items, keeps
76
+ * a Server-Sent Events stream open (with backoff reconnects), and marks items read.
77
+ */
78
+ export function useInbox(options = {}) {
79
+ const client = useYouSaid();
80
+ const live = options.live ?? true;
81
+ const [items, setItems] = useState([]);
82
+ const [unread, setUnread] = useState(0);
83
+ const [loading, setLoading] = useState(true);
84
+ const [connected, setConnected] = useState(false);
85
+ const [error, setError] = useState("");
86
+ const refresh = useCallback(async () => {
87
+ setError("");
88
+ try {
89
+ const page = await client.listInbox({ limit: options.limit ?? 50 });
90
+ setItems(page.items);
91
+ setUnread(page.unread);
92
+ }
93
+ catch (reason) {
94
+ setError(reason instanceof Error ? reason.message : "The inbox could not be loaded.");
95
+ }
96
+ finally {
97
+ setLoading(false);
98
+ }
99
+ }, [client, options.limit]);
100
+ useEffect(() => { void refresh(); }, [refresh]);
101
+ useEffect(() => {
102
+ if (!live)
103
+ return;
104
+ const controller = new AbortController();
105
+ let attempt = 0;
106
+ const apply = (event) => {
107
+ attempt = 0;
108
+ setConnected(true);
109
+ setUnread(event.unread);
110
+ if (event.type === "item")
111
+ setItems((current) => current.some((item) => item.id === event.item.id) ? current : [event.item, ...current]);
112
+ if (event.type === "unread") {
113
+ const readAt = new Date().toISOString();
114
+ setItems((current) => current.map((item) => item.readAt || !(event.all || event.readIds?.includes(item.id)) ? item : { ...item, readAt }));
115
+ }
116
+ };
117
+ const run = async () => {
118
+ while (!controller.signal.aborted) {
119
+ try {
120
+ await client.streamInbox(apply, controller.signal);
121
+ }
122
+ catch { /* reconnect below */ }
123
+ setConnected(false);
124
+ if (controller.signal.aborted)
125
+ return;
126
+ attempt += 1;
127
+ await new Promise((resolve) => setTimeout(resolve, Math.min(30_000, 1_000 * 2 ** Math.min(attempt, 5))));
128
+ }
129
+ };
130
+ void run();
131
+ return () => controller.abort();
132
+ }, [client, live]);
133
+ const markRead = useCallback(async (ids) => {
134
+ const readAt = new Date().toISOString();
135
+ setItems((current) => current.map((item) => !item.readAt && (!ids || ids.includes(item.id)) ? { ...item, readAt } : item));
136
+ try {
137
+ const result = await client.markInboxRead(ids);
138
+ setUnread(result.unread);
139
+ }
140
+ catch (reason) {
141
+ setError(reason instanceof Error ? reason.message : "The inbox could not be updated.");
142
+ void refresh();
143
+ }
144
+ }, [client, refresh]);
145
+ return { items, unread, loading, connected, error, refresh, markRead };
146
+ }
147
+ export function Inbox({ title = "Inbox", emptyMessage = "Replies from the team will appear here.", onOpen }) {
148
+ const { items, unread, loading, error, markRead } = useInbox();
149
+ return (_jsxs("section", { className: "ys-inbox", "aria-label": title, children: [_jsx("style", { children: inboxStyles }), _jsxs("header", { children: [_jsxs("h2", { children: [title, unread ? _jsx("span", { "aria-label": `${unread} unread`, children: unread }) : null] }), unread ? _jsx("button", { type: "button", onClick: () => void markRead(), children: "Mark all read" }) : null] }), error ? _jsx("p", { role: "alert", className: "ys-inbox__error", children: error }) : null, loading ? _jsx("p", { className: "ys-inbox__empty", children: "Loading\u2026" }) : items.length === 0 ? _jsx("p", { className: "ys-inbox__empty", children: emptyMessage }) : (_jsx("ul", { children: items.map((item) => (_jsx("li", { children: _jsxs("button", { type: "button", className: item.readAt ? "" : "is-unread", onClick: () => { if (!item.readAt)
150
+ void markRead([item.id]); onOpen?.(item); }, children: [_jsx("strong", { children: item.subject }), _jsx("small", { children: item.preview }), _jsx("time", { dateTime: item.createdAt, children: new Date(item.createdAt).toLocaleString() })] }) }, item.id))) }))] }));
151
+ }
152
+ const inboxStyles = `.ys-inbox{color:#f4f1eb;background:#111116;border:1px solid #2c2b34;border-radius:20px;overflow:hidden;font:500 14px/1.5 ui-sans-serif,system-ui,sans-serif}.ys-inbox header{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:16px 18px;border-bottom:1px solid #2c2b34}.ys-inbox h2{margin:0;font-size:18px;display:flex;gap:8px;align-items:center}.ys-inbox h2 span{min-width:22px;padding:1px 7px;border-radius:9px;background:#8878ff;font-size:11px;text-align:center}.ys-inbox header button{border:0;border-radius:10px;background:#212129;color:#f4f1eb;padding:7px 11px;cursor:pointer}.ys-inbox ul{list-style:none;margin:0;padding:6px}.ys-inbox li button{width:100%;display:grid;gap:3px;text-align:left;border:0;border-radius:12px;padding:12px;background:transparent;color:#aaa6b2;cursor:pointer}.ys-inbox li button:hover{background:#212129}.ys-inbox li button.is-unread{color:#f4f1eb}.ys-inbox li button.is-unread strong::before{content:"";display:inline-block;width:7px;height:7px;margin-right:7px;border-radius:50%;background:#8878ff;vertical-align:middle}.ys-inbox small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ys-inbox time{font-size:10px;color:#aaa6b2}.ys-inbox__empty{padding:28px 18px;text-align:center;color:#aaa6b2}.ys-inbox__error{margin:10px;padding:10px;border-radius:10px;background:#3b2023;color:#ffb6ae}.ys-inbox button:focus-visible{outline:2px solid #8878ff;outline-offset:2px}`;
153
+ /** @experimental */
154
+ export function useRooms() {
155
+ const client = useYouSaid();
156
+ const [rooms, setRooms] = useState([]);
157
+ const [loading, setLoading] = useState(true);
158
+ const [error, setError] = useState("");
159
+ const refresh = useCallback(async () => {
160
+ setLoading(true);
161
+ setError("");
162
+ try {
163
+ setRooms(await client.listRooms());
164
+ }
165
+ catch (reason) {
166
+ setError(reason instanceof Error ? reason.message : "Rooms could not be loaded.");
167
+ }
168
+ finally {
169
+ setLoading(false);
170
+ }
171
+ }, [client]);
172
+ useEffect(() => {
173
+ void refresh();
174
+ }, [refresh]);
175
+ return { rooms, loading, error, refresh };
176
+ }
177
+ /** @experimental */
178
+ export function useRoom(roomId) {
179
+ const client = useYouSaid();
180
+ const [room, setRoom] = useState(null);
181
+ const [loading, setLoading] = useState(Boolean(roomId));
182
+ const [error, setError] = useState("");
183
+ const refresh = useCallback(async () => {
184
+ if (!roomId) {
185
+ setRoom(null);
186
+ setLoading(false);
187
+ return;
188
+ }
189
+ setLoading(true);
190
+ setError("");
191
+ try {
192
+ setRoom(await client.getRoom(roomId));
193
+ }
194
+ catch (reason) {
195
+ setError(reason instanceof Error
196
+ ? reason.message
197
+ : "This Room could not be opened.");
198
+ }
199
+ finally {
200
+ setLoading(false);
201
+ }
202
+ }, [client, roomId]);
203
+ useEffect(() => {
204
+ void refresh();
205
+ }, [refresh]);
206
+ return { room, loading, error, refresh };
207
+ }
208
+ /** @experimental */
209
+ export function useRoomComposer(roomId, onSent) {
210
+ const client = useYouSaid();
211
+ const [pending, setPending] = useState(false);
212
+ const [error, setError] = useState("");
213
+ const send = useCallback(async (body) => {
214
+ if (!roomId)
215
+ return false;
216
+ setPending(true);
217
+ setError("");
218
+ try {
219
+ await client.postRoomMessage(roomId, body);
220
+ await onSent?.();
221
+ return true;
222
+ }
223
+ catch (reason) {
224
+ setError(reason instanceof Error
225
+ ? reason.message
226
+ : "The message could not be sent.");
227
+ return false;
228
+ }
229
+ finally {
230
+ setPending(false);
231
+ }
232
+ }, [client, roomId, onSent]);
233
+ return { send, pending, error };
234
+ }
235
+ /** @experimental */
236
+ export function YouSaidRooms({ embedded = false, initialRoomId = null, onSolveSession, }) {
237
+ const client = useYouSaid();
238
+ const roomsState = useRooms();
239
+ const [selectedId, setSelectedId] = useState(initialRoomId);
240
+ const [creating, setCreating] = useState(false);
241
+ const [createError, setCreateError] = useState("");
242
+ useEffect(() => {
243
+ if (!selectedId && roomsState.rooms[0])
244
+ setSelectedId(roomsState.rooms[0].id);
245
+ }, [roomsState.rooms, selectedId]);
246
+ const detail = useRoom(selectedId);
247
+ async function create(event) {
248
+ event.preventDefault();
249
+ const data = new FormData(event.currentTarget);
250
+ setCreating(true);
251
+ setCreateError("");
252
+ try {
253
+ const created = await client.createRoom({
254
+ template: data.get("template"),
255
+ title: String(data.get("title") ?? ""),
256
+ purpose: String(data.get("purpose") ?? "") || undefined,
257
+ });
258
+ setSelectedId(created.id);
259
+ event.currentTarget.reset();
260
+ await roomsState.refresh();
261
+ }
262
+ catch (reason) {
263
+ setCreateError(reason instanceof Error
264
+ ? reason.message
265
+ : "The Room could not be opened.");
266
+ }
267
+ finally {
268
+ setCreating(false);
269
+ }
270
+ }
271
+ return (_jsxs("section", { className: `ys-rooms ${embedded ? "ys-rooms--embedded" : ""}`, "data-yousaid-rooms": "true", children: [_jsx("style", { children: roomStyles + communityStyles + roomAgentStyles }), _jsxs("header", { className: "ys-rooms__header", children: [_jsxs("div", { children: [_jsx("span", { className: "ys-rooms__eyebrow", children: "Shared with the product team" }), _jsx("h2", { children: "Rooms" }), _jsx("p", { children: "Follow an issue or idea all the way to an outcome." })] }), _jsx("span", { className: "ys-rooms__quiet", children: "Quiet by default" })] }), _jsxs("div", { className: "ys-rooms__grid", children: [_jsxs("aside", { className: "ys-rooms__list", "aria-label": "Your Rooms", children: [roomsState.loading ? (_jsx(RoomSkeleton, {})) : roomsState.error ? (_jsx("p", { role: "alert", className: "ys-rooms__error", children: roomsState.error })) : roomsState.rooms.length ? (roomsState.rooms.map((room) => (_jsxs("button", { type: "button", onClick: () => setSelectedId(room.id), className: selectedId === room.id ? "is-active" : "", children: [_jsx("span", { className: `ys-room-dot ys-room-dot--${room.template_key}` }), _jsxs("span", { children: [_jsx("strong", { children: room.title }), _jsxs("small", { children: [friendlyStatus(room.status), " \u00B7", " ", room.last_message || room.purpose] })] }), Number(room.unread_count) > 0 ? (_jsx("b", { children: room.unread_count })) : null] }, room.id)))) : (_jsxs("div", { className: "ys-rooms__empty", children: [_jsx("strong", { children: "No Rooms yet" }), _jsx("p", { children: "Open one when a question deserves a shared outcome, not another disappearing chat." })] })), _jsxs("details", { className: "ys-rooms__new", children: [_jsx("summary", { children: "Open a Room" }), _jsxs("form", { onSubmit: create, children: [_jsxs("label", { children: ["What is this about?", _jsx("input", { name: "title", required: true, minLength: 3, maxLength: 140, placeholder: "Login recovery on mobile" })] }), _jsxs("label", { children: ["Shape", _jsxs("select", { name: "template", defaultValue: "direct", children: [_jsx("option", { value: "direct", children: "Private question" }), _jsx("option", { value: "feature", children: "Feature discussion" }), _jsx("option", { value: "topic", children: "Product topic" }), _jsx("option", { value: "office_hours", children: "Office hours" })] })] }), _jsxs("label", { children: ["What would a useful outcome be?", _jsx("textarea", { name: "purpose", maxLength: 600, placeholder: "Optional. We will suggest a clear outcome." })] }), createError ? (_jsx("p", { role: "alert", className: "ys-rooms__error", children: createError })) : null, _jsx("button", { type: "submit", disabled: creating, children: creating ? "Opening…" : "Open Room" })] })] })] }), _jsx("div", { className: "ys-rooms__detail", children: detail.loading ? (_jsx(RoomSkeleton, { detail: true })) : detail.error ? (_jsx("p", { role: "alert", className: "ys-rooms__error", children: detail.error })) : detail.room ? (_jsx(RoomView, { detail: detail.room, onRefresh: detail.refresh, onSolveSession: onSolveSession })) : (_jsxs("div", { className: "ys-rooms__welcome", children: [_jsx("span", { children: "\u25CE" }), _jsx("h3", { children: "Choose a Room" }), _jsx("p", { children: "The current state and next step stay visible without reading every message." })] })) })] })] }));
272
+ }
273
+ /** @experimental */
274
+ export function RoomsList({ rooms, selectedId, onSelect, }) {
275
+ return (_jsx("div", { children: rooms.map((room) => (_jsx("button", { type: "button", "aria-pressed": selectedId === room.id, onClick: () => onSelect(room.id), children: room.title }, room.id))) }));
276
+ }
277
+ /** @experimental */
278
+ export function RoomView({ detail, onRefresh, onSolveSession, }) {
279
+ const client = useYouSaid();
280
+ const [message, setMessage] = useState("");
281
+ const [uploading, setUploading] = useState(false);
282
+ const [uploadError, setUploadError] = useState("");
283
+ const [safetyNote, setSafetyNote] = useState("");
284
+ const composer = useRoomComposer(detail.room.id, onRefresh);
285
+ const canvas = detail.canvas?.content ?? {};
286
+ const [caret, setCaret] = useState(0);
287
+ const [mentionIndex, setMentionIndex] = useState(0);
288
+ const [mentionDismissed, setMentionDismissed] = useState(false);
289
+ const mention = mentionDismissed ? null : mentionAtCaret(message, caret);
290
+ const mentionMatches = mention ? filterAgents(DEFAULT_MENTION_AGENTS, mention.query) : [];
291
+ function insertMention(alias) {
292
+ if (!mention)
293
+ return;
294
+ const next = `${message.slice(0, mention.start)}@${alias} ${message.slice(caret)}`;
295
+ setMessage(next);
296
+ setCaret(mention.start + alias.length + 2);
297
+ setMentionIndex(0);
298
+ }
299
+ async function submit(event) {
300
+ event.preventDefault();
301
+ if (await composer.send(message))
302
+ setMessage("");
303
+ }
304
+ async function upload(file) {
305
+ setUploading(true);
306
+ setUploadError("");
307
+ try {
308
+ await client.uploadRoomAttachment(detail.room.id, file);
309
+ await onRefresh();
310
+ }
311
+ catch (reason) {
312
+ setUploadError(reason instanceof Error
313
+ ? reason.message
314
+ : "The file could not be attached.");
315
+ }
316
+ finally {
317
+ setUploading(false);
318
+ }
319
+ }
320
+ return (_jsxs("article", { className: "ys-room-view", children: [_jsxs("header", { children: [_jsxs("div", { children: [_jsxs("span", { className: "ys-room-view__type", children: [detail.room.channel_kind === "office_hours"
321
+ ? "Office hours"
322
+ : `${detail.room.template_key} Room`, " ", "\u00B7 ", friendlyStatus(detail.room.status)] }), _jsx("h3", { children: detail.room.title }), _jsx("p", { children: detail.room.purpose }), detail.solveSessions
323
+ .filter((session) => ["scheduled", "live"].includes(session.state))
324
+ .map((session) => (_jsxs("button", { type: "button", className: "ys-room-view__live", onClick: () => void client
325
+ .joinSolveSession(detail.room.id, session.id)
326
+ .then(({ authToken, mode }) => onSolveSession?.({ authToken, mode }))
327
+ .catch((reason) => setUploadError(reason instanceof Error
328
+ ? reason.message
329
+ : "The live session is unavailable.")), children: ["\u25CF", " ", session.state === "live"
330
+ ? "Join live Solve Session"
331
+ : "Open scheduled session"] }, session.id)))] }), _jsxs("div", { children: [_jsxs("details", { className: "ys-room-view__canvas", children: [_jsx("summary", { children: String(canvas.currentState ?? "Waiting for the next useful response.") }), _jsx(RoomCanvas, { detail: detail, title: "Current outcome" })] }), _jsx(RequestHumanButton, { roomId: detail.room.id })] })] }), _jsxs("div", { className: "ys-room-view__messages", "aria-live": "polite", children: [detail.messages.map((item) => (_jsxs("article", { className: item.author_type === "end_user"
332
+ ? "is-customer"
333
+ : item.author_type === "agent"
334
+ ? "is-agent"
335
+ : "is-team", children: [_jsxs("div", { children: [_jsx("strong", { children: item.author_label }), item.agent_alias ? _jsx("span", { children: "Agent \u00B7 sources visible" }) : null, _jsx("time", { children: new Date(item.created_at).toLocaleString() })] }), _jsx("p", { children: item.body_plain }), _jsx("button", { className: "ys-room-view__report", type: "button", onClick: () => void client
336
+ .reportRoomContent(detail.room.id, {
337
+ messageId: item.id,
338
+ category: "other",
339
+ reason: "This message needs a human moderator to review it in context.",
340
+ })
341
+ .then((result) => setSafetyNote(result.message))
342
+ .catch((reason) => setSafetyNote(reason instanceof Error
343
+ ? reason.message
344
+ : "The report could not be sent.")), children: "Report" })] }, item.id))), detail.polls
345
+ .filter((poll) => poll.status === "open")
346
+ .map((poll) => (_jsx(CustomerPoll, { roomId: detail.room.id, poll: poll, onSaved: onRefresh }, poll.id))), detail.attachments.length ? (_jsx("div", { className: "ys-room-view__files", children: detail.attachments.map((file) => (_jsxs("button", { type: "button", onClick: () => void client
347
+ .downloadRoomAttachment(detail.room.id, file.id)
348
+ .then((blob) => {
349
+ const url = URL.createObjectURL(blob);
350
+ const link = document.createElement("a");
351
+ link.href = url;
352
+ link.download = file.file_name;
353
+ link.click();
354
+ setTimeout(() => URL.revokeObjectURL(url), 1_000);
355
+ })
356
+ .catch((reason) => setUploadError(reason instanceof Error
357
+ ? reason.message
358
+ : "The file is unavailable.")), children: [_jsx("span", { children: "\u2197" }), file.file_name, _jsxs("small", { children: [Math.max(1, Math.ceil(file.byte_size / 1024)), " KB"] })] }, file.id))) })) : null, safetyNote ? (_jsx("p", { role: "status", className: "ys-room-view__safety", children: safetyNote })) : null] }), _jsxs("form", { className: "ys-room-view__composer", onSubmit: submit, children: [_jsx("label", { htmlFor: `ys-room-message-${detail.room.id}`, children: "Reply to this Room" }), _jsx("textarea", { id: `ys-room-message-${detail.room.id}`, value: message, onChange: (event) => {
359
+ setMessage(event.target.value);
360
+ setCaret(event.target.selectionStart ?? event.target.value.length);
361
+ setMentionDismissed(false);
362
+ }, onSelect: (event) => setCaret(event.currentTarget.selectionStart ?? 0), onKeyDown: (event) => {
363
+ if (!mention || !mentionMatches.length)
364
+ return;
365
+ const result = mentionMenuKey(event.key, mentionIndex, mentionMatches.length);
366
+ if (!result.handled)
367
+ return;
368
+ event.preventDefault();
369
+ if (result.close)
370
+ setMentionDismissed(true);
371
+ else if (result.select)
372
+ insertMention(mentionMatches[mentionIndex]?.alias ?? mentionMatches[0].alias);
373
+ else
374
+ setMentionIndex(result.activeIndex);
375
+ }, "aria-controls": mention ? `ys-mentions-${detail.room.id}` : undefined, "aria-autocomplete": "list", required: true, maxLength: 20_000, placeholder: "Write a reply or type @ to ask an agent\u2026" }), mention ? (_jsx(AgentMentionMenu, { id: `ys-mentions-${detail.room.id}`, query: mention.query, activeIndex: mentionIndex, onActiveIndexChange: setMentionIndex, onSelect: (agent) => insertMention(agent.alias), onClose: () => setMentionDismissed(true) })) : null, _jsxs("div", { children: [_jsx("small", { children: "Agents are labeled and cannot make external changes without approval." }), _jsxs("span", { className: "ys-room-view__actions", children: [_jsxs("label", { className: "ys-room-view__attach", children: [uploading ? "Attaching…" : "Attach", _jsx("input", { type: "file", accept: ".pdf,.json,.txt,.md,.csv,.png,.jpg,.jpeg,.webp,.gif", disabled: uploading, onChange: (event) => {
376
+ const file = event.target.files?.[0];
377
+ if (file)
378
+ void upload(file);
379
+ event.currentTarget.value = "";
380
+ } })] }), _jsx("button", { type: "submit", disabled: composer.pending || !message.trim(), children: composer.pending ? "Sending…" : "Send" })] })] }), composer.error || uploadError ? (_jsx("p", { role: "alert", className: "ys-rooms__error", children: composer.error || uploadError })) : null] })] }));
381
+ }
382
+ function CustomerPoll({ roomId, poll, onSaved, }) {
383
+ const client = useYouSaid();
384
+ const [selected, setSelected] = useState("");
385
+ const [answer, setAnswer] = useState("");
386
+ const [pending, setPending] = useState(false);
387
+ return (_jsxs("section", { className: "ys-room-poll", children: [_jsxs("small", { children: ["Room poll \u00B7 ", poll.responses.length, " responses"] }), _jsx("strong", { children: poll.question }), poll.poll_type === "open_question" ? (_jsx("textarea", { value: answer, onChange: (event) => setAnswer(event.target.value), placeholder: "Your answer" })) : (poll.options.map((option) => (_jsx("button", { type: "button", "aria-pressed": selected === option.id, onClick: () => setSelected(option.id), children: option.label }, option.id)))), _jsx("button", { type: "button", disabled: pending ||
388
+ (poll.poll_type === "open_question" ? !answer.trim() : !selected), onClick: () => {
389
+ setPending(true);
390
+ void client
391
+ .respondToRoomPoll(roomId, poll.id, {
392
+ optionIds: selected ? [selected] : [],
393
+ answer: answer || null,
394
+ })
395
+ .then(onSaved)
396
+ .finally(() => setPending(false));
397
+ }, children: pending ? "Saving…" : "Record response" })] }));
398
+ }
399
+ /** @experimental */
400
+ export function RoomComposer({ roomId, onSent, }) {
401
+ const [body, setBody] = useState("");
402
+ const composer = useRoomComposer(roomId, onSent);
403
+ return (_jsxs("form", { onSubmit: (event) => {
404
+ event.preventDefault();
405
+ void composer.send(body).then((sent) => {
406
+ if (sent)
407
+ setBody("");
408
+ });
409
+ }, children: [_jsx("textarea", { value: body, onChange: (event) => setBody(event.target.value), required: true }), _jsx("button", { disabled: composer.pending, children: "Send" }), composer.error ? _jsx("p", { role: "alert", children: composer.error }) : null] }));
410
+ }
411
+ /** @experimental */
412
+ export function AgentActionCard({ state, children, }) {
413
+ return (_jsxs("section", { "data-agent-state": state, children: [_jsx("strong", { children: state.replace("_", " ") }), children] }));
414
+ }
415
+ /** @experimental */
416
+ export function ApprovalCard({ summary, onApprove, onReject, }) {
417
+ return (_jsxs("section", { children: [_jsx("p", { children: summary }), _jsx("button", { type: "button", onClick: onApprove, children: "Approve exact action" }), _jsx("button", { type: "button", onClick: onReject, children: "Reject" })] }));
418
+ }
419
+ export function LifecycleReceipt({ title, state, }) {
420
+ return (_jsxs("section", { role: "status", children: [_jsx("strong", { children: title }), _jsx("p", { children: state })] }));
421
+ }
422
+ function RoomSkeleton({ detail = false }) {
423
+ return (_jsxs("div", { className: `ys-skeleton ${detail ? "is-detail" : ""}`, "aria-label": "Loading Rooms", children: [_jsx("i", {}), _jsx("i", {}), _jsx("i", {})] }));
424
+ }
425
+ function friendlyStatus(status) {
426
+ return status
427
+ .replaceAll("_", " ")
428
+ .replace(/^./, (value) => value.toUpperCase());
429
+ }
430
+ const communityStyles = `.ys-room-view__live{margin-top:12px;border:0;border-radius:10px;background:#18342c;color:var(--ys-mint);padding:8px 11px;font-weight:700;cursor:pointer}.ys-room-view__report{margin-top:8px;border:0;background:transparent;color:var(--ys-muted);font-size:9px;cursor:pointer}.ys-room-view__safety{max-width:none!important;border-radius:10px;background:#18342c;padding:10px;color:var(--ys-mint)}.ys-room-poll{max-width:100%;display:grid;gap:8px;border:1px solid var(--ys-line);border-radius:16px;background:var(--ys-panel);padding:15px}.ys-room-poll small{color:var(--ys-accent)}.ys-room-poll>button{border:1px solid var(--ys-line);border-radius:10px;background:var(--ys-inset);color:var(--ys-text);padding:9px;text-align:left;cursor:pointer}.ys-room-poll>button[aria-pressed=true]{border-color:var(--ys-accent);background:#2b2746}.ys-room-poll textarea{border:1px solid var(--ys-line);border-radius:10px;background:#0e0e13;color:var(--ys-text);padding:10px}`;
431
+ const roomStyles = `.ys-rooms{--ys-bg:#111116;--ys-panel:#19191f;--ys-inset:#212129;--ys-text:#f4f1eb;--ys-muted:#aaa6b2;--ys-line:#2c2b34;--ys-accent:#8878ff;--ys-mint:#6eddbb;color:var(--ys-text);background:var(--ys-bg);border:1px solid var(--ys-line);border-radius:24px;overflow:hidden;font:500 14px/1.5 ui-sans-serif,system-ui,sans-serif}.ys-rooms *{box-sizing:border-box}.ys-rooms button,.ys-rooms input,.ys-rooms textarea,.ys-rooms select{font:inherit}.ys-rooms__header{display:flex;justify-content:space-between;gap:24px;padding:24px 26px;border-bottom:1px solid var(--ys-line)}.ys-rooms__header h2{margin:3px 0 0;font-size:28px;line-height:1;letter-spacing:-.045em}.ys-rooms__header p{margin:9px 0 0;color:var(--ys-muted)}.ys-rooms__eyebrow{color:var(--ys-accent);font-size:11px;font-weight:750}.ys-rooms__quiet{align-self:flex-start;padding:7px 10px;border-radius:10px;background:#18342c;color:var(--ys-mint);font-size:11px}.ys-rooms__grid{display:grid;grid-template-columns:minmax(220px,34%) minmax(0,1fr);min-height:520px}.ys-rooms__list{padding:10px;border-right:1px solid var(--ys-line);background:#141419}.ys-rooms__list>button{width:100%;display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:10px;text-align:left;border:0;border-radius:14px;padding:13px;background:transparent;color:var(--ys-text);cursor:pointer;transition:background .2s ease,transform .2s ease}.ys-rooms__list>button:hover,.ys-rooms__list>button.is-active{background:var(--ys-inset)}.ys-rooms__list>button:active{transform:scale(.985)}.ys-rooms__list strong,.ys-rooms__list small{display:block}.ys-rooms__list small{margin-top:4px;color:var(--ys-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ys-rooms__list b{min-width:20px;text-align:center;padding:2px 5px;border-radius:8px;background:var(--ys-accent);font-size:10px}.ys-room-dot{width:9px;height:9px;margin-top:5px;border-radius:3px;background:var(--ys-accent)}.ys-room-dot--incident{background:#ff8178}.ys-room-dot--topic{background:var(--ys-mint)}.ys-rooms__new{margin:8px}.ys-rooms__new summary{cursor:pointer;padding:11px;color:var(--ys-accent);font-weight:700}.ys-rooms__new form{display:grid;gap:10px;padding:11px}.ys-rooms__new label{display:grid;gap:5px;color:var(--ys-muted);font-size:12px}.ys-rooms__new input,.ys-rooms__new textarea,.ys-rooms__new select,.ys-room-view__composer textarea{width:100%;border:1px solid var(--ys-line);border-radius:11px;background:#0e0e13;color:var(--ys-text);padding:10px;outline:none}.ys-rooms input:focus,.ys-rooms textarea:focus,.ys-rooms select:focus,.ys-rooms button:focus-visible{outline:2px solid var(--ys-accent);outline-offset:2px}.ys-rooms__new button,.ys-room-view__composer button{border:0;border-radius:11px;background:var(--ys-text);color:var(--ys-bg);padding:10px 14px;font-weight:750;cursor:pointer}.ys-rooms__new button:disabled,.ys-room-view__composer button:disabled{opacity:.5}.ys-rooms__detail{min-width:0}.ys-room-view>header{padding:24px;border-bottom:1px solid var(--ys-line);display:grid;grid-template-columns:minmax(0,1fr) minmax(180px,34%);gap:20px}.ys-room-view h3{margin:4px 0;font-size:25px;letter-spacing:-.035em}.ys-room-view header p{margin:8px 0 0;max-width:55ch;color:var(--ys-muted)}.ys-room-view__type{color:var(--ys-accent);font-size:11px;font-weight:750;text-transform:capitalize}.ys-room-view__canvas{background:var(--ys-inset);border-radius:14px;padding:13px}.ys-room-view__canvas summary{cursor:pointer;font-weight:750}.ys-room-view__canvas dl{margin:10px 0 0}.ys-room-view__canvas dl div+div{margin-top:10px}.ys-room-view__canvas dt{color:var(--ys-muted);font-size:10px;text-transform:uppercase;letter-spacing:.08em}.ys-room-view__canvas dd{margin:3px 0 0}.ys-room-view__messages{padding:22px;display:grid;gap:11px;max-height:460px;overflow:auto}.ys-room-view__messages article{max-width:82%;padding:13px 15px;border-radius:16px;background:var(--ys-inset)}.ys-room-view__messages article.is-customer{margin-left:auto;background:#4539a3}.ys-room-view__messages article.is-agent{border:1px solid #3c3860;background:#1f1d2d}.ys-room-view__messages article>div{display:flex;gap:8px;align-items:center}.ys-room-view__messages article span{color:var(--ys-accent);font-size:10px}.ys-room-view__messages time{margin-left:auto;color:var(--ys-muted);font-size:9px}.ys-room-view__messages p{margin:8px 0 0;white-space:pre-wrap}.ys-room-view__composer{border-top:1px solid var(--ys-line);padding:16px 22px}.ys-room-view__composer label{display:block;margin-bottom:6px;font-size:11px;color:var(--ys-muted)}.ys-room-view__composer textarea{min-height:80px;resize:vertical}.ys-room-view__composer>div{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-top:8px}.ys-room-view__composer small{color:var(--ys-muted)}.ys-room-view__actions{display:flex;align-items:center;gap:8px}.ys-room-view__attach{position:relative;overflow:hidden;cursor:pointer;padding:9px 11px;border:1px solid var(--ys-line);border-radius:11px;font-weight:700}.ys-room-view__attach input{position:absolute;inset:0;opacity:0;cursor:pointer}.ys-room-view__files{display:flex;flex-wrap:wrap;gap:7px;margin-top:7px}.ys-room-view__files button{display:flex;align-items:center;gap:6px;padding:8px 10px;border:0;border-radius:11px;background:var(--ys-inset);color:var(--ys-text);font-size:12px;cursor:pointer}.ys-room-view__files small{color:var(--ys-muted)}.ys-rooms__welcome,.ys-rooms__empty{padding:40px 24px;text-align:center;color:var(--ys-muted)}.ys-rooms__welcome span{display:block;color:var(--ys-accent);font-size:38px}.ys-rooms__welcome h3,.ys-rooms__empty strong{color:var(--ys-text)}.ys-rooms__error{padding:10px;border-radius:10px;background:#3b2023;color:#ffb6ae}.ys-skeleton{display:grid;gap:10px;padding:14px}.ys-skeleton i{display:block;height:54px;border-radius:13px;background:linear-gradient(90deg,var(--ys-inset),#292832,var(--ys-inset));background-size:200% 100%;animation:ys-shimmer 1.4s infinite}.ys-skeleton.is-detail i:first-child{height:120px}.ys-skeleton.is-detail i:nth-child(2){height:220px}@keyframes ys-shimmer{to{background-position:-200% 0}}@media(max-width:700px){.ys-rooms__header{padding:20px}.ys-rooms__quiet{display:none}.ys-rooms__grid{grid-template-columns:1fr}.ys-rooms__list{border-right:0;border-bottom:1px solid var(--ys-line);max-height:250px;overflow:auto}.ys-room-view>header{grid-template-columns:1fr}.ys-room-view__messages article{max-width:92%}}@media(prefers-reduced-motion:reduce){.ys-rooms *{animation:none!important;scroll-behavior:auto!important;transition:none!important}}`;
432
+ export * from "./proof.js";
433
+ export { ProofPromptView, useProofPrompt } from "./proof-prompt.js";
434
+ /**
435
+ * Native in-app Proof prompt using the provider's signed identity. See ProofPromptView for props.
436
+ * @experimental
437
+ */
438
+ export function ProofPrompt(props) {
439
+ return _jsx(ProofPromptViewInternal, { client: useYouSaid(), ...props });
440
+ }
441
+ import { ProofPromptView as ProofPromptViewInternal } from "./proof-prompt.js";
@@ -0,0 +1,57 @@
1
+ import type { ProofEligibility, ProofPromptAction, ProofPromptEvent, ProofPromptPresentation, YouSaidClient } from "@sansavision/yousaid";
2
+ /** @experimental */
3
+ export type ProofPromptOptions = {
4
+ client: YouSaidClient;
5
+ formId: string;
6
+ presentation?: ProofPromptPresentation;
7
+ /** Check server-side frequency caps / "never ask" before opening automatically. */
8
+ respectFrequency?: boolean;
9
+ onEvent?: (event: ProofPromptEvent) => void;
10
+ };
11
+ /** @experimental */
12
+ export type ProofPromptState = {
13
+ status: "idle" | "checking" | "ineligible" | "ready" | "open" | "completed" | "error";
14
+ eligibility: ProofEligibility | null;
15
+ frameUrl: string | null;
16
+ error: string | null;
17
+ open: () => Promise<void>;
18
+ close: (action?: ProofPromptAction) => Promise<void>;
19
+ frameRef: React.RefObject<HTMLIFrameElement | null>;
20
+ };
21
+ /**
22
+ * Headless hook behind <ProofPrompt>: eligibility, a signed known-customer
23
+ * session, the frame URL and lifecycle events (open, step, consent, complete,
24
+ * abandon) from the collection frame.
25
+ * @experimental
26
+ */
27
+ export declare function useProofPrompt({ client, formId, presentation, respectFrequency, onEvent }: ProofPromptOptions): ProofPromptState;
28
+ /** @experimental */
29
+ export type ProofPromptProps = {
30
+ client: YouSaidClient;
31
+ formId: string;
32
+ presentation?: ProofPromptPresentation;
33
+ /** The product moment that justified the ask, e.g. { event: "release_outcome_verified", id: "rel_123" }. Passed to onEvent. */
34
+ trigger?: {
35
+ event: string;
36
+ id?: string;
37
+ };
38
+ /** Open as soon as the prompt is eligible (still subject to server frequency caps). */
39
+ autoOpen?: boolean;
40
+ label?: string;
41
+ className?: string;
42
+ onEvent?: (event: ProofPromptEvent & {
43
+ trigger?: {
44
+ event: string;
45
+ id?: string;
46
+ };
47
+ }) => void;
48
+ onComplete?: (event: ProofPromptEvent) => void;
49
+ };
50
+ /**
51
+ * Native in-app Proof prompt (PRD §13.1, §17.4). Inline, modal, drawer,
52
+ * popover or full-screen. Offers "Not now" and "Don't ask again", which are
53
+ * enforced server-side across devices and products.
54
+ * @experimental
55
+ */
56
+ export declare function ProofPromptView({ client, formId, presentation, trigger, autoOpen, label, className, onEvent, onComplete }: ProofPromptProps): import("react").JSX.Element | null;
57
+ //# sourceMappingURL=proof-prompt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proof-prompt.d.ts","sourceRoot":"","sources":["../src/proof-prompt.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,uBAAuB,EAAgB,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAExJ,oBAAoB;AACpB,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,aAAa,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,uBAAuB,CAAC;IACvC,mFAAmF;IACnF,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;CAC7C,CAAC;AAEF,oBAAoB;AACpB,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,YAAY,GAAG,OAAO,GAAG,MAAM,GAAG,WAAW,GAAG,OAAO,CAAC;IACtF,WAAW,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACrC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;CACrD,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,YAAuB,EAAE,gBAAuB,EAAE,OAAO,EAAE,EAAE,kBAAkB,GAAG,gBAAgB,CAiDlJ;AAWD,oBAAoB;AACpB,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE,aAAa,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,uBAAuB,CAAC;IACvC,+HAA+H;IAC/H,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,uFAAuF;IACvF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,gBAAgB,GAAG;QAAE,OAAO,CAAC,EAAE;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,EAAE,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,KAAK,IAAI,CAAC;IAC3F,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;CAChD,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,YAAuB,EAAE,OAAO,EAAE,QAAgB,EAAE,KAA0B,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,gBAAgB,sCA+BnL"}
@@ -0,0 +1,106 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+ /**
5
+ * Headless hook behind <ProofPrompt>: eligibility, a signed known-customer
6
+ * session, the frame URL and lifecycle events (open, step, consent, complete,
7
+ * abandon) from the collection frame.
8
+ * @experimental
9
+ */
10
+ export function useProofPrompt({ client, formId, presentation = "drawer", respectFrequency = true, onEvent }) {
11
+ const proof = useMemo(() => client.proof(), [client]);
12
+ const [status, setStatus] = useState("idle");
13
+ const [eligibility, setEligibility] = useState(null);
14
+ const [session, setSession] = useState(null);
15
+ const [error, setError] = useState(null);
16
+ const frameRef = useRef(null);
17
+ const eventRef = useRef(onEvent);
18
+ eventRef.current = onEvent;
19
+ useEffect(() => {
20
+ if (!respectFrequency) {
21
+ setStatus("ready");
22
+ return;
23
+ }
24
+ let cancelled = false;
25
+ setStatus("checking");
26
+ proof.eligibility(formId).then((result) => { if (cancelled)
27
+ return; setEligibility(result); setStatus(result.eligible ? "ready" : "ineligible"); })
28
+ .catch(() => { if (!cancelled)
29
+ setStatus("ready"); });
30
+ return () => { cancelled = true; };
31
+ }, [proof, formId, respectFrequency]);
32
+ useEffect(() => {
33
+ function listen(message) {
34
+ const event = proof.parseEvent(message, frameRef.current);
35
+ if (!event)
36
+ return;
37
+ if (event.type === "resize" && frameRef.current && presentation === "inline" && typeof event.height === "number")
38
+ frameRef.current.style.height = `${Math.min(4000, Math.max(400, event.height + 8))}px`;
39
+ if (event.type === "complete")
40
+ setStatus("completed");
41
+ eventRef.current?.(event);
42
+ }
43
+ window.addEventListener("message", listen);
44
+ return () => window.removeEventListener("message", listen);
45
+ }, [proof, presentation]);
46
+ const open = useCallback(async () => {
47
+ setError(null);
48
+ try {
49
+ const next = session ?? await proof.startSession(formId, presentation);
50
+ setSession(next);
51
+ setStatus("open");
52
+ }
53
+ catch (caught) {
54
+ setError(caught instanceof Error ? caught.message : "The story prompt could not open.");
55
+ setStatus("error");
56
+ }
57
+ }, [proof, formId, presentation, session]);
58
+ const close = useCallback(async (action) => {
59
+ setStatus((current) => (current === "completed" ? current : "ready"));
60
+ if (action)
61
+ await proof.recordPreference(formId, action).catch(() => undefined);
62
+ }, [proof, formId]);
63
+ return { status, eligibility, frameUrl: session ? proof.frameUrl(formId, session) : null, error, open, close, frameRef };
64
+ }
65
+ const overlayStyle = { position: "fixed", inset: 0, zIndex: 2147483000, background: "rgba(10,10,14,.55)", display: "flex" };
66
+ function panelStyle(presentation) {
67
+ const base = { background: "#fff", display: "flex", flexDirection: "column", overflow: "hidden", boxShadow: "0 24px 80px rgba(0,0,0,.3)" };
68
+ if (presentation === "drawer")
69
+ return { ...base, marginLeft: "auto", width: "min(520px, 100vw)", height: "100vh" };
70
+ if (presentation === "fullscreen")
71
+ return { ...base, width: "100vw", height: "100vh" };
72
+ if (presentation === "popover")
73
+ return { ...base, position: "fixed", right: 16, bottom: 16, width: "min(420px, calc(100vw - 32px))", height: "min(640px, calc(100vh - 32px))", borderRadius: 18 };
74
+ return { ...base, margin: "auto", width: "min(680px, calc(100vw - 24px))", height: "min(860px, calc(100vh - 24px))", borderRadius: 18 };
75
+ }
76
+ /**
77
+ * Native in-app Proof prompt (PRD §13.1, §17.4). Inline, modal, drawer,
78
+ * popover or full-screen. Offers "Not now" and "Don't ask again", which are
79
+ * enforced server-side across devices and products.
80
+ * @experimental
81
+ */
82
+ export function ProofPromptView({ client, formId, presentation = "drawer", trigger, autoOpen = false, label = "Share your story", className, onEvent, onComplete }) {
83
+ const prompt = useProofPrompt({
84
+ client, formId, presentation, respectFrequency: presentation !== "inline",
85
+ onEvent: (event) => { onEvent?.({ ...event, ...(trigger ? { trigger } : {}) }); if (event.type === "complete")
86
+ onComplete?.(event); },
87
+ });
88
+ const { status, open } = prompt;
89
+ useEffect(() => { if ((autoOpen || presentation === "inline") && status === "ready")
90
+ void open(); }, [autoOpen, presentation, status, open]);
91
+ useEffect(() => {
92
+ if (status !== "open" || presentation === "inline")
93
+ return;
94
+ const onKey = (event) => { if (event.key === "Escape")
95
+ void prompt.close("dismissed"); };
96
+ document.addEventListener("keydown", onKey);
97
+ return () => document.removeEventListener("keydown", onKey);
98
+ }, [status, presentation, prompt]);
99
+ const frame = prompt.frameUrl ? _jsx("iframe", { ref: prompt.frameRef, src: prompt.frameUrl, title: label, allow: "camera; microphone; display-capture; clipboard-write", referrerPolicy: "no-referrer", style: { border: 0, width: "100%", flex: 1, minHeight: presentation === "inline" ? 560 : 0 } }) : null;
100
+ if (presentation === "inline")
101
+ return _jsx("div", { className: className, children: status === "error" ? _jsx("p", { role: "alert", children: prompt.error }) : frame });
102
+ if (status === "ineligible" && !autoOpen)
103
+ return null;
104
+ return _jsxs("div", { className: className, children: [status !== "open" ? _jsx("button", { type: "button", onClick: () => void open(), disabled: status === "checking", children: label }) : null, status === "error" ? _jsx("p", { role: "alert", children: prompt.error }) : null, status === "open" && frame ? _jsx("div", { role: "dialog", "aria-modal": "true", "aria-label": label, style: overlayStyle, onClick: (event) => { if (event.target === event.currentTarget)
105
+ void prompt.close("dismissed"); }, children: _jsxs("div", { style: panelStyle(presentation), children: [_jsxs("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8, padding: "8px 12px", borderBottom: "1px solid rgba(0,0,0,.08)", fontSize: 13 }, children: [_jsx("button", { type: "button", onClick: () => void prompt.close("not_now"), children: "Not now" }), _jsx("button", { type: "button", onClick: () => void prompt.close("never"), children: "Don't ask again" }), _jsx("button", { type: "button", "aria-label": "Close", onClick: () => void prompt.close("dismissed"), children: "×" })] }), frame] }) }) : null] });
106
+ }