@bbki.ng/bb-msg-history 0.2.0 → 0.4.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 (45) hide show
  1. package/README.md +44 -0
  2. package/dist/component.d.ts +37 -0
  3. package/dist/component.js +178 -0
  4. package/dist/const/authors.d.ts +10 -0
  5. package/dist/const/authors.js +29 -0
  6. package/dist/const/styles.d.ts +12 -0
  7. package/dist/const/styles.js +360 -0
  8. package/dist/const/theme.d.ts +6 -0
  9. package/dist/const/theme.js +32 -0
  10. package/dist/index.d.ts +4 -41
  11. package/dist/index.dev.js +8 -542
  12. package/dist/index.js +1 -1
  13. package/dist/types/index.d.ts +33 -0
  14. package/dist/types/index.js +4 -0
  15. package/dist/utils/author-resolver.d.ts +11 -0
  16. package/dist/utils/author-resolver.js +71 -0
  17. package/dist/utils/avatar.d.ts +4 -0
  18. package/dist/utils/avatar.js +18 -0
  19. package/dist/utils/html.d.ts +12 -0
  20. package/dist/utils/html.js +33 -0
  21. package/dist/utils/message-builder.d.ts +13 -0
  22. package/dist/utils/message-builder.js +49 -0
  23. package/dist/utils/message-parser.d.ts +6 -0
  24. package/dist/utils/message-parser.js +22 -0
  25. package/dist/utils/registration.d.ts +8 -0
  26. package/dist/utils/registration.js +29 -0
  27. package/dist/utils/scroll-button.d.ts +4 -0
  28. package/dist/utils/scroll-button.js +12 -0
  29. package/dist/utils/tooltip.d.ts +5 -0
  30. package/dist/utils/tooltip.js +17 -0
  31. package/package.json +7 -6
  32. package/src/component.ts +217 -0
  33. package/src/const/authors.ts +32 -0
  34. package/src/const/styles.ts +363 -0
  35. package/src/const/theme.ts +34 -0
  36. package/src/index.ts +11 -602
  37. package/src/types/index.ts +37 -0
  38. package/src/utils/author-resolver.ts +81 -0
  39. package/src/utils/avatar.ts +19 -0
  40. package/src/utils/html.ts +35 -0
  41. package/src/utils/message-builder.ts +58 -0
  42. package/src/utils/message-parser.ts +27 -0
  43. package/src/utils/registration.ts +35 -0
  44. package/src/utils/scroll-button.ts +12 -0
  45. package/src/utils/tooltip.ts +16 -0
@@ -0,0 +1,81 @@
1
+ import type { AuthorConfig, AuthorOptions } from '../types/index.js';
2
+ import { THEME } from '../const/theme.js';
3
+ import { AUTHOR_CONFIG, FIRST_CHAR_AVATAR_AUTHORS } from '../const/authors.js';
4
+ import { wrapAvatarHtml } from './html.js';
5
+ import { generateLetterAvatar } from './avatar.js';
6
+
7
+ /**
8
+ * Resolve author configuration with the following priority:
9
+ * 1. User config (exact match)
10
+ * 2. User config (fuzzy match - author name contains configured key)
11
+ * 3. Built-in first-char avatar authors
12
+ * 4. Built-in exact match
13
+ * 5. Built-in fuzzy match
14
+ * 6. Default: letter avatar, left side
15
+ */
16
+ export function resolveAuthorConfig(
17
+ author: string,
18
+ userAuthors: Map<string, AuthorOptions>
19
+ ): AuthorConfig {
20
+ // 1. User custom config (exact match)
21
+ const userConfig = userAuthors.get(author);
22
+ if (userConfig) {
23
+ return {
24
+ avatar: userConfig.avatar
25
+ ? wrapAvatarHtml(userConfig.avatar)
26
+ : generateLetterAvatar(author.charAt(0).toUpperCase()),
27
+ bubbleColor: userConfig.bubbleColor || THEME.gray[50],
28
+ textColor: userConfig.textColor || THEME.gray[900],
29
+ side: userConfig.side || 'left',
30
+ isCustomAvatar: !!userConfig.avatar,
31
+ };
32
+ }
33
+
34
+ // 2. User custom config (fuzzy match)
35
+ for (const [key, cfg] of userAuthors.entries()) {
36
+ if (author.includes(key)) {
37
+ return {
38
+ avatar: cfg.avatar
39
+ ? wrapAvatarHtml(cfg.avatar)
40
+ : generateLetterAvatar(author.charAt(0).toUpperCase()),
41
+ bubbleColor: cfg.bubbleColor || THEME.gray[50],
42
+ textColor: cfg.textColor || THEME.gray[900],
43
+ side: cfg.side || 'left',
44
+ isCustomAvatar: !!cfg.avatar,
45
+ };
46
+ }
47
+ }
48
+
49
+ // 3. Built-in first-char avatar authors
50
+ if (FIRST_CHAR_AVATAR_AUTHORS.has(author)) {
51
+ const config = AUTHOR_CONFIG[author];
52
+ const firstChar = author.charAt(0);
53
+ return {
54
+ ...(config || { bubbleColor: THEME.gray[50], textColor: THEME.gray[900], side: 'left' as const }),
55
+ avatar: generateLetterAvatar(firstChar),
56
+ isCustomAvatar: false
57
+ };
58
+ }
59
+
60
+ // 4. Built-in exact match
61
+ if (AUTHOR_CONFIG[author]) {
62
+ return { ...AUTHOR_CONFIG[author], isCustomAvatar: true };
63
+ }
64
+
65
+ // 5. Built-in fuzzy match
66
+ for (const [key, config] of Object.entries(AUTHOR_CONFIG)) {
67
+ if (author.includes(key)) {
68
+ return { ...config, isCustomAvatar: true };
69
+ }
70
+ }
71
+
72
+ // 6. Default: letter avatar, left side
73
+ const firstChar = author.charAt(0).toUpperCase();
74
+ return {
75
+ avatar: generateLetterAvatar(firstChar),
76
+ bubbleColor: THEME.gray[50],
77
+ textColor: THEME.gray[900],
78
+ side: 'left',
79
+ isCustomAvatar: false
80
+ };
81
+ }
@@ -0,0 +1,19 @@
1
+ import { THEME } from '../const/theme.js';
2
+
3
+ /**
4
+ * Generate a letter avatar with the given letter
5
+ */
6
+ export function generateLetterAvatar(letter: string): string {
7
+ return `<div style="
8
+ width: 100%;
9
+ height: 100%;
10
+ display: flex;
11
+ align-items: center;
12
+ justify-content: center;
13
+ background: #ffffff;
14
+ color: ${THEME.gray[600]};
15
+ font-size: 14px;
16
+ font-weight: 600;
17
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
18
+ ">${letter}</div>`;
19
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * HTML utility functions
3
+ */
4
+
5
+ /**
6
+ * Escape HTML special characters to prevent XSS
7
+ */
8
+ export function escapeHtml(str: string): string {
9
+ return str
10
+ .replace(/&/g, '&amp;')
11
+ .replace(/</g, '&lt;')
12
+ .replace(/>/g, '&gt;')
13
+ .replace(/"/g, '&quot;')
14
+ .replace(/'/g, '&#39;');
15
+ }
16
+
17
+ /**
18
+ * Wrap plain text/emoji avatar in a styled container
19
+ * If HTML tags are present, return as-is
20
+ */
21
+ export function wrapAvatarHtml(html: string): string {
22
+ // If it looks like a single emoji or short text (no HTML tags), wrap in a styled div
23
+ if (!html.includes('<')) {
24
+ return `<div style="
25
+ width: 100%;
26
+ height: 100%;
27
+ display: flex;
28
+ align-items: center;
29
+ justify-content: center;
30
+ font-size: 18px;
31
+ line-height: 1;
32
+ ">${html}</div>`;
33
+ }
34
+ return html;
35
+ }
@@ -0,0 +1,58 @@
1
+ import type { AuthorConfig } from '../types/index.js';
2
+ import { escapeHtml } from './html.js';
3
+
4
+ /**
5
+ * Build avatar HTML string
6
+ */
7
+ export function buildAvatarHtml(author: string, config: AuthorConfig, showAvatar: boolean): string {
8
+ return `
9
+ <div class="avatar-wrapper ${showAvatar ? '' : 'avatar-wrapper--hidden'}"
10
+ data-author="${escapeHtml(author)}">
11
+ <div class="avatar">${config.avatar}</div>
12
+ <div class="avatar-tooltip">${escapeHtml(author)}</div>
13
+ </div>
14
+ `;
15
+ }
16
+
17
+ /**
18
+ * Build a single message row HTML string
19
+ */
20
+ export function buildMessageRowHtml(
21
+ author: string,
22
+ text: string,
23
+ config: AuthorConfig,
24
+ isSubsequent: boolean
25
+ ): string {
26
+ const showAvatar = !isSubsequent;
27
+ const side = config.side;
28
+ const avatarHtml = buildAvatarHtml(author, config, showAvatar);
29
+
30
+ return `
31
+ <div class="msg-row msg-row--${side} ${isSubsequent ? 'msg-row--subsequent' : 'msg-row--new-author'}">
32
+ ${side === 'left' ? avatarHtml : ''}
33
+
34
+ <div class="msg-content">
35
+ <div class="msg-bubble msg-bubble--${side}"
36
+ style="background-color: ${config.bubbleColor}; color: ${config.textColor};">
37
+ ${escapeHtml(text)}
38
+ </div>
39
+ </div>
40
+
41
+ ${side === 'right' ? avatarHtml : ''}
42
+ </div>
43
+ `;
44
+ }
45
+
46
+ /**
47
+ * Setup tooltip for a single avatar wrapper element
48
+ */
49
+ export function setupTooltipForElement(wrapper: Element): void {
50
+ wrapper.addEventListener('mouseenter', () => {
51
+ const tooltip = wrapper.querySelector('.avatar-tooltip') as HTMLElement;
52
+ if (!tooltip) return;
53
+ const rect = wrapper.getBoundingClientRect();
54
+ const tooltipRect = tooltip.getBoundingClientRect();
55
+ tooltip.style.left = `${rect.left + rect.width / 2 - tooltipRect.width / 2}px`;
56
+ tooltip.style.top = `${rect.top - tooltipRect.height - 8}px`;
57
+ });
58
+ }
@@ -0,0 +1,27 @@
1
+ import type { Message } from '../types/index.js';
2
+
3
+ /**
4
+ * Parse text content into message array
5
+ * Format: `author: text` (one message per line)
6
+ */
7
+ export function parseMessages(textContent: string | null): Message[] {
8
+ const raw = textContent || '';
9
+ const messages: Message[] = [];
10
+
11
+ for (const line of raw.split('\n')) {
12
+ const trimmed = line.trim();
13
+ if (!trimmed) continue;
14
+
15
+ const colonIdx = trimmed.indexOf(':');
16
+ if (colonIdx <= 0) continue;
17
+
18
+ const author = trimmed.slice(0, colonIdx).trim();
19
+ const text = trimmed.slice(colonIdx + 1).trim();
20
+
21
+ if (author && text) {
22
+ messages.push({ author, text });
23
+ }
24
+ }
25
+
26
+ return messages;
27
+ }
@@ -0,0 +1,35 @@
1
+ import { THEME } from '../const/theme.js';
2
+ import { FALLBACK_STYLES } from '../const/styles.js';
3
+
4
+ /**
5
+ * Define the custom element
6
+ */
7
+ export function define(
8
+ BBMsgHistoryClass: CustomElementConstructor,
9
+ tagName = 'bb-msg-history'
10
+ ): void {
11
+ if (!customElements.get(tagName)) {
12
+ customElements.define(tagName, tagName === 'bb-msg-history'
13
+ ? BBMsgHistoryClass
14
+ : class extends BBMsgHistoryClass {}
15
+ );
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Initialize with fallback for unsupported browsers
21
+ */
22
+ export function initBBMsgHistory(BBMsgHistoryClass: CustomElementConstructor): void {
23
+ try {
24
+ define(BBMsgHistoryClass);
25
+ } catch (error) {
26
+ console.warn('BBMsgHistory registration failed, falling back to plain text:', error);
27
+
28
+ document.querySelectorAll('bb-msg-history').forEach(el => {
29
+ const pre = document.createElement('pre');
30
+ pre.style.cssText = FALLBACK_STYLES;
31
+ pre.textContent = el.textContent || '';
32
+ el.replaceWith(pre);
33
+ });
34
+ }
35
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Build scroll-to-bottom button HTML string
3
+ */
4
+ export function buildScrollButtonHtml(): string {
5
+ return `
6
+ <button class="scroll-to-bottom" aria-label="Scroll to bottom" title="Scroll to bottom">
7
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
8
+ <polyline points="6 9 12 15 18 9"></polyline>
9
+ </svg>
10
+ </button>
11
+ `;
12
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Setup dynamic tooltip positioning
3
+ * Tooltips are positioned fixed to avoid overflow clipping from parent containers
4
+ */
5
+ export function setupTooltips(shadowRoot: ShadowRoot): void {
6
+ shadowRoot.querySelectorAll('.avatar-wrapper').forEach(wrapper => {
7
+ wrapper.addEventListener('mouseenter', () => {
8
+ const tooltip = wrapper.querySelector('.avatar-tooltip') as HTMLElement;
9
+ if (!tooltip) return;
10
+ const rect = wrapper.getBoundingClientRect();
11
+ const tooltipRect = tooltip.getBoundingClientRect();
12
+ tooltip.style.left = `${rect.left + rect.width / 2 - tooltipRect.width / 2}px`;
13
+ tooltip.style.top = `${rect.top - tooltipRect.height - 8}px`;
14
+ });
15
+ });
16
+ }