@bbki.ng/bb-msg-history 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -101,6 +101,35 @@ Returns `this` for chaining. This is ideal for chat applications where messages
101
101
 
102
102
  **Note:** Unlike modifying `textContent` directly, `appendMessage()` scrolls smoothly to the newly added message.
103
103
 
104
+ ### `setLoading(isLoading)`
105
+
106
+ Show or hide a loading animation overlay. Useful when fetching messages from an API.
107
+
108
+ | Parameter | Type | Description |
109
+ |-----------|------|-------------|
110
+ | `isLoading` | `boolean` | `true` to show loading, `false` to hide |
111
+
112
+ ```js
113
+ const el = document.querySelector('bb-msg-history');
114
+
115
+ // Show loading
116
+ el.setLoading(true);
117
+
118
+ // Fetch messages
119
+ fetchMessages().then(messages => {
120
+ // Hide loading and display messages
121
+ el.setLoading(false);
122
+ });
123
+ ```
124
+
125
+ You can also use the HTML attribute:
126
+
127
+ ```html
128
+ <bb-msg-history loading>
129
+ alice: Loading previous messages...
130
+ </bb-msg-history>
131
+ ```
132
+
104
133
  ## Customization
105
134
 
106
135
  ### CSS Custom Properties
@@ -138,6 +167,7 @@ define('my-chat-history');
138
167
  - Consecutive messages from the same author are grouped (avatar hidden)
139
168
  - Auto-scroll to the latest message on render
140
169
  - **`appendMessage()` API** — programmatically add messages with smooth scroll
170
+ - **`setLoading()` API** — show loading animation while fetching messages
141
171
  - Long text word-wrap and overflow handling
142
172
  - Empty state when no messages are provided
143
173
  - Dark mode support via `prefers-color-scheme`
@@ -234,6 +264,34 @@ Use `appendMessage()` to add messages programmatically with smooth scrolling:
234
264
  </script>
235
265
  ```
236
266
 
267
+ ### Loading state
268
+
269
+ Show a loading animation while fetching messages from an API:
270
+
271
+ ```html
272
+ <bb-msg-history id="chat" loading>
273
+ <!-- Messages will be loaded -->
274
+ </bb-msg-history>
275
+
276
+ <script>
277
+ const el = document.getElementById('chat');
278
+
279
+ // Show loading (already set via HTML attribute above)
280
+ // el.setLoading(true);
281
+
282
+ // Fetch messages from API
283
+ fetch('/api/messages')
284
+ .then(res => res.json())
285
+ .then(messages => {
286
+ // Hide loading and populate messages
287
+ el.setLoading(false);
288
+ messages.forEach(msg => {
289
+ el.appendMessage({ author: msg.author, text: msg.text });
290
+ });
291
+ });
292
+ </script>
293
+ ```
294
+
237
295
  ### Full page example
238
296
 
239
297
  ```html
@@ -7,7 +7,7 @@ export declare class BBMsgHistory extends HTMLElement {
7
7
  private _scrollButtonVisible;
8
8
  static get observedAttributes(): string[];
9
9
  constructor();
10
- attributeChangedCallback(): void;
10
+ attributeChangedCallback(name: string): void;
11
11
  /**
12
12
  * Configure an author's avatar, side, and colors.
13
13
  * Call before or after rendering — the component re-renders automatically.
@@ -21,6 +21,14 @@ export declare class BBMsgHistory extends HTMLElement {
21
21
  * Remove a previously set author config.
22
22
  */
23
23
  removeAuthor(name: string): this;
24
+ /**
25
+ * Show or hide the loading overlay.
26
+ *
27
+ * @example
28
+ * el.setLoading(true); // Show loading animation
29
+ * el.setLoading(false); // Hide loading animation
30
+ */
31
+ setLoading(isLoading: boolean): this;
24
32
  /**
25
33
  * Append a message to the history.
26
34
  * Automatically scrolls to the new message with smooth animation.
package/dist/component.js CHANGED
@@ -1,4 +1,4 @@
1
- import { MAIN_STYLES, EMPTY_STYLES } from './const/styles.js';
1
+ import { EMPTY_STYLES, LOADING_STYLES, MAIN_STYLES } from './const/styles.js';
2
2
  import { parseMessages } from './utils/message-parser.js';
3
3
  import { resolveAuthorConfig } from './utils/author-resolver.js';
4
4
  import { setupTooltips } from './utils/tooltip.js';
@@ -6,7 +6,7 @@ import { buildMessageRowHtml, setupTooltipForElement } from './utils/message-bui
6
6
  import { buildScrollButtonHtml } from './utils/scroll-button.js';
7
7
  export class BBMsgHistory extends HTMLElement {
8
8
  static get observedAttributes() {
9
- return ['theme'];
9
+ return ['theme', 'loading'];
10
10
  }
11
11
  constructor() {
12
12
  super();
@@ -15,8 +15,10 @@ export class BBMsgHistory extends HTMLElement {
15
15
  this._scrollButtonVisible = false;
16
16
  this.attachShadow({ mode: 'open' });
17
17
  }
18
- attributeChangedCallback() {
19
- this.render();
18
+ attributeChangedCallback(name) {
19
+ if (name === 'theme' || name === 'loading') {
20
+ this.render();
21
+ }
20
22
  }
21
23
  /**
22
24
  * Configure an author's avatar, side, and colors.
@@ -39,6 +41,17 @@ export class BBMsgHistory extends HTMLElement {
39
41
  this.render();
40
42
  return this;
41
43
  }
44
+ /**
45
+ * Show or hide the loading overlay.
46
+ *
47
+ * @example
48
+ * el.setLoading(true); // Show loading animation
49
+ * el.setLoading(false); // Hide loading animation
50
+ */
51
+ setLoading(isLoading) {
52
+ this.toggleAttribute('loading', isLoading);
53
+ return this;
54
+ }
42
55
  /**
43
56
  * Append a message to the history.
44
57
  * Automatically scrolls to the new message with smooth animation.
@@ -191,12 +204,18 @@ export class BBMsgHistory extends HTMLElement {
191
204
  })
192
205
  .join('');
193
206
  this._lastAuthor = lastAuthor;
207
+ const loadingOverlay = this.hasAttribute('loading')
208
+ ? `<div class="loading-overlay" role="status" aria-label="Loading messages">
209
+ <div class="loading-spinner"></div>
210
+ </div>`
211
+ : '';
194
212
  this.shadowRoot.innerHTML = `
195
- <style>${MAIN_STYLES}</style>
213
+ <style>${MAIN_STYLES}${LOADING_STYLES}</style>
196
214
  <div class="history" role="log" aria-live="polite" aria-label="Message history">
197
215
  ${messagesHtml}
198
216
  </div>
199
217
  ${buildScrollButtonHtml()}
218
+ ${loadingOverlay}
200
219
  `;
201
220
  requestAnimationFrame(() => {
202
221
  const container = this.shadowRoot.querySelector('.history');
@@ -217,10 +236,24 @@ export class BBMsgHistory extends HTMLElement {
217
236
  });
218
237
  }
219
238
  _renderEmpty() {
220
- this.shadowRoot.innerHTML = `
221
- <style>${EMPTY_STYLES}</style>
222
- <div class="empty-state">No messages</div>
223
- `;
239
+ const isLoading = this.hasAttribute('loading');
240
+ if (isLoading) {
241
+ // Show loading overlay with minimum height for better appearance
242
+ this.shadowRoot.innerHTML = `
243
+ <style>${EMPTY_STYLES}${LOADING_STYLES}</style>
244
+ <div style="position: relative; min-height: 120px;">
245
+ <div class="loading-overlay" role="status" aria-label="Loading messages">
246
+ <div class="loading-spinner"></div>
247
+ </div>
248
+ </div>
249
+ `;
250
+ }
251
+ else {
252
+ this.shadowRoot.innerHTML = `
253
+ <style>${EMPTY_STYLES}</style>
254
+ <div class="empty-state">No messages</div>
255
+ `;
256
+ }
224
257
  }
225
258
  _setupScrollTracking(container, button) {
226
259
  const checkScrollPosition = () => {
@@ -6,7 +6,7 @@ import { THEME } from './theme.js';
6
6
  export const AUTHOR_CONFIG = {
7
7
  'bbki.ng': {
8
8
  avatar: `<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 48 48" fill="none"><path d="M29.1152 21.3106C32.0605 21.3106 34.4481 18.9101 34.4481 15.9489V24.6457C34.4481 25.7585 33.5508 26.6607 32.444 26.6607H15.1207C14.0138 26.6607 13.1166 25.7585 13.1166 24.6457V15.9489C13.1166 18.9101 15.5042 21.3106 18.4494 21.3106C21.3947 21.3106 23.7823 18.9101 23.7823 15.9489C23.7823 18.9101 26.17 21.3106 29.1152 21.3106Z" fill="${THEME.gray[400]}"/><path d="M23.7823 15.9373L23.7823 15.9489C23.7823 15.9451 23.7823 15.9412 23.7823 15.9373Z" fill="${THEME.gray[400]}"/><path d="M23.1143 28.004C23.1205 30.9598 25.5057 33.3541 28.4472 33.3541C31.3886 33.3541 33.7738 30.9598 33.7801 28.004H23.1143Z" fill="${THEME.gray[400]}"/><path d="M13.7846 28.004C13.7846 28.0079 13.7846 28.0117 13.7846 28.0156C13.7908 30.9714 16.1761 33.3657 19.1175 33.3657C22.0589 33.3657 24.4442 30.9714 24.4504 28.0156H13.7846V28.004Z" fill="${THEME.gray[400]}"/><path d="M14.4527 15.9373C14.4527 16.6792 13.8545 17.2806 13.1166 17.2806C12.3786 17.2806 11.7805 16.6792 11.7805 15.9373C11.7805 15.1954 12.3786 14.594 13.1166 14.594C13.8545 14.594 14.4527 15.1954 14.4527 15.9373Z" fill="${THEME.gray[400]}"/><path d="M25.1184 15.2657C25.1184 16.0076 24.5202 16.609 23.7823 16.609C23.0444 16.609 22.4462 16.0076 22.4462 15.2657C22.4462 14.5238 23.0444 13.9224 23.7823 13.9224C24.5202 13.9224 25.1184 14.5238 25.1184 15.2657Z" fill="${THEME.gray[400]}"/><path d="M35.7842 15.9373C35.7842 16.6792 35.186 17.2806 34.4481 17.2806C33.7102 17.2806 33.112 16.6792 33.112 15.9373C33.112 15.1954 33.7102 14.594 34.4481 14.594C35.186 14.594 35.7842 15.1954 35.7842 15.9373Z" fill="${THEME.gray[400]}"/></svg>`,
9
- bubbleColor: THEME.gray[100],
9
+ bubbleColor: THEME.gray[200],
10
10
  textColor: THEME.gray[900],
11
11
  side: 'right',
12
12
  },
@@ -6,6 +6,10 @@ export declare const MAIN_STYLES: string;
6
6
  * Empty state styles
7
7
  */
8
8
  export declare const EMPTY_STYLES: string;
9
+ /**
10
+ * Loading overlay styles with elegant spinner
11
+ */
12
+ export declare const LOADING_STYLES: string;
9
13
  /**
10
14
  * Fallback styles for when custom elements are not supported
11
15
  */
@@ -193,6 +193,7 @@ export const MAIN_STYLES = `
193
193
  display: flex;
194
194
  flex-direction: column;
195
195
  position: relative;
196
+ padding-bottom: 12px;
196
197
  }
197
198
 
198
199
  /* Timestamp styles */
@@ -204,7 +205,7 @@ export const MAIN_STYLES = `
204
205
  visibility: hidden;
205
206
  transition: opacity 0.2s ease, visibility 0.2s ease;
206
207
  white-space: nowrap;
207
- bottom: -12px;
208
+ bottom: 1px;
208
209
  line-height: 1;
209
210
  pointer-events: none;
210
211
  }
@@ -245,6 +246,15 @@ export const MAIN_STYLES = `
245
246
  color: ${THEME.gray[900]};
246
247
  }
247
248
 
249
+ /* Subsequent messages - all corners rounded */
250
+ .msg-row--subsequent .msg-bubble--left {
251
+ border-bottom-left-radius: 1rem;
252
+ }
253
+
254
+ .msg-row--subsequent .msg-bubble--right {
255
+ border-bottom-right-radius: 1rem;
256
+ }
257
+
248
258
  /* Empty state */
249
259
  .empty-state {
250
260
  text-align: center;
@@ -434,6 +444,68 @@ export const EMPTY_STYLES = `
434
444
  font-family: inherit;
435
445
  }
436
446
  `;
447
+ /**
448
+ * Loading overlay styles with elegant spinner
449
+ */
450
+ export const LOADING_STYLES = `
451
+ .loading-overlay {
452
+ position: absolute;
453
+ inset: 0;
454
+ display: flex;
455
+ align-items: center;
456
+ justify-content: center;
457
+ background: rgba(255, 255, 255, 0.6);
458
+ backdrop-filter: blur(1px);
459
+ z-index: 20;
460
+ border-radius: 0.5rem;
461
+ min-height: 120px;
462
+ }
463
+
464
+ .loading-spinner {
465
+ width: 24px;
466
+ height: 24px;
467
+ border: 2px solid ${THEME.gray[200]};
468
+ border-top-color: ${THEME.gray[500]};
469
+ border-radius: 50%;
470
+ animation: spin 0.8s linear infinite;
471
+ }
472
+
473
+ @keyframes spin {
474
+ to {
475
+ transform: rotate(360deg);
476
+ }
477
+ }
478
+
479
+ /* Dark mode support */
480
+ :host([theme="dark"]) .loading-overlay {
481
+ background: rgba(17, 24, 39, 0.6);
482
+ }
483
+
484
+ :host([theme="dark"]) .loading-spinner {
485
+ border-color: ${THEME.gray[700]};
486
+ border-top-color: ${THEME.gray[400]};
487
+ }
488
+
489
+ /* System dark mode preference */
490
+ @media (prefers-color-scheme: dark) {
491
+ :host .loading-overlay {
492
+ background: rgba(17, 24, 39, 0.6);
493
+ }
494
+
495
+ :host .loading-spinner {
496
+ border-color: ${THEME.gray[700]};
497
+ border-top-color: ${THEME.gray[400]};
498
+ }
499
+ }
500
+
501
+ /* Reduced motion */
502
+ @media (prefers-reduced-motion: reduce) {
503
+ .loading-spinner {
504
+ animation-duration: 1.5s;
505
+ opacity: 0.8;
506
+ }
507
+ }
508
+ `;
437
509
  /**
438
510
  * Fallback styles for when custom elements are not supported
439
511
  */
@@ -43,10 +43,10 @@ export function buildMessageRowHtml(author, text, config, isSubsequent, timestam
43
43
  ${side === 'left' ? avatarHtml : ''}
44
44
 
45
45
  <div class="msg-content">
46
- ${timestampHtml}
47
46
  <div class="msg-bubble msg-bubble--${side}"${styleAttr}>
48
47
  ${escapeHtml(text)}
49
48
  </div>
49
+ ${timestampHtml}
50
50
  </div>
51
51
 
52
52
  ${side === 'right' ? avatarHtml : ''}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bbki.ng/bb-msg-history",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "A chat-style message history web component",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
package/src/component.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { AuthorOptions, Message } from './types/index.js';
2
- import { MAIN_STYLES, EMPTY_STYLES } from './const/styles.js';
2
+ import { EMPTY_STYLES, LOADING_STYLES, MAIN_STYLES } from './const/styles.js';
3
3
  import { parseMessages } from './utils/message-parser.js';
4
4
  import { resolveAuthorConfig } from './utils/author-resolver.js';
5
5
  import { setupTooltips } from './utils/tooltip.js';
@@ -14,7 +14,7 @@ export class BBMsgHistory extends HTMLElement {
14
14
  private _scrollButtonVisible = false;
15
15
 
16
16
  static get observedAttributes() {
17
- return ['theme'];
17
+ return ['theme', 'loading'];
18
18
  }
19
19
 
20
20
  constructor() {
@@ -22,8 +22,10 @@ export class BBMsgHistory extends HTMLElement {
22
22
  this.attachShadow({ mode: 'open' });
23
23
  }
24
24
 
25
- attributeChangedCallback() {
26
- this.render();
25
+ attributeChangedCallback(name: string) {
26
+ if (name === 'theme' || name === 'loading') {
27
+ this.render();
28
+ }
27
29
  }
28
30
 
29
31
  /**
@@ -49,6 +51,18 @@ export class BBMsgHistory extends HTMLElement {
49
51
  return this;
50
52
  }
51
53
 
54
+ /**
55
+ * Show or hide the loading overlay.
56
+ *
57
+ * @example
58
+ * el.setLoading(true); // Show loading animation
59
+ * el.setLoading(false); // Hide loading animation
60
+ */
61
+ setLoading(isLoading: boolean): this {
62
+ this.toggleAttribute('loading', isLoading);
63
+ return this;
64
+ }
65
+
52
66
  /**
53
67
  * Append a message to the history.
54
68
  * Automatically scrolls to the new message with smooth animation.
@@ -246,12 +260,19 @@ export class BBMsgHistory extends HTMLElement {
246
260
 
247
261
  this._lastAuthor = lastAuthor;
248
262
 
263
+ const loadingOverlay = this.hasAttribute('loading')
264
+ ? `<div class="loading-overlay" role="status" aria-label="Loading messages">
265
+ <div class="loading-spinner"></div>
266
+ </div>`
267
+ : '';
268
+
249
269
  this.shadowRoot!.innerHTML = `
250
- <style>${MAIN_STYLES}</style>
270
+ <style>${MAIN_STYLES}${LOADING_STYLES}</style>
251
271
  <div class="history" role="log" aria-live="polite" aria-label="Message history">
252
272
  ${messagesHtml}
253
273
  </div>
254
274
  ${buildScrollButtonHtml()}
275
+ ${loadingOverlay}
255
276
  `;
256
277
 
257
278
  requestAnimationFrame(() => {
@@ -277,10 +298,24 @@ export class BBMsgHistory extends HTMLElement {
277
298
  }
278
299
 
279
300
  private _renderEmpty() {
280
- this.shadowRoot!.innerHTML = `
281
- <style>${EMPTY_STYLES}</style>
282
- <div class="empty-state">No messages</div>
283
- `;
301
+ const isLoading = this.hasAttribute('loading');
302
+
303
+ if (isLoading) {
304
+ // Show loading overlay with minimum height for better appearance
305
+ this.shadowRoot!.innerHTML = `
306
+ <style>${EMPTY_STYLES}${LOADING_STYLES}</style>
307
+ <div style="position: relative; min-height: 120px;">
308
+ <div class="loading-overlay" role="status" aria-label="Loading messages">
309
+ <div class="loading-spinner"></div>
310
+ </div>
311
+ </div>
312
+ `;
313
+ } else {
314
+ this.shadowRoot!.innerHTML = `
315
+ <style>${EMPTY_STYLES}</style>
316
+ <div class="empty-state">No messages</div>
317
+ `;
318
+ }
284
319
  }
285
320
 
286
321
  private _setupScrollTracking(container: HTMLElement, button: HTMLButtonElement): void {
@@ -8,7 +8,7 @@ import { THEME } from './theme.js';
8
8
  export const AUTHOR_CONFIG: Record<string, Omit<AuthorConfig, 'isCustomAvatar'>> = {
9
9
  'bbki.ng': {
10
10
  avatar: `<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 48 48" fill="none"><path d="M29.1152 21.3106C32.0605 21.3106 34.4481 18.9101 34.4481 15.9489V24.6457C34.4481 25.7585 33.5508 26.6607 32.444 26.6607H15.1207C14.0138 26.6607 13.1166 25.7585 13.1166 24.6457V15.9489C13.1166 18.9101 15.5042 21.3106 18.4494 21.3106C21.3947 21.3106 23.7823 18.9101 23.7823 15.9489C23.7823 18.9101 26.17 21.3106 29.1152 21.3106Z" fill="${THEME.gray[400]}"/><path d="M23.7823 15.9373L23.7823 15.9489C23.7823 15.9451 23.7823 15.9412 23.7823 15.9373Z" fill="${THEME.gray[400]}"/><path d="M23.1143 28.004C23.1205 30.9598 25.5057 33.3541 28.4472 33.3541C31.3886 33.3541 33.7738 30.9598 33.7801 28.004H23.1143Z" fill="${THEME.gray[400]}"/><path d="M13.7846 28.004C13.7846 28.0079 13.7846 28.0117 13.7846 28.0156C13.7908 30.9714 16.1761 33.3657 19.1175 33.3657C22.0589 33.3657 24.4442 30.9714 24.4504 28.0156H13.7846V28.004Z" fill="${THEME.gray[400]}"/><path d="M14.4527 15.9373C14.4527 16.6792 13.8545 17.2806 13.1166 17.2806C12.3786 17.2806 11.7805 16.6792 11.7805 15.9373C11.7805 15.1954 12.3786 14.594 13.1166 14.594C13.8545 14.594 14.4527 15.1954 14.4527 15.9373Z" fill="${THEME.gray[400]}"/><path d="M25.1184 15.2657C25.1184 16.0076 24.5202 16.609 23.7823 16.609C23.0444 16.609 22.4462 16.0076 22.4462 15.2657C22.4462 14.5238 23.0444 13.9224 23.7823 13.9224C24.5202 13.9224 25.1184 14.5238 25.1184 15.2657Z" fill="${THEME.gray[400]}"/><path d="M35.7842 15.9373C35.7842 16.6792 35.186 17.2806 34.4481 17.2806C33.7102 17.2806 33.112 16.6792 33.112 15.9373C33.112 15.1954 33.7102 14.594 34.4481 14.594C35.186 14.594 35.7842 15.1954 35.7842 15.9373Z" fill="${THEME.gray[400]}"/></svg>`,
11
- bubbleColor: THEME.gray[100],
11
+ bubbleColor: THEME.gray[200],
12
12
  textColor: THEME.gray[900],
13
13
  side: 'right',
14
14
  },
@@ -194,6 +194,7 @@ export const MAIN_STYLES = `
194
194
  display: flex;
195
195
  flex-direction: column;
196
196
  position: relative;
197
+ padding-bottom: 12px;
197
198
  }
198
199
 
199
200
  /* Timestamp styles */
@@ -205,7 +206,7 @@ export const MAIN_STYLES = `
205
206
  visibility: hidden;
206
207
  transition: opacity 0.2s ease, visibility 0.2s ease;
207
208
  white-space: nowrap;
208
- bottom: -12px;
209
+ bottom: 1px;
209
210
  line-height: 1;
210
211
  pointer-events: none;
211
212
  }
@@ -246,6 +247,15 @@ export const MAIN_STYLES = `
246
247
  color: ${THEME.gray[900]};
247
248
  }
248
249
 
250
+ /* Subsequent messages - all corners rounded */
251
+ .msg-row--subsequent .msg-bubble--left {
252
+ border-bottom-left-radius: 1rem;
253
+ }
254
+
255
+ .msg-row--subsequent .msg-bubble--right {
256
+ border-bottom-right-radius: 1rem;
257
+ }
258
+
249
259
  /* Empty state */
250
260
  .empty-state {
251
261
  text-align: center;
@@ -437,6 +447,69 @@ export const EMPTY_STYLES = `
437
447
  }
438
448
  `;
439
449
 
450
+ /**
451
+ * Loading overlay styles with elegant spinner
452
+ */
453
+ export const LOADING_STYLES = `
454
+ .loading-overlay {
455
+ position: absolute;
456
+ inset: 0;
457
+ display: flex;
458
+ align-items: center;
459
+ justify-content: center;
460
+ background: rgba(255, 255, 255, 0.6);
461
+ backdrop-filter: blur(1px);
462
+ z-index: 20;
463
+ border-radius: 0.5rem;
464
+ min-height: 120px;
465
+ }
466
+
467
+ .loading-spinner {
468
+ width: 24px;
469
+ height: 24px;
470
+ border: 2px solid ${THEME.gray[200]};
471
+ border-top-color: ${THEME.gray[500]};
472
+ border-radius: 50%;
473
+ animation: spin 0.8s linear infinite;
474
+ }
475
+
476
+ @keyframes spin {
477
+ to {
478
+ transform: rotate(360deg);
479
+ }
480
+ }
481
+
482
+ /* Dark mode support */
483
+ :host([theme="dark"]) .loading-overlay {
484
+ background: rgba(17, 24, 39, 0.6);
485
+ }
486
+
487
+ :host([theme="dark"]) .loading-spinner {
488
+ border-color: ${THEME.gray[700]};
489
+ border-top-color: ${THEME.gray[400]};
490
+ }
491
+
492
+ /* System dark mode preference */
493
+ @media (prefers-color-scheme: dark) {
494
+ :host .loading-overlay {
495
+ background: rgba(17, 24, 39, 0.6);
496
+ }
497
+
498
+ :host .loading-spinner {
499
+ border-color: ${THEME.gray[700]};
500
+ border-top-color: ${THEME.gray[400]};
501
+ }
502
+ }
503
+
504
+ /* Reduced motion */
505
+ @media (prefers-reduced-motion: reduce) {
506
+ .loading-spinner {
507
+ animation-duration: 1.5s;
508
+ opacity: 0.8;
509
+ }
510
+ }
511
+ `;
512
+
440
513
  /**
441
514
  * Fallback styles for when custom elements are not supported
442
515
  */
@@ -58,10 +58,10 @@ export function buildMessageRowHtml(
58
58
  ${side === 'left' ? avatarHtml : ''}
59
59
 
60
60
  <div class="msg-content">
61
- ${timestampHtml}
62
61
  <div class="msg-bubble msg-bubble--${side}"${styleAttr}>
63
62
  ${escapeHtml(text)}
64
63
  </div>
64
+ ${timestampHtml}
65
65
  </div>
66
66
 
67
67
  ${side === 'right' ? avatarHtml : ''}