@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.
Files changed (54) hide show
  1. package/dist/component.d.ts +6 -18
  2. package/dist/component.js +43 -275
  3. package/dist/components/bb-custom-avatar.d.ts +20 -0
  4. package/dist/components/bb-custom-avatar.js +145 -0
  5. package/dist/components/bb-letter-avatar.d.ts +14 -0
  6. package/dist/components/bb-letter-avatar.js +61 -0
  7. package/dist/components/bb-loading-overlay.d.ts +14 -0
  8. package/dist/components/bb-loading-overlay.js +89 -0
  9. package/dist/components/bb-message-bubble.d.ts +19 -0
  10. package/dist/components/bb-message-bubble.js +116 -0
  11. package/dist/components/bb-message.d.ts +27 -0
  12. package/dist/components/bb-message.js +174 -0
  13. package/dist/components/bb-msg-history.d.ts +111 -0
  14. package/dist/components/bb-msg-history.js +473 -0
  15. package/dist/components/bb-scroll-button.d.ts +16 -0
  16. package/dist/components/bb-scroll-button.js +161 -0
  17. package/dist/components/bb-timestamp.d.ts +15 -0
  18. package/dist/components/bb-timestamp.js +59 -0
  19. package/dist/components/index.d.ts +7 -0
  20. package/dist/components/index.js +7 -0
  21. package/dist/const/authors.js +1 -1
  22. package/dist/const/styles.js +0 -33
  23. package/dist/contexts/author-context.d.ts +8 -0
  24. package/dist/contexts/author-context.js +6 -0
  25. package/dist/controllers/scroll-controller.d.ts +52 -0
  26. package/dist/controllers/scroll-controller.js +138 -0
  27. package/dist/core/message-processor.d.ts +56 -0
  28. package/dist/core/message-processor.js +85 -0
  29. package/dist/core/renderer.d.ts +87 -0
  30. package/dist/core/renderer.js +196 -0
  31. package/dist/core/scroll-manager.d.ts +54 -0
  32. package/dist/core/scroll-manager.js +119 -0
  33. package/dist/parsers/base.d.ts +21 -0
  34. package/dist/parsers/base.js +1 -0
  35. package/dist/parsers/default-parser.d.ts +10 -0
  36. package/dist/parsers/default-parser.js +40 -0
  37. package/dist/parsers/index.d.ts +2 -0
  38. package/dist/parsers/index.js +1 -0
  39. package/dist/utils/event-tracker.d.ts +23 -0
  40. package/dist/utils/event-tracker.js +33 -0
  41. package/dist/utils/message-builder.d.ts +0 -4
  42. package/dist/utils/message-builder.js +0 -15
  43. package/dist/utils/tooltip.d.ts +11 -2
  44. package/dist/utils/tooltip.js +56 -13
  45. package/package.json +1 -1
  46. package/src/component.ts +56 -338
  47. package/src/const/authors.ts +3 -2
  48. package/src/const/styles.ts +0 -33
  49. package/src/core/message-processor.ts +120 -0
  50. package/src/core/renderer.ts +276 -0
  51. package/src/core/scroll-manager.ts +148 -0
  52. package/src/utils/event-tracker.ts +38 -0
  53. package/src/utils/message-builder.ts +0 -15
  54. package/src/utils/tooltip.ts +0 -16
@@ -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
- * Check if two messages can be grouped (same author, no timestamp conflict)
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 { resolveAuthorConfig } from './utils/author-resolver.js';
4
- import { setupTooltips } from './utils/tooltip.js';
5
- import { buildMessageRowHtml, setupTooltipForElement } from './utils/message-builder.js';
6
- import { buildScrollButtonHtml } from './utils/scroll-button.js';
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
- // Create MutationObserver once - will be connected in connectedCallback
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 (name === 'theme' || name === 'loading' || name === 'hide-scroll-bar' || name === 'infinite' || name === 'hide-scroll-button') {
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
- if (this.hasAttribute('infinite')) {
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
- * Check if two messages can be grouped (same author, no timestamp conflict)
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 container = this.shadowRoot.querySelector('.history');
125
- // If empty state or no container, do full render first
126
- if (!container) {
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
- const author = message.author;
131
- const text = message.text;
132
- const timestamp = message.timestamp;
133
- const config = resolveAuthorConfig(author, this._userAuthors);
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
- container.scrollTo({
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._cleanupListeners();
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._renderEmpty();
148
+ this._renderer.renderEmpty(this.hasAttribute('loading'));
226
149
  return;
227
150
  }
228
- // First pass: determine which messages are last in their group
229
- const lastInGroupFlags = messages.map((msg, i) => {
230
- const next = messages[i + 1];
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
- // Check if we need to create or update the structure
271
- const historyContainer = this.shadowRoot.querySelector('.history');
272
- const needsFullSetup = !historyContainer;
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.shadowRoot.innerHTML = `
295
- <style>${MAIN_STYLES}${LOADING_STYLES}</style>
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.shadowRoot.querySelector('.history');
337
- const scrollButton = this.shadowRoot.querySelector('.scroll-to-bottom');
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
- if (shouldScrollToBottom) {
341
- container.scrollTop = container.scrollHeight;
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 };