@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
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Main component styles
3
+ */
4
+ export declare const MAIN_STYLES: string;
5
+ /**
6
+ * Empty state styles
7
+ */
8
+ export declare const EMPTY_STYLES: string;
9
+ /**
10
+ * Fallback styles for when custom elements are not supported
11
+ */
12
+ export declare const FALLBACK_STYLES: string;
@@ -0,0 +1,425 @@
1
+ import { THEME } from './theme.js';
2
+ /**
3
+ * Main component styles
4
+ */
5
+ export const MAIN_STYLES = `
6
+ :host {
7
+ display: block;
8
+ position: relative;
9
+ font-family: "PT Sans", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
10
+ "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
11
+ "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
12
+ "Noto Color Emoji";
13
+ --bb-bg-color: ${THEME.gray[50]};
14
+ --bb-max-height: 600px;
15
+ --bb-avatar-bg: #ffffff;
16
+ --bb-avatar-color: ${THEME.gray[600]};
17
+ }
18
+
19
+ .history {
20
+ margin: 0 auto;
21
+ display: flex;
22
+ flex-direction: column;
23
+ gap: 0.25rem;
24
+ max-height: var(--bb-max-height, 600px);
25
+ overflow-y: auto;
26
+ scroll-behavior: smooth;
27
+ background-color: transparent;
28
+ border-radius: 0.5rem;
29
+ /* Firefox scrollbar */
30
+ scrollbar-width: thin;
31
+ scrollbar-color: ${THEME.gray[400]} transparent;
32
+ }
33
+
34
+ /* Custom scrollbar for webkit browsers */
35
+ .history::-webkit-scrollbar {
36
+ width: 6px;
37
+ }
38
+
39
+ .history::-webkit-scrollbar-track {
40
+ background: transparent;
41
+ border-radius: 3px;
42
+ }
43
+
44
+ .history::-webkit-scrollbar-thumb {
45
+ background: ${THEME.gray[400]};
46
+ border-radius: 3px;
47
+ }
48
+
49
+ .history::-webkit-scrollbar-thumb:hover {
50
+ background: ${THEME.gray[500]};
51
+ }
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
+
97
+ /* Message row layout */
98
+ .msg-row {
99
+ display: flex;
100
+ align-items: flex-end;
101
+ gap: 0.5rem;
102
+ max-width: 80%;
103
+ }
104
+
105
+ .msg-row--left {
106
+ align-self: flex-start;
107
+ margin-right: auto;
108
+ }
109
+
110
+ .msg-row--right {
111
+ align-self: flex-end;
112
+ margin-left: auto;
113
+ }
114
+
115
+ .msg-row--subsequent {
116
+ margin-top: 0.125rem;
117
+ }
118
+
119
+ .msg-row--new-author {
120
+ margin-top: 0.75rem;
121
+ }
122
+
123
+ .msg-row--new-author:first-child {
124
+ margin-top: 0;
125
+ }
126
+
127
+ /* Avatar container */
128
+ .avatar-wrapper {
129
+ position: relative;
130
+ flex-shrink: 0;
131
+ width: 1.75rem;
132
+ height: 1.75rem;
133
+ background: #ffffff;
134
+ border-radius: 50%;
135
+ overflow: hidden;
136
+ cursor: help;
137
+ }
138
+
139
+ .avatar-wrapper--hidden {
140
+ opacity: 0;
141
+ pointer-events: none;
142
+ }
143
+
144
+ .avatar {
145
+ width: 100%;
146
+ height: 100%;
147
+ display: flex;
148
+ align-items: center;
149
+ justify-content: center;
150
+ border-radius: 50%;
151
+ overflow: hidden;
152
+ }
153
+
154
+ .avatar svg {
155
+ width: 100%;
156
+ height: 100%;
157
+ }
158
+
159
+ /* Hover tooltip */
160
+ .avatar-tooltip {
161
+ position: fixed;
162
+ padding: 0.25rem 0.5rem;
163
+ background: ${THEME.gray[800]};
164
+ color: ${THEME.gray[50]};
165
+ font-size: 0.75rem;
166
+ border-radius: 0.25rem;
167
+ white-space: nowrap;
168
+ opacity: 0;
169
+ visibility: hidden;
170
+ pointer-events: none;
171
+ z-index: 10;
172
+ font-weight: 500;
173
+ letter-spacing: 0.02em;
174
+ }
175
+
176
+ .avatar-tooltip::after {
177
+ content: '';
178
+ position: absolute;
179
+ top: calc(100% - 1px);
180
+ left: 50%;
181
+ transform: translateX(-50%);
182
+ border: 4px solid transparent;
183
+ border-top-color: ${THEME.gray[800]};
184
+ }
185
+
186
+ .avatar-wrapper:hover .avatar-tooltip {
187
+ opacity: 1;
188
+ visibility: visible;
189
+ }
190
+
191
+ /* Message content area */
192
+ .msg-content {
193
+ display: flex;
194
+ flex-direction: column;
195
+ }
196
+
197
+ .msg-bubble {
198
+ padding: 0.625rem 0.875rem;
199
+ font-size: 0.9375rem;
200
+ line-height: 1.5;
201
+ word-wrap: break-word;
202
+ overflow-wrap: anywhere;
203
+ word-break: break-word;
204
+ border-radius: 1rem;
205
+ }
206
+
207
+ /* Left bubble */
208
+ .msg-bubble--left {
209
+ border-bottom-left-radius: 0.25rem;
210
+ background-color: ${THEME.gray[200]};
211
+ color: ${THEME.gray[900]};
212
+ }
213
+
214
+ /* Right bubble */
215
+ .msg-bubble--right {
216
+ border-bottom-right-radius: 0.25rem;
217
+ color: ${THEME.gray[900]};
218
+ }
219
+
220
+ /* Empty state */
221
+ .empty-state {
222
+ text-align: center;
223
+ padding: 2rem;
224
+ color: ${THEME.gray[400]};
225
+ font-size: 0.875rem;
226
+ }
227
+
228
+ /* Mobile responsive */
229
+ @media (max-width: 480px) {
230
+ .history {
231
+ max-height: var(--bb-max-height, 70vh);
232
+ }
233
+
234
+ .msg-row {
235
+ max-width: 85%;
236
+ }
237
+
238
+ .msg-bubble {
239
+ font-size: 0.9375rem;
240
+ padding: 0.5rem 0.75rem;
241
+ }
242
+
243
+ .avatar-wrapper {
244
+ width: 1.5rem;
245
+ height: 1.5rem;
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;
270
+ }
271
+
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 */
316
+ @media (prefers-color-scheme: dark) {
317
+ :host {
318
+ --bb-bg-color: ${THEME.gray[900]};
319
+ --bb-avatar-bg: ${THEME.slate[600]};
320
+ --bb-avatar-color: ${THEME.slate[200]};
321
+ }
322
+
323
+ .history {
324
+ background-color: transparent;
325
+ scrollbar-color: ${THEME.gray[600]} transparent;
326
+ }
327
+
328
+ .history::-webkit-scrollbar-thumb {
329
+ background: ${THEME.gray[600]};
330
+ }
331
+
332
+ .history::-webkit-scrollbar-thumb:hover {
333
+ background: ${THEME.gray[500]};
334
+ }
335
+
336
+ .msg-bubble {
337
+ color: ${THEME.slate[100]};
338
+ }
339
+
340
+ .msg-bubble--right {
341
+ background-color: ${THEME.slate[700]};
342
+ color: ${THEME.slate[100]};
343
+ }
344
+
345
+ .msg-bubble--left {
346
+ background-color: ${THEME.slate[800]};
347
+ border: 1px solid ${THEME.slate[700]};
348
+ color: ${THEME.slate[100]};
349
+ }
350
+
351
+ .avatar-wrapper {
352
+ background: ${THEME.slate[600]};
353
+ }
354
+
355
+ .empty-state {
356
+ color: ${THEME.gray[500]};
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
+ }
370
+ }
371
+
372
+ /* Reduced motion */
373
+ @media (prefers-reduced-motion: reduce) {
374
+ .history {
375
+ scroll-behavior: auto;
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
+ }
394
+ }
395
+ `;
396
+ /**
397
+ * Empty state styles
398
+ */
399
+ export const EMPTY_STYLES = `
400
+ :host { display: block; }
401
+ .empty-state {
402
+ text-align: center;
403
+ padding: 2rem;
404
+ color: ${THEME.gray[400]};
405
+ font-size: 0.875rem;
406
+ font-family: inherit;
407
+ }
408
+ `;
409
+ /**
410
+ * Fallback styles for when custom elements are not supported
411
+ */
412
+ export const FALLBACK_STYLES = `
413
+ background: ${THEME.gray[100]};
414
+ padding: 1rem;
415
+ border-radius: 0.5rem;
416
+ overflow-x: auto;
417
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
418
+ font-size: 0.875rem;
419
+ line-height: 1.5;
420
+ color: ${THEME.gray[900]};
421
+ margin: 0;
422
+ white-space: pre-wrap;
423
+ word-wrap: break-word;
424
+ border: 1px solid ${THEME.gray[200]};
425
+ `;
@@ -0,0 +1,6 @@
1
+ import type { Theme } from '../types/index.js';
2
+ /**
3
+ * Theme color palette
4
+ * Based on Tailwind-like gray scale and custom pink colors
5
+ */
6
+ export declare const THEME: Theme;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Theme color palette
3
+ * Based on Tailwind-like gray scale and custom pink colors
4
+ */
5
+ export const THEME = {
6
+ gray: {
7
+ 50: '#f9fafb',
8
+ 100: '#f3f4f6',
9
+ 200: '#f5f5f5',
10
+ 300: '#d1d5db',
11
+ 400: '#9ca3af',
12
+ 500: '#6b7280',
13
+ 600: '#4b5563',
14
+ 700: '#374151',
15
+ 800: '#1f2937',
16
+ 900: '#111827',
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
+ },
30
+ red: {
31
+ 50: '#fef2f2',
32
+ 100: '#fee2e2',
33
+ 200: '#fecaca',
34
+ 300: '#fca5a5',
35
+ 400: '#f87171',
36
+ 500: '#ef4444',
37
+ 600: '#dc2626',
38
+ },
39
+ yyPink: {
40
+ 50: '#fdf4f4',
41
+ 100: '#fbd1d2',
42
+ 150: '#f8babc',
43
+ },
44
+ };
@@ -0,0 +1,9 @@
1
+ import { BBMsgHistory } from './component.js';
2
+ export { BBMsgHistory };
3
+ export { define } from './utils/registration.js';
4
+ export type { AuthorOptions, AuthorConfig, Message } from './types/index.js';
5
+ declare global {
6
+ interface HTMLElementTagNameMap {
7
+ 'bb-msg-history': BBMsgHistory;
8
+ }
9
+ }
@@ -0,0 +1,12 @@
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';
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Type definitions for bb-msg-history
3
+ */
4
+ /** Single message structure */
5
+ export interface Message {
6
+ author: string;
7
+ text: string;
8
+ }
9
+ /** Internal author configuration with resolved values */
10
+ export interface AuthorConfig {
11
+ avatar: string;
12
+ bubbleColor: string;
13
+ textColor: string;
14
+ side: 'left' | 'right';
15
+ isCustomAvatar: boolean;
16
+ }
17
+ /** User-facing options for configuring an author */
18
+ export interface AuthorOptions {
19
+ /** Avatar HTML string: SVG, <img>, emoji, or plain text */
20
+ avatar?: string;
21
+ /** Which side the messages appear on */
22
+ side?: 'left' | 'right';
23
+ /** Bubble background color */
24
+ bubbleColor?: string;
25
+ /** Text color inside bubble */
26
+ textColor?: string;
27
+ }
28
+ /** Theme color palette */
29
+ export interface Theme {
30
+ gray: Record<string, string>;
31
+ slate: Record<string, string>;
32
+ red: Record<string, string>;
33
+ yyPink: Record<string, string>;
34
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Type definitions for bb-msg-history
3
+ */
4
+ export {};
@@ -0,0 +1,11 @@
1
+ import type { AuthorConfig, AuthorOptions } from '../types/index.js';
2
+ /**
3
+ * Resolve author configuration with the following priority:
4
+ * 1. User config (exact match)
5
+ * 2. User config (fuzzy match - author name contains configured key)
6
+ * 3. Built-in first-char avatar authors
7
+ * 4. Built-in exact match
8
+ * 5. Built-in fuzzy match
9
+ * 6. Default: letter avatar, left side
10
+ */
11
+ export declare function resolveAuthorConfig(author: string, userAuthors: Map<string, AuthorOptions>): AuthorConfig;
@@ -0,0 +1,75 @@
1
+ import { THEME } from '../const/theme.js';
2
+ import { AUTHOR_CONFIG, FIRST_CHAR_AVATAR_AUTHORS } from '../const/authors.js';
3
+ import { wrapAvatarHtml } from './html.js';
4
+ import { generateLetterAvatar } from './avatar.js';
5
+ /**
6
+ * Resolve author configuration with the following priority:
7
+ * 1. User config (exact match)
8
+ * 2. User config (fuzzy match - author name contains configured key)
9
+ * 3. Built-in first-char avatar authors
10
+ * 4. Built-in exact match
11
+ * 5. Built-in fuzzy match
12
+ * 6. Default: letter avatar, left side
13
+ */
14
+ export function resolveAuthorConfig(author, userAuthors) {
15
+ // 1. User custom config (exact match)
16
+ const userConfig = userAuthors.get(author);
17
+ if (userConfig) {
18
+ return {
19
+ avatar: userConfig.avatar
20
+ ? wrapAvatarHtml(userConfig.avatar)
21
+ : generateLetterAvatar(author.charAt(0).toUpperCase()),
22
+ bubbleColor: userConfig.bubbleColor || THEME.gray[50],
23
+ textColor: userConfig.textColor || THEME.gray[900],
24
+ side: userConfig.side || 'left',
25
+ isCustomAvatar: !!userConfig.avatar,
26
+ };
27
+ }
28
+ // 2. User custom config (fuzzy match)
29
+ for (const [key, cfg] of userAuthors.entries()) {
30
+ if (author.includes(key)) {
31
+ return {
32
+ avatar: cfg.avatar
33
+ ? wrapAvatarHtml(cfg.avatar)
34
+ : generateLetterAvatar(author.charAt(0).toUpperCase()),
35
+ bubbleColor: cfg.bubbleColor || THEME.gray[50],
36
+ textColor: cfg.textColor || THEME.gray[900],
37
+ side: cfg.side || 'left',
38
+ isCustomAvatar: !!cfg.avatar,
39
+ };
40
+ }
41
+ }
42
+ // 3. Built-in first-char avatar authors
43
+ if (FIRST_CHAR_AVATAR_AUTHORS.has(author)) {
44
+ const config = AUTHOR_CONFIG[author];
45
+ const firstChar = author.charAt(0);
46
+ return {
47
+ ...(config || {
48
+ bubbleColor: THEME.gray[50],
49
+ textColor: THEME.gray[900],
50
+ side: 'left',
51
+ }),
52
+ avatar: generateLetterAvatar(firstChar),
53
+ isCustomAvatar: false,
54
+ };
55
+ }
56
+ // 4. Built-in exact match
57
+ if (AUTHOR_CONFIG[author]) {
58
+ return { ...AUTHOR_CONFIG[author], isCustomAvatar: true };
59
+ }
60
+ // 5. Built-in fuzzy match
61
+ for (const [key, config] of Object.entries(AUTHOR_CONFIG)) {
62
+ if (author.includes(key)) {
63
+ return { ...config, isCustomAvatar: true };
64
+ }
65
+ }
66
+ // 6. Default: letter avatar, left side
67
+ const firstChar = author.charAt(0).toUpperCase();
68
+ return {
69
+ avatar: generateLetterAvatar(firstChar),
70
+ bubbleColor: THEME.gray[50],
71
+ textColor: THEME.gray[900],
72
+ side: 'left',
73
+ isCustomAvatar: false,
74
+ };
75
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Generate a letter avatar with the given letter
3
+ */
4
+ export declare function generateLetterAvatar(letter: string): string;
@@ -0,0 +1,18 @@
1
+ import { THEME } from '../const/theme.js';
2
+ /**
3
+ * Generate a letter avatar with the given letter
4
+ */
5
+ export function generateLetterAvatar(letter) {
6
+ return `<div style="
7
+ width: 100%;
8
+ height: 100%;
9
+ display: flex;
10
+ align-items: center;
11
+ justify-content: center;
12
+ background: var(--bb-avatar-bg, #ffffff);
13
+ color: var(--bb-avatar-color, ${THEME.gray[600]});
14
+ font-size: 14px;
15
+ font-weight: 600;
16
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
17
+ ">${letter}</div>`;
18
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * HTML utility functions
3
+ */
4
+ /**
5
+ * Escape HTML special characters to prevent XSS
6
+ */
7
+ export declare function escapeHtml(str: string): string;
8
+ /**
9
+ * Wrap plain text/emoji avatar in a styled container
10
+ * If HTML tags are present, return as-is
11
+ */
12
+ export declare function wrapAvatarHtml(html: string): string;