framepexls-ui-lib 1.6.0 → 1.7.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.
@@ -60,6 +60,11 @@ type AuthTopbarProps = {
60
60
  threads: ChatThread[];
61
61
  messages: ChatMessage[];
62
62
  presence?: ChatPresenceUser[];
63
+ typingUsers?: ChatPresenceUser[];
64
+ onTyping?: (payload: {
65
+ threadId: string;
66
+ typing: boolean;
67
+ }) => void;
63
68
  onSendMessage: (payload: {
64
69
  threadId: string;
65
70
  text: string;
@@ -60,6 +60,11 @@ type AuthTopbarProps = {
60
60
  threads: ChatThread[];
61
61
  messages: ChatMessage[];
62
62
  presence?: ChatPresenceUser[];
63
+ typingUsers?: ChatPresenceUser[];
64
+ onTyping?: (payload: {
65
+ threadId: string;
66
+ typing: boolean;
67
+ }) => void;
63
68
  onSendMessage: (payload: {
64
69
  threadId: string;
65
70
  text: string;
@@ -156,6 +156,8 @@ function AuthTopbar({
156
156
  threads: chat.threads,
157
157
  messages: chat.messages,
158
158
  presence: chat.presence,
159
+ typingUsers: chat.typingUsers,
160
+ onTyping: chat.onTyping,
159
161
  onSendMessage: chat.onSendMessage,
160
162
  labels: { trigger: "Chat", title: "Chat", online: "Activos" }
161
163
  }
@@ -135,6 +135,8 @@ function AuthTopbar({
135
135
  threads: chat.threads,
136
136
  messages: chat.messages,
137
137
  presence: chat.presence,
138
+ typingUsers: chat.typingUsers,
139
+ onTyping: chat.onTyping,
138
140
  onSendMessage: chat.onSendMessage,
139
141
  labels: { trigger: "Chat", title: "Chat", online: "Activos" }
140
142
  }
@@ -64,11 +64,16 @@ type ChatCenterProps = {
64
64
  text: string;
65
65
  }) => void | Promise<void>;
66
66
  presence?: ChatPresenceUser[];
67
+ typingUsers?: ChatPresenceUser[];
68
+ onTyping?: (payload: {
69
+ threadId: string;
70
+ typing: boolean;
71
+ }) => void;
67
72
  unreadCount?: number;
68
73
  drawerWidthClass?: string;
69
74
  className?: string;
70
75
  labels?: ChatCenterLabels;
71
76
  };
72
- declare function ChatCenter({ me, threads, messages, open: openProp, onOpenChange, defaultThreadId, activeThreadId: activeThreadIdProp, onThreadChange, onSendMessage, presence, unreadCount: unreadCountProp, drawerWidthClass, className, labels, }: ChatCenterProps): react_jsx_runtime.JSX.Element;
77
+ declare function ChatCenter({ me, threads, messages, open: openProp, onOpenChange, defaultThreadId, activeThreadId: activeThreadIdProp, onThreadChange, onSendMessage, presence, typingUsers, onTyping, unreadCount: unreadCountProp, drawerWidthClass, className, labels, }: ChatCenterProps): react_jsx_runtime.JSX.Element;
73
78
 
74
79
  export { type ChatCenterLabels, type ChatCenterProps, type ChatMessage, type ChatPresenceUser, type ChatThread, type ChatUser, ChatCenter as default };
@@ -64,11 +64,16 @@ type ChatCenterProps = {
64
64
  text: string;
65
65
  }) => void | Promise<void>;
66
66
  presence?: ChatPresenceUser[];
67
+ typingUsers?: ChatPresenceUser[];
68
+ onTyping?: (payload: {
69
+ threadId: string;
70
+ typing: boolean;
71
+ }) => void;
67
72
  unreadCount?: number;
68
73
  drawerWidthClass?: string;
69
74
  className?: string;
70
75
  labels?: ChatCenterLabels;
71
76
  };
72
- declare function ChatCenter({ me, threads, messages, open: openProp, onOpenChange, defaultThreadId, activeThreadId: activeThreadIdProp, onThreadChange, onSendMessage, presence, unreadCount: unreadCountProp, drawerWidthClass, className, labels, }: ChatCenterProps): react_jsx_runtime.JSX.Element;
77
+ declare function ChatCenter({ me, threads, messages, open: openProp, onOpenChange, defaultThreadId, activeThreadId: activeThreadIdProp, onThreadChange, onSendMessage, presence, typingUsers, onTyping, unreadCount: unreadCountProp, drawerWidthClass, className, labels, }: ChatCenterProps): react_jsx_runtime.JSX.Element;
73
78
 
74
79
  export { type ChatCenterLabels, type ChatCenterProps, type ChatMessage, type ChatPresenceUser, type ChatThread, type ChatUser, ChatCenter as default };
@@ -65,6 +65,8 @@ function ChatCenter({
65
65
  onThreadChange,
66
66
  onSendMessage,
67
67
  presence,
68
+ typingUsers,
69
+ onTyping,
68
70
  unreadCount: unreadCountProp,
69
71
  drawerWidthClass,
70
72
  className,
@@ -89,7 +91,11 @@ function ChatCenter({
89
91
  const [text, setText] = import_react.default.useState("");
90
92
  const [sending, setSending] = import_react.default.useState(false);
91
93
  const endRef = import_react.default.useRef(null);
94
+ const messagesScrollRef = import_react.default.useRef(null);
92
95
  const composerRef = import_react.default.useRef(null);
96
+ const typingTimeoutRef = import_react.default.useRef(null);
97
+ const typingActiveRef = import_react.default.useRef(false);
98
+ const typingLastEmitAtRef = import_react.default.useRef(0);
93
99
  const labelTrigger = (_a = labels == null ? void 0 : labels.trigger) != null ? _a : "Chat";
94
100
  const labelTitle = (_b = labels == null ? void 0 : labels.title) != null ? _b : "Chat";
95
101
  const labelSearchThreads = (_c = labels == null ? void 0 : labels.searchThreads) != null ? _c : "Buscar chats\u2026";
@@ -142,22 +148,107 @@ function ChatCenter({
142
148
  return (_a2 = composerRef.current) == null ? void 0 : _a2.focus();
143
149
  }, 0);
144
150
  }, [open, activeThreadId]);
151
+ import_react.default.useEffect(() => {
152
+ if (typingTimeoutRef.current) window.clearTimeout(typingTimeoutRef.current);
153
+ typingTimeoutRef.current = null;
154
+ if (typingActiveRef.current && activeThreadId) {
155
+ try {
156
+ onTyping == null ? void 0 : onTyping({ threadId: activeThreadId, typing: false });
157
+ } catch {
158
+ }
159
+ }
160
+ typingActiveRef.current = false;
161
+ }, [activeThreadId, open]);
145
162
  import_react.default.useEffect(() => {
146
163
  var _a2;
147
164
  if (!open) return;
148
- (_a2 = endRef.current) == null ? void 0 : _a2.scrollIntoView({ behavior: "smooth", block: "end" });
165
+ const el = messagesScrollRef.current;
166
+ if (!el) {
167
+ (_a2 = endRef.current) == null ? void 0 : _a2.scrollIntoView({ behavior: "auto", block: "end" });
168
+ return;
169
+ }
170
+ const raf1 = requestAnimationFrame(() => {
171
+ const raf2 = requestAnimationFrame(() => {
172
+ var _a3;
173
+ try {
174
+ el.scrollTop = el.scrollHeight;
175
+ } catch {
176
+ (_a3 = endRef.current) == null ? void 0 : _a3.scrollIntoView({ behavior: "auto", block: "end" });
177
+ }
178
+ });
179
+ void raf2;
180
+ });
181
+ return () => cancelAnimationFrame(raf1);
149
182
  }, [open, activeThreadId, filteredMessages.length]);
183
+ const setTextWithTyping = (next) => {
184
+ setText(next);
185
+ if (!activeThreadId) return;
186
+ if (!onTyping) return;
187
+ const shouldTyping = Boolean(next.trim().length);
188
+ const emitTypingTrue = () => {
189
+ typingLastEmitAtRef.current = Date.now();
190
+ try {
191
+ onTyping({ threadId: activeThreadId, typing: true });
192
+ } catch {
193
+ }
194
+ };
195
+ if (shouldTyping) {
196
+ if (!typingActiveRef.current) {
197
+ typingActiveRef.current = true;
198
+ emitTypingTrue();
199
+ } else {
200
+ const now = Date.now();
201
+ if (now - typingLastEmitAtRef.current > 900) {
202
+ emitTypingTrue();
203
+ }
204
+ }
205
+ }
206
+ if (typingTimeoutRef.current) window.clearTimeout(typingTimeoutRef.current);
207
+ typingTimeoutRef.current = window.setTimeout(() => {
208
+ typingTimeoutRef.current = null;
209
+ if (!typingActiveRef.current) return;
210
+ typingActiveRef.current = false;
211
+ typingLastEmitAtRef.current = 0;
212
+ try {
213
+ onTyping({ threadId: activeThreadId, typing: false });
214
+ } catch {
215
+ }
216
+ }, 1800);
217
+ if (!shouldTyping && typingActiveRef.current) {
218
+ typingActiveRef.current = false;
219
+ typingLastEmitAtRef.current = 0;
220
+ try {
221
+ onTyping({ threadId: activeThreadId, typing: false });
222
+ } catch {
223
+ }
224
+ }
225
+ };
150
226
  const send = async () => {
151
227
  const t = activeThreadId;
152
228
  const msg = text.trim();
153
229
  if (!t || !msg) return;
154
230
  try {
155
231
  setSending(true);
232
+ if (typingTimeoutRef.current) window.clearTimeout(typingTimeoutRef.current);
233
+ typingTimeoutRef.current = null;
234
+ if (typingActiveRef.current) {
235
+ typingActiveRef.current = false;
236
+ try {
237
+ onTyping == null ? void 0 : onTyping({ threadId: t, typing: false });
238
+ } catch {
239
+ }
240
+ }
156
241
  await onSendMessage({ threadId: t, text: msg });
157
242
  setText("");
158
243
  window.setTimeout(() => {
159
- var _a2;
160
- return (_a2 = endRef.current) == null ? void 0 : _a2.scrollIntoView({ behavior: "smooth", block: "end" });
244
+ var _a2, _b2;
245
+ try {
246
+ const el = messagesScrollRef.current;
247
+ if (el) el.scrollTop = el.scrollHeight;
248
+ else (_a2 = endRef.current) == null ? void 0 : _a2.scrollIntoView({ behavior: "auto", block: "end" });
249
+ } catch {
250
+ (_b2 = endRef.current) == null ? void 0 : _b2.scrollIntoView({ behavior: "auto", block: "end" });
251
+ }
161
252
  }, 0);
162
253
  } finally {
163
254
  setSending(false);
@@ -293,7 +384,7 @@ function ChatCenter({
293
384
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "md:hidden", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_Tooltip.default, { content: labelBack, placement: "left", offset: 10, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ActionIconButton.default, { title: labelBack, size: "sm", onClick: () => setMobileTab("threads"), className: "border border-[var(--border)] bg-[var(--card)] shadow-sm", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_iconos.ArrowLeftIcon, { "aria-hidden": true }) }) }) })
294
385
  ] })
295
386
  ] }) }),
296
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "min-h-0 flex-1 overflow-y-auto px-4 py-4", children: filteredMessages.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "grid place-items-center rounded-3xl border border-[var(--border)] bg-[color-mix(in_oklab,var(--card)_92%,transparent)] p-10 text-center", children: [
387
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ref: messagesScrollRef, className: "min-h-0 flex-1 overflow-y-auto px-4 py-4", children: filteredMessages.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "grid place-items-center rounded-3xl border border-[var(--border)] bg-[color-mix(in_oklab,var(--card)_92%,transparent)] p-10 text-center", children: [
297
388
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "text-sm font-semibold text-[var(--foreground)]", children: emptyMessagesTitle }),
298
389
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mt-1 text-xs text-[var(--muted)]", children: emptyMessagesSubtitle })
299
390
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "space-y-2", children: [
@@ -336,36 +427,42 @@ function ChatCenter({
336
427
  }),
337
428
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ref: endRef })
338
429
  ] }) }),
339
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "border-t border-[var(--border)] bg-[var(--card)] px-4 py-3", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-end gap-2", children: [
340
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
341
- import_Input.default,
342
- {
343
- ref: composerRef,
344
- value: text,
345
- onChange: (e) => setText(e.currentTarget.value),
346
- placeholder: composerPlaceholder,
347
- className: "flex-1",
348
- onKeyDown: (e) => {
349
- if (e.key === "Enter" && !e.shiftKey) {
350
- e.preventDefault();
351
- void send();
430
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "border-t border-[var(--border)] bg-[var(--card)] px-4 py-3", children: [
431
+ typingUsers && typingUsers.length ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mb-2 flex items-center gap-2 px-1 text-[0.75rem] text-[var(--muted)]", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "inline-flex items-center gap-1", children: [
432
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "h-1.5 w-1.5 rounded-full bg-[var(--primary)] opacity-80", "aria-hidden": true }),
433
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "truncate", children: typingUsers.length === 1 ? `${typingUsers[0].name} est\xE1 escribiendo\u2026` : `${typingUsers.slice(0, 2).map((u) => u.name).join(", ")}${typingUsers.length > 2 ? ` y ${typingUsers.length - 2} m\xE1s` : ""} est\xE1n escribiendo\u2026` })
434
+ ] }) }) : null,
435
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-end gap-2", children: [
436
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
437
+ import_Input.default,
438
+ {
439
+ ref: composerRef,
440
+ value: text,
441
+ onChange: (e) => setTextWithTyping(e.currentTarget.value),
442
+ placeholder: composerPlaceholder,
443
+ className: "flex-1",
444
+ onKeyDown: (e) => {
445
+ if (e.key === "Enter" && !e.shiftKey) {
446
+ e.preventDefault();
447
+ void send();
448
+ }
352
449
  }
353
450
  }
354
- }
355
- ),
356
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
357
- import_Button.default,
358
- {
359
- size: "sm",
360
- variant: "primary",
361
- disabled: !activeThreadId || !text.trim() || sending,
362
- loading: sending,
363
- onClick: () => void send(),
364
- leftIcon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_iconos.PaperPlaneIcon, { "aria-hidden": true, className: "h-4 w-4" }),
365
- children: labelSend
366
- }
367
- )
368
- ] }) })
451
+ ),
452
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
453
+ import_Button.default,
454
+ {
455
+ size: "sm",
456
+ variant: "primary",
457
+ disabled: !activeThreadId || !text.trim() || sending,
458
+ loading: sending,
459
+ onClick: () => void send(),
460
+ leftIcon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_iconos.PaperPlaneIcon, { "aria-hidden": true, className: "h-4 w-4" }),
461
+ children: labelSend
462
+ }
463
+ )
464
+ ] })
465
+ ] })
369
466
  ] });
370
467
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
371
468
  Trigger,
@@ -394,32 +491,25 @@ function ChatCenter({
394
491
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ActionIconButton.default, { title: "Cerrar", size: "md", onClick: () => setOpen(false), className: "border border-[var(--border)] bg-[var(--card)] shadow-sm", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_iconos.CloseIcon, { "aria-hidden": true, className: "h-4.5 w-4.5" }) })
395
492
  ] })
396
493
  ] }) }),
397
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_Drawer.default.Body, { className: "bg-[linear-gradient(180deg,color-mix(in_oklab,var(--surface)_65%,transparent),color-mix(in_oklab,var(--bg)_88%,transparent))]", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "h-[calc(100vh-10rem)] min-h-[520px]", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "grid h-full grid-cols-1 md:grid-cols-[360px_1fr]", children: [
494
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_Drawer.default.Body, { className: "overflow-hidden bg-[linear-gradient(180deg,color-mix(in_oklab,var(--surface)_65%,transparent),color-mix(in_oklab,var(--bg)_88%,transparent))]", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "h-full min-h-0", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "grid h-full min-h-0 grid-cols-1 md:grid-cols-[360px_1fr]", children: [
398
495
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: cx("h-full border-r border-[var(--border)] bg-[color-mix(in_oklab,var(--card)_90%,transparent)]", mobileTab === "threads" ? "" : "hidden md:block"), children: ThreadsPane }),
399
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: cx("h-full", mobileTab === "chat" ? "" : "hidden md:block"), children: activeThread ? MessagesPane : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "grid h-full place-items-center p-8", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "rounded-3xl border border-[var(--border)] bg-[color-mix(in_oklab,var(--card)_92%,transparent)] p-10 text-center", children: [
496
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: cx("h-full min-h-0", mobileTab === "chat" ? "" : "hidden md:block"), children: activeThread ? MessagesPane : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "grid h-full place-items-center p-8", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "rounded-3xl border border-[var(--border)] bg-[color-mix(in_oklab,var(--card)_92%,transparent)] p-10 text-center", children: [
400
497
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "text-sm font-semibold text-[var(--foreground)]", children: "Selecciona un chat" }),
401
498
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mt-1 text-xs text-[var(--muted)]", children: "Elige una conversaci\xF3n para ver mensajes." })
402
499
  ] }) }) })
403
500
  ] }) }) }),
404
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_Drawer.default.Footer, { sticky: true, className: "bg-[color-mix(in_oklab,var(--card)_90%,transparent)]", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center justify-between gap-3", children: [
405
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "text-xs text-[var(--muted)]", children: [
406
- "Conectado como ",
407
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "font-semibold text-[var(--foreground)]", children: me.name })
408
- ] }),
409
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "md:hidden", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
410
- import_SegmentedTabs.default,
411
- {
412
- value: mobileTab,
413
- size: "sm",
414
- onChange: (v) => setMobileTab(v),
415
- options: [
416
- { value: "threads", label: "Chats" },
417
- { value: "chat", label: "Mensaje" }
418
- ]
419
- }
420
- ) }),
421
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "hidden md:block", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_Button.default, { variant: "outline", size: "sm", onClick: () => setOpen(false), children: "Cerrar" }) })
422
- ] }) })
501
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_Drawer.default.Footer, { sticky: true, className: "bg-[color-mix(in_oklab,var(--card)_90%,transparent)] md:hidden", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
502
+ import_SegmentedTabs.default,
503
+ {
504
+ value: mobileTab,
505
+ size: "sm",
506
+ onChange: (v) => setMobileTab(v),
507
+ options: [
508
+ { value: "threads", label: "Chats" },
509
+ { value: "chat", label: "Mensaje" }
510
+ ]
511
+ }
512
+ ) })
423
513
  ] })
424
514
  ] });
425
515
  }
@@ -32,6 +32,8 @@ function ChatCenter({
32
32
  onThreadChange,
33
33
  onSendMessage,
34
34
  presence,
35
+ typingUsers,
36
+ onTyping,
35
37
  unreadCount: unreadCountProp,
36
38
  drawerWidthClass,
37
39
  className,
@@ -56,7 +58,11 @@ function ChatCenter({
56
58
  const [text, setText] = React.useState("");
57
59
  const [sending, setSending] = React.useState(false);
58
60
  const endRef = React.useRef(null);
61
+ const messagesScrollRef = React.useRef(null);
59
62
  const composerRef = React.useRef(null);
63
+ const typingTimeoutRef = React.useRef(null);
64
+ const typingActiveRef = React.useRef(false);
65
+ const typingLastEmitAtRef = React.useRef(0);
60
66
  const labelTrigger = (_a = labels == null ? void 0 : labels.trigger) != null ? _a : "Chat";
61
67
  const labelTitle = (_b = labels == null ? void 0 : labels.title) != null ? _b : "Chat";
62
68
  const labelSearchThreads = (_c = labels == null ? void 0 : labels.searchThreads) != null ? _c : "Buscar chats\u2026";
@@ -109,22 +115,107 @@ function ChatCenter({
109
115
  return (_a2 = composerRef.current) == null ? void 0 : _a2.focus();
110
116
  }, 0);
111
117
  }, [open, activeThreadId]);
118
+ React.useEffect(() => {
119
+ if (typingTimeoutRef.current) window.clearTimeout(typingTimeoutRef.current);
120
+ typingTimeoutRef.current = null;
121
+ if (typingActiveRef.current && activeThreadId) {
122
+ try {
123
+ onTyping == null ? void 0 : onTyping({ threadId: activeThreadId, typing: false });
124
+ } catch {
125
+ }
126
+ }
127
+ typingActiveRef.current = false;
128
+ }, [activeThreadId, open]);
112
129
  React.useEffect(() => {
113
130
  var _a2;
114
131
  if (!open) return;
115
- (_a2 = endRef.current) == null ? void 0 : _a2.scrollIntoView({ behavior: "smooth", block: "end" });
132
+ const el = messagesScrollRef.current;
133
+ if (!el) {
134
+ (_a2 = endRef.current) == null ? void 0 : _a2.scrollIntoView({ behavior: "auto", block: "end" });
135
+ return;
136
+ }
137
+ const raf1 = requestAnimationFrame(() => {
138
+ const raf2 = requestAnimationFrame(() => {
139
+ var _a3;
140
+ try {
141
+ el.scrollTop = el.scrollHeight;
142
+ } catch {
143
+ (_a3 = endRef.current) == null ? void 0 : _a3.scrollIntoView({ behavior: "auto", block: "end" });
144
+ }
145
+ });
146
+ void raf2;
147
+ });
148
+ return () => cancelAnimationFrame(raf1);
116
149
  }, [open, activeThreadId, filteredMessages.length]);
150
+ const setTextWithTyping = (next) => {
151
+ setText(next);
152
+ if (!activeThreadId) return;
153
+ if (!onTyping) return;
154
+ const shouldTyping = Boolean(next.trim().length);
155
+ const emitTypingTrue = () => {
156
+ typingLastEmitAtRef.current = Date.now();
157
+ try {
158
+ onTyping({ threadId: activeThreadId, typing: true });
159
+ } catch {
160
+ }
161
+ };
162
+ if (shouldTyping) {
163
+ if (!typingActiveRef.current) {
164
+ typingActiveRef.current = true;
165
+ emitTypingTrue();
166
+ } else {
167
+ const now = Date.now();
168
+ if (now - typingLastEmitAtRef.current > 900) {
169
+ emitTypingTrue();
170
+ }
171
+ }
172
+ }
173
+ if (typingTimeoutRef.current) window.clearTimeout(typingTimeoutRef.current);
174
+ typingTimeoutRef.current = window.setTimeout(() => {
175
+ typingTimeoutRef.current = null;
176
+ if (!typingActiveRef.current) return;
177
+ typingActiveRef.current = false;
178
+ typingLastEmitAtRef.current = 0;
179
+ try {
180
+ onTyping({ threadId: activeThreadId, typing: false });
181
+ } catch {
182
+ }
183
+ }, 1800);
184
+ if (!shouldTyping && typingActiveRef.current) {
185
+ typingActiveRef.current = false;
186
+ typingLastEmitAtRef.current = 0;
187
+ try {
188
+ onTyping({ threadId: activeThreadId, typing: false });
189
+ } catch {
190
+ }
191
+ }
192
+ };
117
193
  const send = async () => {
118
194
  const t = activeThreadId;
119
195
  const msg = text.trim();
120
196
  if (!t || !msg) return;
121
197
  try {
122
198
  setSending(true);
199
+ if (typingTimeoutRef.current) window.clearTimeout(typingTimeoutRef.current);
200
+ typingTimeoutRef.current = null;
201
+ if (typingActiveRef.current) {
202
+ typingActiveRef.current = false;
203
+ try {
204
+ onTyping == null ? void 0 : onTyping({ threadId: t, typing: false });
205
+ } catch {
206
+ }
207
+ }
123
208
  await onSendMessage({ threadId: t, text: msg });
124
209
  setText("");
125
210
  window.setTimeout(() => {
126
- var _a2;
127
- return (_a2 = endRef.current) == null ? void 0 : _a2.scrollIntoView({ behavior: "smooth", block: "end" });
211
+ var _a2, _b2;
212
+ try {
213
+ const el = messagesScrollRef.current;
214
+ if (el) el.scrollTop = el.scrollHeight;
215
+ else (_a2 = endRef.current) == null ? void 0 : _a2.scrollIntoView({ behavior: "auto", block: "end" });
216
+ } catch {
217
+ (_b2 = endRef.current) == null ? void 0 : _b2.scrollIntoView({ behavior: "auto", block: "end" });
218
+ }
128
219
  }, 0);
129
220
  } finally {
130
221
  setSending(false);
@@ -260,7 +351,7 @@ function ChatCenter({
260
351
  /* @__PURE__ */ jsx("div", { className: "md:hidden", children: /* @__PURE__ */ jsx(Tooltip, { content: labelBack, placement: "left", offset: 10, children: /* @__PURE__ */ jsx(ActionIconButton, { title: labelBack, size: "sm", onClick: () => setMobileTab("threads"), className: "border border-[var(--border)] bg-[var(--card)] shadow-sm", children: /* @__PURE__ */ jsx(ArrowLeftIcon, { "aria-hidden": true }) }) }) })
261
352
  ] })
262
353
  ] }) }),
263
- /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-y-auto px-4 py-4", children: filteredMessages.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "grid place-items-center rounded-3xl border border-[var(--border)] bg-[color-mix(in_oklab,var(--card)_92%,transparent)] p-10 text-center", children: [
354
+ /* @__PURE__ */ jsx("div", { ref: messagesScrollRef, className: "min-h-0 flex-1 overflow-y-auto px-4 py-4", children: filteredMessages.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "grid place-items-center rounded-3xl border border-[var(--border)] bg-[color-mix(in_oklab,var(--card)_92%,transparent)] p-10 text-center", children: [
264
355
  /* @__PURE__ */ jsx("div", { className: "text-sm font-semibold text-[var(--foreground)]", children: emptyMessagesTitle }),
265
356
  /* @__PURE__ */ jsx("div", { className: "mt-1 text-xs text-[var(--muted)]", children: emptyMessagesSubtitle })
266
357
  ] }) : /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
@@ -303,36 +394,42 @@ function ChatCenter({
303
394
  }),
304
395
  /* @__PURE__ */ jsx("div", { ref: endRef })
305
396
  ] }) }),
306
- /* @__PURE__ */ jsx("div", { className: "border-t border-[var(--border)] bg-[var(--card)] px-4 py-3", children: /* @__PURE__ */ jsxs("div", { className: "flex items-end gap-2", children: [
307
- /* @__PURE__ */ jsx(
308
- Input,
309
- {
310
- ref: composerRef,
311
- value: text,
312
- onChange: (e) => setText(e.currentTarget.value),
313
- placeholder: composerPlaceholder,
314
- className: "flex-1",
315
- onKeyDown: (e) => {
316
- if (e.key === "Enter" && !e.shiftKey) {
317
- e.preventDefault();
318
- void send();
397
+ /* @__PURE__ */ jsxs("div", { className: "border-t border-[var(--border)] bg-[var(--card)] px-4 py-3", children: [
398
+ typingUsers && typingUsers.length ? /* @__PURE__ */ jsx("div", { className: "mb-2 flex items-center gap-2 px-1 text-[0.75rem] text-[var(--muted)]", children: /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1", children: [
399
+ /* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-[var(--primary)] opacity-80", "aria-hidden": true }),
400
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: typingUsers.length === 1 ? `${typingUsers[0].name} est\xE1 escribiendo\u2026` : `${typingUsers.slice(0, 2).map((u) => u.name).join(", ")}${typingUsers.length > 2 ? ` y ${typingUsers.length - 2} m\xE1s` : ""} est\xE1n escribiendo\u2026` })
401
+ ] }) }) : null,
402
+ /* @__PURE__ */ jsxs("div", { className: "flex items-end gap-2", children: [
403
+ /* @__PURE__ */ jsx(
404
+ Input,
405
+ {
406
+ ref: composerRef,
407
+ value: text,
408
+ onChange: (e) => setTextWithTyping(e.currentTarget.value),
409
+ placeholder: composerPlaceholder,
410
+ className: "flex-1",
411
+ onKeyDown: (e) => {
412
+ if (e.key === "Enter" && !e.shiftKey) {
413
+ e.preventDefault();
414
+ void send();
415
+ }
319
416
  }
320
417
  }
321
- }
322
- ),
323
- /* @__PURE__ */ jsx(
324
- Button,
325
- {
326
- size: "sm",
327
- variant: "primary",
328
- disabled: !activeThreadId || !text.trim() || sending,
329
- loading: sending,
330
- onClick: () => void send(),
331
- leftIcon: /* @__PURE__ */ jsx(PaperPlaneIcon, { "aria-hidden": true, className: "h-4 w-4" }),
332
- children: labelSend
333
- }
334
- )
335
- ] }) })
418
+ ),
419
+ /* @__PURE__ */ jsx(
420
+ Button,
421
+ {
422
+ size: "sm",
423
+ variant: "primary",
424
+ disabled: !activeThreadId || !text.trim() || sending,
425
+ loading: sending,
426
+ onClick: () => void send(),
427
+ leftIcon: /* @__PURE__ */ jsx(PaperPlaneIcon, { "aria-hidden": true, className: "h-4 w-4" }),
428
+ children: labelSend
429
+ }
430
+ )
431
+ ] })
432
+ ] })
336
433
  ] });
337
434
  return /* @__PURE__ */ jsxs(Fragment, { children: [
338
435
  Trigger,
@@ -361,32 +458,25 @@ function ChatCenter({
361
458
  /* @__PURE__ */ jsx(ActionIconButton, { title: "Cerrar", size: "md", onClick: () => setOpen(false), className: "border border-[var(--border)] bg-[var(--card)] shadow-sm", children: /* @__PURE__ */ jsx(CloseIcon, { "aria-hidden": true, className: "h-4.5 w-4.5" }) })
362
459
  ] })
363
460
  ] }) }),
364
- /* @__PURE__ */ jsx(Drawer.Body, { className: "bg-[linear-gradient(180deg,color-mix(in_oklab,var(--surface)_65%,transparent),color-mix(in_oklab,var(--bg)_88%,transparent))]", children: /* @__PURE__ */ jsx("div", { className: "h-[calc(100vh-10rem)] min-h-[520px]", children: /* @__PURE__ */ jsxs("div", { className: "grid h-full grid-cols-1 md:grid-cols-[360px_1fr]", children: [
461
+ /* @__PURE__ */ jsx(Drawer.Body, { className: "overflow-hidden bg-[linear-gradient(180deg,color-mix(in_oklab,var(--surface)_65%,transparent),color-mix(in_oklab,var(--bg)_88%,transparent))]", children: /* @__PURE__ */ jsx("div", { className: "h-full min-h-0", children: /* @__PURE__ */ jsxs("div", { className: "grid h-full min-h-0 grid-cols-1 md:grid-cols-[360px_1fr]", children: [
365
462
  /* @__PURE__ */ jsx("div", { className: cx("h-full border-r border-[var(--border)] bg-[color-mix(in_oklab,var(--card)_90%,transparent)]", mobileTab === "threads" ? "" : "hidden md:block"), children: ThreadsPane }),
366
- /* @__PURE__ */ jsx("div", { className: cx("h-full", mobileTab === "chat" ? "" : "hidden md:block"), children: activeThread ? MessagesPane : /* @__PURE__ */ jsx("div", { className: "grid h-full place-items-center p-8", children: /* @__PURE__ */ jsxs("div", { className: "rounded-3xl border border-[var(--border)] bg-[color-mix(in_oklab,var(--card)_92%,transparent)] p-10 text-center", children: [
463
+ /* @__PURE__ */ jsx("div", { className: cx("h-full min-h-0", mobileTab === "chat" ? "" : "hidden md:block"), children: activeThread ? MessagesPane : /* @__PURE__ */ jsx("div", { className: "grid h-full place-items-center p-8", children: /* @__PURE__ */ jsxs("div", { className: "rounded-3xl border border-[var(--border)] bg-[color-mix(in_oklab,var(--card)_92%,transparent)] p-10 text-center", children: [
367
464
  /* @__PURE__ */ jsx("div", { className: "text-sm font-semibold text-[var(--foreground)]", children: "Selecciona un chat" }),
368
465
  /* @__PURE__ */ jsx("div", { className: "mt-1 text-xs text-[var(--muted)]", children: "Elige una conversaci\xF3n para ver mensajes." })
369
466
  ] }) }) })
370
467
  ] }) }) }),
371
- /* @__PURE__ */ jsx(Drawer.Footer, { sticky: true, className: "bg-[color-mix(in_oklab,var(--card)_90%,transparent)]", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
372
- /* @__PURE__ */ jsxs("div", { className: "text-xs text-[var(--muted)]", children: [
373
- "Conectado como ",
374
- /* @__PURE__ */ jsx("span", { className: "font-semibold text-[var(--foreground)]", children: me.name })
375
- ] }),
376
- /* @__PURE__ */ jsx("div", { className: "md:hidden", children: /* @__PURE__ */ jsx(
377
- SegmentedTabs,
378
- {
379
- value: mobileTab,
380
- size: "sm",
381
- onChange: (v) => setMobileTab(v),
382
- options: [
383
- { value: "threads", label: "Chats" },
384
- { value: "chat", label: "Mensaje" }
385
- ]
386
- }
387
- ) }),
388
- /* @__PURE__ */ jsx("div", { className: "hidden md:block", children: /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: () => setOpen(false), children: "Cerrar" }) })
389
- ] }) })
468
+ /* @__PURE__ */ jsx(Drawer.Footer, { sticky: true, className: "bg-[color-mix(in_oklab,var(--card)_90%,transparent)] md:hidden", children: /* @__PURE__ */ jsx(
469
+ SegmentedTabs,
470
+ {
471
+ value: mobileTab,
472
+ size: "sm",
473
+ onChange: (v) => setMobileTab(v),
474
+ options: [
475
+ { value: "threads", label: "Chats" },
476
+ { value: "chat", label: "Mensaje" }
477
+ ]
478
+ }
479
+ ) })
390
480
  ] })
391
481
  ] });
392
482
  }
@@ -11,7 +11,7 @@ type CheckboxProps = Omit<React__default.InputHTMLAttributes<HTMLInputElement>,
11
11
  className?: string;
12
12
  inputClassName?: string;
13
13
  };
14
- declare const Checkbox: React__default.ForwardRefExoticComponent<Omit<React__default.InputHTMLAttributes<HTMLInputElement>, "size" | "type"> & {
14
+ declare const Checkbox: React__default.ForwardRefExoticComponent<Omit<React__default.InputHTMLAttributes<HTMLInputElement>, "type" | "size"> & {
15
15
  label?: React__default.ReactNode;
16
16
  description?: React__default.ReactNode;
17
17
  error?: boolean;
@@ -11,7 +11,7 @@ type CheckboxProps = Omit<React__default.InputHTMLAttributes<HTMLInputElement>,
11
11
  className?: string;
12
12
  inputClassName?: string;
13
13
  };
14
- declare const Checkbox: React__default.ForwardRefExoticComponent<Omit<React__default.InputHTMLAttributes<HTMLInputElement>, "size" | "type"> & {
14
+ declare const Checkbox: React__default.ForwardRefExoticComponent<Omit<React__default.InputHTMLAttributes<HTMLInputElement>, "type" | "size"> & {
15
15
  label?: React__default.ReactNode;
16
16
  description?: React__default.ReactNode;
17
17
  error?: boolean;
package/dist/Sidebar.js CHANGED
@@ -1857,6 +1857,7 @@ function SidebarInner({
1857
1857
  const hasWorkspace = Boolean((workspaces == null ? void 0 : workspaces.length) && onWorkspaceSelect);
1858
1858
  const hasSlot = Boolean(sidebarSlot || sidebarSlotCollapsed);
1859
1859
  const showUser = Boolean(user || userMenuSlot);
1860
+ const showWorkspaceInTopPanel = Boolean(showBrand && hasWorkspace);
1860
1861
  const showTopPanel = Boolean((showBrand ? hasWorkspace : false) || showUser || hasSlot);
1861
1862
  const skGroups = Math.max(1, (_a = skeleton == null ? void 0 : skeleton.groups) != null ? _a : 4);
1862
1863
  const skItems = Math.max(2, (_b = skeleton == null ? void 0 : skeleton.itemsPerGroup) != null ? _b : 6);
@@ -2069,7 +2070,7 @@ function SidebarInner({
2069
2070
  ] })
2070
2071
  ] }) })
2071
2072
  ] }) }) : collapsed ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "rounded-3xl border border-[var(--border)] bg-[color-mix(in_oklab,var(--card)_92%,transparent)] p-2 shadow-sm", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-col items-center justify-center gap-2", children: [
2072
- hasWorkspace ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
2073
+ showWorkspaceInTopPanel ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
2073
2074
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2074
2075
  WorkspaceSwitcher,
2075
2076
  {
@@ -2102,7 +2103,7 @@ function SidebarInner({
2102
2103
  ] }) : null,
2103
2104
  sidebarSlotCollapsed ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex items-center justify-center", children: sidebarSlotCollapsed }) : null
2104
2105
  ] }) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "overflow-hidden rounded-3xl border border-[var(--border)] bg-[color-mix(in_oklab,var(--card)_92%,transparent)] shadow-sm", children: [
2105
- hasWorkspace ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "px-2 py-2", children: [
2106
+ showWorkspaceInTopPanel ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "px-2 py-2", children: [
2106
2107
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2107
2108
  WorkspaceSwitcher,
2108
2109
  {
@@ -2118,7 +2119,7 @@ function SidebarInner({
2118
2119
  ),
2119
2120
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mt-2 px-1", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SidebarTools, {}) })
2120
2121
  ] }) : null,
2121
- hasWorkspace ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "h-px bg-[var(--border)] opacity-70" }) : null,
2122
+ showWorkspaceInTopPanel ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "h-px bg-[var(--border)] opacity-70" }) : null,
2122
2123
  showUser ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "px-2 py-2", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-3 rounded-2xl px-2.5 py-2 hover:bg-[color-mix(in_oklab,var(--surface)_70%,transparent)]", children: [
2123
2124
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2124
2125
  import_AvatarSquare.default,
package/dist/Sidebar.mjs CHANGED
@@ -1836,6 +1836,7 @@ function SidebarInner({
1836
1836
  const hasWorkspace = Boolean((workspaces == null ? void 0 : workspaces.length) && onWorkspaceSelect);
1837
1837
  const hasSlot = Boolean(sidebarSlot || sidebarSlotCollapsed);
1838
1838
  const showUser = Boolean(user || userMenuSlot);
1839
+ const showWorkspaceInTopPanel = Boolean(showBrand && hasWorkspace);
1839
1840
  const showTopPanel = Boolean((showBrand ? hasWorkspace : false) || showUser || hasSlot);
1840
1841
  const skGroups = Math.max(1, (_a = skeleton == null ? void 0 : skeleton.groups) != null ? _a : 4);
1841
1842
  const skItems = Math.max(2, (_b = skeleton == null ? void 0 : skeleton.itemsPerGroup) != null ? _b : 6);
@@ -2048,7 +2049,7 @@ function SidebarInner({
2048
2049
  ] })
2049
2050
  ] }) })
2050
2051
  ] }) }) : collapsed ? /* @__PURE__ */ jsx("div", { className: "rounded-3xl border border-[var(--border)] bg-[color-mix(in_oklab,var(--card)_92%,transparent)] p-2 shadow-sm", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center gap-2", children: [
2051
- hasWorkspace ? /* @__PURE__ */ jsxs(Fragment, { children: [
2052
+ showWorkspaceInTopPanel ? /* @__PURE__ */ jsxs(Fragment, { children: [
2052
2053
  /* @__PURE__ */ jsx(
2053
2054
  WorkspaceSwitcher,
2054
2055
  {
@@ -2081,7 +2082,7 @@ function SidebarInner({
2081
2082
  ] }) : null,
2082
2083
  sidebarSlotCollapsed ? /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: sidebarSlotCollapsed }) : null
2083
2084
  ] }) }) : /* @__PURE__ */ jsxs("div", { className: "overflow-hidden rounded-3xl border border-[var(--border)] bg-[color-mix(in_oklab,var(--card)_92%,transparent)] shadow-sm", children: [
2084
- hasWorkspace ? /* @__PURE__ */ jsxs("div", { className: "px-2 py-2", children: [
2085
+ showWorkspaceInTopPanel ? /* @__PURE__ */ jsxs("div", { className: "px-2 py-2", children: [
2085
2086
  /* @__PURE__ */ jsx(
2086
2087
  WorkspaceSwitcher,
2087
2088
  {
@@ -2097,7 +2098,7 @@ function SidebarInner({
2097
2098
  ),
2098
2099
  /* @__PURE__ */ jsx("div", { className: "mt-2 px-1", children: /* @__PURE__ */ jsx(SidebarTools, {}) })
2099
2100
  ] }) : null,
2100
- hasWorkspace ? /* @__PURE__ */ jsx("div", { className: "h-px bg-[var(--border)] opacity-70" }) : null,
2101
+ showWorkspaceInTopPanel ? /* @__PURE__ */ jsx("div", { className: "h-px bg-[var(--border)] opacity-70" }) : null,
2101
2102
  showUser ? /* @__PURE__ */ jsx("div", { className: "px-2 py-2", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 rounded-2xl px-2.5 py-2 hover:bg-[color-mix(in_oklab,var(--surface)_70%,transparent)]", children: [
2102
2103
  /* @__PURE__ */ jsx(
2103
2104
  AvatarSquare,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "framepexls-ui-lib",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "description": "Componentes UI de Framepexls para React/Next.",