@samirify/messenger 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1278 @@
1
+ 'use strict';
2
+
3
+ var framerMotion = require('framer-motion');
4
+ var react = require('react');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+ var reactQuery = require('@tanstack/react-query');
7
+
8
+ // src/web/ChatDock.tsx
9
+ var MessengerContext = react.createContext(null);
10
+ function useMessengerConfig() {
11
+ const ctx = react.useContext(MessengerContext);
12
+ if (!ctx) {
13
+ throw new Error(
14
+ "@gymlium/messenger: useMessengerConfig must be used within MessengerProvider"
15
+ );
16
+ }
17
+ return ctx;
18
+ }
19
+ function useOptionalMessengerConfig() {
20
+ return react.useContext(MessengerContext);
21
+ }
22
+ var MessengerUiContext = react.createContext(null);
23
+ function useMessengerUi() {
24
+ const ctx = react.useContext(MessengerUiContext);
25
+ if (!ctx) {
26
+ throw new Error(
27
+ "@gymlium/messenger: useMessengerUi must be used within MessengerUiProvider"
28
+ );
29
+ }
30
+ return ctx;
31
+ }
32
+
33
+ // src/api.ts
34
+ async function listThreads(http) {
35
+ const res = await http.getJson(
36
+ "/api/messages/threads"
37
+ );
38
+ return { ...res, data: res.data?.threads ?? [] };
39
+ }
40
+ async function openThread(http, clientUserId) {
41
+ return http.postJson("/api/messages/threads", {
42
+ ...clientUserId ? { client_user_id: clientUserId } : {}
43
+ });
44
+ }
45
+ async function getThread(http, threadId, limit = 50, beforeId) {
46
+ const params = new URLSearchParams({ limit: String(limit) });
47
+ return http.getJson(`/api/messages/threads/${threadId}?${params.toString()}`);
48
+ }
49
+ async function sendMessage(http, threadId, body, image) {
50
+ const path = `/api/messages/threads/${threadId}/messages`;
51
+ if (image?.uri || image?.file) {
52
+ const form = new FormData();
53
+ if (body.trim()) form.append("body", body.trim());
54
+ const filename = image.name ?? image.uri?.split("/").pop() ?? "photo.jpg";
55
+ const match = /\.(\w+)$/.exec(filename);
56
+ const type = image.type ?? (match ? `image/${match[1]}` : "image/jpeg");
57
+ if (image.file) {
58
+ form.append("image", image.file, filename);
59
+ } else if (image.uri.startsWith("blob:") || image.uri.startsWith("data:")) {
60
+ const blob = await fetch(image.uri).then((r) => r.blob());
61
+ form.append("image", blob, filename);
62
+ } else {
63
+ form.append("image", {
64
+ uri: image.uri,
65
+ name: filename,
66
+ type
67
+ });
68
+ }
69
+ return http.postForm(path, form);
70
+ }
71
+ return http.postJson(path, { body });
72
+ }
73
+ function sumUnread(threads) {
74
+ return threads.reduce((sum, th) => sum + Number(th.unread_count ?? 0), 0);
75
+ }
76
+
77
+ // src/queryKeys.ts
78
+ var messengerKeys = {
79
+ all: ["messenger"],
80
+ threads: () => [...messengerKeys.all, "threads"],
81
+ thread: (id) => [...messengerKeys.all, "thread", id]
82
+ };
83
+
84
+ // src/hooks/useUnreadTotal.ts
85
+ function useUnreadTotal(options) {
86
+ const cfg = useOptionalMessengerConfig();
87
+ const featureOn = cfg?.enabled === true;
88
+ const http = cfg?.http;
89
+ const enabled = (options?.enabled ?? true) && featureOn && !!http;
90
+ const query = reactQuery.useQuery({
91
+ queryKey: messengerKeys.threads(),
92
+ queryFn: async () => {
93
+ const res = await listThreads(http);
94
+ if (!res.success) {
95
+ throw new Error(res.message ?? "Failed to load threads");
96
+ }
97
+ return res.data;
98
+ },
99
+ enabled
100
+ });
101
+ const total = react.useMemo(
102
+ () => query.data ? sumUnread(query.data) : 0,
103
+ [query.data]
104
+ );
105
+ return {
106
+ total,
107
+ isLoading: query.isLoading,
108
+ isFetching: query.isFetching,
109
+ refetch: query.refetch,
110
+ enabled: featureOn
111
+ };
112
+ }
113
+
114
+ // src/i18nKeys.ts
115
+ var MessengerI18n = {
116
+ off: {
117
+ key: "mobile.messenger.off",
118
+ fallback: "Messaging is not enabled for this account."
119
+ },
120
+ chat: { key: "mobile.messenger.chat", fallback: "Chat" },
121
+ /** Dock / list title — prefer over inbox for web UI copy. */
122
+ messages: { key: "mobile.messenger.messages", fallback: "Chat" },
123
+ messagesHint: {
124
+ key: "mobile.messenger.messagesHint",
125
+ fallback: "Message your coach anytime"
126
+ },
127
+ messageCoach: {
128
+ key: "mobile.messenger.messageCoach",
129
+ fallback: "Message your coach"
130
+ },
131
+ empty: {
132
+ key: "mobile.messenger.empty",
133
+ fallback: "No conversations yet"
134
+ },
135
+ yourCoach: {
136
+ key: "mobile.messenger.yourCoach",
137
+ fallback: "Your coach"
138
+ },
139
+ photo: { key: "mobile.messenger.photo", fallback: "Photo" },
140
+ noMessages: {
141
+ key: "mobile.messenger.noMessages",
142
+ fallback: "No messages yet"
143
+ },
144
+ startConversation: {
145
+ key: "mobile.messenger.startConversation",
146
+ fallback: "Say hello \u2014 send the first message."
147
+ },
148
+ typeMessage: {
149
+ key: "mobile.messenger.typeMessage",
150
+ fallback: "Type a message\u2026"
151
+ },
152
+ client: { key: "mobile.trainer.client", fallback: "Client" },
153
+ back: { key: "common.back", fallback: "Back" }
154
+ };
155
+ function useThreads(options) {
156
+ const { http, enabled: featureOn } = useMessengerConfig();
157
+ const enabled = featureOn;
158
+ return reactQuery.useQuery({
159
+ queryKey: messengerKeys.threads(),
160
+ queryFn: async () => {
161
+ const res = await listThreads(http);
162
+ if (!res.success) {
163
+ throw new Error(res.message ?? "Failed to load threads");
164
+ }
165
+ return res.data;
166
+ },
167
+ enabled
168
+ });
169
+ }
170
+ function useOpenThread() {
171
+ const { http, navigation } = useMessengerConfig();
172
+ const qc = reactQuery.useQueryClient();
173
+ return reactQuery.useMutation({
174
+ mutationFn: async (clientUserId) => {
175
+ const res = await openThread(http, clientUserId);
176
+ if (!res.success || !res.data?.thread?.id) {
177
+ throw new Error(res.message ?? "Could not open thread");
178
+ }
179
+ return res.data.thread;
180
+ },
181
+ onSuccess: (thread) => {
182
+ void qc.invalidateQueries({ queryKey: messengerKeys.threads() });
183
+ navigation.openThread(thread.id);
184
+ }
185
+ });
186
+ }
187
+ function initials(name) {
188
+ const parts = name.trim().split(/\s+/).filter(Boolean);
189
+ if (parts.length === 0) return "?";
190
+ if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
191
+ return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
192
+ }
193
+ function ThreadRow({
194
+ thread,
195
+ onSelect
196
+ }) {
197
+ const { user, t, isRtl, theme } = useMessengerConfig();
198
+ const unread = Number(thread.unread_count ?? 0);
199
+ const title = user?.role === "admin" ? thread.client_name ?? t(MessengerI18n.client.key, MessengerI18n.client.fallback) : t(MessengerI18n.yourCoach.key, MessengerI18n.yourCoach.fallback);
200
+ const preview = thread.last_body || (thread.last_image_url ? t(MessengerI18n.photo.key, MessengerI18n.photo.fallback) : t(MessengerI18n.noMessages.key, MessengerI18n.noMessages.fallback));
201
+ return /* @__PURE__ */ jsxRuntime.jsxs(
202
+ "button",
203
+ {
204
+ type: "button",
205
+ onClick: () => onSelect(thread.id),
206
+ style: {
207
+ display: "flex",
208
+ flexDirection: isRtl ? "row-reverse" : "row",
209
+ alignItems: "center",
210
+ gap: 12,
211
+ width: "calc(100% - 16px)",
212
+ textAlign: isRtl ? "right" : "left",
213
+ padding: "12px 14px",
214
+ margin: "0 8px",
215
+ border: "none",
216
+ borderRadius: 16,
217
+ background: unread > 0 ? `color-mix(in srgb, ${theme.accentColor} 8%, transparent)` : "transparent",
218
+ cursor: "pointer",
219
+ color: theme.colors.text,
220
+ transition: "background 160ms ease"
221
+ },
222
+ onMouseEnter: (e) => {
223
+ e.currentTarget.style.background = `color-mix(in srgb, ${theme.accentColor} 12%, ${theme.colors.surfaceSecondary})`;
224
+ },
225
+ onMouseLeave: (e) => {
226
+ e.currentTarget.style.background = unread > 0 ? `color-mix(in srgb, ${theme.accentColor} 8%, transparent)` : "transparent";
227
+ },
228
+ children: [
229
+ /* @__PURE__ */ jsxRuntime.jsx(
230
+ "div",
231
+ {
232
+ "aria-hidden": true,
233
+ style: {
234
+ width: 44,
235
+ height: 44,
236
+ borderRadius: 14,
237
+ flexShrink: 0,
238
+ display: "grid",
239
+ placeItems: "center",
240
+ fontFamily: theme.fonts.displaySemiBold,
241
+ fontWeight: 700,
242
+ fontSize: 13,
243
+ letterSpacing: "0.02em",
244
+ color: theme.accentColor,
245
+ background: `
246
+ linear-gradient(145deg,
247
+ color-mix(in srgb, ${theme.accentColor} 22%, ${theme.colors.surface}),
248
+ color-mix(in srgb, ${theme.accentColor} 8%, ${theme.colors.surfaceSecondary})
249
+ )
250
+ `,
251
+ border: `1px solid color-mix(in srgb, ${theme.accentColor} 22%, transparent)`
252
+ },
253
+ children: initials(title)
254
+ }
255
+ ),
256
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [
257
+ /* @__PURE__ */ jsxRuntime.jsxs(
258
+ "div",
259
+ {
260
+ style: {
261
+ display: "flex",
262
+ flexDirection: isRtl ? "row-reverse" : "row",
263
+ alignItems: "baseline",
264
+ gap: 8
265
+ },
266
+ children: [
267
+ /* @__PURE__ */ jsxRuntime.jsx(
268
+ "div",
269
+ {
270
+ style: {
271
+ flex: 1,
272
+ fontFamily: theme.fonts.displaySemiBold,
273
+ fontWeight: unread > 0 ? 700 : 600,
274
+ fontSize: 14.5,
275
+ overflow: "hidden",
276
+ textOverflow: "ellipsis",
277
+ whiteSpace: "nowrap"
278
+ },
279
+ children: title
280
+ }
281
+ ),
282
+ unread > 0 ? /* @__PURE__ */ jsxRuntime.jsx(
283
+ "span",
284
+ {
285
+ style: {
286
+ minWidth: 20,
287
+ height: 20,
288
+ borderRadius: 10,
289
+ padding: "0 6px",
290
+ background: theme.accentColor,
291
+ color: "var(--color-text-inverse, #fff)",
292
+ fontSize: 11,
293
+ fontFamily: theme.fonts.bodySemiBold,
294
+ fontWeight: 700,
295
+ display: "inline-flex",
296
+ alignItems: "center",
297
+ justifyContent: "center",
298
+ flexShrink: 0
299
+ },
300
+ children: unread
301
+ }
302
+ ) : null
303
+ ]
304
+ }
305
+ ),
306
+ /* @__PURE__ */ jsxRuntime.jsx(
307
+ "div",
308
+ {
309
+ style: {
310
+ fontFamily: theme.fonts.body,
311
+ fontSize: 12.5,
312
+ color: unread > 0 ? theme.colors.textSecondary : theme.colors.textMuted,
313
+ marginTop: 3,
314
+ overflow: "hidden",
315
+ textOverflow: "ellipsis",
316
+ whiteSpace: "nowrap",
317
+ fontWeight: unread > 0 ? 500 : 400
318
+ },
319
+ children: preview
320
+ }
321
+ )
322
+ ] })
323
+ ]
324
+ }
325
+ );
326
+ }
327
+ function ThreadList({
328
+ onSelectThread
329
+ }) {
330
+ const { user, t, theme } = useMessengerConfig();
331
+ const threadsQuery = useThreads();
332
+ const openThread2 = useOpenThread();
333
+ const threads = threadsQuery.data ?? [];
334
+ return /* @__PURE__ */ jsxRuntime.jsxs(
335
+ "div",
336
+ {
337
+ style: {
338
+ display: "flex",
339
+ flexDirection: "column",
340
+ height: "100%",
341
+ paddingTop: 6,
342
+ paddingBottom: 10
343
+ },
344
+ children: [
345
+ user?.role === "client" && threads.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "8px 16px 4px" }, children: /* @__PURE__ */ jsxRuntime.jsx(
346
+ "button",
347
+ {
348
+ type: "button",
349
+ onClick: () => openThread2.mutate(void 0),
350
+ disabled: openThread2.isPending,
351
+ style: {
352
+ width: "100%",
353
+ padding: "13px 16px",
354
+ borderRadius: 14,
355
+ border: "none",
356
+ background: `
357
+ linear-gradient(135deg,
358
+ color-mix(in srgb, ${theme.accentColor} 92%, #fff),
359
+ ${theme.accentColor}
360
+ )
361
+ `,
362
+ color: "var(--color-text-inverse, #fff)",
363
+ fontFamily: theme.fonts.bodySemiBold,
364
+ fontWeight: 600,
365
+ fontSize: 14,
366
+ cursor: openThread2.isPending ? "default" : "pointer",
367
+ boxShadow: `0 10px 24px color-mix(in srgb, ${theme.accentColor} 32%, transparent)`
368
+ },
369
+ children: openThread2.isPending ? "\u2026" : t(
370
+ MessengerI18n.messageCoach.key,
371
+ MessengerI18n.messageCoach.fallback
372
+ )
373
+ }
374
+ ) }),
375
+ threadsQuery.isLoading && threads.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(
376
+ "div",
377
+ {
378
+ style: {
379
+ padding: 48,
380
+ textAlign: "center",
381
+ color: theme.colors.textMuted,
382
+ fontFamily: theme.fonts.body
383
+ },
384
+ children: "\u2026"
385
+ }
386
+ ) : threads.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs(
387
+ "div",
388
+ {
389
+ style: {
390
+ flex: 1,
391
+ display: "flex",
392
+ flexDirection: "column",
393
+ alignItems: "center",
394
+ justifyContent: "center",
395
+ padding: 32,
396
+ textAlign: "center",
397
+ gap: 10
398
+ },
399
+ children: [
400
+ /* @__PURE__ */ jsxRuntime.jsx(
401
+ "div",
402
+ {
403
+ "aria-hidden": true,
404
+ style: {
405
+ width: 56,
406
+ height: 56,
407
+ borderRadius: 18,
408
+ display: "grid",
409
+ placeItems: "center",
410
+ background: `color-mix(in srgb, ${theme.accentColor} 12%, ${theme.colors.surfaceSecondary})`,
411
+ color: theme.accentColor,
412
+ fontSize: 22,
413
+ marginBottom: 4
414
+ },
415
+ children: "\u2726"
416
+ }
417
+ ),
418
+ /* @__PURE__ */ jsxRuntime.jsx(
419
+ "div",
420
+ {
421
+ style: {
422
+ fontFamily: theme.fonts.displaySemiBold,
423
+ fontWeight: 600,
424
+ fontSize: 15,
425
+ color: theme.colors.text
426
+ },
427
+ children: t(MessengerI18n.empty.key, MessengerI18n.empty.fallback)
428
+ }
429
+ ),
430
+ /* @__PURE__ */ jsxRuntime.jsx(
431
+ "div",
432
+ {
433
+ style: {
434
+ fontFamily: theme.fonts.body,
435
+ fontSize: 13,
436
+ color: theme.colors.textMuted,
437
+ maxWidth: 220,
438
+ lineHeight: 1.45
439
+ },
440
+ children: t(
441
+ MessengerI18n.startConversation.key,
442
+ MessengerI18n.startConversation.fallback
443
+ )
444
+ }
445
+ )
446
+ ]
447
+ }
448
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
449
+ "div",
450
+ {
451
+ style: {
452
+ overflowY: "auto",
453
+ flex: 1,
454
+ display: "flex",
455
+ flexDirection: "column",
456
+ gap: 2,
457
+ paddingBottom: 8
458
+ },
459
+ children: threads.map((th) => /* @__PURE__ */ jsxRuntime.jsx(ThreadRow, { thread: th, onSelect: onSelectThread }, th.id))
460
+ }
461
+ )
462
+ ]
463
+ }
464
+ );
465
+ }
466
+ var POLL_MS = 18e3;
467
+ function useThread(threadId, options) {
468
+ const { http, enabled: featureOn } = useMessengerConfig();
469
+ const enabled = featureOn && Number.isFinite(threadId) && threadId > 0;
470
+ const poll = true;
471
+ return reactQuery.useQuery({
472
+ queryKey: messengerKeys.thread(threadId),
473
+ queryFn: async () => {
474
+ const res = await getThread(http, threadId);
475
+ if (!res.success) {
476
+ throw new Error(res.message ?? "Failed to load thread");
477
+ }
478
+ return res.data;
479
+ },
480
+ enabled,
481
+ refetchInterval: enabled && poll ? POLL_MS : false
482
+ });
483
+ }
484
+ function useSendMessage(threadId) {
485
+ const { http } = useMessengerConfig();
486
+ const qc = reactQuery.useQueryClient();
487
+ return reactQuery.useMutation({
488
+ mutationFn: async (input) => {
489
+ const res = await sendMessage(http, threadId, input.body, input.image);
490
+ if (!res.success || !res.data?.message) {
491
+ throw new Error(res.message ?? "Send failed");
492
+ }
493
+ return res.data.message;
494
+ },
495
+ onSuccess: (message) => {
496
+ qc.setQueryData(
497
+ messengerKeys.thread(threadId),
498
+ (prev) => {
499
+ if (!prev) return prev;
500
+ if (prev.messages.some((m) => m.id === message.id)) return prev;
501
+ return { ...prev, messages: [...prev.messages, message] };
502
+ }
503
+ );
504
+ void qc.invalidateQueries({ queryKey: messengerKeys.threads() });
505
+ }
506
+ });
507
+ }
508
+ function ChatBubbleIcon({ size = 26 }) {
509
+ return /* @__PURE__ */ jsxRuntime.jsxs(
510
+ "svg",
511
+ {
512
+ width: size,
513
+ height: size,
514
+ viewBox: "0 0 24 24",
515
+ fill: "none",
516
+ "aria-hidden": true,
517
+ style: { display: "block" },
518
+ children: [
519
+ /* @__PURE__ */ jsxRuntime.jsx(
520
+ "path",
521
+ {
522
+ d: "M5 7.2A3.2 3.2 0 0 1 8.2 4h7.6A3.2 3.2 0 0 1 19 7.2v5.6A3.2 3.2 0 0 1 15.8 16H12.1l-3.55 2.75a.75.75 0 0 1-1.2-.6V16H8.2A3.2 3.2 0 0 1 5 12.8V7.2Z",
523
+ fill: "currentColor"
524
+ }
525
+ ),
526
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9.1", cy: "10", r: "1.05", fill: "rgba(0,0,0,0.28)" }),
527
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "10", r: "1.05", fill: "rgba(0,0,0,0.28)" }),
528
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "14.9", cy: "10", r: "1.05", fill: "rgba(0,0,0,0.28)" })
529
+ ]
530
+ }
531
+ );
532
+ }
533
+ function CloseIcon({ size = 18 }) {
534
+ return /* @__PURE__ */ jsxRuntime.jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", "aria-hidden": true, style: { display: "block" }, children: /* @__PURE__ */ jsxRuntime.jsx(
535
+ "path",
536
+ {
537
+ d: "M6.4 6.4a1 1 0 0 1 1.4 0L12 10.6l4.2-4.2a1 1 0 1 1 1.4 1.4L13.4 12l4.2 4.2a1 1 0 0 1-1.4 1.4L12 13.4l-4.2 4.2a1 1 0 0 1-1.4-1.4L10.6 12 6.4 7.8a1 1 0 0 1 0-1.4Z",
538
+ fill: "currentColor"
539
+ }
540
+ ) });
541
+ }
542
+ function BackIcon({ size = 18, flip }) {
543
+ return /* @__PURE__ */ jsxRuntime.jsx(
544
+ "svg",
545
+ {
546
+ width: size,
547
+ height: size,
548
+ viewBox: "0 0 24 24",
549
+ fill: "none",
550
+ "aria-hidden": true,
551
+ style: { display: "block", transform: flip ? "scaleX(-1)" : void 0 },
552
+ children: /* @__PURE__ */ jsxRuntime.jsx(
553
+ "path",
554
+ {
555
+ d: "M14.7 5.3a1 1 0 0 1 0 1.4L9.4 12l5.3 5.3a1 1 0 0 1-1.4 1.4l-6-6a1 1 0 0 1 0-1.4l6-6a1 1 0 0 1 1.4 0Z",
556
+ fill: "currentColor"
557
+ }
558
+ )
559
+ }
560
+ );
561
+ }
562
+ function ImageIcon({ size = 18 }) {
563
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", "aria-hidden": true, style: { display: "block" }, children: [
564
+ /* @__PURE__ */ jsxRuntime.jsx(
565
+ "path",
566
+ {
567
+ d: "M6.5 4h11A2.5 2.5 0 0 1 20 6.5v11A2.5 2.5 0 0 1 17.5 20h-11A2.5 2.5 0 0 1 4 17.5v-11A2.5 2.5 0 0 1 6.5 4Z",
568
+ stroke: "currentColor",
569
+ strokeWidth: "1.7"
570
+ }
571
+ ),
572
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9.2", cy: "9", r: "1.4", fill: "currentColor" }),
573
+ /* @__PURE__ */ jsxRuntime.jsx(
574
+ "path",
575
+ {
576
+ d: "M5.5 16.2 9.8 12l2.4 2.3 2.6-3.2 4.2 5.1",
577
+ stroke: "currentColor",
578
+ strokeWidth: "1.7",
579
+ strokeLinecap: "round",
580
+ strokeLinejoin: "round"
581
+ }
582
+ )
583
+ ] });
584
+ }
585
+ function SendIcon({ size = 16 }) {
586
+ return /* @__PURE__ */ jsxRuntime.jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", "aria-hidden": true, style: { display: "block" }, children: /* @__PURE__ */ jsxRuntime.jsx(
587
+ "path",
588
+ {
589
+ d: "M5.2 11.2 19.1 4.4a.8.8 0 0 1 1.1.9l-3.4 14.2a.8.8 0 0 1-1.3.45l-4.2-3.5-2.5 2.4a.7.7 0 0 1-1.2-.45v-4.05l-3.1-1.6a.8.8 0 0 1 .5-1.55Z",
590
+ fill: "currentColor"
591
+ }
592
+ ) });
593
+ }
594
+ function WebComposer({
595
+ sending,
596
+ onSend
597
+ }) {
598
+ const { t, isRtl, theme, pickImage } = useMessengerConfig();
599
+ const [body, setBody] = react.useState("");
600
+ const [focused, setFocused] = react.useState(false);
601
+ const fileRef = react.useRef(null);
602
+ const canSend = Boolean(body.trim()) && !sending;
603
+ async function handleSend() {
604
+ if (!body.trim() || sending) return;
605
+ const text = body;
606
+ setBody("");
607
+ await onSend(text, null);
608
+ }
609
+ async function handlePick() {
610
+ const fromHost = await pickImage();
611
+ if (fromHost) {
612
+ await onSend(body, fromHost);
613
+ setBody("");
614
+ return;
615
+ }
616
+ fileRef.current?.click();
617
+ }
618
+ async function onFileChange(e) {
619
+ const file = e.target.files?.[0];
620
+ e.target.value = "";
621
+ if (!file) return;
622
+ const uri = URL.createObjectURL(file);
623
+ await onSend(body, {
624
+ uri,
625
+ name: file.name,
626
+ type: file.type,
627
+ file
628
+ });
629
+ setBody("");
630
+ }
631
+ return /* @__PURE__ */ jsxRuntime.jsx(
632
+ "div",
633
+ {
634
+ style: {
635
+ padding: "10px 12px 14px",
636
+ borderTop: `1px solid color-mix(in srgb, ${theme.colors.border} 75%, transparent)`,
637
+ background: `linear-gradient(0deg, ${theme.colors.surface}, color-mix(in srgb, ${theme.colors.surface} 88%, transparent))`
638
+ },
639
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
640
+ "div",
641
+ {
642
+ style: {
643
+ display: "flex",
644
+ flexDirection: isRtl ? "row-reverse" : "row",
645
+ alignItems: "flex-end",
646
+ gap: 8,
647
+ padding: "6px 6px 6px 8px",
648
+ borderRadius: 18,
649
+ border: `1px solid ${focused ? `color-mix(in srgb, ${theme.accentColor} 45%, ${theme.colors.border})` : `color-mix(in srgb, ${theme.colors.border} 85%, transparent)`}`,
650
+ background: theme.colors.background,
651
+ boxShadow: focused ? `0 0 0 3px color-mix(in srgb, ${theme.accentColor} 16%, transparent)` : "none",
652
+ transition: "border-color 160ms ease, box-shadow 160ms ease"
653
+ },
654
+ children: [
655
+ /* @__PURE__ */ jsxRuntime.jsx(
656
+ "input",
657
+ {
658
+ ref: fileRef,
659
+ type: "file",
660
+ accept: "image/*",
661
+ hidden: true,
662
+ onChange: onFileChange
663
+ }
664
+ ),
665
+ /* @__PURE__ */ jsxRuntime.jsx(
666
+ "button",
667
+ {
668
+ type: "button",
669
+ onClick: () => void handlePick(),
670
+ disabled: sending,
671
+ "aria-label": "Attach image",
672
+ style: {
673
+ width: 36,
674
+ height: 36,
675
+ borderRadius: 12,
676
+ border: "none",
677
+ background: "transparent",
678
+ color: theme.colors.textMuted,
679
+ cursor: sending ? "default" : "pointer",
680
+ display: "grid",
681
+ placeItems: "center",
682
+ flexShrink: 0
683
+ },
684
+ children: /* @__PURE__ */ jsxRuntime.jsx(ImageIcon, { size: 18 })
685
+ }
686
+ ),
687
+ /* @__PURE__ */ jsxRuntime.jsx(
688
+ "textarea",
689
+ {
690
+ value: body,
691
+ onChange: (e) => setBody(e.target.value),
692
+ onFocus: () => setFocused(true),
693
+ onBlur: () => setFocused(false),
694
+ onKeyDown: (e) => {
695
+ if (e.key === "Enter" && !e.shiftKey) {
696
+ e.preventDefault();
697
+ void handleSend();
698
+ }
699
+ },
700
+ placeholder: t(
701
+ MessengerI18n.typeMessage.key,
702
+ MessengerI18n.typeMessage.fallback
703
+ ),
704
+ rows: 1,
705
+ style: {
706
+ flex: 1,
707
+ resize: "none",
708
+ maxHeight: 96,
709
+ minHeight: 36,
710
+ border: "none",
711
+ padding: "8px 4px",
712
+ fontFamily: theme.fonts.body,
713
+ fontSize: 14,
714
+ color: theme.colors.text,
715
+ background: "transparent",
716
+ textAlign: isRtl ? "right" : "left",
717
+ outline: "none",
718
+ lineHeight: 1.4
719
+ }
720
+ }
721
+ ),
722
+ /* @__PURE__ */ jsxRuntime.jsx(
723
+ "button",
724
+ {
725
+ type: "button",
726
+ onClick: () => void handleSend(),
727
+ disabled: !canSend,
728
+ "aria-label": "Send",
729
+ style: {
730
+ width: 36,
731
+ height: 36,
732
+ borderRadius: 12,
733
+ border: "none",
734
+ background: canSend ? `linear-gradient(145deg, color-mix(in srgb, ${theme.accentColor} 90%, #fff), ${theme.accentColor})` : `color-mix(in srgb, ${theme.colors.surfaceSecondary} 90%, transparent)`,
735
+ color: canSend ? "var(--color-text-inverse, #fff)" : theme.colors.textMuted,
736
+ cursor: canSend ? "pointer" : "default",
737
+ display: "grid",
738
+ placeItems: "center",
739
+ flexShrink: 0,
740
+ boxShadow: canSend ? `0 6px 16px color-mix(in srgb, ${theme.accentColor} 35%, transparent)` : "none",
741
+ transition: "background 160ms ease, box-shadow 160ms ease"
742
+ },
743
+ children: sending ? /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 14, lineHeight: 1 }, children: "\u2026" }) : /* @__PURE__ */ jsxRuntime.jsx(SendIcon, { size: 15 })
744
+ }
745
+ )
746
+ ]
747
+ }
748
+ )
749
+ }
750
+ );
751
+ }
752
+ function initials2(name) {
753
+ const parts = name.trim().split(/\s+/).filter(Boolean);
754
+ if (parts.length === 0) return "?";
755
+ if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
756
+ return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
757
+ }
758
+ function Bubble({ message }) {
759
+ const { user, isRtl, theme } = useMessengerConfig();
760
+ const mine = message.sender_user_id === user?.id;
761
+ const alignEnd = mine !== isRtl;
762
+ return /* @__PURE__ */ jsxRuntime.jsxs(
763
+ "div",
764
+ {
765
+ style: {
766
+ alignSelf: alignEnd ? "flex-end" : "flex-start",
767
+ maxWidth: "78%",
768
+ marginBottom: 8,
769
+ padding: "10px 13px",
770
+ borderRadius: mine ? alignEnd ? "18px 18px 6px 18px" : "18px 18px 18px 6px" : alignEnd ? "18px 18px 6px 18px" : "18px 18px 18px 6px",
771
+ background: mine ? `linear-gradient(145deg, color-mix(in srgb, ${theme.accentColor} 88%, #fff), ${theme.accentColor})` : `color-mix(in srgb, ${theme.colors.surfaceSecondary} 92%, ${theme.colors.surface})`,
772
+ color: mine ? "var(--color-text-inverse, #fff)" : theme.colors.text,
773
+ fontFamily: theme.fonts.body,
774
+ fontSize: 14,
775
+ lineHeight: 1.45,
776
+ boxShadow: mine ? `0 8px 20px color-mix(in srgb, ${theme.accentColor} 28%, transparent)` : `0 1px 0 color-mix(in srgb, ${theme.colors.border} 80%, transparent)`,
777
+ border: mine ? "none" : `1px solid color-mix(in srgb, ${theme.colors.border} 70%, transparent)`
778
+ },
779
+ children: [
780
+ message.image_url ? (
781
+ // eslint-disable-next-line @next/next/no-img-element
782
+ /* @__PURE__ */ jsxRuntime.jsx(
783
+ "img",
784
+ {
785
+ src: message.image_url,
786
+ alt: "",
787
+ style: {
788
+ width: "100%",
789
+ maxWidth: 220,
790
+ borderRadius: 12,
791
+ display: "block",
792
+ marginBottom: message.body ? 8 : 0
793
+ }
794
+ }
795
+ )
796
+ ) : null,
797
+ message.body ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { whiteSpace: "pre-wrap" }, children: message.body }) : null
798
+ ]
799
+ }
800
+ );
801
+ }
802
+ function Conversation({
803
+ threadId,
804
+ onBack,
805
+ onClose
806
+ }) {
807
+ const { user, t, isRtl, theme } = useMessengerConfig();
808
+ const listRef = react.useRef(null);
809
+ const threadQuery = useThread(threadId);
810
+ const threadsQuery = useThreads();
811
+ const sendMutation = useSendMessage(threadId);
812
+ const messages = threadQuery.data?.messages ?? [];
813
+ const threadMeta = threadsQuery.data?.find((th) => th.id === threadId);
814
+ const title = user?.role === "admin" ? threadMeta?.client_name ?? t(MessengerI18n.client.key, MessengerI18n.client.fallback) : t(MessengerI18n.yourCoach.key, MessengerI18n.yourCoach.fallback);
815
+ react.useEffect(() => {
816
+ const el = listRef.current;
817
+ if (el) el.scrollTop = el.scrollHeight;
818
+ }, [messages.length, threadId]);
819
+ async function handleSend(body, image) {
820
+ if (!body.trim() && !image || sendMutation.isPending) return;
821
+ await sendMutation.mutateAsync({ body, image });
822
+ }
823
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", height: "100%" }, children: [
824
+ /* @__PURE__ */ jsxRuntime.jsxs(
825
+ "header",
826
+ {
827
+ style: {
828
+ display: "flex",
829
+ flexDirection: isRtl ? "row-reverse" : "row",
830
+ alignItems: "center",
831
+ gap: 10,
832
+ padding: "12px 14px",
833
+ borderBottom: `1px solid color-mix(in srgb, ${theme.colors.border} 80%, transparent)`,
834
+ background: `linear-gradient(180deg, color-mix(in srgb, ${theme.colors.surface} 94%, transparent), color-mix(in srgb, ${theme.colors.surface} 72%, transparent))`,
835
+ backdropFilter: "blur(12px)"
836
+ },
837
+ children: [
838
+ onBack ? /* @__PURE__ */ jsxRuntime.jsx(
839
+ "button",
840
+ {
841
+ type: "button",
842
+ onClick: onBack,
843
+ "aria-label": t(MessengerI18n.back.key, MessengerI18n.back.fallback),
844
+ style: {
845
+ border: `1px solid color-mix(in srgb, ${theme.colors.border} 90%, transparent)`,
846
+ background: `color-mix(in srgb, ${theme.colors.surfaceSecondary} 75%, transparent)`,
847
+ color: theme.colors.text,
848
+ cursor: "pointer",
849
+ width: 36,
850
+ height: 36,
851
+ borderRadius: 12,
852
+ display: "grid",
853
+ placeItems: "center",
854
+ flexShrink: 0
855
+ },
856
+ children: /* @__PURE__ */ jsxRuntime.jsx(BackIcon, { flip: isRtl })
857
+ }
858
+ ) : null,
859
+ /* @__PURE__ */ jsxRuntime.jsx(
860
+ "div",
861
+ {
862
+ "aria-hidden": true,
863
+ style: {
864
+ width: 36,
865
+ height: 36,
866
+ borderRadius: 12,
867
+ flexShrink: 0,
868
+ display: "grid",
869
+ placeItems: "center",
870
+ fontFamily: theme.fonts.displaySemiBold,
871
+ fontWeight: 700,
872
+ fontSize: 12,
873
+ color: theme.accentColor,
874
+ background: `color-mix(in srgb, ${theme.accentColor} 14%, ${theme.colors.surfaceSecondary})`,
875
+ border: `1px solid color-mix(in srgb, ${theme.accentColor} 20%, transparent)`
876
+ },
877
+ children: initials2(title)
878
+ }
879
+ ),
880
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { flex: 1, minWidth: 0, textAlign: isRtl ? "right" : "left" }, children: [
881
+ /* @__PURE__ */ jsxRuntime.jsx(
882
+ "div",
883
+ {
884
+ style: {
885
+ fontFamily: theme.fonts.displayBold,
886
+ fontWeight: 700,
887
+ fontSize: 15,
888
+ color: theme.colors.text,
889
+ overflow: "hidden",
890
+ textOverflow: "ellipsis",
891
+ whiteSpace: "nowrap"
892
+ },
893
+ children: title
894
+ }
895
+ ),
896
+ /* @__PURE__ */ jsxRuntime.jsx(
897
+ "div",
898
+ {
899
+ style: {
900
+ fontFamily: theme.fonts.body,
901
+ fontSize: 11.5,
902
+ color: theme.colors.textMuted,
903
+ marginTop: 1
904
+ },
905
+ children: t(MessengerI18n.chat.key, MessengerI18n.chat.fallback)
906
+ }
907
+ )
908
+ ] }),
909
+ onClose ? /* @__PURE__ */ jsxRuntime.jsx(
910
+ "button",
911
+ {
912
+ type: "button",
913
+ onClick: onClose,
914
+ "aria-label": "Close",
915
+ style: {
916
+ border: `1px solid color-mix(in srgb, ${theme.colors.border} 90%, transparent)`,
917
+ background: `color-mix(in srgb, ${theme.colors.surfaceSecondary} 75%, transparent)`,
918
+ color: theme.colors.textSecondary,
919
+ width: 36,
920
+ height: 36,
921
+ borderRadius: 12,
922
+ cursor: "pointer",
923
+ display: "grid",
924
+ placeItems: "center",
925
+ flexShrink: 0
926
+ },
927
+ children: /* @__PURE__ */ jsxRuntime.jsx(CloseIcon, { size: 16 })
928
+ }
929
+ ) : null
930
+ ]
931
+ }
932
+ ),
933
+ /* @__PURE__ */ jsxRuntime.jsx(
934
+ "div",
935
+ {
936
+ ref: listRef,
937
+ style: {
938
+ flex: 1,
939
+ overflowY: "auto",
940
+ padding: "16px 14px 12px",
941
+ display: "flex",
942
+ flexDirection: "column",
943
+ background: `
944
+ radial-gradient(80% 50% at 50% 0%, color-mix(in srgb, ${theme.accentColor} 6%, transparent), transparent 70%),
945
+ ${theme.colors.background}
946
+ `
947
+ },
948
+ children: threadQuery.isLoading && messages.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { color: theme.colors.textMuted, textAlign: "center", marginTop: 40 }, children: "\u2026" }) : messages.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(
949
+ "div",
950
+ {
951
+ style: {
952
+ color: theme.colors.textMuted,
953
+ textAlign: "center",
954
+ marginTop: 48,
955
+ fontFamily: theme.fonts.body,
956
+ fontSize: 13.5,
957
+ lineHeight: 1.5,
958
+ padding: "0 24px"
959
+ },
960
+ children: t(
961
+ MessengerI18n.startConversation.key,
962
+ MessengerI18n.startConversation.fallback
963
+ )
964
+ }
965
+ ) : messages.map((m) => /* @__PURE__ */ jsxRuntime.jsx(Bubble, { message: m }, m.id))
966
+ }
967
+ ),
968
+ /* @__PURE__ */ jsxRuntime.jsx(WebComposer, { sending: sendMutation.isPending, onSend: handleSend })
969
+ ] });
970
+ }
971
+ function ChatPanel({
972
+ threadId,
973
+ onSelectThread,
974
+ onBackToList,
975
+ onClose,
976
+ variant = "dock"
977
+ }) {
978
+ const { enabled, t, isRtl, theme, onFeatureOff } = useMessengerConfig();
979
+ if (!enabled) {
980
+ if (onFeatureOff) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: onFeatureOff() });
981
+ return /* @__PURE__ */ jsxRuntime.jsx(
982
+ "div",
983
+ {
984
+ style: {
985
+ padding: 32,
986
+ textAlign: "center",
987
+ color: theme.colors.textMuted,
988
+ fontFamily: theme.fonts.body
989
+ },
990
+ children: t(MessengerI18n.off.key, MessengerI18n.off.fallback)
991
+ }
992
+ );
993
+ }
994
+ const shell = variant === "page" ? {
995
+ display: "flex",
996
+ flexDirection: "column",
997
+ height: "min(70vh, 640px)",
998
+ maxWidth: 720,
999
+ margin: "0 auto",
1000
+ borderRadius: 24,
1001
+ overflow: "hidden",
1002
+ border: `1px solid ${theme.colors.border}`,
1003
+ background: theme.colors.surface,
1004
+ boxShadow: `0 24px 64px color-mix(in srgb, ${theme.colors.text} 14%, transparent)`
1005
+ } : {
1006
+ display: "flex",
1007
+ flexDirection: "column",
1008
+ height: "100%",
1009
+ background: `
1010
+ radial-gradient(120% 80% at 100% 0%, color-mix(in srgb, ${theme.accentColor} 14%, transparent), transparent 55%),
1011
+ radial-gradient(90% 60% at 0% 100%, color-mix(in srgb, ${theme.accentColor} 8%, transparent), transparent 50%),
1012
+ ${theme.colors.surface}
1013
+ `
1014
+ };
1015
+ const inThread = threadId != null;
1016
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: shell, children: [
1017
+ !inThread ? /* @__PURE__ */ jsxRuntime.jsxs(
1018
+ "header",
1019
+ {
1020
+ style: {
1021
+ display: "flex",
1022
+ flexDirection: isRtl ? "row-reverse" : "row",
1023
+ alignItems: "center",
1024
+ gap: 12,
1025
+ padding: "16px 16px 14px",
1026
+ borderBottom: `1px solid color-mix(in srgb, ${theme.colors.border} 80%, transparent)`,
1027
+ background: `linear-gradient(180deg, color-mix(in srgb, ${theme.colors.surface} 92%, transparent), color-mix(in srgb, ${theme.colors.surface} 70%, transparent))`,
1028
+ backdropFilter: "blur(12px)"
1029
+ },
1030
+ children: [
1031
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { flex: 1, minWidth: 0, textAlign: isRtl ? "right" : "left" }, children: [
1032
+ /* @__PURE__ */ jsxRuntime.jsx(
1033
+ "div",
1034
+ {
1035
+ style: {
1036
+ fontFamily: theme.fonts.displayBold,
1037
+ fontWeight: 700,
1038
+ fontSize: 20,
1039
+ letterSpacing: "-0.02em",
1040
+ color: theme.colors.text,
1041
+ lineHeight: 1.15
1042
+ },
1043
+ children: t(MessengerI18n.messages.key, MessengerI18n.messages.fallback)
1044
+ }
1045
+ ),
1046
+ /* @__PURE__ */ jsxRuntime.jsx(
1047
+ "div",
1048
+ {
1049
+ style: {
1050
+ marginTop: 3,
1051
+ fontFamily: theme.fonts.body,
1052
+ fontSize: 12.5,
1053
+ color: theme.colors.textMuted
1054
+ },
1055
+ children: t(MessengerI18n.messagesHint.key, MessengerI18n.messagesHint.fallback)
1056
+ }
1057
+ )
1058
+ ] }),
1059
+ onClose ? /* @__PURE__ */ jsxRuntime.jsx(
1060
+ "button",
1061
+ {
1062
+ type: "button",
1063
+ onClick: onClose,
1064
+ "aria-label": "Close",
1065
+ style: {
1066
+ border: `1px solid color-mix(in srgb, ${theme.colors.border} 90%, transparent)`,
1067
+ background: `color-mix(in srgb, ${theme.colors.surfaceSecondary} 80%, transparent)`,
1068
+ color: theme.colors.textSecondary,
1069
+ width: 36,
1070
+ height: 36,
1071
+ borderRadius: 12,
1072
+ cursor: "pointer",
1073
+ display: "grid",
1074
+ placeItems: "center",
1075
+ flexShrink: 0
1076
+ },
1077
+ children: /* @__PURE__ */ jsxRuntime.jsx(CloseIcon, { size: 16 })
1078
+ }
1079
+ ) : null
1080
+ ]
1081
+ }
1082
+ ) : null,
1083
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: 1, minHeight: 0, position: "relative" }, children: /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { mode: "wait", initial: false, children: inThread ? /* @__PURE__ */ jsxRuntime.jsx(
1084
+ framerMotion.motion.div,
1085
+ {
1086
+ initial: { opacity: 0, x: isRtl ? -16 : 16 },
1087
+ animate: { opacity: 1, x: 0 },
1088
+ exit: { opacity: 0, x: isRtl ? 12 : -12 },
1089
+ transition: { duration: 0.22, ease: [0.22, 1, 0.36, 1] },
1090
+ style: { height: "100%" },
1091
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1092
+ Conversation,
1093
+ {
1094
+ threadId,
1095
+ onBack: onBackToList,
1096
+ onClose
1097
+ }
1098
+ )
1099
+ },
1100
+ `thread-${threadId}`
1101
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
1102
+ framerMotion.motion.div,
1103
+ {
1104
+ initial: { opacity: 0, x: isRtl ? 16 : -16 },
1105
+ animate: { opacity: 1, x: 0 },
1106
+ exit: { opacity: 0, x: isRtl ? -12 : 12 },
1107
+ transition: { duration: 0.22, ease: [0.22, 1, 0.36, 1] },
1108
+ style: { height: "100%" },
1109
+ children: /* @__PURE__ */ jsxRuntime.jsx(ThreadList, { onSelectThread })
1110
+ },
1111
+ "list"
1112
+ ) }) })
1113
+ ] });
1114
+ }
1115
+ function ChatDock() {
1116
+ const { enabled, isRtl, theme, t } = useMessengerConfig();
1117
+ const ui = useMessengerUi();
1118
+ const unread = useUnreadTotal({ enabled });
1119
+ if (!enabled) return null;
1120
+ const side = isRtl ? "left" : "right";
1121
+ const open = ui.open;
1122
+ const label = open ? "Close chat" : t(MessengerI18n.messages.key, MessengerI18n.messages.fallback);
1123
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1124
+ "div",
1125
+ {
1126
+ style: {
1127
+ position: "fixed",
1128
+ bottom: "max(20px, env(safe-area-inset-bottom))",
1129
+ [side]: "max(20px, env(safe-area-inset-right, env(safe-area-inset-left)))",
1130
+ zIndex: 1e4,
1131
+ display: "flex",
1132
+ flexDirection: "column",
1133
+ alignItems: isRtl ? "flex-start" : "flex-end",
1134
+ gap: 14,
1135
+ pointerEvents: "none",
1136
+ // Lets SVG dots / text on accent pick up inverse ink
1137
+ ["--gymlium-on-accent"]: "var(--color-text-inverse, #fff)"
1138
+ },
1139
+ children: [
1140
+ /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { children: open ? /* @__PURE__ */ jsxRuntime.jsx(
1141
+ framerMotion.motion.div,
1142
+ {
1143
+ initial: { opacity: 0, y: 28, scale: 0.94, filter: "blur(6px)" },
1144
+ animate: { opacity: 1, y: 0, scale: 1, filter: "blur(0px)" },
1145
+ exit: { opacity: 0, y: 18, scale: 0.96, filter: "blur(4px)" },
1146
+ transition: { duration: 0.32, ease: [0.22, 1, 0.36, 1] },
1147
+ style: {
1148
+ width: "min(380px, calc(100vw - 28px))",
1149
+ height: "min(440px, calc(100vh - 160px))",
1150
+ maxHeight: 440,
1151
+ borderRadius: 24,
1152
+ overflow: "hidden",
1153
+ border: `1px solid color-mix(in srgb, ${theme.colors.border} 85%, transparent)`,
1154
+ background: theme.colors.surface,
1155
+ boxShadow: `
1156
+ 0 0 0 1px color-mix(in srgb, ${theme.accentColor} 12%, transparent),
1157
+ 0 28px 80px color-mix(in srgb, ${theme.colors.text} 22%, transparent),
1158
+ 0 8px 24px color-mix(in srgb, ${theme.accentColor} 18%, transparent)
1159
+ `,
1160
+ pointerEvents: "auto",
1161
+ backdropFilter: "blur(20px)"
1162
+ },
1163
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1164
+ ChatPanel,
1165
+ {
1166
+ threadId: ui.threadId,
1167
+ onSelectThread: ui.openThread,
1168
+ onBackToList: ui.openInbox,
1169
+ onClose: ui.close,
1170
+ variant: "dock"
1171
+ }
1172
+ )
1173
+ },
1174
+ "panel"
1175
+ ) : null }),
1176
+ /* @__PURE__ */ jsxRuntime.jsxs(
1177
+ framerMotion.motion.button,
1178
+ {
1179
+ type: "button",
1180
+ onClick: () => open ? ui.close() : ui.openInbox(),
1181
+ "aria-label": label,
1182
+ "aria-expanded": open,
1183
+ whileHover: { scale: 1.05 },
1184
+ whileTap: { scale: 0.94 },
1185
+ transition: { type: "spring", stiffness: 420, damping: 24 },
1186
+ style: {
1187
+ pointerEvents: "auto",
1188
+ width: 58,
1189
+ height: 58,
1190
+ borderRadius: 20,
1191
+ border: "none",
1192
+ padding: 0,
1193
+ background: `
1194
+ radial-gradient(120% 120% at 30% 20%, color-mix(in srgb, #fff 28%, transparent), transparent 55%),
1195
+ linear-gradient(145deg, color-mix(in srgb, ${theme.accentColor} 88%, #fff) 0%, ${theme.accentColor} 55%, color-mix(in srgb, ${theme.accentColor} 78%, #000) 100%)
1196
+ `,
1197
+ color: "var(--color-text-inverse, #fff)",
1198
+ cursor: "pointer",
1199
+ boxShadow: `
1200
+ 0 14px 36px color-mix(in srgb, ${theme.accentColor} 42%, transparent),
1201
+ 0 2px 0 color-mix(in srgb, #fff 22%, transparent) inset
1202
+ `,
1203
+ position: "relative",
1204
+ display: "grid",
1205
+ placeItems: "center",
1206
+ overflow: "visible"
1207
+ },
1208
+ children: [
1209
+ !open && unread.total > 0 ? /* @__PURE__ */ jsxRuntime.jsx(
1210
+ framerMotion.motion.span,
1211
+ {
1212
+ "aria-hidden": true,
1213
+ initial: { opacity: 0.55, scale: 1 },
1214
+ animate: { opacity: [0.45, 0.15, 0.45], scale: [1, 1.28, 1] },
1215
+ transition: { duration: 2.4, repeat: Infinity, ease: "easeInOut" },
1216
+ style: {
1217
+ position: "absolute",
1218
+ inset: -6,
1219
+ borderRadius: 24,
1220
+ background: `color-mix(in srgb, ${theme.accentColor} 35%, transparent)`,
1221
+ zIndex: -1
1222
+ }
1223
+ }
1224
+ ) : null,
1225
+ /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { mode: "wait", initial: false, children: /* @__PURE__ */ jsxRuntime.jsx(
1226
+ framerMotion.motion.span,
1227
+ {
1228
+ initial: { opacity: 0, rotate: open ? -40 : 40, scale: 0.7 },
1229
+ animate: { opacity: 1, rotate: 0, scale: 1 },
1230
+ exit: { opacity: 0, rotate: open ? 40 : -40, scale: 0.7 },
1231
+ transition: { duration: 0.18 },
1232
+ style: { display: "grid", placeItems: "center", lineHeight: 0 },
1233
+ children: open ? /* @__PURE__ */ jsxRuntime.jsx(CloseIcon, { size: 20 }) : /* @__PURE__ */ jsxRuntime.jsx(ChatBubbleIcon, { size: 26 })
1234
+ },
1235
+ open ? "close" : "chat"
1236
+ ) }),
1237
+ !open && unread.total > 0 ? /* @__PURE__ */ jsxRuntime.jsx(
1238
+ framerMotion.motion.span,
1239
+ {
1240
+ initial: { scale: 0 },
1241
+ animate: { scale: 1 },
1242
+ style: {
1243
+ position: "absolute",
1244
+ top: -3,
1245
+ [isRtl ? "left" : "right"]: -3,
1246
+ minWidth: 22,
1247
+ height: 22,
1248
+ borderRadius: 11,
1249
+ padding: "0 6px",
1250
+ background: theme.colors.surface,
1251
+ color: theme.colors.text,
1252
+ fontSize: 11,
1253
+ fontWeight: 700,
1254
+ fontFamily: theme.fonts.bodySemiBold,
1255
+ display: "inline-flex",
1256
+ alignItems: "center",
1257
+ justifyContent: "center",
1258
+ border: `2px solid ${theme.accentColor}`,
1259
+ boxShadow: `0 4px 12px color-mix(in srgb, ${theme.colors.text} 25%, transparent)`
1260
+ },
1261
+ children: unread.total > 99 ? "99+" : unread.total
1262
+ }
1263
+ ) : null
1264
+ ]
1265
+ }
1266
+ )
1267
+ ]
1268
+ }
1269
+ );
1270
+ }
1271
+
1272
+ exports.ChatDock = ChatDock;
1273
+ exports.ChatPanel = ChatPanel;
1274
+ exports.Conversation = Conversation;
1275
+ exports.ThreadList = ThreadList;
1276
+ exports.WebComposer = WebComposer;
1277
+ //# sourceMappingURL=index.cjs.map
1278
+ //# sourceMappingURL=index.cjs.map