@bbki.ng/bb-msg-history 0.6.1 → 0.7.1

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,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
  */
@@ -444,6 +444,68 @@ export const EMPTY_STYLES = `
444
444
  font-family: inherit;
445
445
  }
446
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
+ `;
447
509
  /**
448
510
  * Fallback styles for when custom elements are not supported
449
511
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bbki.ng/bb-msg-history",
3
- "version": "0.6.1",
3
+ "version": "0.7.1",
4
4
  "description": "A chat-style message history web component",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -23,19 +23,6 @@
23
23
  "dist",
24
24
  "src"
25
25
  ],
26
- "scripts": {
27
- "start": "tsc -w",
28
- "preview": "python3 -m http.server 8000",
29
- "prepare": "tsc && cp dist/index.js dist/index.dev.js && terser dist/index.js --compress --mangle --source-map -o dist/index.js",
30
- "build": "tsc && cp dist/index.js dist/index.dev.js && terser dist/index.js --compress --mangle --source-map -o dist/index.js",
31
- "lint": "eslint src/**/*.ts",
32
- "lint:fix": "eslint src/**/*.ts --fix",
33
- "format": "prettier --write \"src/**/*.ts\"",
34
- "format:check": "prettier --check \"src/**/*.ts\"",
35
- "test": "vitest run",
36
- "test:watch": "vitest",
37
- "release": "release-it"
38
- },
39
26
  "lint-staged": {
40
27
  "*.ts": [
41
28
  "eslint --fix",
@@ -60,5 +47,17 @@
60
47
  "terser": "^5.46.0",
61
48
  "typescript": "^5.9.3",
62
49
  "vitest": "^3.2.4"
50
+ },
51
+ "scripts": {
52
+ "start": "tsc -w",
53
+ "preview": "python3 -m http.server 8000",
54
+ "build": "tsc && cp dist/index.js dist/index.dev.js && terser dist/index.js --compress --mangle --source-map -o dist/index.js",
55
+ "lint": "eslint src/**/*.ts",
56
+ "lint:fix": "eslint src/**/*.ts --fix",
57
+ "format": "prettier --write \"src/**/*.ts\"",
58
+ "format:check": "prettier --check \"src/**/*.ts\"",
59
+ "test": "vitest run",
60
+ "test:watch": "vitest",
61
+ "release": "release-it"
63
62
  }
64
- }
63
+ }
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 {
@@ -447,6 +447,69 @@ export const EMPTY_STYLES = `
447
447
  }
448
448
  `;
449
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
+
450
513
  /**
451
514
  * Fallback styles for when custom elements are not supported
452
515
  */