@bbki.ng/bb-msg-history 0.3.0 → 0.5.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 (58) hide show
  1. package/dist/component.d.ts +4 -0
  2. package/dist/component.js +44 -1
  3. package/dist/const/authors.js +6 -6
  4. package/dist/const/styles.js +156 -6
  5. package/dist/const/theme.js +14 -2
  6. package/dist/index.dev.js +1 -12
  7. package/dist/index.js.map +1 -0
  8. package/dist/src/component.d.ts +39 -0
  9. package/dist/src/component.js +184 -0
  10. package/dist/src/const/authors.d.ts +10 -0
  11. package/dist/src/const/authors.js +29 -0
  12. package/dist/src/const/styles.d.ts +12 -0
  13. package/dist/src/const/styles.js +425 -0
  14. package/dist/src/const/theme.d.ts +6 -0
  15. package/dist/src/const/theme.js +44 -0
  16. package/dist/src/index.d.ts +9 -0
  17. package/dist/src/index.js +12 -0
  18. package/dist/src/types/index.d.ts +34 -0
  19. package/dist/src/types/index.js +4 -0
  20. package/dist/src/utils/author-resolver.d.ts +11 -0
  21. package/dist/src/utils/author-resolver.js +75 -0
  22. package/dist/src/utils/avatar.d.ts +4 -0
  23. package/dist/src/utils/avatar.js +18 -0
  24. package/dist/src/utils/html.d.ts +12 -0
  25. package/dist/src/utils/html.js +33 -0
  26. package/dist/src/utils/message-builder.d.ts +13 -0
  27. package/dist/src/utils/message-builder.js +60 -0
  28. package/dist/src/utils/message-parser.d.ts +6 -0
  29. package/dist/src/utils/message-parser.js +22 -0
  30. package/dist/src/utils/registration.d.ts +8 -0
  31. package/dist/src/utils/registration.js +28 -0
  32. package/dist/src/utils/scroll-button.d.ts +4 -0
  33. package/dist/src/utils/scroll-button.js +12 -0
  34. package/dist/src/utils/tooltip.d.ts +5 -0
  35. package/dist/src/utils/tooltip.js +17 -0
  36. package/dist/tests/html.test.d.ts +1 -0
  37. package/dist/tests/html.test.js +35 -0
  38. package/dist/types/index.d.ts +1 -0
  39. package/dist/utils/author-resolver.js +7 -3
  40. package/dist/utils/avatar.js +8 -8
  41. package/dist/utils/message-builder.js +15 -4
  42. package/dist/utils/registration.js +3 -4
  43. package/dist/utils/scroll-button.d.ts +4 -0
  44. package/dist/utils/scroll-button.js +12 -0
  45. package/dist/vitest.config.d.ts +2 -0
  46. package/dist/vitest.config.js +8 -0
  47. package/package.json +32 -7
  48. package/src/component.ts +67 -13
  49. package/src/const/authors.ts +6 -6
  50. package/src/const/styles.ts +156 -6
  51. package/src/const/theme.ts +14 -2
  52. package/src/types/index.ts +1 -0
  53. package/src/utils/author-resolver.ts +8 -4
  54. package/src/utils/avatar.ts +8 -8
  55. package/src/utils/message-builder.ts +19 -5
  56. package/src/utils/message-parser.ts +5 -5
  57. package/src/utils/registration.ts +5 -5
  58. package/src/utils/scroll-button.ts +12 -0
@@ -3,7 +3,10 @@ export declare class BBMsgHistory extends HTMLElement {
3
3
  private _mutationObserver?;
4
4
  private _userAuthors;
5
5
  private _lastAuthor;
6
+ private _scrollButtonVisible;
7
+ static get observedAttributes(): string[];
6
8
  constructor();
9
+ attributeChangedCallback(): void;
7
10
  /**
8
11
  * Configure an author's avatar, side, and colors.
9
12
  * Call before or after rendering — the component re-renders automatically.
@@ -32,4 +35,5 @@ export declare class BBMsgHistory extends HTMLElement {
32
35
  private _setupMutationObserver;
33
36
  private render;
34
37
  private _renderEmpty;
38
+ private _setupScrollTracking;
35
39
  }
package/dist/component.js CHANGED
@@ -3,13 +3,21 @@ import { parseMessages } from './utils/message-parser.js';
3
3
  import { resolveAuthorConfig } from './utils/author-resolver.js';
4
4
  import { setupTooltips } from './utils/tooltip.js';
5
5
  import { buildMessageRowHtml, setupTooltipForElement } from './utils/message-builder.js';
6
+ import { buildScrollButtonHtml } from './utils/scroll-button.js';
6
7
  export class BBMsgHistory extends HTMLElement {
8
+ static get observedAttributes() {
9
+ return ['theme'];
10
+ }
7
11
  constructor() {
8
12
  super();
9
13
  this._userAuthors = new Map();
10
14
  this._lastAuthor = '';
15
+ this._scrollButtonVisible = false;
11
16
  this.attachShadow({ mode: 'open' });
12
17
  }
18
+ attributeChangedCallback() {
19
+ this.render();
20
+ }
13
21
  /**
14
22
  * Configure an author's avatar, side, and colors.
15
23
  * Call before or after rendering — the component re-renders automatically.
@@ -77,8 +85,14 @@ export class BBMsgHistory extends HTMLElement {
77
85
  // Smooth scroll to bottom
78
86
  container.scrollTo({
79
87
  top: container.scrollHeight,
80
- behavior: 'smooth'
88
+ behavior: 'smooth',
81
89
  });
90
+ // Hide scroll button since we're scrolling to bottom
91
+ const scrollButton = this.shadowRoot.querySelector('.scroll-to-bottom');
92
+ if (scrollButton && this._scrollButtonVisible) {
93
+ this._scrollButtonVisible = false;
94
+ scrollButton.classList.remove('visible');
95
+ }
82
96
  }
83
97
  connectedCallback() {
84
98
  this.render();
@@ -123,11 +137,22 @@ export class BBMsgHistory extends HTMLElement {
123
137
  <div class="history" role="log" aria-live="polite" aria-label="Message history">
124
138
  ${messagesHtml}
125
139
  </div>
140
+ ${buildScrollButtonHtml()}
126
141
  `;
127
142
  requestAnimationFrame(() => {
128
143
  const container = this.shadowRoot.querySelector('.history');
144
+ const scrollButton = this.shadowRoot.querySelector('.scroll-to-bottom');
129
145
  if (container) {
130
146
  container.scrollTop = container.scrollHeight;
147
+ this._setupScrollTracking(container, scrollButton);
148
+ }
149
+ if (scrollButton) {
150
+ scrollButton.addEventListener('click', () => {
151
+ container?.scrollTo({
152
+ top: container.scrollHeight,
153
+ behavior: 'smooth',
154
+ });
155
+ });
131
156
  }
132
157
  setupTooltips(this.shadowRoot);
133
158
  });
@@ -138,4 +163,22 @@ export class BBMsgHistory extends HTMLElement {
138
163
  <div class="empty-state">No messages</div>
139
164
  `;
140
165
  }
166
+ _setupScrollTracking(container, button) {
167
+ const checkScrollPosition = () => {
168
+ const threshold = 50; // pixels from bottom
169
+ const isAtBottom = container.scrollHeight - container.scrollTop - container.clientHeight < threshold;
170
+ const hasOverflow = container.scrollHeight > container.clientHeight;
171
+ const shouldShow = !isAtBottom && hasOverflow;
172
+ if (shouldShow !== this._scrollButtonVisible) {
173
+ this._scrollButtonVisible = shouldShow;
174
+ button.classList.toggle('visible', shouldShow);
175
+ }
176
+ };
177
+ // Check initial state
178
+ checkScrollPosition();
179
+ // Listen for scroll events with passive listener for performance
180
+ container.addEventListener('scroll', checkScrollPosition, { passive: true });
181
+ // Also check on resize
182
+ window.addEventListener('resize', checkScrollPosition, { passive: true });
183
+ }
141
184
  }
@@ -8,20 +8,20 @@ export const AUTHOR_CONFIG = {
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
9
  bubbleColor: THEME.gray[100],
10
10
  textColor: THEME.gray[900],
11
- side: 'right'
11
+ side: 'right',
12
12
  },
13
- 'xwy': {
13
+ xwy: {
14
14
  avatar: `<svg width="28" height="28" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12.821 17.5305C10.709 18.17 9.68345 19.4423 9.22624 20.1359C9.11159 20.3099 9.21615 20.5428 9.42038 20.5839L12.67 21.2381C12.8291 21.2702 12.9328 21.4275 12.9084 21.5879C11.3004 32.1653 21.5275 36.7547 28.6638 33.0597C28.7443 33.018 28.8408 33.0139 28.9245 33.0487C32.8032 34.6598 35.967 34.5662 37.8217 34.3099C38.131 34.2671 38.1505 33.841 37.855 33.7401C29.1343 30.7633 26.0152 24.5245 25.5144 18.8022C25.3835 17.3066 23.8172 13.2016 19.2675 13.0058C15.7934 12.8563 13.6137 15.6103 13.0319 17.325C12.9986 17.4231 12.9201 17.5004 12.821 17.5305Z" fill="${THEME.yyPink[100]}"/><circle cx="17.6178" cy="18.2688" r="0.995689" fill="white"/></svg>`,
15
15
  bubbleColor: THEME.yyPink[50],
16
16
  textColor: THEME.gray[900],
17
- side: 'left'
17
+ side: 'left',
18
18
  },
19
- '小乌鸦': {
19
+ 小乌鸦: {
20
20
  avatar: `<svg width="28" height="28" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12.821 17.5305C10.709 18.17 9.68345 19.4423 9.22624 20.1359C9.11159 20.3099 9.21615 20.5428 9.42038 20.5839L12.67 21.2381C12.8291 21.2702 12.9328 21.4275 12.9084 21.5879C11.3004 32.1653 21.5275 36.7547 28.6638 33.0597C28.7443 33.018 28.8408 33.0139 28.9245 33.0487C32.8032 34.6598 35.967 34.5662 37.8217 34.3099C38.131 34.2671 38.1505 33.841 37.855 33.7401C29.1343 30.7633 26.0152 24.5245 25.5144 18.8022C25.3835 17.3066 23.8172 13.2016 19.2675 13.0058C15.7934 12.8563 13.6137 15.6103 13.0319 17.325C12.9986 17.4231 12.9201 17.5004 12.821 17.5305Z" fill="${THEME.yyPink[100]}"/><circle cx="17.6178" cy="18.2688" r="0.995689" fill="white"/></svg>`,
21
21
  bubbleColor: THEME.yyPink[50],
22
22
  textColor: THEME.gray[900],
23
- side: 'left'
24
- }
23
+ side: 'left',
24
+ },
25
25
  };
26
26
  /**
27
27
  * Authors that should use first-character avatar instead of SVG
@@ -5,12 +5,15 @@ import { THEME } from './theme.js';
5
5
  export const MAIN_STYLES = `
6
6
  :host {
7
7
  display: block;
8
+ position: relative;
8
9
  font-family: "PT Sans", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
9
10
  "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
10
11
  "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
11
12
  "Noto Color Emoji";
12
13
  --bb-bg-color: ${THEME.gray[50]};
13
14
  --bb-max-height: 600px;
15
+ --bb-avatar-bg: #ffffff;
16
+ --bb-avatar-color: ${THEME.gray[600]};
14
17
  }
15
18
 
16
19
  .history {
@@ -47,6 +50,50 @@ export const MAIN_STYLES = `
47
50
  background: ${THEME.gray[500]};
48
51
  }
49
52
 
53
+ /* Scroll to bottom button */
54
+ .scroll-to-bottom {
55
+ position: absolute;
56
+ bottom: 16px;
57
+ left: 50%;
58
+ width: 36px;
59
+ height: 36px;
60
+ border-radius: 50%;
61
+ background: transparent;
62
+ border: none;
63
+ color: ${THEME.gray[500]};
64
+ cursor: pointer;
65
+ display: flex;
66
+ align-items: center;
67
+ justify-content: center;
68
+ opacity: 0;
69
+ visibility: hidden;
70
+ transform: translateX(-50%) translateY(10px) scale(0.9);
71
+ transition: opacity 0.3s ease, transform 0.3s ease, visibility 0.3s ease;
72
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
73
+ z-index: 10;
74
+ }
75
+
76
+ .scroll-to-bottom.visible {
77
+ opacity: 1;
78
+ visibility: visible;
79
+ transform: translateX(-50%) translateY(0) scale(1);
80
+ }
81
+
82
+ .scroll-to-bottom:hover {
83
+ color: ${THEME.gray[700]};
84
+ transform: translateX(-50%) translateY(-2px);
85
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
86
+ }
87
+
88
+ .scroll-to-bottom:active {
89
+ transform: translateX(-50%) translateY(0) scale(0.95);
90
+ }
91
+
92
+ .scroll-to-bottom svg {
93
+ width: 20px;
94
+ height: 20px;
95
+ }
96
+
50
97
  /* Message row layout */
51
98
  .msg-row {
52
99
  display: flex;
@@ -167,6 +214,7 @@ export const MAIN_STYLES = `
167
214
  /* Right bubble */
168
215
  .msg-bubble--right {
169
216
  border-bottom-right-radius: 0.25rem;
217
+ color: ${THEME.gray[900]};
170
218
  }
171
219
 
172
220
  /* Empty state */
@@ -196,17 +244,84 @@ export const MAIN_STYLES = `
196
244
  width: 1.5rem;
197
245
  height: 1.5rem;
198
246
  }
247
+
248
+ .scroll-to-bottom {
249
+ width: 32px;
250
+ height: 32px;
251
+ bottom: 12px;
252
+ }
253
+
254
+ .scroll-to-bottom svg {
255
+ width: 18px;
256
+ height: 18px;
257
+ }
258
+ }
259
+
260
+ /* Dark mode styles - shared between media query and attribute */
261
+ :host([theme="dark"]) {
262
+ --bb-bg-color: ${THEME.gray[900]};
263
+ --bb-avatar-bg: ${THEME.slate[600]};
264
+ --bb-avatar-color: ${THEME.slate[200]};
265
+ }
266
+
267
+ :host([theme="dark"]) .history {
268
+ background-color: transparent;
269
+ scrollbar-color: ${THEME.gray[600]} transparent;
199
270
  }
200
271
 
201
- /* Dark mode */
272
+ :host([theme="dark"]) .history::-webkit-scrollbar-thumb {
273
+ background: ${THEME.gray[600]};
274
+ }
275
+
276
+ :host([theme="dark"]) .history::-webkit-scrollbar-thumb:hover {
277
+ background: ${THEME.gray[500]};
278
+ }
279
+
280
+ :host([theme="dark"]) .msg-bubble {
281
+ color: ${THEME.slate[100]};
282
+ }
283
+
284
+ :host([theme="dark"]) .msg-bubble--right {
285
+ background-color: ${THEME.slate[700]};
286
+ color: ${THEME.slate[100]};
287
+ }
288
+
289
+ :host([theme="dark"]) .msg-bubble--left {
290
+ background-color: ${THEME.slate[800]};
291
+ border: 1px solid ${THEME.slate[700]};
292
+ color: ${THEME.slate[100]};
293
+ }
294
+
295
+ :host([theme="dark"]) .avatar-wrapper {
296
+ background: ${THEME.slate[600]};
297
+ }
298
+
299
+ :host([theme="dark"]) .empty-state {
300
+ color: ${THEME.gray[500]};
301
+ }
302
+
303
+ :host([theme="dark"]) .scroll-to-bottom {
304
+ background: ${THEME.slate[800]};
305
+ border: none;
306
+ color: ${THEME.slate[300]};
307
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
308
+ }
309
+
310
+ :host([theme="dark"]) .scroll-to-bottom:hover {
311
+ color: ${THEME.slate[200]};
312
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
313
+ }
314
+
315
+ /* System dark mode preference */
202
316
  @media (prefers-color-scheme: dark) {
203
317
  :host {
204
318
  --bb-bg-color: ${THEME.gray[900]};
319
+ --bb-avatar-bg: ${THEME.slate[600]};
320
+ --bb-avatar-color: ${THEME.slate[200]};
205
321
  }
206
322
 
207
323
  .history {
208
324
  background-color: transparent;
209
- /* Firefox scrollbar dark mode */
210
325
  scrollbar-color: ${THEME.gray[600]} transparent;
211
326
  }
212
327
 
@@ -219,21 +334,39 @@ export const MAIN_STYLES = `
219
334
  }
220
335
 
221
336
  .msg-bubble {
222
- color: ${THEME.gray[100]};
337
+ color: ${THEME.slate[100]};
338
+ }
339
+
340
+ .msg-bubble--right {
341
+ background-color: ${THEME.slate[700]};
342
+ color: ${THEME.slate[100]};
223
343
  }
224
344
 
225
345
  .msg-bubble--left {
226
- background-color: ${THEME.gray[700]};
227
- color: ${THEME.gray[100]};
346
+ background-color: ${THEME.slate[800]};
347
+ border: 1px solid ${THEME.slate[700]};
348
+ color: ${THEME.slate[100]};
228
349
  }
229
350
 
230
351
  .avatar-wrapper {
231
- background: ${THEME.gray[800]};
352
+ background: ${THEME.slate[600]};
232
353
  }
233
354
 
234
355
  .empty-state {
235
356
  color: ${THEME.gray[500]};
236
357
  }
358
+
359
+ .scroll-to-bottom {
360
+ background: ${THEME.slate[800]};
361
+ border: none;
362
+ color: ${THEME.slate[300]};
363
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
364
+ }
365
+
366
+ .scroll-to-bottom:hover {
367
+ color: ${THEME.slate[200]};
368
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
369
+ }
237
370
  }
238
371
 
239
372
  /* Reduced motion */
@@ -241,6 +374,23 @@ export const MAIN_STYLES = `
241
374
  .history {
242
375
  scroll-behavior: auto;
243
376
  }
377
+
378
+ .scroll-to-bottom {
379
+ transition: opacity 0.15s ease, visibility 0.15s ease;
380
+ transform: translateX(-50%);
381
+ }
382
+
383
+ .scroll-to-bottom.visible {
384
+ transform: translateX(-50%);
385
+ }
386
+
387
+ .scroll-to-bottom:hover {
388
+ transform: translateX(-50%);
389
+ }
390
+
391
+ .scroll-to-bottom:active {
392
+ transform: translateX(-50%);
393
+ }
244
394
  }
245
395
  `;
246
396
  /**
@@ -6,7 +6,7 @@ export const THEME = {
6
6
  gray: {
7
7
  50: '#f9fafb',
8
8
  100: '#f3f4f6',
9
- 200: '#e5e7eb',
9
+ 200: '#f5f5f5',
10
10
  300: '#d1d5db',
11
11
  400: '#9ca3af',
12
12
  500: '#6b7280',
@@ -15,6 +15,18 @@ export const THEME = {
15
15
  800: '#1f2937',
16
16
  900: '#111827',
17
17
  },
18
+ slate: {
19
+ 50: '#f8fafc',
20
+ 100: '#f1f5f9',
21
+ 200: '#e2e8f0',
22
+ 300: '#cbd5e1',
23
+ 400: '#94a3b8',
24
+ 500: '#64748b',
25
+ 600: '#475569',
26
+ 700: '#334155',
27
+ 800: '#1e293b',
28
+ 900: '#0f172a',
29
+ },
18
30
  red: {
19
31
  50: '#fef2f2',
20
32
  100: '#fee2e2',
@@ -28,5 +40,5 @@ export const THEME = {
28
40
  50: '#fdf4f4',
29
41
  100: '#fbd1d2',
30
42
  150: '#f8babc',
31
- }
43
+ },
32
44
  };
package/dist/index.dev.js CHANGED
@@ -1,12 +1 @@
1
- import { BBMsgHistory } from './component.js';
2
- import { initBBMsgHistory } from './utils/registration.js';
3
- // Auto-initialize
4
- if (document.readyState === 'loading') {
5
- document.addEventListener('DOMContentLoaded', () => initBBMsgHistory(BBMsgHistory));
6
- }
7
- else {
8
- initBBMsgHistory(BBMsgHistory);
9
- }
10
- // Re-exports
11
- export { BBMsgHistory };
12
- export { define } from './utils/registration.js';
1
+ import{BBMsgHistory}from"./component.js";import{initBBMsgHistory}from"./utils/registration.js";"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>initBBMsgHistory(BBMsgHistory)):initBBMsgHistory(BBMsgHistory);export{BBMsgHistory};export{define}from"./utils/registration.js";
@@ -0,0 +1 @@
1
+ {"version":3,"names":["BBMsgHistory","initBBMsgHistory","document","readyState","addEventListener","define"],"sources":["dist/index.js"],"mappings":"OAAOA,iBAAiB,wBAAwBC,qBAAqB,0BAA0B,YAAYC,SAASC,WAAWD,SAASE,iBAAiB,mBAAmB,IAAIH,iBAAiBD,eAAeC,iBAAiBD,qBAAqBA,qBAAqBK,WAAW","ignoreList":[]}
@@ -0,0 +1,39 @@
1
+ import type { AuthorOptions, Message } from './types/index.js';
2
+ export declare class BBMsgHistory extends HTMLElement {
3
+ private _mutationObserver?;
4
+ private _userAuthors;
5
+ private _lastAuthor;
6
+ private _scrollButtonVisible;
7
+ static get observedAttributes(): string[];
8
+ constructor();
9
+ attributeChangedCallback(): void;
10
+ /**
11
+ * Configure an author's avatar, side, and colors.
12
+ * Call before or after rendering — the component re-renders automatically.
13
+ *
14
+ * @example
15
+ * el.setAuthor('alice', { avatar: '🐱', side: 'right', bubbleColor: '#e0f2fe' });
16
+ * el.setAuthor('bob', { avatar: '<img src="bob.png" />', side: 'left' });
17
+ */
18
+ setAuthor(name: string, options: AuthorOptions): this;
19
+ /**
20
+ * Remove a previously set author config.
21
+ */
22
+ removeAuthor(name: string): this;
23
+ /**
24
+ * Append a message to the history.
25
+ * Automatically scrolls to the new message with smooth animation.
26
+ *
27
+ * @example
28
+ * el.appendMessage({ author: 'alice', text: 'Hello!' });
29
+ * el.appendMessage({ author: 'bob', text: 'How are you?' });
30
+ */
31
+ appendMessage(message: Message): this;
32
+ private _appendSingleMessage;
33
+ connectedCallback(): void;
34
+ disconnectedCallback(): void;
35
+ private _setupMutationObserver;
36
+ private render;
37
+ private _renderEmpty;
38
+ private _setupScrollTracking;
39
+ }
@@ -0,0 +1,184 @@
1
+ import { MAIN_STYLES, EMPTY_STYLES } from './const/styles.js';
2
+ 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';
7
+ export class BBMsgHistory extends HTMLElement {
8
+ static get observedAttributes() {
9
+ return ['theme'];
10
+ }
11
+ constructor() {
12
+ super();
13
+ this._userAuthors = new Map();
14
+ this._lastAuthor = '';
15
+ this._scrollButtonVisible = false;
16
+ this.attachShadow({ mode: 'open' });
17
+ }
18
+ attributeChangedCallback() {
19
+ this.render();
20
+ }
21
+ /**
22
+ * Configure an author's avatar, side, and colors.
23
+ * Call before or after rendering — the component re-renders automatically.
24
+ *
25
+ * @example
26
+ * el.setAuthor('alice', { avatar: '🐱', side: 'right', bubbleColor: '#e0f2fe' });
27
+ * el.setAuthor('bob', { avatar: '<img src="bob.png" />', side: 'left' });
28
+ */
29
+ setAuthor(name, options) {
30
+ this._userAuthors.set(name, options);
31
+ this.render();
32
+ return this;
33
+ }
34
+ /**
35
+ * Remove a previously set author config.
36
+ */
37
+ removeAuthor(name) {
38
+ this._userAuthors.delete(name);
39
+ this.render();
40
+ return this;
41
+ }
42
+ /**
43
+ * Append a message to the history.
44
+ * Automatically scrolls to the new message with smooth animation.
45
+ *
46
+ * @example
47
+ * el.appendMessage({ author: 'alice', text: 'Hello!' });
48
+ * el.appendMessage({ author: 'bob', text: 'How are you?' });
49
+ */
50
+ appendMessage(message) {
51
+ // Update textContent
52
+ const currentText = this.textContent || '';
53
+ const separator = currentText && !currentText.endsWith('\n') ? '\n' : '';
54
+ this.textContent = currentText + separator + `${message.author}: ${message.text}`;
55
+ // Temporarily disconnect observer to prevent recursive render
56
+ this._mutationObserver?.disconnect();
57
+ // Append single message without re-rendering entire list
58
+ this._appendSingleMessage(message);
59
+ // Reconnect observer
60
+ this._setupMutationObserver();
61
+ return this;
62
+ }
63
+ _appendSingleMessage(message) {
64
+ const container = this.shadowRoot.querySelector('.history');
65
+ // If empty state or no container, do full render first
66
+ if (!container) {
67
+ this.render();
68
+ return;
69
+ }
70
+ const author = message.author;
71
+ const text = message.text;
72
+ const config = resolveAuthorConfig(author, this._userAuthors);
73
+ const isFirstFromAuthor = author !== this._lastAuthor;
74
+ this._lastAuthor = author;
75
+ const isSubsequent = !isFirstFromAuthor;
76
+ // Use utility function to build message HTML
77
+ const msgHtml = buildMessageRowHtml(author, text, config, isSubsequent);
78
+ // Append to container
79
+ container.insertAdjacentHTML('beforeend', msgHtml);
80
+ // Setup tooltip for new element using utility function
81
+ const newWrapper = container.lastElementChild?.querySelector('.avatar-wrapper');
82
+ if (newWrapper) {
83
+ setupTooltipForElement(newWrapper);
84
+ }
85
+ // Smooth scroll to bottom
86
+ container.scrollTo({
87
+ top: container.scrollHeight,
88
+ behavior: 'smooth',
89
+ });
90
+ // Hide scroll button since we're scrolling to bottom
91
+ const scrollButton = this.shadowRoot.querySelector('.scroll-to-bottom');
92
+ if (scrollButton && this._scrollButtonVisible) {
93
+ this._scrollButtonVisible = false;
94
+ scrollButton.classList.remove('visible');
95
+ }
96
+ }
97
+ connectedCallback() {
98
+ this.render();
99
+ this._setupMutationObserver();
100
+ }
101
+ disconnectedCallback() {
102
+ this._mutationObserver?.disconnect();
103
+ }
104
+ _setupMutationObserver() {
105
+ let debounceTimer;
106
+ this._mutationObserver = new MutationObserver(() => {
107
+ clearTimeout(debounceTimer);
108
+ debounceTimer = setTimeout(() => this.render(), 50);
109
+ });
110
+ this._mutationObserver.observe(this, {
111
+ childList: true,
112
+ characterData: true,
113
+ subtree: true,
114
+ });
115
+ }
116
+ render() {
117
+ const messages = parseMessages(this.textContent);
118
+ if (messages.length === 0) {
119
+ this._lastAuthor = '';
120
+ this._renderEmpty();
121
+ return;
122
+ }
123
+ let lastAuthor = '';
124
+ const messagesHtml = messages
125
+ .map(({ author, text }) => {
126
+ const config = resolveAuthorConfig(author, this._userAuthors);
127
+ const isFirstFromAuthor = author !== lastAuthor;
128
+ lastAuthor = author;
129
+ const isSubsequent = !isFirstFromAuthor;
130
+ // Use utility function to build message HTML
131
+ return buildMessageRowHtml(author, text, config, isSubsequent);
132
+ })
133
+ .join('');
134
+ this._lastAuthor = lastAuthor;
135
+ this.shadowRoot.innerHTML = `
136
+ <style>${MAIN_STYLES}</style>
137
+ <div class="history" role="log" aria-live="polite" aria-label="Message history">
138
+ ${messagesHtml}
139
+ </div>
140
+ ${buildScrollButtonHtml()}
141
+ `;
142
+ requestAnimationFrame(() => {
143
+ const container = this.shadowRoot.querySelector('.history');
144
+ const scrollButton = this.shadowRoot.querySelector('.scroll-to-bottom');
145
+ if (container) {
146
+ container.scrollTop = container.scrollHeight;
147
+ this._setupScrollTracking(container, scrollButton);
148
+ }
149
+ if (scrollButton) {
150
+ scrollButton.addEventListener('click', () => {
151
+ container?.scrollTo({
152
+ top: container.scrollHeight,
153
+ behavior: 'smooth',
154
+ });
155
+ });
156
+ }
157
+ setupTooltips(this.shadowRoot);
158
+ });
159
+ }
160
+ _renderEmpty() {
161
+ this.shadowRoot.innerHTML = `
162
+ <style>${EMPTY_STYLES}</style>
163
+ <div class="empty-state">No messages</div>
164
+ `;
165
+ }
166
+ _setupScrollTracking(container, button) {
167
+ const checkScrollPosition = () => {
168
+ const threshold = 50; // pixels from bottom
169
+ const isAtBottom = container.scrollHeight - container.scrollTop - container.clientHeight < threshold;
170
+ const hasOverflow = container.scrollHeight > container.clientHeight;
171
+ const shouldShow = !isAtBottom && hasOverflow;
172
+ if (shouldShow !== this._scrollButtonVisible) {
173
+ this._scrollButtonVisible = shouldShow;
174
+ button.classList.toggle('visible', shouldShow);
175
+ }
176
+ };
177
+ // Check initial state
178
+ checkScrollPosition();
179
+ // Listen for scroll events with passive listener for performance
180
+ container.addEventListener('scroll', checkScrollPosition, { passive: true });
181
+ // Also check on resize
182
+ window.addEventListener('resize', checkScrollPosition, { passive: true });
183
+ }
184
+ }
@@ -0,0 +1,10 @@
1
+ import type { AuthorConfig } from '../types/index.js';
2
+ /**
3
+ * Built-in author configurations (secret presets)
4
+ * These are used for specific authors in personal projects
5
+ */
6
+ export declare const AUTHOR_CONFIG: Record<string, Omit<AuthorConfig, 'isCustomAvatar'>>;
7
+ /**
8
+ * Authors that should use first-character avatar instead of SVG
9
+ */
10
+ export declare const FIRST_CHAR_AVATAR_AUTHORS: Set<string>;
@@ -0,0 +1,29 @@
1
+ import { THEME } from './theme.js';
2
+ /**
3
+ * Built-in author configurations (secret presets)
4
+ * These are used for specific authors in personal projects
5
+ */
6
+ export const AUTHOR_CONFIG = {
7
+ 'bbki.ng': {
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],
10
+ textColor: THEME.gray[900],
11
+ side: 'right',
12
+ },
13
+ xwy: {
14
+ avatar: `<svg width="28" height="28" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12.821 17.5305C10.709 18.17 9.68345 19.4423 9.22624 20.1359C9.11159 20.3099 9.21615 20.5428 9.42038 20.5839L12.67 21.2381C12.8291 21.2702 12.9328 21.4275 12.9084 21.5879C11.3004 32.1653 21.5275 36.7547 28.6638 33.0597C28.7443 33.018 28.8408 33.0139 28.9245 33.0487C32.8032 34.6598 35.967 34.5662 37.8217 34.3099C38.131 34.2671 38.1505 33.841 37.855 33.7401C29.1343 30.7633 26.0152 24.5245 25.5144 18.8022C25.3835 17.3066 23.8172 13.2016 19.2675 13.0058C15.7934 12.8563 13.6137 15.6103 13.0319 17.325C12.9986 17.4231 12.9201 17.5004 12.821 17.5305Z" fill="${THEME.yyPink[100]}"/><circle cx="17.6178" cy="18.2688" r="0.995689" fill="white"/></svg>`,
15
+ bubbleColor: THEME.yyPink[50],
16
+ textColor: THEME.gray[900],
17
+ side: 'left',
18
+ },
19
+ 小乌鸦: {
20
+ avatar: `<svg width="28" height="28" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12.821 17.5305C10.709 18.17 9.68345 19.4423 9.22624 20.1359C9.11159 20.3099 9.21615 20.5428 9.42038 20.5839L12.67 21.2381C12.8291 21.2702 12.9328 21.4275 12.9084 21.5879C11.3004 32.1653 21.5275 36.7547 28.6638 33.0597C28.7443 33.018 28.8408 33.0139 28.9245 33.0487C32.8032 34.6598 35.967 34.5662 37.8217 34.3099C38.131 34.2671 38.1505 33.841 37.855 33.7401C29.1343 30.7633 26.0152 24.5245 25.5144 18.8022C25.3835 17.3066 23.8172 13.2016 19.2675 13.0058C15.7934 12.8563 13.6137 15.6103 13.0319 17.325C12.9986 17.4231 12.9201 17.5004 12.821 17.5305Z" fill="${THEME.yyPink[100]}"/><circle cx="17.6178" cy="18.2688" r="0.995689" fill="white"/></svg>`,
21
+ bubbleColor: THEME.yyPink[50],
22
+ textColor: THEME.gray[900],
23
+ side: 'left',
24
+ },
25
+ };
26
+ /**
27
+ * Authors that should use first-character avatar instead of SVG
28
+ */
29
+ export const FIRST_CHAR_AVATAR_AUTHORS = new Set(['小乌鸦']);