@bbki.ng/bb-msg-history 0.14.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/component.d.ts +6 -18
- package/dist/component.js +43 -275
- package/dist/components/bb-custom-avatar.d.ts +20 -0
- package/dist/components/bb-custom-avatar.js +145 -0
- package/dist/components/bb-letter-avatar.d.ts +14 -0
- package/dist/components/bb-letter-avatar.js +61 -0
- package/dist/components/bb-loading-overlay.d.ts +14 -0
- package/dist/components/bb-loading-overlay.js +89 -0
- package/dist/components/bb-message-bubble.d.ts +19 -0
- package/dist/components/bb-message-bubble.js +116 -0
- package/dist/components/bb-message.d.ts +27 -0
- package/dist/components/bb-message.js +174 -0
- package/dist/components/bb-msg-history.d.ts +111 -0
- package/dist/components/bb-msg-history.js +473 -0
- package/dist/components/bb-scroll-button.d.ts +16 -0
- package/dist/components/bb-scroll-button.js +161 -0
- package/dist/components/bb-timestamp.d.ts +15 -0
- package/dist/components/bb-timestamp.js +59 -0
- package/dist/components/index.d.ts +7 -0
- package/dist/components/index.js +7 -0
- package/dist/const/authors.js +1 -1
- package/dist/const/styles.js +0 -33
- package/dist/contexts/author-context.d.ts +8 -0
- package/dist/contexts/author-context.js +6 -0
- package/dist/controllers/scroll-controller.d.ts +52 -0
- package/dist/controllers/scroll-controller.js +138 -0
- package/dist/core/message-processor.d.ts +56 -0
- package/dist/core/message-processor.js +85 -0
- package/dist/core/renderer.d.ts +87 -0
- package/dist/core/renderer.js +196 -0
- package/dist/core/scroll-manager.d.ts +54 -0
- package/dist/core/scroll-manager.js +119 -0
- package/dist/parsers/base.d.ts +21 -0
- package/dist/parsers/base.js +1 -0
- package/dist/parsers/default-parser.d.ts +10 -0
- package/dist/parsers/default-parser.js +40 -0
- package/dist/parsers/index.d.ts +2 -0
- package/dist/parsers/index.js +1 -0
- package/dist/utils/event-tracker.d.ts +23 -0
- package/dist/utils/event-tracker.js +33 -0
- package/dist/utils/message-builder.d.ts +0 -4
- package/dist/utils/message-builder.js +0 -15
- package/dist/utils/tooltip.d.ts +11 -2
- package/dist/utils/tooltip.js +56 -13
- package/package.json +1 -1
- package/src/component.ts +56 -338
- package/src/const/authors.ts +3 -2
- package/src/const/styles.ts +0 -33
- package/src/core/message-processor.ts +120 -0
- package/src/core/renderer.ts +276 -0
- package/src/core/scroll-manager.ts +148 -0
- package/src/utils/event-tracker.ts +38 -0
- package/src/utils/message-builder.ts +0 -15
- package/src/utils/tooltip.ts +0 -16
package/dist/component.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { AuthorOptions, Message } from './types/index.js';
|
|
2
2
|
export declare class BBMsgHistory extends HTMLElement {
|
|
3
3
|
private _mutationObserver?;
|
|
4
|
+
private _debounceTimer?;
|
|
5
|
+
private _eventTracker;
|
|
6
|
+
private _messageProcessor;
|
|
7
|
+
private _scrollManager;
|
|
8
|
+
private _renderer;
|
|
4
9
|
private _userAuthors;
|
|
5
10
|
private _lastAuthor;
|
|
6
11
|
private _lastGroupTimestamp;
|
|
7
|
-
private _scrollButtonVisible;
|
|
8
|
-
private _scrollListeners;
|
|
9
|
-
private _debounceTimer?;
|
|
10
12
|
static get observedAttributes(): string[];
|
|
11
13
|
constructor();
|
|
12
14
|
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
|
|
@@ -48,26 +50,12 @@ export declare class BBMsgHistory extends HTMLElement {
|
|
|
48
50
|
*/
|
|
49
51
|
scrollToBottom(): this;
|
|
50
52
|
/**
|
|
51
|
-
*
|
|
53
|
+
* Internal: Append a single message with incremental DOM update
|
|
52
54
|
*/
|
|
53
|
-
private _canGroupMessages;
|
|
54
55
|
private _appendSingleMessage;
|
|
55
56
|
connectedCallback(): void;
|
|
56
57
|
disconnectedCallback(): void;
|
|
57
|
-
/**
|
|
58
|
-
* Track an event listener for cleanup on disconnect
|
|
59
|
-
*/
|
|
60
|
-
private _addTrackedListener;
|
|
61
|
-
/**
|
|
62
|
-
* Remove all tracked event listeners
|
|
63
|
-
*/
|
|
64
|
-
private _cleanupListeners;
|
|
65
58
|
private _setupMutationObserver;
|
|
66
59
|
private render;
|
|
67
|
-
private _renderFullStructure;
|
|
68
|
-
private _updateContent;
|
|
69
|
-
private _updateLoadingOverlay;
|
|
70
60
|
private _setupAfterRender;
|
|
71
|
-
private _renderEmpty;
|
|
72
|
-
private _setupScrollTracking;
|
|
73
61
|
}
|
package/dist/component.js
CHANGED
|
@@ -1,21 +1,28 @@
|
|
|
1
|
-
import { EMPTY_STYLES, LOADING_STYLES, MAIN_STYLES } from './const/styles.js';
|
|
2
1
|
import { parseMessages } from './utils/message-parser.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
2
|
+
import { EventTracker } from './utils/event-tracker.js';
|
|
3
|
+
import { MessageProcessor } from './core/message-processor.js';
|
|
4
|
+
import { ScrollManager } from './core/scroll-manager.js';
|
|
5
|
+
import { Renderer } from './core/renderer.js';
|
|
7
6
|
export class BBMsgHistory extends HTMLElement {
|
|
8
7
|
static get observedAttributes() {
|
|
9
8
|
return ['theme', 'loading', 'hide-scroll-bar', 'infinite', 'hide-scroll-button'];
|
|
10
9
|
}
|
|
11
10
|
constructor() {
|
|
12
11
|
super();
|
|
12
|
+
// Core modules
|
|
13
|
+
this._eventTracker = new EventTracker();
|
|
14
|
+
this._messageProcessor = new MessageProcessor();
|
|
15
|
+
// State
|
|
13
16
|
this._userAuthors = new Map();
|
|
14
17
|
this._lastAuthor = '';
|
|
15
|
-
this._scrollButtonVisible = false;
|
|
16
|
-
this._scrollListeners = [];
|
|
17
18
|
this.attachShadow({ mode: 'open' });
|
|
18
|
-
//
|
|
19
|
+
// Initialize renderer with shadow root
|
|
20
|
+
this._renderer = new Renderer(this.shadowRoot);
|
|
21
|
+
// Initialize scroll manager with callback
|
|
22
|
+
this._scrollManager = new ScrollManager(this, this.shadowRoot, this._eventTracker, _ => {
|
|
23
|
+
// Callback for visibility changes (state tracking if needed)
|
|
24
|
+
});
|
|
25
|
+
// Create MutationObserver for reactive rendering
|
|
19
26
|
this._mutationObserver = new MutationObserver(() => {
|
|
20
27
|
clearTimeout(this._debounceTimer);
|
|
21
28
|
this._debounceTimer = setTimeout(() => this.render(), 50);
|
|
@@ -24,7 +31,7 @@ export class BBMsgHistory extends HTMLElement {
|
|
|
24
31
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
25
32
|
if (oldValue === newValue)
|
|
26
33
|
return;
|
|
27
|
-
if (
|
|
34
|
+
if (['theme', 'loading', 'hide-scroll-bar', 'infinite', 'hide-scroll-button'].includes(name)) {
|
|
28
35
|
this.render();
|
|
29
36
|
}
|
|
30
37
|
}
|
|
@@ -93,95 +100,28 @@ export class BBMsgHistory extends HTMLElement {
|
|
|
93
100
|
* el.scrollToBottom(); // Scroll with smooth animation
|
|
94
101
|
*/
|
|
95
102
|
scrollToBottom() {
|
|
96
|
-
|
|
97
|
-
return this;
|
|
98
|
-
}
|
|
99
|
-
const container = this.shadowRoot?.querySelector('.history');
|
|
100
|
-
if (!container) {
|
|
101
|
-
return this;
|
|
102
|
-
}
|
|
103
|
-
container.scrollTo({
|
|
104
|
-
top: container.scrollHeight,
|
|
105
|
-
behavior: 'smooth',
|
|
106
|
-
});
|
|
103
|
+
this._scrollManager.scrollToBottom();
|
|
107
104
|
return this;
|
|
108
105
|
}
|
|
109
106
|
/**
|
|
110
|
-
*
|
|
107
|
+
* Internal: Append a single message with incremental DOM update
|
|
111
108
|
*/
|
|
112
|
-
_canGroupMessages(prev, curr) {
|
|
113
|
-
if (!prev)
|
|
114
|
-
return false;
|
|
115
|
-
if (prev.author !== curr.author)
|
|
116
|
-
return false;
|
|
117
|
-
// Different timestamps = break group
|
|
118
|
-
if (prev.timestamp && curr.timestamp && prev.timestamp !== curr.timestamp) {
|
|
119
|
-
return false;
|
|
120
|
-
}
|
|
121
|
-
return true;
|
|
122
|
-
}
|
|
123
109
|
_appendSingleMessage(message) {
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
110
|
+
const result = this._renderer.appendSingleMessage(message, this._userAuthors, {
|
|
111
|
+
author: this._lastAuthor,
|
|
112
|
+
groupTimestamp: this._lastGroupTimestamp,
|
|
113
|
+
});
|
|
114
|
+
if (!result.success) {
|
|
115
|
+
// Container not ready, do full render
|
|
127
116
|
this.render();
|
|
128
117
|
return;
|
|
129
118
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// Build previous message object for grouping check
|
|
135
|
-
const prevMessage = this._lastAuthor
|
|
136
|
-
? { author: this._lastAuthor, text: '', timestamp: this._lastGroupTimestamp }
|
|
137
|
-
: null;
|
|
138
|
-
// Use unified grouping logic
|
|
139
|
-
const canGroupWithLast = this._canGroupMessages(prevMessage, message);
|
|
140
|
-
const isFirstFromAuthor = !canGroupWithLast;
|
|
141
|
-
this._lastAuthor = author;
|
|
142
|
-
const isSubsequent = !isFirstFromAuthor;
|
|
143
|
-
// Update group timestamp tracking (consistent with render())
|
|
144
|
-
if (isFirstFromAuthor) {
|
|
145
|
-
// Start new group
|
|
146
|
-
this._lastGroupTimestamp = timestamp;
|
|
147
|
-
}
|
|
148
|
-
else if (!this._lastGroupTimestamp && timestamp) {
|
|
149
|
-
// If no timestamp in group yet and current has one, use it
|
|
150
|
-
this._lastGroupTimestamp = timestamp;
|
|
151
|
-
}
|
|
152
|
-
// When appending, we assume this IS the last in group (for now)
|
|
153
|
-
// If another message from same author comes, we'll re-render
|
|
154
|
-
const isLastInGroup = true;
|
|
155
|
-
const groupTimestamp = this._lastGroupTimestamp;
|
|
156
|
-
// Use utility function to build message HTML
|
|
157
|
-
const msgHtml = buildMessageRowHtml(author, text, config, isSubsequent, groupTimestamp, isLastInGroup);
|
|
158
|
-
// Append to container
|
|
159
|
-
container.insertAdjacentHTML('beforeend', msgHtml);
|
|
160
|
-
// Setup tooltip for new element using utility function
|
|
161
|
-
const newWrapper = container.lastElementChild?.querySelector('.avatar-wrapper');
|
|
162
|
-
if (newWrapper) {
|
|
163
|
-
setupTooltipForElement(newWrapper);
|
|
164
|
-
}
|
|
165
|
-
// Smooth scroll to bottom (skip in infinite mode)
|
|
119
|
+
// Update state
|
|
120
|
+
this._lastAuthor = result.lastAuthor;
|
|
121
|
+
this._lastGroupTimestamp = result.lastGroupTimestamp;
|
|
122
|
+
// Scroll to bottom (skip in infinite mode)
|
|
166
123
|
if (!this.hasAttribute('infinite')) {
|
|
167
|
-
|
|
168
|
-
top: container.scrollHeight,
|
|
169
|
-
behavior: 'smooth',
|
|
170
|
-
});
|
|
171
|
-
// Hide scroll button since we're scrolling to bottom
|
|
172
|
-
if (this._scrollButtonVisible) {
|
|
173
|
-
this._scrollButtonVisible = false;
|
|
174
|
-
const scrollButton = this.shadowRoot.querySelector('.scroll-to-bottom');
|
|
175
|
-
if (scrollButton) {
|
|
176
|
-
scrollButton.classList.remove('visible');
|
|
177
|
-
}
|
|
178
|
-
// Dispatch hide event (always, regardless of button visibility)
|
|
179
|
-
this.dispatchEvent(new CustomEvent('bb-scrollbuttonhide', {
|
|
180
|
-
bubbles: true,
|
|
181
|
-
composed: true,
|
|
182
|
-
detail: { visible: false }
|
|
183
|
-
}));
|
|
184
|
-
}
|
|
124
|
+
this._scrollManager.scrollToBottom();
|
|
185
125
|
}
|
|
186
126
|
}
|
|
187
127
|
connectedCallback() {
|
|
@@ -191,26 +131,9 @@ export class BBMsgHistory extends HTMLElement {
|
|
|
191
131
|
disconnectedCallback() {
|
|
192
132
|
this._mutationObserver?.disconnect();
|
|
193
133
|
clearTimeout(this._debounceTimer);
|
|
194
|
-
this.
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* Track an event listener for cleanup on disconnect
|
|
198
|
-
*/
|
|
199
|
-
_addTrackedListener(el, type, fn) {
|
|
200
|
-
el.addEventListener(type, fn);
|
|
201
|
-
this._scrollListeners.push({ el, type, fn });
|
|
202
|
-
}
|
|
203
|
-
/**
|
|
204
|
-
* Remove all tracked event listeners
|
|
205
|
-
*/
|
|
206
|
-
_cleanupListeners() {
|
|
207
|
-
this._scrollListeners.forEach(({ el, type, fn }) => {
|
|
208
|
-
el.removeEventListener(type, fn);
|
|
209
|
-
});
|
|
210
|
-
this._scrollListeners = [];
|
|
134
|
+
this._eventTracker.cleanup();
|
|
211
135
|
}
|
|
212
136
|
_setupMutationObserver() {
|
|
213
|
-
// Observer was already created in constructor, just need to connect it
|
|
214
137
|
this._mutationObserver?.observe(this, {
|
|
215
138
|
childList: true,
|
|
216
139
|
characterData: true,
|
|
@@ -222,185 +145,30 @@ export class BBMsgHistory extends HTMLElement {
|
|
|
222
145
|
if (messages.length === 0) {
|
|
223
146
|
this._lastAuthor = '';
|
|
224
147
|
this._lastGroupTimestamp = undefined;
|
|
225
|
-
this.
|
|
148
|
+
this._renderer.renderEmpty(this.hasAttribute('loading'));
|
|
226
149
|
return;
|
|
227
150
|
}
|
|
228
|
-
//
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
return !next || !this._canGroupMessages(msg, next);
|
|
232
|
-
});
|
|
233
|
-
// Second pass: collect the timestamp for each group
|
|
234
|
-
// Use the first non-empty timestamp in the group
|
|
235
|
-
const groupTimestamps = new Map();
|
|
236
|
-
let currentGroupTimestamp;
|
|
237
|
-
messages.forEach((msg, i) => {
|
|
238
|
-
// Start of a new group
|
|
239
|
-
if (i === 0 || !this._canGroupMessages(messages[i - 1], msg)) {
|
|
240
|
-
currentGroupTimestamp = msg.timestamp;
|
|
241
|
-
}
|
|
242
|
-
else if (!currentGroupTimestamp && msg.timestamp) {
|
|
243
|
-
// If no timestamp yet and current msg has one, use it
|
|
244
|
-
currentGroupTimestamp = msg.timestamp;
|
|
245
|
-
}
|
|
246
|
-
// If this is the last message in the group, save the timestamp
|
|
247
|
-
if (lastInGroupFlags[i]) {
|
|
248
|
-
groupTimestamps.set(i, currentGroupTimestamp);
|
|
249
|
-
currentGroupTimestamp = undefined;
|
|
250
|
-
}
|
|
251
|
-
});
|
|
252
|
-
// Third pass: build HTML
|
|
253
|
-
let lastAuthor = '';
|
|
254
|
-
const messagesHtml = messages
|
|
255
|
-
.map((msg, i) => {
|
|
256
|
-
const { author, text } = msg;
|
|
257
|
-
const config = resolveAuthorConfig(author, this._userAuthors);
|
|
258
|
-
// Determine if this is a new author group (can't group with previous)
|
|
259
|
-
const isFirstFromAuthor = i === 0 || !this._canGroupMessages(messages[i - 1], msg);
|
|
260
|
-
lastAuthor = author;
|
|
261
|
-
const isSubsequent = !isFirstFromAuthor;
|
|
262
|
-
// Get timestamp if this is the last in group
|
|
263
|
-
const isLastInGroup = lastInGroupFlags[i];
|
|
264
|
-
const groupTimestamp = groupTimestamps.get(i);
|
|
265
|
-
// Use utility function to build message HTML
|
|
266
|
-
return buildMessageRowHtml(author, text, config, isSubsequent, groupTimestamp, isLastInGroup);
|
|
267
|
-
})
|
|
268
|
-
.join('');
|
|
151
|
+
// Process messages (single-pass algorithm)
|
|
152
|
+
const { processed, lastAuthor, lastGroupTimestamp } = this._messageProcessor.process(messages);
|
|
153
|
+
// Update state
|
|
269
154
|
this._lastAuthor = lastAuthor;
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const
|
|
273
|
-
if (needsFullSetup) {
|
|
274
|
-
// First render - create full structure
|
|
275
|
-
this._renderFullStructure(messagesHtml);
|
|
276
|
-
}
|
|
277
|
-
else {
|
|
278
|
-
// Update only - preserve DOM structure, just update content
|
|
279
|
-
this._updateContent(historyContainer, messagesHtml);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
_renderFullStructure(messagesHtml) {
|
|
283
|
-
// Check if we should preserve scroll position before re-rendering
|
|
284
|
-
const existingContainer = this.shadowRoot.querySelector('.history');
|
|
285
|
-
const wasAtBottom = existingContainer
|
|
286
|
-
? existingContainer.scrollHeight - existingContainer.scrollTop - existingContainer.clientHeight < 50
|
|
287
|
-
: true; // Default to true for initial render
|
|
288
|
-
const loadingOverlay = this.hasAttribute('loading')
|
|
289
|
-
? `<div class="loading-overlay" role="status" aria-label="Loading messages">
|
|
290
|
-
<div class="loading-spinner"></div>
|
|
291
|
-
</div>`
|
|
292
|
-
: '';
|
|
155
|
+
this._lastGroupTimestamp = lastGroupTimestamp;
|
|
156
|
+
// Render messages
|
|
157
|
+
const isLoading = this.hasAttribute('loading');
|
|
293
158
|
const hideScrollButton = this.hasAttribute('hide-scroll-button');
|
|
294
|
-
this.
|
|
295
|
-
|
|
296
|
-
<div class="history" role="log" aria-live="polite" aria-label="Message history">
|
|
297
|
-
${messagesHtml}
|
|
298
|
-
</div>
|
|
299
|
-
${hideScrollButton ? '' : buildScrollButtonHtml()}
|
|
300
|
-
${loadingOverlay}
|
|
301
|
-
`;
|
|
159
|
+
const { wasAtBottom } = this._renderer.render(processed, this._userAuthors, isLoading, hideScrollButton);
|
|
160
|
+
// Setup scroll tracking and other post-render tasks
|
|
302
161
|
this._setupAfterRender(wasAtBottom);
|
|
303
162
|
}
|
|
304
|
-
_updateContent(historyContainer, messagesHtml) {
|
|
305
|
-
// Preserve scroll position before update
|
|
306
|
-
const scrollContainer = historyContainer;
|
|
307
|
-
const wasAtBottom = scrollContainer.scrollHeight - scrollContainer.scrollTop - scrollContainer.clientHeight < 50;
|
|
308
|
-
// Update messages content only
|
|
309
|
-
historyContainer.innerHTML = messagesHtml;
|
|
310
|
-
// Update loading overlay
|
|
311
|
-
this._updateLoadingOverlay();
|
|
312
|
-
// Restore scroll position or scroll to bottom if we were there
|
|
313
|
-
if (wasAtBottom) {
|
|
314
|
-
scrollContainer.scrollTop = scrollContainer.scrollHeight;
|
|
315
|
-
}
|
|
316
|
-
// Re-setup tooltips for new content
|
|
317
|
-
setupTooltips(this.shadowRoot);
|
|
318
|
-
}
|
|
319
|
-
_updateLoadingOverlay() {
|
|
320
|
-
const existingOverlay = this.shadowRoot.querySelector('.loading-overlay');
|
|
321
|
-
const shouldShow = this.hasAttribute('loading');
|
|
322
|
-
if (shouldShow && !existingOverlay) {
|
|
323
|
-
const overlay = document.createElement('div');
|
|
324
|
-
overlay.className = 'loading-overlay';
|
|
325
|
-
overlay.setAttribute('role', 'status');
|
|
326
|
-
overlay.setAttribute('aria-label', 'Loading messages');
|
|
327
|
-
overlay.innerHTML = '<div class="loading-spinner"></div>';
|
|
328
|
-
this.shadowRoot.appendChild(overlay);
|
|
329
|
-
}
|
|
330
|
-
else if (!shouldShow && existingOverlay) {
|
|
331
|
-
existingOverlay.remove();
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
163
|
_setupAfterRender(shouldScrollToBottom = true) {
|
|
335
164
|
requestAnimationFrame(() => {
|
|
336
|
-
const container = this.
|
|
337
|
-
const scrollButton = this.
|
|
165
|
+
const container = this._renderer.getHistoryContainer();
|
|
166
|
+
const scrollButton = this._renderer.getScrollButton();
|
|
338
167
|
const isInfinite = this.hasAttribute('infinite');
|
|
339
168
|
if (container && !isInfinite) {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
343
|
-
this._setupScrollTracking(container, scrollButton, { skipInitialCheck: true });
|
|
344
|
-
}
|
|
345
|
-
if (scrollButton && !isInfinite) {
|
|
346
|
-
scrollButton.addEventListener('click', () => {
|
|
347
|
-
container?.scrollTo({
|
|
348
|
-
top: container.scrollHeight,
|
|
349
|
-
behavior: 'smooth',
|
|
350
|
-
});
|
|
351
|
-
});
|
|
169
|
+
// Initialize scroll manager
|
|
170
|
+
this._scrollManager.init(container, scrollButton, shouldScrollToBottom);
|
|
352
171
|
}
|
|
353
|
-
setupTooltips(this.shadowRoot);
|
|
354
172
|
});
|
|
355
173
|
}
|
|
356
|
-
_renderEmpty() {
|
|
357
|
-
const isLoading = this.hasAttribute('loading');
|
|
358
|
-
if (isLoading) {
|
|
359
|
-
// Show loading overlay with minimum height for better appearance
|
|
360
|
-
this.shadowRoot.innerHTML = `
|
|
361
|
-
<style>${EMPTY_STYLES}${LOADING_STYLES}</style>
|
|
362
|
-
<div style="position: relative; min-height: 120px;">
|
|
363
|
-
<div class="loading-overlay" role="status" aria-label="Loading messages">
|
|
364
|
-
<div class="loading-spinner"></div>
|
|
365
|
-
</div>
|
|
366
|
-
</div>
|
|
367
|
-
`;
|
|
368
|
-
}
|
|
369
|
-
else {
|
|
370
|
-
this.shadowRoot.innerHTML = `
|
|
371
|
-
<style>${EMPTY_STYLES}</style>
|
|
372
|
-
<div class="empty-state">No messages</div>
|
|
373
|
-
`;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
_setupScrollTracking(container, button, options) {
|
|
377
|
-
const checkScrollPosition = () => {
|
|
378
|
-
const threshold = 50; // pixels from bottom
|
|
379
|
-
const isAtBottom = container.scrollHeight - container.scrollTop - container.clientHeight < threshold;
|
|
380
|
-
const hasOverflow = container.scrollHeight > container.clientHeight;
|
|
381
|
-
// Show button when not at bottom and content has overflow
|
|
382
|
-
const shouldShow = !isAtBottom && hasOverflow;
|
|
383
|
-
if (shouldShow !== this._scrollButtonVisible) {
|
|
384
|
-
this._scrollButtonVisible = shouldShow;
|
|
385
|
-
// Only toggle button visibility if button exists
|
|
386
|
-
if (button) {
|
|
387
|
-
button.classList.toggle('visible', shouldShow);
|
|
388
|
-
}
|
|
389
|
-
// Dispatch custom event (always, regardless of button visibility)
|
|
390
|
-
this.dispatchEvent(new CustomEvent(shouldShow ? 'bb-scrollbuttonshow' : 'bb-scrollbuttonhide', {
|
|
391
|
-
bubbles: true,
|
|
392
|
-
composed: true,
|
|
393
|
-
detail: { visible: shouldShow }
|
|
394
|
-
}));
|
|
395
|
-
}
|
|
396
|
-
};
|
|
397
|
-
// Check initial state unless skipped
|
|
398
|
-
if (!options?.skipInitialCheck) {
|
|
399
|
-
checkScrollPosition();
|
|
400
|
-
}
|
|
401
|
-
// Listen for scroll events with passive listener for performance
|
|
402
|
-
this._addTrackedListener(container, 'scroll', checkScrollPosition);
|
|
403
|
-
// Also check on resize
|
|
404
|
-
this._addTrackedListener(window, 'resize', checkScrollPosition);
|
|
405
|
-
}
|
|
406
174
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { LitElement } from 'lit';
|
|
2
|
+
/**
|
|
3
|
+
* Custom avatar component - displays SVG, img, emoji, or HTML content
|
|
4
|
+
* Includes tooltip on hover
|
|
5
|
+
*/
|
|
6
|
+
export declare class BBCustomAvatar extends LitElement {
|
|
7
|
+
static styles: import("lit").CSSResult;
|
|
8
|
+
tooltip: string;
|
|
9
|
+
private _tooltipRef;
|
|
10
|
+
private _showTooltip;
|
|
11
|
+
private _onMouseEnter;
|
|
12
|
+
private _onMouseLeave;
|
|
13
|
+
private _positionTooltip;
|
|
14
|
+
render(): import("lit").TemplateResult<1>;
|
|
15
|
+
}
|
|
16
|
+
declare global {
|
|
17
|
+
interface HTMLElementTagNameMap {
|
|
18
|
+
'bb-custom-avatar': BBCustomAvatar;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
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;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { LitElement, html, css, unsafeCSS } from 'lit';
|
|
8
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
9
|
+
import { THEME } from '../const/theme.js';
|
|
10
|
+
/**
|
|
11
|
+
* Custom avatar component - displays SVG, img, emoji, or HTML content
|
|
12
|
+
* Includes tooltip on hover
|
|
13
|
+
*/
|
|
14
|
+
let BBCustomAvatar = class BBCustomAvatar extends LitElement {
|
|
15
|
+
constructor() {
|
|
16
|
+
super(...arguments);
|
|
17
|
+
this.tooltip = '';
|
|
18
|
+
this._tooltipRef = null;
|
|
19
|
+
this._showTooltip = false;
|
|
20
|
+
}
|
|
21
|
+
static { this.styles = css `
|
|
22
|
+
:host {
|
|
23
|
+
display: block;
|
|
24
|
+
width: 1.75rem;
|
|
25
|
+
height: 1.75rem;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.avatar-wrapper {
|
|
29
|
+
position: relative;
|
|
30
|
+
width: 100%;
|
|
31
|
+
height: 100%;
|
|
32
|
+
background: #ffffff;
|
|
33
|
+
border-radius: 50%;
|
|
34
|
+
overflow: hidden;
|
|
35
|
+
cursor: help;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.avatar {
|
|
39
|
+
width: 100%;
|
|
40
|
+
height: 100%;
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
justify-content: center;
|
|
44
|
+
border-radius: 50%;
|
|
45
|
+
overflow: hidden;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.avatar ::slotted(svg) {
|
|
49
|
+
width: 100%;
|
|
50
|
+
height: 100%;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.avatar ::slotted(img) {
|
|
54
|
+
width: 100%;
|
|
55
|
+
height: 100%;
|
|
56
|
+
object-fit: cover;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* Tooltip styles */
|
|
60
|
+
.avatar-tooltip {
|
|
61
|
+
position: fixed;
|
|
62
|
+
padding: 0.25rem 0.5rem;
|
|
63
|
+
background: ${unsafeCSS(THEME.gray[800])};
|
|
64
|
+
color: ${unsafeCSS(THEME.gray[50])};
|
|
65
|
+
font-size: 0.75rem;
|
|
66
|
+
border-radius: 0.25rem;
|
|
67
|
+
white-space: nowrap;
|
|
68
|
+
opacity: 0;
|
|
69
|
+
visibility: hidden;
|
|
70
|
+
pointer-events: none;
|
|
71
|
+
z-index: 10000;
|
|
72
|
+
font-weight: 500;
|
|
73
|
+
letter-spacing: 0.02em;
|
|
74
|
+
transition:
|
|
75
|
+
opacity 0.2s ease,
|
|
76
|
+
visibility 0.2s ease;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.avatar-tooltip::after {
|
|
80
|
+
content: '';
|
|
81
|
+
position: absolute;
|
|
82
|
+
top: calc(100% - 1px);
|
|
83
|
+
left: 50%;
|
|
84
|
+
transform: translateX(-50%);
|
|
85
|
+
border: 4px solid transparent;
|
|
86
|
+
border-top-color: ${unsafeCSS(THEME.gray[800])};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.avatar-tooltip.visible {
|
|
90
|
+
opacity: 1;
|
|
91
|
+
visibility: visible;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@media (max-width: 480px) {
|
|
95
|
+
:host {
|
|
96
|
+
width: 1.5rem;
|
|
97
|
+
height: 1.5rem;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
`; }
|
|
101
|
+
_onMouseEnter() {
|
|
102
|
+
this._showTooltip = true;
|
|
103
|
+
this._positionTooltip();
|
|
104
|
+
this.requestUpdate();
|
|
105
|
+
}
|
|
106
|
+
_onMouseLeave() {
|
|
107
|
+
this._showTooltip = false;
|
|
108
|
+
this.requestUpdate();
|
|
109
|
+
}
|
|
110
|
+
_positionTooltip() {
|
|
111
|
+
if (!this._tooltipRef || !this._showTooltip)
|
|
112
|
+
return;
|
|
113
|
+
const rect = this.getBoundingClientRect();
|
|
114
|
+
const tooltipRect = this._tooltipRef.getBoundingClientRect();
|
|
115
|
+
this._tooltipRef.style.left = `${rect.left + rect.width / 2 - tooltipRect.width / 2}px`;
|
|
116
|
+
this._tooltipRef.style.top = `${rect.top - tooltipRect.height - 8}px`;
|
|
117
|
+
}
|
|
118
|
+
render() {
|
|
119
|
+
return html `
|
|
120
|
+
<div
|
|
121
|
+
class="avatar-wrapper"
|
|
122
|
+
@mouseenter=${this._onMouseEnter}
|
|
123
|
+
@mouseleave=${this._onMouseLeave}
|
|
124
|
+
>
|
|
125
|
+
<div class="avatar"><slot></slot></div>
|
|
126
|
+
</div>
|
|
127
|
+
<div
|
|
128
|
+
class="avatar-tooltip ${this._showTooltip ? 'visible' : ''}"
|
|
129
|
+
${(el) => {
|
|
130
|
+
if (el)
|
|
131
|
+
this._tooltipRef = el;
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
${this.tooltip}
|
|
135
|
+
</div>
|
|
136
|
+
`;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
__decorate([
|
|
140
|
+
property()
|
|
141
|
+
], BBCustomAvatar.prototype, "tooltip", void 0);
|
|
142
|
+
BBCustomAvatar = __decorate([
|
|
143
|
+
customElement('bb-custom-avatar')
|
|
144
|
+
], BBCustomAvatar);
|
|
145
|
+
export { BBCustomAvatar };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { LitElement } from 'lit';
|
|
2
|
+
/**
|
|
3
|
+
* Letter avatar component - displays a letter in a circular container
|
|
4
|
+
*/
|
|
5
|
+
export declare class BBLetterAvatar extends LitElement {
|
|
6
|
+
static styles: import("lit").CSSResult;
|
|
7
|
+
letter: string;
|
|
8
|
+
render(): import("lit").TemplateResult<1>;
|
|
9
|
+
}
|
|
10
|
+
declare global {
|
|
11
|
+
interface HTMLElementTagNameMap {
|
|
12
|
+
'bb-letter-avatar': BBLetterAvatar;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
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;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { LitElement, html, css, unsafeCSS } from 'lit';
|
|
8
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
9
|
+
import { THEME } from '../const/theme.js';
|
|
10
|
+
/**
|
|
11
|
+
* Letter avatar component - displays a letter in a circular container
|
|
12
|
+
*/
|
|
13
|
+
let BBLetterAvatar = class BBLetterAvatar extends LitElement {
|
|
14
|
+
constructor() {
|
|
15
|
+
super(...arguments);
|
|
16
|
+
this.letter = '';
|
|
17
|
+
}
|
|
18
|
+
static { this.styles = css `
|
|
19
|
+
:host {
|
|
20
|
+
display: block;
|
|
21
|
+
width: 1.75rem;
|
|
22
|
+
height: 1.75rem;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.avatar {
|
|
26
|
+
width: 100%;
|
|
27
|
+
height: 100%;
|
|
28
|
+
display: flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
justify-content: center;
|
|
31
|
+
background: var(--bb-avatar-bg, #ffffff);
|
|
32
|
+
color: var(--bb-avatar-color, ${unsafeCSS(THEME.gray[600])});
|
|
33
|
+
font-size: 14px;
|
|
34
|
+
font-weight: 600;
|
|
35
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
|
|
36
|
+
border-radius: 50%;
|
|
37
|
+
overflow: hidden;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@media (max-width: 480px) {
|
|
41
|
+
:host {
|
|
42
|
+
width: 1.5rem;
|
|
43
|
+
height: 1.5rem;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.avatar {
|
|
47
|
+
font-size: 12px;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
`; }
|
|
51
|
+
render() {
|
|
52
|
+
return html `<div class="avatar">${this.letter}</div>`;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
__decorate([
|
|
56
|
+
property()
|
|
57
|
+
], BBLetterAvatar.prototype, "letter", void 0);
|
|
58
|
+
BBLetterAvatar = __decorate([
|
|
59
|
+
customElement('bb-letter-avatar')
|
|
60
|
+
], BBLetterAvatar);
|
|
61
|
+
export { BBLetterAvatar };
|