@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
@@ -0,0 +1,473 @@
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, nothing, unsafeCSS } from 'lit';
8
+ import { customElement, property, state } from 'lit/decorators.js';
9
+ import { provide } from '@lit/context';
10
+ import { authorContext } from '../contexts/author-context.js';
11
+ import { ScrollController } from '../controllers/scroll-controller.js';
12
+ import { DefaultMessageParser } from '../parsers/default-parser.js';
13
+ import { THEME } from '../const/theme.js';
14
+ import './bb-message.js';
15
+ import './bb-scroll-button.js';
16
+ import './bb-loading-overlay.js';
17
+ /**
18
+ * BBMsgHistory - A chat-style message history web component
19
+ *
20
+ * Uses Lit for reactive rendering with a compositional architecture.
21
+ * Preserves backward compatibility with the lightweight textContent mode.
22
+ *
23
+ * @example
24
+ * ```html
25
+ * <!-- Lightweight mode -->
26
+ * <bb-msg-history>
27
+ * alice: Hello!
28
+ * bob: Hi there!
29
+ * </bb-msg-history>
30
+ *
31
+ * <!-- With custom authors -->
32
+ * <bb-msg-history id="chat"></bb-msg-history>
33
+ * <script>
34
+ * document.getElementById('chat')
35
+ * .setAuthor('alice', { avatar: '🐱', side: 'right' });
36
+ * </script>
37
+ * ```
38
+ */
39
+ let BBMsgHistory = class BBMsgHistory extends LitElement {
40
+ constructor() {
41
+ super(...arguments);
42
+ // Public properties
43
+ this.loading = false;
44
+ this.hideScrollBar = false;
45
+ this.infinite = false;
46
+ this.hideScrollButton = false;
47
+ this.theme = null;
48
+ // Private state
49
+ this._messages = [];
50
+ this._processedMessages = [];
51
+ // Context provider for author configurations
52
+ this._userAuthors = new Map();
53
+ this._isParsing = false;
54
+ this._parser = new DefaultMessageParser();
55
+ // Scroll controller
56
+ this._scrollController = new ScrollController(this);
57
+ }
58
+ static { this.styles = css `
59
+ :host {
60
+ display: block;
61
+ position: relative;
62
+ font-family:
63
+ 'PT Sans',
64
+ ui-sans-serif,
65
+ system-ui,
66
+ -apple-system,
67
+ BlinkMacSystemFont,
68
+ 'Segoe UI',
69
+ Roboto,
70
+ 'Helvetica Neue',
71
+ Arial,
72
+ 'Noto Sans',
73
+ sans-serif,
74
+ 'Apple Color Emoji',
75
+ 'Segoe UI Emoji',
76
+ 'Segoe UI Symbol',
77
+ 'Noto Color Emoji';
78
+ --bb-bg-color: ${unsafeCSS(THEME.gray[50])};
79
+ --bb-max-height: 600px;
80
+ --bb-avatar-bg: #ffffff;
81
+ --bb-avatar-color: ${unsafeCSS(THEME.gray[600])};
82
+ }
83
+
84
+ :host([theme='dark']) {
85
+ --bb-bg-color: ${unsafeCSS(THEME.gray[900])};
86
+ --bb-avatar-bg: ${unsafeCSS(THEME.slate[600])};
87
+ --bb-avatar-color: ${unsafeCSS(THEME.slate[200])};
88
+ }
89
+
90
+ @media (prefers-color-scheme: dark) {
91
+ :host {
92
+ --bb-bg-color: ${unsafeCSS(THEME.gray[900])};
93
+ --bb-avatar-bg: ${unsafeCSS(THEME.slate[600])};
94
+ --bb-avatar-color: ${unsafeCSS(THEME.slate[200])};
95
+ }
96
+ }
97
+
98
+ .history-container {
99
+ margin: 0 auto;
100
+ display: flex;
101
+ flex-direction: column;
102
+ gap: 0.25rem;
103
+ max-height: var(--bb-max-height, 600px);
104
+ overflow-y: auto;
105
+ scroll-behavior: smooth;
106
+ background-color: transparent;
107
+ border-radius: 0.5rem;
108
+ /* Firefox scrollbar */
109
+ scrollbar-width: thin;
110
+ scrollbar-color: ${unsafeCSS(THEME.gray[400])} transparent;
111
+ }
112
+
113
+ /* Custom scrollbar for webkit browsers */
114
+ .history-container::-webkit-scrollbar {
115
+ width: 6px;
116
+ }
117
+
118
+ .history-container::-webkit-scrollbar-track {
119
+ background: transparent;
120
+ border-radius: 3px;
121
+ }
122
+
123
+ .history-container::-webkit-scrollbar-thumb {
124
+ background: ${unsafeCSS(THEME.gray[400])};
125
+ border-radius: 3px;
126
+ }
127
+
128
+ .history-container::-webkit-scrollbar-thumb:hover {
129
+ background: ${unsafeCSS(THEME.gray[500])};
130
+ }
131
+
132
+ /* Hide scrollbar */
133
+ :host([hide-scroll-bar]) .history-container {
134
+ scrollbar-width: none;
135
+ -ms-overflow-style: none;
136
+ }
137
+
138
+ :host([hide-scroll-bar]) .history-container::-webkit-scrollbar {
139
+ display: none;
140
+ }
141
+
142
+ /* Infinite mode */
143
+ :host([infinite]) .history-container {
144
+ max-height: none;
145
+ overflow-y: visible;
146
+ }
147
+
148
+ /* Empty state */
149
+ .empty-state {
150
+ text-align: center;
151
+ padding: 2rem;
152
+ color: ${unsafeCSS(THEME.gray[400])};
153
+ font-size: 0.875rem;
154
+ }
155
+
156
+ @media (prefers-color-scheme: dark) {
157
+ .empty-state {
158
+ color: ${unsafeCSS(THEME.gray[500])};
159
+ }
160
+
161
+ .history-container {
162
+ scrollbar-color: ${unsafeCSS(THEME.gray[600])} transparent;
163
+ }
164
+
165
+ .history-container::-webkit-scrollbar-thumb {
166
+ background: ${unsafeCSS(THEME.gray[600])};
167
+ }
168
+
169
+ .history-container::-webkit-scrollbar-thumb:hover {
170
+ background: ${unsafeCSS(THEME.gray[500])};
171
+ }
172
+ }
173
+
174
+ @media (max-width: 480px) {
175
+ .history-container {
176
+ max-height: var(--bb-max-height, 70vh);
177
+ }
178
+ }
179
+
180
+ @media (prefers-reduced-motion: reduce) {
181
+ .history-container {
182
+ scroll-behavior: auto;
183
+ }
184
+ }
185
+ `; }
186
+ connectedCallback() {
187
+ super.connectedCallback();
188
+ this._initLightDOMObserver();
189
+ // Initial parse if content exists (for SSR or static HTML)
190
+ if (this.textContent?.trim()) {
191
+ this._parseLightDOM();
192
+ }
193
+ }
194
+ disconnectedCallback() {
195
+ super.disconnectedCallback();
196
+ this._mutationObserver?.disconnect();
197
+ }
198
+ willUpdate(changedProps) {
199
+ if (changedProps.has('_messages') ||
200
+ changedProps.has('_userAuthors')) {
201
+ this._processedMessages = this._computeGroups(this._messages);
202
+ }
203
+ }
204
+ updated(changedProps) {
205
+ if (changedProps.has('_processedMessages')) {
206
+ // Update scroll observer when messages change
207
+ this._scrollController.updateObservedMessage();
208
+ // Auto-scroll if not in infinite mode
209
+ if (!this.infinite) {
210
+ this._scrollController.scrollToBottom('auto');
211
+ }
212
+ }
213
+ }
214
+ /**
215
+ * Configure an author's avatar, side, and colors.
216
+ * Call before or after rendering — the component re-renders automatically.
217
+ */
218
+ setAuthor(name, options) {
219
+ this._userAuthors = new Map([...this._userAuthors, [name, options]]);
220
+ return this;
221
+ }
222
+ /**
223
+ * Remove a previously set author config.
224
+ */
225
+ removeAuthor(name) {
226
+ const newMap = new Map(this._userAuthors);
227
+ newMap.delete(name);
228
+ this._userAuthors = newMap;
229
+ return this;
230
+ }
231
+ /**
232
+ * Show or hide the loading overlay.
233
+ */
234
+ setLoading(isLoading) {
235
+ this.loading = isLoading;
236
+ return this;
237
+ }
238
+ /**
239
+ * Append a message to the history.
240
+ * Automatically scrolls to the new message with smooth animation.
241
+ */
242
+ appendMessage(input) {
243
+ const msg = {
244
+ author: input.author,
245
+ text: input.text,
246
+ timestamp: input.timestamp,
247
+ };
248
+ // Pause observation to avoid loop
249
+ this._mutationObserver?.disconnect();
250
+ // Update messages array
251
+ this._messages = [...this._messages, msg];
252
+ // Optionally sync back to light DOM (for consistency)
253
+ this._syncToLightDOM();
254
+ // Restore observation after current event loop
255
+ requestAnimationFrame(() => {
256
+ this._initLightDOMObserver();
257
+ });
258
+ // Scroll to bottom (skip in infinite mode)
259
+ if (!this.infinite) {
260
+ this._scrollController.scrollToBottom();
261
+ }
262
+ return this;
263
+ }
264
+ /**
265
+ * Scroll to the bottom of the message history.
266
+ */
267
+ scrollToBottom() {
268
+ this._scrollController.scrollToBottom();
269
+ return this;
270
+ }
271
+ /**
272
+ * Set a custom parser for message parsing
273
+ */
274
+ setParser(parser) {
275
+ this._parser = parser;
276
+ this._parseLightDOM();
277
+ return this;
278
+ }
279
+ /**
280
+ * Initialize MutationObserver for Light DOM observation
281
+ */
282
+ _initLightDOMObserver() {
283
+ if (this._mutationObserver) {
284
+ this._mutationObserver.disconnect();
285
+ }
286
+ this._mutationObserver = new MutationObserver(records => {
287
+ // Only react to light DOM changes, not shadow DOM
288
+ const hasLightDOMChange = records.some(r => {
289
+ const root = r.target.getRootNode();
290
+ return root === document || root === this;
291
+ });
292
+ if (hasLightDOMChange && !this._isParsing) {
293
+ this._parseLightDOM();
294
+ }
295
+ });
296
+ this._mutationObserver.observe(this, {
297
+ childList: true,
298
+ subtree: true,
299
+ characterData: true,
300
+ });
301
+ }
302
+ /**
303
+ * Parse Light DOM textContent into messages
304
+ */
305
+ _parseLightDOM() {
306
+ const rawText = this.textContent || '';
307
+ if (!rawText.trim()) {
308
+ this._messages = [];
309
+ return;
310
+ }
311
+ this._isParsing = true;
312
+ const parsed = this._parser.parse(rawText);
313
+ // Only update if messages actually changed (avoid unnecessary re-renders)
314
+ if (this._messagesChanged(parsed)) {
315
+ this._messages = parsed;
316
+ }
317
+ // Hide light DOM content visually but keep it accessible
318
+ this._hideLightDOMContent();
319
+ requestAnimationFrame(() => {
320
+ this._isParsing = false;
321
+ });
322
+ }
323
+ /**
324
+ * Check if messages have changed
325
+ */
326
+ _messagesChanged(newMessages) {
327
+ if (newMessages.length !== this._messages.length)
328
+ return true;
329
+ return newMessages.some((msg, i) => {
330
+ const old = this._messages[i];
331
+ return (msg.author !== old?.author || msg.text !== old?.text || msg.timestamp !== old?.timestamp);
332
+ });
333
+ }
334
+ /**
335
+ * Hide light DOM content visually while keeping it for accessibility/parsing
336
+ */
337
+ _hideLightDOMContent() {
338
+ // The content is parsed and rendered in shadow DOM,
339
+ // so we don't need to hide it explicitly - the shadow DOM overlays it.
340
+ // But we can ensure proper accessibility by marking it.
341
+ this.setAttribute('aria-live', 'polite');
342
+ this.setAttribute('role', 'log');
343
+ this.setAttribute('aria-label', 'Message history');
344
+ }
345
+ /**
346
+ * Sync current messages back to light DOM
347
+ */
348
+ _syncToLightDOM() {
349
+ const content = this._messages
350
+ .map(m => {
351
+ const timestamp = m.timestamp ? `[${m.timestamp}] ` : '';
352
+ return `${timestamp}${m.author}: ${m.text}`;
353
+ })
354
+ .join('\n');
355
+ // Temporarily disconnect observer to prevent loop
356
+ this._mutationObserver?.disconnect();
357
+ this.textContent = content;
358
+ this._initLightDOMObserver();
359
+ }
360
+ /**
361
+ * Compute message groups for rendering
362
+ */
363
+ _computeGroups(messages) {
364
+ if (messages.length === 0)
365
+ return [];
366
+ const processed = [];
367
+ let currentGroupTimestamp;
368
+ for (let i = 0; i < messages.length; i++) {
369
+ const msg = messages[i];
370
+ const prev = i > 0 ? messages[i - 1] : null;
371
+ const next = i < messages.length - 1 ? messages[i + 1] : null;
372
+ // Determine if this is first from author
373
+ const isFirstFromAuthor = !this._canGroup(prev, msg);
374
+ // Start of new group - initialize group timestamp
375
+ if (isFirstFromAuthor) {
376
+ currentGroupTimestamp = msg.timestamp;
377
+ }
378
+ else if (!currentGroupTimestamp && msg.timestamp) {
379
+ currentGroupTimestamp = msg.timestamp;
380
+ }
381
+ // Determine if this is last in group
382
+ const isLastInGroup = !next || !this._canGroup(msg, next);
383
+ processed.push({
384
+ ...msg,
385
+ isFirstFromAuthor,
386
+ isLastInGroup,
387
+ groupTimestamp: isLastInGroup ? currentGroupTimestamp : undefined,
388
+ });
389
+ // Reset group timestamp at end of group
390
+ if (isLastInGroup) {
391
+ currentGroupTimestamp = undefined;
392
+ }
393
+ }
394
+ return processed;
395
+ }
396
+ /**
397
+ * Check if two messages can be grouped together
398
+ */
399
+ _canGroup(prev, curr) {
400
+ if (!prev)
401
+ return false;
402
+ if (prev.author !== curr.author)
403
+ return false;
404
+ if (prev.timestamp && curr.timestamp && prev.timestamp !== curr.timestamp) {
405
+ return false;
406
+ }
407
+ return true;
408
+ }
409
+ /**
410
+ * Handle scroll button click
411
+ */
412
+ _onScrollButtonClick() {
413
+ this._scrollController.scrollToBottom();
414
+ }
415
+ render() {
416
+ const hasMessages = this._processedMessages.length > 0;
417
+ return html `
418
+ <div class="history-container" role="log" aria-live="polite" aria-label="Message history">
419
+ ${hasMessages
420
+ ? this._processedMessages.map((msg, index) => html `
421
+ <bb-message
422
+ .author=${msg.author}
423
+ .text=${msg.text}
424
+ .timestamp=${msg.groupTimestamp || ''}
425
+ ?subsequent=${!msg.isFirstFromAuthor}
426
+ ?lastInGroup=${msg.isLastInGroup}
427
+ data-index="${index}"
428
+ ></bb-message>
429
+ `)
430
+ : html ` <div class="empty-state">${this.loading ? '' : 'No messages'}</div> `}
431
+ </div>
432
+
433
+ ${!this.hideScrollButton && !this.infinite
434
+ ? html `
435
+ <bb-scroll-button
436
+ .visible=${this._scrollController.isVisible}
437
+ @bb-scroll-to-bottom=${this._onScrollButtonClick}
438
+ ></bb-scroll-button>
439
+ `
440
+ : nothing}
441
+ ${this.loading ? html `<bb-loading-overlay visible></bb-loading-overlay>` : nothing}
442
+ `;
443
+ }
444
+ };
445
+ __decorate([
446
+ property({ type: Boolean, reflect: true })
447
+ ], BBMsgHistory.prototype, "loading", void 0);
448
+ __decorate([
449
+ property({ type: Boolean, reflect: true, attribute: 'hide-scroll-bar' })
450
+ ], BBMsgHistory.prototype, "hideScrollBar", void 0);
451
+ __decorate([
452
+ property({ type: Boolean, reflect: true })
453
+ ], BBMsgHistory.prototype, "infinite", void 0);
454
+ __decorate([
455
+ property({ type: Boolean, reflect: true, attribute: 'hide-scroll-button' })
456
+ ], BBMsgHistory.prototype, "hideScrollButton", void 0);
457
+ __decorate([
458
+ property({ reflect: true })
459
+ ], BBMsgHistory.prototype, "theme", void 0);
460
+ __decorate([
461
+ state()
462
+ ], BBMsgHistory.prototype, "_messages", void 0);
463
+ __decorate([
464
+ state()
465
+ ], BBMsgHistory.prototype, "_processedMessages", void 0);
466
+ __decorate([
467
+ provide({ context: authorContext }),
468
+ state()
469
+ ], BBMsgHistory.prototype, "_userAuthors", void 0);
470
+ BBMsgHistory = __decorate([
471
+ customElement('bb-msg-history')
472
+ ], BBMsgHistory);
473
+ export { BBMsgHistory };
@@ -0,0 +1,16 @@
1
+ import { LitElement } from 'lit';
2
+ /**
3
+ * Scroll-to-bottom button component
4
+ * Emits bb-scroll-to-bottom event when clicked
5
+ */
6
+ export declare class BBScrollButton extends LitElement {
7
+ static styles: import("lit").CSSResult;
8
+ visible: boolean;
9
+ private _handleClick;
10
+ render(): import("lit").TemplateResult<1>;
11
+ }
12
+ declare global {
13
+ interface HTMLElementTagNameMap {
14
+ 'bb-scroll-button': BBScrollButton;
15
+ }
16
+ }
@@ -0,0 +1,161 @@
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
+ * Scroll-to-bottom button component
12
+ * Emits bb-scroll-to-bottom event when clicked
13
+ */
14
+ let BBScrollButton = class BBScrollButton extends LitElement {
15
+ constructor() {
16
+ super(...arguments);
17
+ this.visible = false;
18
+ }
19
+ static { this.styles = css `
20
+ :host {
21
+ display: block;
22
+ position: absolute;
23
+ bottom: 16px;
24
+ left: 50%;
25
+ transform: translateX(-50%);
26
+ z-index: 10;
27
+ }
28
+
29
+ .scroll-btn {
30
+ width: 36px;
31
+ height: 36px;
32
+ border-radius: 50%;
33
+ background: #ffffff;
34
+ border: none;
35
+ color: ${unsafeCSS(THEME.gray[500])};
36
+ cursor: pointer;
37
+ display: flex;
38
+ align-items: center;
39
+ justify-content: center;
40
+ opacity: 0;
41
+ visibility: hidden;
42
+ transform: translateY(10px) scale(0);
43
+ transition:
44
+ opacity 0.2s ease,
45
+ transform 0.2s ease,
46
+ visibility 0.2s ease,
47
+ box-shadow 0.2s ease;
48
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
49
+ }
50
+
51
+ .scroll-btn.visible {
52
+ opacity: 1;
53
+ visibility: visible;
54
+ transform: translateY(0) scale(1);
55
+ }
56
+
57
+ .scroll-btn:hover {
58
+ color: ${unsafeCSS(THEME.gray[700])};
59
+ transform: translateY(-2px) scale(1.05);
60
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
61
+ }
62
+
63
+ .scroll-btn:active {
64
+ transform: translateY(-1px) scale(0.95);
65
+ }
66
+
67
+ .scroll-btn svg {
68
+ width: 20px;
69
+ height: 20px;
70
+ }
71
+
72
+ @media (max-width: 480px) {
73
+ :host {
74
+ bottom: 12px;
75
+ }
76
+
77
+ .scroll-btn {
78
+ width: 32px;
79
+ height: 32px;
80
+ }
81
+
82
+ .scroll-btn svg {
83
+ width: 18px;
84
+ height: 18px;
85
+ }
86
+ }
87
+
88
+ @media (prefers-reduced-motion: reduce) {
89
+ .scroll-btn {
90
+ transition:
91
+ opacity 0.15s ease,
92
+ visibility 0.15s ease;
93
+ transform: translateY(10px) scale(0);
94
+ }
95
+
96
+ .scroll-btn.visible {
97
+ transform: translateY(0) scale(1);
98
+ }
99
+
100
+ .scroll-btn:hover {
101
+ transform: translateY(-2px) scale(1);
102
+ }
103
+
104
+ .scroll-btn:active {
105
+ transform: translateY(0) scale(0.95);
106
+ }
107
+ }
108
+
109
+ /* Dark mode */
110
+ @media (prefers-color-scheme: dark) {
111
+ .scroll-btn {
112
+ background: ${unsafeCSS(THEME.slate[800])};
113
+ color: ${unsafeCSS(THEME.slate[300])};
114
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
115
+ }
116
+
117
+ .scroll-btn:hover {
118
+ color: ${unsafeCSS(THEME.slate[200])};
119
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
120
+ }
121
+ }
122
+ `; }
123
+ _handleClick() {
124
+ this.dispatchEvent(new CustomEvent('bb-scroll-to-bottom', {
125
+ bubbles: true,
126
+ composed: true,
127
+ detail: { behavior: 'smooth' },
128
+ }));
129
+ }
130
+ render() {
131
+ return html `
132
+ <button
133
+ class="scroll-btn ${this.visible ? 'visible' : ''}"
134
+ @click=${this._handleClick}
135
+ aria-label="Scroll to bottom"
136
+ title="Scroll to bottom"
137
+ >
138
+ <svg
139
+ xmlns="http://www.w3.org/2000/svg"
140
+ width="20"
141
+ height="20"
142
+ viewBox="0 0 24 24"
143
+ fill="none"
144
+ stroke="currentColor"
145
+ stroke-width="2"
146
+ stroke-linecap="round"
147
+ stroke-linejoin="round"
148
+ >
149
+ <polyline points="6 9 12 15 18 9"></polyline>
150
+ </svg>
151
+ </button>
152
+ `;
153
+ }
154
+ };
155
+ __decorate([
156
+ property({ type: Boolean, reflect: true })
157
+ ], BBScrollButton.prototype, "visible", void 0);
158
+ BBScrollButton = __decorate([
159
+ customElement('bb-scroll-button')
160
+ ], BBScrollButton);
161
+ export { BBScrollButton };
@@ -0,0 +1,15 @@
1
+ import { LitElement } from 'lit';
2
+ /**
3
+ * Timestamp component - displays message timestamp
4
+ */
5
+ export declare class BBTimestamp extends LitElement {
6
+ static styles: import("lit").CSSResult;
7
+ value: string;
8
+ side: 'left' | 'right';
9
+ render(): import("lit").TemplateResult<1>;
10
+ }
11
+ declare global {
12
+ interface HTMLElementTagNameMap {
13
+ 'bb-timestamp': BBTimestamp;
14
+ }
15
+ }