@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.
- package/README.md +44 -0
- package/dist/component.d.ts +37 -0
- package/dist/component.js +178 -0
- package/dist/const/authors.d.ts +10 -0
- package/dist/const/authors.js +29 -0
- package/dist/const/styles.d.ts +12 -0
- package/dist/const/styles.js +360 -0
- package/dist/const/theme.d.ts +6 -0
- package/dist/const/theme.js +32 -0
- package/dist/index.d.ts +4 -41
- package/dist/index.dev.js +8 -542
- package/dist/index.js +1 -1
- package/dist/types/index.d.ts +33 -0
- package/dist/types/index.js +4 -0
- package/dist/utils/author-resolver.d.ts +11 -0
- package/dist/utils/author-resolver.js +71 -0
- package/dist/utils/avatar.d.ts +4 -0
- package/dist/utils/avatar.js +18 -0
- package/dist/utils/html.d.ts +12 -0
- package/dist/utils/html.js +33 -0
- package/dist/utils/message-builder.d.ts +13 -0
- package/dist/utils/message-builder.js +49 -0
- package/dist/utils/message-parser.d.ts +6 -0
- package/dist/utils/message-parser.js +22 -0
- package/dist/utils/registration.d.ts +8 -0
- package/dist/utils/registration.js +29 -0
- package/dist/utils/scroll-button.d.ts +4 -0
- package/dist/utils/scroll-button.js +12 -0
- package/dist/utils/tooltip.d.ts +5 -0
- package/dist/utils/tooltip.js +17 -0
- package/package.json +7 -6
- package/src/component.ts +217 -0
- package/src/const/authors.ts +32 -0
- package/src/const/styles.ts +363 -0
- package/src/const/theme.ts +34 -0
- package/src/index.ts +11 -602
- package/src/types/index.ts +37 -0
- package/src/utils/author-resolver.ts +81 -0
- package/src/utils/avatar.ts +19 -0
- package/src/utils/html.ts +35 -0
- package/src/utils/message-builder.ts +58 -0
- package/src/utils/message-parser.ts +27 -0
- package/src/utils/registration.ts +35 -0
- package/src/utils/scroll-button.ts +12 -0
- 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, '&')
|
|
11
|
+
.replace(/</g, '<')
|
|
12
|
+
.replace(/>/g, '>')
|
|
13
|
+
.replace(/"/g, '"')
|
|
14
|
+
.replace(/'/g, ''');
|
|
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
|
+
}
|