@nuraly/lumenui 0.2.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/cdn.js +1 -0
  2. package/dist/nuralyui.bundle.js +2467 -1857
  3. package/dist/nuralyui.bundle.js.gz +0 -0
  4. package/dist/src/components/canvas/bundle.js +569 -204
  5. package/dist/src/components/canvas/bundle.js.gz +0 -0
  6. package/dist/src/components/canvas/canvas.constants.d.ts +1 -1
  7. package/dist/src/components/canvas/canvas.constants.js +1 -1
  8. package/dist/src/components/canvas/workflow-canvas.component.d.ts +7 -0
  9. package/dist/src/components/canvas/workflow-canvas.component.js +46 -2
  10. package/dist/src/components/canvas/workflow-canvas.types.d.ts +8 -1
  11. package/dist/src/components/canvas/workflow-canvas.types.js +157 -0
  12. package/dist/src/components/chat-panel/bundle.js +216 -0
  13. package/dist/src/components/chat-panel/bundle.js.gz +0 -0
  14. package/dist/src/components/chat-panel/chat-panel.component.d.ts +85 -0
  15. package/dist/src/components/chat-panel/chat-panel.component.js +510 -0
  16. package/dist/src/components/chat-panel/chat-panel.style.d.ts +2 -0
  17. package/dist/src/components/chat-panel/chat-panel.style.js +127 -0
  18. package/dist/src/components/chat-panel/chat-panel.types.d.ts +54 -0
  19. package/dist/src/components/chat-panel/chat-panel.types.js +2 -0
  20. package/dist/src/components/chat-panel/index.d.ts +3 -0
  21. package/dist/src/components/chat-panel/index.js +2 -0
  22. package/dist/src/components/chatbot/bundle.js +1 -1
  23. package/dist/src/components/chatbot/bundle.js.gz +0 -0
  24. package/dist/src/components/chatbot/providers/workflow-socket-provider.js +1 -1
  25. package/dist/src/components/presence/bundle.js +69 -42
  26. package/dist/src/components/presence/bundle.js.gz +0 -0
  27. package/dist/src/components/presence/presence-chat.component.d.ts +2 -0
  28. package/dist/src/components/presence/presence-chat.component.js +12 -2
  29. package/dist/src/components/presence/presence.component.d.ts +30 -6
  30. package/dist/src/components/presence/presence.component.js +277 -30
  31. package/dist/src/components/presence/presence.types.d.ts +2 -0
  32. package/dist/src/components/toast/bundle.js +11 -9
  33. package/dist/src/components/toast/bundle.js.gz +0 -0
  34. package/dist/src/components/toast/toast.component.d.ts +8 -0
  35. package/dist/src/components/toast/toast.component.js +17 -7
  36. package/dist/src/components/video/bundle.js +13 -12
  37. package/dist/src/components/video/bundle.js.gz +0 -0
  38. package/dist/src/components/video/video.component.d.ts +15 -1
  39. package/dist/src/components/video/video.component.js +131 -3
  40. package/package.json +5 -10
  41. package/packages/common/dist/VERSIONS.md +1 -1
@@ -0,0 +1,510 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2024 Nuraly, Laabidi Aymen
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
7
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
8
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
9
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
10
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
11
+ };
12
+ import { LitElement, html, nothing } from 'lit';
13
+ import { customElement, property, state } from 'lit/decorators.js';
14
+ import { chatPanelStyles } from './chat-panel.style.js';
15
+ const REACTIONS = [
16
+ { key: '👍', label: 'Like' },
17
+ { key: '❤️', label: 'Love' },
18
+ { key: '😂', label: 'Haha' },
19
+ { key: '😮', label: 'Wow' },
20
+ { key: '😢', label: 'Sad' },
21
+ { key: '🔥', label: 'Fire' },
22
+ ];
23
+ const EMOJI_GRID = ['😀', '😂', '🥰', '😎', '🤔', '🎉', '✨', '🙏', '💯', '🚀', '👏', '😍', '🤣', '💪', '😊', '🥳', '😤', '💀', '🫡', '👀', '💜', '🤝', '⭐', '🙌'];
24
+ const svgCheck = html `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>`;
25
+ const svgCheckDouble = html `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="18 6 7 17 2 12"/><polyline points="23 6 12 17" opacity="0.5"/></svg>`;
26
+ const svgCheckDoubleRead = html `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#7c3aed" stroke-width="2"><polyline points="18 6 7 17 2 12"/><polyline points="23 6 12 17"/></svg>`;
27
+ const svgSend = html `<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>`;
28
+ const svgSmile = html `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><line x1="9" y1="9" x2="9.01" y2="9"/><line x1="15" y1="9" x2="15.01" y2="9"/></svg>`;
29
+ const svgPlay = html `<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>`;
30
+ const svgFile = html `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>`;
31
+ const svgDownload = html `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`;
32
+ const svgPhone = html `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07 19.5 19.5 0 01-6-6 19.79 19.79 0 01-3.07-8.67A2 2 0 014.11 2h3a2 2 0 012 1.72c.127.96.361 1.903.7 2.81a2 2 0 01-.45 2.11L8.09 9.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0122 16.92z"/></svg>`;
33
+ const svgVideo = html `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>`;
34
+ const svgChat = html `<svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>`;
35
+ const svgAttach = html `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48"/></svg>`;
36
+ const svgImage = html `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>`;
37
+ /**
38
+ * Shared chat panel — renders messages, input bar, reactions, context menu.
39
+ * Used inside both the floating presence chat and the full messages page.
40
+ *
41
+ * @fires message-send - User sends a message. Detail: `{ text, replyTo? }`
42
+ * @fires message-edit - User edits a message. Detail: `{ messageId, content }`
43
+ * @fires message-delete - User deletes a message. Detail: `{ messageId }`
44
+ * @fires message-react - User reacts to a message. Detail: `{ messageId, emoji }`
45
+ * @fires typing-start
46
+ * @fires typing-stop
47
+ * @fires file-select - User selected a file. Detail: `{ file: File, imageOnly: boolean }`
48
+ */
49
+ let NrChatPanelElement = class NrChatPanelElement extends LitElement {
50
+ constructor() {
51
+ super(...arguments);
52
+ /** Messages to display */
53
+ this.messages = [];
54
+ /** Current user info */
55
+ this.currentUser = null;
56
+ /** Conversation ID */
57
+ this.conversationId = '';
58
+ /** Whether this is a group conversation */
59
+ this.isGroup = false;
60
+ /** Loading state */
61
+ this.loading = false;
62
+ /** Compact mode for floating panels */
63
+ this.compact = false;
64
+ /** Empty state text */
65
+ this.emptyText = 'No messages yet';
66
+ /** Hide the built-in input bar (for pages that provide their own) */
67
+ this.hideInput = false;
68
+ this._showEmoji = false;
69
+ this._contextMenu = null;
70
+ this._replyTo = null;
71
+ this._editingIdx = null;
72
+ this._typing = [];
73
+ this._typingTimer = null;
74
+ this._longPressTimer = null;
75
+ this._touchMoved = false;
76
+ }
77
+ updated(changed) {
78
+ // Auto-scroll on new messages
79
+ if (changed.has('messages')) {
80
+ requestAnimationFrame(() => {
81
+ var _a;
82
+ const el = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.chat-messages');
83
+ if (el)
84
+ el.scrollTop = el.scrollHeight;
85
+ });
86
+ }
87
+ }
88
+ // ── Public methods (called by parent for socket events) ──
89
+ /** Add a typing indicator */
90
+ addTyping(userName) {
91
+ if (!this._typing.includes(userName)) {
92
+ this._typing = [...this._typing, userName];
93
+ }
94
+ }
95
+ /** Remove a typing indicator */
96
+ removeTyping(userName) {
97
+ this._typing = this._typing.filter(n => n !== userName);
98
+ }
99
+ // ── Send ──
100
+ _sendMessage() {
101
+ var _a;
102
+ const input = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.chat-input textarea');
103
+ if (!(input === null || input === void 0 ? void 0 : input.value.trim()))
104
+ return;
105
+ const text = input.value.trim();
106
+ input.value = '';
107
+ input.focus();
108
+ this.dispatchEvent(new CustomEvent('message-send', {
109
+ detail: { text, replyTo: this._replyTo || undefined },
110
+ bubbles: true, composed: true,
111
+ }));
112
+ this._replyTo = null;
113
+ this._showEmoji = false;
114
+ this._playSound();
115
+ }
116
+ _onInputKeydown(e) {
117
+ if (e.key === 'Enter' && !e.shiftKey) {
118
+ e.preventDefault();
119
+ this._sendMessage();
120
+ return;
121
+ }
122
+ this.dispatchEvent(new CustomEvent('typing-start', { bubbles: true, composed: true }));
123
+ clearTimeout(this._typingTimer);
124
+ this._typingTimer = setTimeout(() => {
125
+ this.dispatchEvent(new CustomEvent('typing-stop', { bubbles: true, composed: true }));
126
+ }, 2000);
127
+ }
128
+ // ── Emoji ──
129
+ _toggleEmoji() { this._showEmoji = !this._showEmoji; }
130
+ _insertEmoji(emoji) {
131
+ var _a;
132
+ const input = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.chat-input textarea');
133
+ if (input) {
134
+ input.value += emoji;
135
+ input.focus();
136
+ }
137
+ this._showEmoji = false;
138
+ }
139
+ // ── Context menu ──
140
+ _menuPos(target) {
141
+ const bubble = (target.closest('.msg-bubble') || target.closest('.msg-row'));
142
+ if (!bubble)
143
+ return { y: 0, left: 0 };
144
+ const rect = bubble.getBoundingClientRect();
145
+ const isMe = !!target.closest('.msg-row.me');
146
+ const menuH = isMe ? 140 : 80;
147
+ const y = rect.top > menuH + 12 ? rect.top - menuH - 4 : rect.bottom + 4;
148
+ if (isMe)
149
+ return { y, right: Math.max(8, window.innerWidth - rect.right) };
150
+ return { y, left: Math.max(8, rect.left) };
151
+ }
152
+ _showContextMenuHandler(e, msgIdx) {
153
+ e.preventDefault();
154
+ this._contextMenu = Object.assign({ msgIdx }, this._menuPos(e.target));
155
+ }
156
+ _onTouchStart(e, msgIdx) {
157
+ this._touchMoved = false;
158
+ this._longPressTimer = setTimeout(() => {
159
+ var _a;
160
+ e.preventDefault();
161
+ (_a = window.getSelection()) === null || _a === void 0 ? void 0 : _a.removeAllRanges();
162
+ this._contextMenu = Object.assign({ msgIdx }, this._menuPos(e.target));
163
+ this._touchMoved = true;
164
+ }, 500);
165
+ }
166
+ _onTouchEnd() { clearTimeout(this._longPressTimer); }
167
+ _onTouchMove() { this._touchMoved = true; clearTimeout(this._longPressTimer); }
168
+ _onMsgTap(e, msgIdx) {
169
+ if (!window.matchMedia('(max-width: 640px)').matches)
170
+ return;
171
+ if (this._touchMoved)
172
+ return;
173
+ const target = e.target;
174
+ if (target.closest('.reaction') || target.closest('.audio-play-btn') || target.closest('.file-card') || target.closest('.edit-inline'))
175
+ return;
176
+ e.stopPropagation();
177
+ this._contextMenu = Object.assign({ msgIdx }, this._menuPos(target));
178
+ }
179
+ _hideContextMenu() { this._contextMenu = null; }
180
+ // ── Message actions ──
181
+ _deleteMsg(idx) {
182
+ const msg = this.messages[idx];
183
+ if (msg === null || msg === void 0 ? void 0 : msg.id) {
184
+ this.dispatchEvent(new CustomEvent('message-delete', {
185
+ detail: { messageId: msg.id }, bubbles: true, composed: true,
186
+ }));
187
+ }
188
+ this._contextMenu = null;
189
+ }
190
+ _replyMsg(idx) {
191
+ var _a, _b;
192
+ const msg = this.messages[idx];
193
+ if (msg) {
194
+ this._replyTo = { text: msg.text, from: msg.from };
195
+ (_b = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.chat-input textarea')) === null || _b === void 0 ? void 0 : _b.focus();
196
+ }
197
+ this._contextMenu = null;
198
+ }
199
+ _editMsg(idx) {
200
+ this._editingIdx = idx;
201
+ this._contextMenu = null;
202
+ requestAnimationFrame(() => {
203
+ var _a;
204
+ const input = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.edit-input');
205
+ if (input) {
206
+ input.focus();
207
+ input.select();
208
+ }
209
+ });
210
+ }
211
+ _confirmEdit(idx) {
212
+ var _a;
213
+ const input = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.edit-input');
214
+ const content = input === null || input === void 0 ? void 0 : input.value.trim();
215
+ if (!content) {
216
+ this._editingIdx = null;
217
+ return;
218
+ }
219
+ const msg = this.messages[idx];
220
+ if (msg === null || msg === void 0 ? void 0 : msg.id) {
221
+ this.dispatchEvent(new CustomEvent('message-edit', {
222
+ detail: { messageId: msg.id, content }, bubbles: true, composed: true,
223
+ }));
224
+ }
225
+ this._editingIdx = null;
226
+ }
227
+ _onEditKeydown(e, idx) {
228
+ if (e.key === 'Enter' && !e.shiftKey) {
229
+ e.preventDefault();
230
+ this._confirmEdit(idx);
231
+ }
232
+ else if (e.key === 'Escape') {
233
+ this._editingIdx = null;
234
+ }
235
+ }
236
+ _reactMsg(idx, emoji) {
237
+ const msg = this.messages[idx];
238
+ if (msg === null || msg === void 0 ? void 0 : msg.id) {
239
+ this.dispatchEvent(new CustomEvent('message-react', {
240
+ detail: { messageId: msg.id, emoji }, bubbles: true, composed: true,
241
+ }));
242
+ }
243
+ this._contextMenu = null;
244
+ }
245
+ // ── Audio player ──
246
+ _toggleAudioPlay(e) {
247
+ var _a;
248
+ const btn = e.currentTarget;
249
+ const container = btn.closest('.audio-msg');
250
+ const audio = container === null || container === void 0 ? void 0 : container.querySelector('audio');
251
+ if (!audio)
252
+ return;
253
+ if (audio.paused) {
254
+ (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelectorAll('.audio-msg audio').forEach((a) => {
255
+ if (a !== audio && !a.paused) {
256
+ a.pause();
257
+ a.currentTime = 0;
258
+ }
259
+ });
260
+ audio.play().catch(() => { });
261
+ }
262
+ else {
263
+ audio.pause();
264
+ }
265
+ }
266
+ _onAudioTimeUpdate(e) {
267
+ const audio = e.target;
268
+ const container = audio.closest('.audio-msg');
269
+ if (!container || !audio.duration)
270
+ return;
271
+ const bars = container.querySelectorAll('.a-bar');
272
+ const progress = audio.currentTime / audio.duration;
273
+ const filledCount = Math.floor(progress * bars.length);
274
+ bars.forEach((bar, idx) => bar.classList.toggle('played', idx <= filledCount));
275
+ }
276
+ _onAudioEnded(e) {
277
+ const audio = e.target;
278
+ const container = audio.closest('.audio-msg');
279
+ container === null || container === void 0 ? void 0 : container.querySelectorAll('.a-bar').forEach(bar => bar.classList.remove('played'));
280
+ }
281
+ // ── File picker ──
282
+ _pickFile(imageOnly = false) {
283
+ const input = document.createElement('input');
284
+ input.type = 'file';
285
+ if (imageOnly)
286
+ input.accept = 'image/jpeg,image/png,image/gif,image/webp,image/heic,image/heif';
287
+ input.style.cssText = 'position:fixed;top:-9999px;left:-9999px;opacity:0';
288
+ document.body.appendChild(input);
289
+ input.onchange = () => {
290
+ var _a;
291
+ const file = (_a = input.files) === null || _a === void 0 ? void 0 : _a[0];
292
+ if (file) {
293
+ this.dispatchEvent(new CustomEvent('file-select', {
294
+ detail: { file, imageOnly }, bubbles: true, composed: true,
295
+ }));
296
+ }
297
+ try {
298
+ document.body.removeChild(input);
299
+ }
300
+ catch (_b) { }
301
+ };
302
+ input.click();
303
+ }
304
+ // ── Sound ──
305
+ _playSound() {
306
+ try {
307
+ const ctx = new AudioContext();
308
+ const gain = ctx.createGain();
309
+ gain.connect(ctx.destination);
310
+ gain.gain.value = 0.12;
311
+ const o = ctx.createOscillator();
312
+ o.connect(gain);
313
+ o.type = 'sine';
314
+ o.frequency.setValueAtTime(600, ctx.currentTime);
315
+ o.frequency.setValueAtTime(900, ctx.currentTime + 0.08);
316
+ gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.15);
317
+ o.start(ctx.currentTime);
318
+ o.stop(ctx.currentTime + 0.15);
319
+ setTimeout(() => ctx.close(), 500);
320
+ }
321
+ catch (_a) { }
322
+ }
323
+ // ── Render ──
324
+ render() {
325
+ if (this.loading)
326
+ return html `<div class="loading">Loading...</div>`;
327
+ return html `
328
+ ${this.messages.length === 0
329
+ ? html `<div class="empty-state">${svgChat}<span>${this.emptyText}</span></div>`
330
+ : html `
331
+ <div class="chat-messages" @click=${() => this._hideContextMenu()} @touchstart=${() => { if (this._contextMenu)
332
+ this._hideContextMenu(); }}>
333
+ ${this.messages.map((m, i, arr) => this._renderMessage(m, i, arr))}
334
+ </div>
335
+ `}
336
+
337
+ ${this._contextMenu ? this._renderContextMenu() : nothing}
338
+ ${this._typing.length > 0 ? html `<div class="typing-indicator">${this._typing.join(', ')} typing...</div>` : nothing}
339
+ ${this._replyTo ? html `
340
+ <div class="reply-bar">
341
+ <span class="reply-text">Replying to: ${this._replyTo.text}</span>
342
+ <button @click=${() => { this._replyTo = null; }}>✕</button>
343
+ </div>
344
+ ` : nothing}
345
+ ${this._showEmoji ? this._renderEmojiPicker() : nothing}
346
+ ${this.hideInput ? nothing : this._renderInputBar()}
347
+ `;
348
+ }
349
+ _renderMessage(m, i, arr) {
350
+ var _a;
351
+ if (m.from === 'date')
352
+ return html `<div class="date-sep">${m.text}</div>`;
353
+ if (m.deleted)
354
+ return html `<div class="msg-row system"><div class="msg-bubble"><div class="msg them msg-deleted">This message was deleted</div></div></div>`;
355
+ const prevMsg = i > 0 ? arr[i - 1] : null;
356
+ const showSenderName = this.isGroup && m.from === 'them' && (!prevMsg || prevMsg.from !== 'them' || prevMsg.senderId !== m.senderId);
357
+ return html `
358
+ <div class="msg-row ${m.from}"
359
+ @contextmenu=${(e) => this._showContextMenuHandler(e, i)}
360
+ @touchstart=${(e) => this._onTouchStart(e, i)}
361
+ @touchend=${() => this._onTouchEnd()}
362
+ @touchmove=${() => this._onTouchMove()}
363
+ @click=${(e) => this._onMsgTap(e, i)}>
364
+ ${m.from === 'them' ? html `<div class="msg-avatar" style="background:${m.senderColor || '#7c3aed'}">${(m.senderInitials || '?').charAt(0)}</div>` : nothing}
365
+ <div class="msg-bubble" @dblclick=${m.from === 'me' && !m.deleted ? () => this._editMsg(i) : undefined}>
366
+ ${showSenderName ? html `<div class="msg-sender-name" style="color:${m.senderColor || '#7c3aed'}">${m.senderName}</div>` : nothing}
367
+ ${m.replyTo ? html `<div class="msg-reply-quote">${m.replyTo.text}</div>` : nothing}
368
+ ${this._renderAttachment(m)}
369
+ ${this._editingIdx === i
370
+ ? html `<div class="edit-inline"><input class="edit-input" type="text" .value=${m.text || ''} @keydown=${(e) => this._onEditKeydown(e, i)}><button class="edit-confirm" @click=${() => this._confirmEdit(i)}>✓</button><button class="edit-cancel" @click=${() => { this._editingIdx = null; }}>✕</button></div>`
371
+ : m.text ? html `<div class="msg ${m.from}">${m.text}${m.edited ? html ` <span class="msg-edited">(edited)</span>` : nothing}${m.encrypted ? html `<span class="msg-e2e" title="End-to-end encrypted">🔒</span>` : nothing}</div>` : nothing}
372
+ <div class="msg-footer ${m.from}">
373
+ <span class="msg-time">${m.time || ''}</span>
374
+ ${m.from === 'me' ? html `<span class="msg-read">${m.status === 'read' ? svgCheckDoubleRead : m.status === 'delivered' ? svgCheckDouble : svgCheck}</span>` : nothing}
375
+ </div>
376
+ ${((_a = m.reactions) === null || _a === void 0 ? void 0 : _a.length) ? html `
377
+ <div class="msg-reactions">
378
+ ${m.reactions.map(r => {
379
+ var _a;
380
+ return html `
381
+ <span class="reaction ${((_a = r.users) === null || _a === void 0 ? void 0 : _a.includes('me')) ? 'mine' : ''}" @click=${() => this._reactMsg(i, r.emoji)}>${r.emoji} <span class="count">${r.count}</span></span>
382
+ `;
383
+ })}
384
+ </div>
385
+ ` : nothing}
386
+ </div>
387
+ </div>
388
+ `;
389
+ }
390
+ _renderAttachment(m) {
391
+ var _a;
392
+ const a = m.attachment;
393
+ if (!a)
394
+ return nothing;
395
+ if (a.type === 'call') {
396
+ const icon = a.callType === 'video' ? svgVideo : svgPhone;
397
+ const color = (a.callStatus === 'missed' || a.callStatus === 'declined') ? '#ef4444' : '#22c55e';
398
+ return html `<div class="msg-call-log">
399
+ <span style="color:${color}">${icon}</span>
400
+ <div class="msg-call-info">
401
+ <div class="msg-call-type">${a.callType === 'video' ? 'Video call' : 'Voice call'}</div>
402
+ <div class="msg-call-status ${a.callStatus}">${a.callStatus === 'completed' ? (a.duration || 'Ended') : a.callStatus === 'declined' ? 'Declined' : 'Missed'}</div>
403
+ </div>
404
+ </div>`;
405
+ }
406
+ if (a.type === 'image') {
407
+ return html `<div class="msg-attachment"><img src="${a.decryptedUrl || a.url}" alt="${a.name || 'Image'}"></div>`;
408
+ }
409
+ if (a.type === 'audio') {
410
+ const waveform = ((_a = a.waveform) === null || _a === void 0 ? void 0 : _a.length) ? a.waveform : Array(36).fill(0.15);
411
+ return html `<div class="msg-attachment"><div class="audio-msg">
412
+ <button class="audio-play-btn" @click=${(e) => this._toggleAudioPlay(e)}>${svgPlay}</button>
413
+ <div class="audio-bars">${waveform.map((h) => html `<div class="a-bar" style="height:${Math.round(h * 22 + 3)}px"></div>`)}</div>
414
+ <span class="audio-dur">${a.duration || '0:00'}</span>
415
+ <audio src="${a.decryptedUrl || a.url}" preload="metadata" @timeupdate=${(e) => this._onAudioTimeUpdate(e)} @ended=${(e) => this._onAudioEnded(e)}></audio>
416
+ </div></div>`;
417
+ }
418
+ // Generic file
419
+ return html `<div class="msg-attachment"><a class="file-card" href="${a.decryptedUrl || a.url}" target="_blank" download="${a.name || 'file'}">
420
+ ${svgFile}
421
+ <div style="flex:1;min-width:0"><div style="font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${a.name || 'File'}</div>${a.fileSize ? html `<div style="font-size:10px;color:var(--text-secondary);margin-top:1px">${(a.fileSize / 1024).toFixed(0)} KB</div>` : nothing}</div>
422
+ ${svgDownload}
423
+ </a></div>`;
424
+ }
425
+ _renderContextMenu() {
426
+ const cm = this._contextMenu;
427
+ const msg = this.messages[cm.msgIdx];
428
+ const isMe = (msg === null || msg === void 0 ? void 0 : msg.from) === 'me';
429
+ return html `
430
+ <div class="ctx-menu" @mousedown=${(e) => e.preventDefault()} @touchstart=${(e) => e.stopPropagation()} style="top:${cm.y}px;${cm.right != null ? `right:${cm.right}px` : `left:${cm.left}px`}">
431
+ <div class="ctx-reactions">
432
+ ${REACTIONS.map(r => html `<button @click=${() => this._reactMsg(cm.msgIdx, r.key)} title=${r.label}>${r.key}</button>`)}
433
+ </div>
434
+ <button @click=${() => this._replyMsg(cm.msgIdx)}>↩ Reply</button>
435
+ ${isMe && !(msg === null || msg === void 0 ? void 0 : msg.deleted) ? html `<button @click=${() => this._editMsg(cm.msgIdx)}>✏️ Edit</button>` : nothing}
436
+ ${isMe && !(msg === null || msg === void 0 ? void 0 : msg.deleted) ? html `<button class="danger" @click=${() => this._deleteMsg(cm.msgIdx)}>🗑 Delete</button>` : nothing}
437
+ </div>
438
+ `;
439
+ }
440
+ _renderEmojiPicker() {
441
+ return html `
442
+ <div class="emoji-picker" @mousedown=${(e) => e.preventDefault()}>
443
+ <div class="emoji-quick">
444
+ ${REACTIONS.map(r => html `<button @click=${() => this._insertEmoji(r.key)} title=${r.label}>${r.key}</button>`)}
445
+ </div>
446
+ <div class="emoji-grid">
447
+ ${EMOJI_GRID.map(e => html `<button @click=${() => this._insertEmoji(e)}>${e}</button>`)}
448
+ </div>
449
+ </div>
450
+ `;
451
+ }
452
+ _renderInputBar() {
453
+ return html `
454
+ <div class="chat-input">
455
+ <div class="input-tools">
456
+ <button @click=${() => this._pickFile()} title="Attach file">${svgAttach}</button>
457
+ <button @click=${() => this._pickFile(true)} title="Send image">${svgImage}</button>
458
+ <button @click=${() => this._toggleEmoji()} title="Emoji">${svgSmile}</button>
459
+ </div>
460
+ <textarea rows="1" placeholder="Type a message..." @keydown=${(e) => this._onInputKeydown(e)}></textarea>
461
+ <button class="send-btn" @mousedown=${(e) => e.preventDefault()} @click=${() => this._sendMessage()} title="Send">${svgSend}</button>
462
+ </div>
463
+ `;
464
+ }
465
+ };
466
+ NrChatPanelElement.styles = chatPanelStyles;
467
+ __decorate([
468
+ property({ type: Array })
469
+ ], NrChatPanelElement.prototype, "messages", void 0);
470
+ __decorate([
471
+ property({ type: Object })
472
+ ], NrChatPanelElement.prototype, "currentUser", void 0);
473
+ __decorate([
474
+ property({ type: String, attribute: 'conversation-id' })
475
+ ], NrChatPanelElement.prototype, "conversationId", void 0);
476
+ __decorate([
477
+ property({ type: Boolean })
478
+ ], NrChatPanelElement.prototype, "isGroup", void 0);
479
+ __decorate([
480
+ property({ type: Boolean })
481
+ ], NrChatPanelElement.prototype, "loading", void 0);
482
+ __decorate([
483
+ property({ type: Boolean, reflect: true })
484
+ ], NrChatPanelElement.prototype, "compact", void 0);
485
+ __decorate([
486
+ property({ type: String, attribute: 'empty-text' })
487
+ ], NrChatPanelElement.prototype, "emptyText", void 0);
488
+ __decorate([
489
+ property({ type: Boolean, attribute: 'hide-input' })
490
+ ], NrChatPanelElement.prototype, "hideInput", void 0);
491
+ __decorate([
492
+ state()
493
+ ], NrChatPanelElement.prototype, "_showEmoji", void 0);
494
+ __decorate([
495
+ state()
496
+ ], NrChatPanelElement.prototype, "_contextMenu", void 0);
497
+ __decorate([
498
+ state()
499
+ ], NrChatPanelElement.prototype, "_replyTo", void 0);
500
+ __decorate([
501
+ state()
502
+ ], NrChatPanelElement.prototype, "_editingIdx", void 0);
503
+ __decorate([
504
+ state()
505
+ ], NrChatPanelElement.prototype, "_typing", void 0);
506
+ NrChatPanelElement = __decorate([
507
+ customElement('nr-chat-panel')
508
+ ], NrChatPanelElement);
509
+ export { NrChatPanelElement };
510
+ //# sourceMappingURL=chat-panel.component.js.map
@@ -0,0 +1,2 @@
1
+ export declare const chatPanelStyles: import("lit").CSSResult;
2
+ //# sourceMappingURL=chat-panel.style.d.ts.map
@@ -0,0 +1,127 @@
1
+ import { css } from 'lit';
2
+ export const chatPanelStyles = css `
3
+ :host { display: flex; flex-direction: column; flex: 1; min-height: 0; overflow: hidden; }
4
+
5
+ /* Messages */
6
+ .chat-messages { flex: 1; overflow-y: auto; padding: 12px; display: flex; flex-direction: column; gap: 2px; }
7
+ .chat-messages::before { content: ''; flex: 1; }
8
+ .date-sep { align-self: center; font-size: 11px; color: var(--text-secondary, #536471); background: var(--input-bg, #f2f2f7); padding: 3px 10px; border-radius: 9999px; margin: 8px 0; }
9
+ .msg-row { display: flex; gap: 6px; align-items: flex-end; -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
10
+ .msg-row .msg { -webkit-user-select: text; user-select: text; }
11
+ .msg-row.me { flex-direction: row-reverse; }
12
+ .msg-avatar { width: 22px; height: 22px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 9px; font-weight: 600; color: #fff; flex-shrink: 0; margin-bottom: 16px; }
13
+ .msg-sender-name { font-size: 10px; font-weight: 600; padding: 0 4px; margin-bottom: 1px; }
14
+ .msg-bubble { max-width: 75%; }
15
+ .msg { padding: 7px 12px; border-radius: 16px; font-size: 13px; line-height: 1.4; word-wrap: break-word; }
16
+ .msg.me { background: var(--accent, #7c3aed); color: #fff; border-bottom-right-radius: 4px; }
17
+ .msg.them { background: var(--input-bg, #f2f2f7); color: var(--text, #1d1d1f); border-bottom-left-radius: 4px; }
18
+ .msg-footer { display: flex; align-items: center; gap: 4px; margin-top: 1px; padding: 0 4px; }
19
+ .msg-footer.me { justify-content: flex-end; }
20
+ .msg-time { font-size: 10px; color: var(--text-secondary, #536471); }
21
+ .msg-read { display: flex; align-items: center; }
22
+ .msg-read svg { width: 14px; height: 14px; }
23
+
24
+ /* Attachments */
25
+ .msg-attachment { margin-top: 4px; }
26
+ .msg-attachment img { max-width: 200px; border-radius: 8px; cursor: pointer; display: block; }
27
+ .msg-attachment .file-card { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: var(--input-bg, #f2f2f7); border-radius: 10px; font-size: 12px; text-decoration: none; color: var(--text); cursor: pointer; min-width: 160px; }
28
+ .msg-attachment .file-card:hover { background: var(--border, #e5e7eb); }
29
+
30
+ /* Audio player */
31
+ .audio-msg { display: flex; align-items: center; gap: 6px; padding: 6px 10px; background: var(--input-bg, #f2f2f7); border-radius: 16px; min-width: 180px; }
32
+ .audio-play-btn { width: 28px; height: 28px; border-radius: 50%; border: none; background: var(--accent, #7c3aed); color: #fff; cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
33
+ .audio-bars { flex: 1; display: flex; align-items: center; gap: 1.5px; height: 24px; }
34
+ .a-bar { width: 2.5px; min-height: 3px; border-radius: 1.5px; background: rgba(0,0,0,0.2); transition: background 0.1s; }
35
+ .a-bar.played { background: var(--accent, #7c3aed); }
36
+ .audio-dur { font-size: 10px; color: var(--text-secondary); font-variant-numeric: tabular-nums; flex-shrink: 0; min-width: 28px; text-align: right; }
37
+ .audio-msg audio { display: none; }
38
+ .me .audio-msg { background: rgba(255,255,255,0.15); }
39
+ .me .audio-play-btn { background: #fff; color: var(--accent, #7c3aed); }
40
+ .me .a-bar { background: rgba(255,255,255,0.3); }
41
+ .me .a-bar.played { background: #fff; }
42
+ .me .audio-dur { color: rgba(255,255,255,0.7); }
43
+
44
+ /* Call logs */
45
+ .msg-call-log { display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: var(--input-bg, #f2f2f7); border-radius: 10px; min-width: 160px; }
46
+ .msg-call-info { flex: 1; }
47
+ .msg-call-type { font-size: 12px; font-weight: 600; }
48
+ .msg-call-status { font-size: 10px; color: var(--text-secondary); }
49
+ .msg-call-status.missed, .msg-call-status.declined { color: #ef4444; }
50
+ .msg-call-status.completed { color: #22c55e; }
51
+
52
+ /* Reactions */
53
+ .msg-reactions { display: flex; gap: 3px; margin-top: 2px; padding: 0 4px; flex-wrap: wrap; }
54
+ .msg-reactions .reaction { display: inline-flex; align-items: center; gap: 2px; padding: 1px 6px; border-radius: 10px; background: var(--input-bg, #f2f2f7); font-size: 11px; cursor: pointer; border: 1px solid transparent; transition: transform 0.12s; }
55
+ .msg-reactions .reaction:hover { border-color: var(--accent, #7c3aed); transform: scale(1.05); }
56
+ .msg-reactions .reaction.mine { background: color-mix(in srgb, var(--accent, #7c3aed) 15%, transparent); border-color: var(--accent, #7c3aed); }
57
+ .msg-reactions .reaction svg { width: 12px; height: 12px; }
58
+ .msg-reactions .reaction .count { font-size: 10px; color: var(--text-secondary); }
59
+
60
+ /* Reply quote */
61
+ .msg-reply-quote { font-size: 10px; color: var(--text-secondary); padding: 3px 6px; margin-bottom: 2px; border-left: 2px solid var(--accent, #7c3aed); border-radius: 2px; background: rgba(124,58,237,0.05); }
62
+ .reply-bar { display: flex; align-items: center; gap: 6px; padding: 4px 12px; background: var(--input-bg, #f2f2f7); border-left: 3px solid var(--accent, #7c3aed); font-size: 11px; color: var(--text-secondary); }
63
+ .reply-bar .reply-text { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
64
+ .reply-bar button { border: none; background: none; cursor: pointer; color: var(--text-secondary); font-size: 14px; padding: 2px; }
65
+
66
+ /* Edited/deleted/encrypted */
67
+ .msg-edited { font-size: 9px; color: var(--text-secondary); font-style: italic; }
68
+ .msg-deleted { font-style: italic; color: var(--text-secondary); }
69
+ .msg-e2e { font-size: 9px; margin-left: 3px; opacity: 0.6; }
70
+
71
+ /* Inline edit */
72
+ .edit-inline { display: flex; align-items: center; gap: 3px; }
73
+ .edit-input { flex: 1; border: 1px solid var(--accent, #7c3aed); border-radius: 6px; padding: 3px 6px; font-size: 12px; outline: none; background: var(--bg, #fff); color: var(--text); min-width: 80px; }
74
+ .edit-confirm, .edit-cancel { border: none; background: none; cursor: pointer; font-size: 13px; padding: 2px 3px; border-radius: 3px; }
75
+ .edit-confirm { color: #16a34a; }
76
+ .edit-cancel { color: #dc2626; }
77
+
78
+ /* Context menu */
79
+ .ctx-menu { position: fixed; background: var(--bg, #fff); border: 1px solid var(--border, #e5e7eb); border-radius: 12px; padding: 3px 0; box-shadow: 0 6px 24px rgba(0,0,0,0.12); z-index: 20; min-width: 120px; backdrop-filter: blur(12px); }
80
+ .ctx-menu button { display: flex; align-items: center; gap: 6px; width: 100%; padding: 6px 12px; border: none; background: none; font-size: 12px; color: var(--text); cursor: pointer; text-align: left; }
81
+ .ctx-menu button:hover { background: var(--input-bg); }
82
+ .ctx-menu button.danger { color: #dc2626; }
83
+ .ctx-reactions { display: flex; gap: 3px; padding: 6px 8px; border-bottom: 1px solid var(--border); }
84
+ .ctx-reactions button { width: 30px; height: 30px; padding: 0; min-width: auto; border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: transform 0.12s, background 0.12s; }
85
+ .ctx-reactions button:hover { background: var(--input-bg); transform: scale(1.2); }
86
+ .ctx-reactions button svg { width: 16px; height: 16px; }
87
+
88
+ /* Emoji picker */
89
+ .emoji-picker { position: absolute; bottom: 50px; left: 8px; background: var(--bg, #fff); border: 1px solid var(--border); border-radius: 12px; padding: 5px; box-shadow: 0 6px 24px rgba(0,0,0,0.12); z-index: 10; max-height: 200px; overflow-y: auto; }
90
+ .emoji-picker .emoji-quick { display: flex; gap: 3px; padding: 3px 2px 5px; border-bottom: 1px solid var(--border); margin-bottom: 3px; }
91
+ .emoji-picker .emoji-quick button { width: 30px; height: 30px; border: none; background: none; cursor: pointer; border-radius: 50%; display: flex; align-items: center; justify-content: center; padding: 0; }
92
+ .emoji-picker .emoji-quick button:hover { background: var(--input-bg); transform: scale(1.2); }
93
+ .emoji-picker .emoji-grid { display: grid; grid-template-columns: repeat(8, 1fr); gap: 1px; }
94
+ .emoji-picker .emoji-grid button { width: 28px; height: 28px; border: none; background: none; font-size: 16px; cursor: pointer; border-radius: 5px; display: flex; align-items: center; justify-content: center; }
95
+ .emoji-picker .emoji-grid button:hover { background: var(--input-bg); transform: scale(1.1); }
96
+
97
+ /* Typing indicator */
98
+ .typing-indicator { padding: 3px 12px; font-size: 11px; color: var(--text-secondary); font-style: italic; }
99
+
100
+ /* Input bar */
101
+ .chat-input { display: flex; gap: 6px; padding: 8px 10px; border-top: 1px solid var(--border, #e5e7eb); align-items: center; flex-shrink: 0; position: relative; }
102
+ .chat-input textarea { flex: 1; padding: 8px 12px; border: 1px solid var(--border, #e5e7eb); border-radius: 18px; font-size: 13px; outline: none; background: var(--input-bg, #f2f2f7); resize: none; font-family: inherit; line-height: 1.4; max-height: 80px; overflow-y: auto; }
103
+ .chat-input textarea:focus { border-color: var(--accent, #7c3aed); background: var(--bg, #fff); }
104
+ .input-tools { display: flex; gap: 1px; }
105
+ .input-tools button { width: 30px; height: 30px; border: none; background: none; color: var(--text-secondary); cursor: pointer; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
106
+ .input-tools button:hover { background: var(--input-bg); color: var(--accent); }
107
+ .send-btn { width: 32px; height: 32px; border: none; background: var(--accent, #7c3aed); color: #fff; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
108
+ .send-btn:hover { background: var(--accent-hover, #6d28d9); }
109
+
110
+ /* Empty state */
111
+ .empty-state { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; color: var(--text-secondary); gap: 6px; padding: 24px; }
112
+ .empty-state svg { width: 36px; height: 36px; opacity: 0.4; }
113
+ .empty-state span { font-size: 12px; }
114
+
115
+ /* Loading */
116
+ .loading { flex: 1; display: flex; align-items: center; justify-content: center; color: var(--text-secondary); font-size: 12px; }
117
+
118
+ /* Full-size mode overrides */
119
+ :host(:not([compact])) .msg { font-size: 14px; padding: 8px 14px; border-radius: 18px; }
120
+ :host(:not([compact])) .msg-bubble { max-width: 65%; }
121
+ :host(:not([compact])) .msg-avatar { width: 24px; height: 24px; font-size: 10px; }
122
+ :host(:not([compact])) .chat-messages { padding: 16px; }
123
+ :host(:not([compact])) .chat-input textarea { font-size: 14px; padding: 10px 16px; }
124
+ :host(:not([compact])) .chat-input { padding: 10px 16px; }
125
+ :host(:not([compact])) .send-btn { width: 38px; height: 38px; }
126
+ `;
127
+ //# sourceMappingURL=chat-panel.style.js.map