@bbki.ng/bb-msg-history 0.1.1 → 0.2.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 +130 -19
- package/dist/index.d.ts +30 -4
- package/dist/index.dev.js +546 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/index.ts +162 -85
package/README.md
CHANGED
|
@@ -37,54 +37,159 @@ Each message is a line with the author name, a colon, and the message text:
|
|
|
37
37
|
|
|
38
38
|
Blank lines and lines without a colon are ignored.
|
|
39
39
|
|
|
40
|
-
##
|
|
40
|
+
## Author Avatars
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
By default, every author gets a **letter avatar** (first character of their name) and appears on the **left** side.
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|--------|------|--------------|
|
|
46
|
-
| `bbki.ng` | right | light gray |
|
|
47
|
-
| `xwy` | left | light pink |
|
|
44
|
+
Use the `setAuthor()` method to customize avatar, side, bubble color, and text color:
|
|
48
45
|
|
|
49
|
-
|
|
46
|
+
```js
|
|
47
|
+
const el = document.querySelector('bb-msg-history');
|
|
48
|
+
|
|
49
|
+
// Emoji avatar, right side
|
|
50
|
+
el.setAuthor('me', { avatar: '🐱', side: 'right' });
|
|
51
|
+
|
|
52
|
+
// Image avatar, custom bubble color
|
|
53
|
+
el.setAuthor('bot', {
|
|
54
|
+
avatar: '<img src="bot.png" width="28" height="28" />',
|
|
55
|
+
side: 'left',
|
|
56
|
+
bubbleColor: '#e0f2fe',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// SVG avatar
|
|
60
|
+
el.setAuthor('alice', {
|
|
61
|
+
avatar: '<svg viewBox="0 0 48 48">...</svg>',
|
|
62
|
+
side: 'left',
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### `setAuthor(name, options)`
|
|
67
|
+
|
|
68
|
+
| Option | Type | Default | Description |
|
|
69
|
+
|--------|------|---------|-------------|
|
|
70
|
+
| `avatar` | `string` | letter avatar | HTML string: emoji, `<img>`, `<svg>`, or text |
|
|
71
|
+
| `side` | `'left' \| 'right'` | `'left'` | Which side the bubbles appear on |
|
|
72
|
+
| `bubbleColor` | `string` | `'#f9fafb'` | Bubble background color |
|
|
73
|
+
| `textColor` | `string` | `'#111827'` | Text color inside bubble |
|
|
74
|
+
|
|
75
|
+
Returns `this` for chaining:
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
el.setAuthor('me', { avatar: '🐱', side: 'right' })
|
|
79
|
+
.setAuthor('you', { avatar: '🐶', side: 'left' });
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Fuzzy matching: if an author name _contains_ a configured key (e.g. you configured `"alice"` and the message is from `"alice(phone)"`), the config is reused.
|
|
83
|
+
|
|
84
|
+
Use `removeAuthor(name)` to remove a custom config.
|
|
85
|
+
|
|
86
|
+
## Customization
|
|
87
|
+
|
|
88
|
+
### CSS Custom Properties
|
|
89
|
+
|
|
90
|
+
| Property | Default | Description |
|
|
91
|
+
|----------|---------|-------------|
|
|
92
|
+
| `--bb-max-height` | `600px` | Maximum height of the message container |
|
|
93
|
+
|
|
94
|
+
```css
|
|
95
|
+
bb-msg-history {
|
|
96
|
+
--bb-max-height: 400px;
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Manual Registration
|
|
101
|
+
|
|
102
|
+
By default, the component auto-registers as `<bb-msg-history>`. You can also register manually with a custom tag name:
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
import { BBMsgHistory, define } from '@bbki.ng/bb-msg-history';
|
|
106
|
+
|
|
107
|
+
// Register with default tag name
|
|
108
|
+
define();
|
|
109
|
+
|
|
110
|
+
// Or use a custom tag name
|
|
111
|
+
define('my-chat-history');
|
|
112
|
+
```
|
|
50
113
|
|
|
51
114
|
## Features
|
|
52
115
|
|
|
53
116
|
- Plain-text message format — no JSON or attributes needed
|
|
54
117
|
- Left/right bubble layout based on author
|
|
55
|
-
-
|
|
118
|
+
- Customizable avatars: emoji, `<img>`, `<svg>`, or letter avatars
|
|
119
|
+
- Hover tooltip showing the author name
|
|
120
|
+
- Consecutive messages from the same author are grouped (avatar hidden)
|
|
56
121
|
- Auto-scroll to the latest message on render
|
|
57
|
-
-
|
|
122
|
+
- Long text word-wrap and overflow handling
|
|
123
|
+
- Empty state when no messages are provided
|
|
58
124
|
- Dark mode support via `prefers-color-scheme`
|
|
59
125
|
- Mobile responsive layout
|
|
60
126
|
- `prefers-reduced-motion` support
|
|
127
|
+
- Reactive: automatically re-renders when content changes
|
|
128
|
+
- Customizable max-height via `--bb-max-height` CSS custom property
|
|
61
129
|
- Graceful degradation to `<pre>` when Custom Elements are unsupported
|
|
62
130
|
|
|
63
131
|
## Examples
|
|
64
132
|
|
|
65
|
-
### Basic
|
|
133
|
+
### Basic
|
|
66
134
|
|
|
67
135
|
```html
|
|
68
136
|
<bb-msg-history>
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
137
|
+
alice: Hey, are you free this weekend?
|
|
138
|
+
bob: Sounds good! When?
|
|
139
|
+
alice: Saturday morning, around 10?
|
|
140
|
+
bob: Perfect. See you then!
|
|
141
|
+
</bb-msg-history>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Custom avatars
|
|
145
|
+
|
|
146
|
+
```html
|
|
147
|
+
<bb-msg-history id="chat">
|
|
148
|
+
me: Hey there!
|
|
149
|
+
friend: What's up?
|
|
150
|
+
</bb-msg-history>
|
|
151
|
+
|
|
152
|
+
<script>
|
|
153
|
+
const el = document.getElementById('chat');
|
|
154
|
+
el.setAuthor('me', { avatar: '🐱', side: 'right', bubbleColor: '#f3f4f6' });
|
|
155
|
+
el.setAuthor('friend', { avatar: '🐶', side: 'left', bubbleColor: '#e0f2fe' });
|
|
156
|
+
</script>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Consecutive messages — avatar grouping
|
|
160
|
+
|
|
161
|
+
When the same author sends multiple messages in a row, the avatar is only shown on the first one:
|
|
162
|
+
|
|
163
|
+
```html
|
|
164
|
+
<bb-msg-history>
|
|
165
|
+
alice: First message
|
|
166
|
+
alice: Second message, avatar hidden
|
|
167
|
+
alice: Third, still hidden
|
|
168
|
+
bob: Got it!
|
|
169
|
+
bob: I'll send two as well
|
|
73
170
|
</bb-msg-history>
|
|
74
171
|
```
|
|
75
172
|
|
|
76
|
-
### Unknown
|
|
173
|
+
### Unknown authors — letter avatars
|
|
77
174
|
|
|
78
|
-
Authors
|
|
175
|
+
Authors without custom config receive a letter avatar and appear on the left:
|
|
79
176
|
|
|
80
177
|
```html
|
|
81
178
|
<bb-msg-history>
|
|
82
|
-
alice:
|
|
179
|
+
alice: Hello!
|
|
83
180
|
bob: Hi there!
|
|
84
|
-
|
|
181
|
+
charlie: Hey everyone!
|
|
85
182
|
</bb-msg-history>
|
|
86
183
|
```
|
|
87
184
|
|
|
185
|
+
### Empty state
|
|
186
|
+
|
|
187
|
+
When no messages are provided, a "No messages" placeholder is shown:
|
|
188
|
+
|
|
189
|
+
```html
|
|
190
|
+
<bb-msg-history></bb-msg-history>
|
|
191
|
+
```
|
|
192
|
+
|
|
88
193
|
### Full page example
|
|
89
194
|
|
|
90
195
|
```html
|
|
@@ -94,13 +199,19 @@ Authors not listed in the built-in config receive a letter avatar and appear on
|
|
|
94
199
|
<script type="module" src="https://cdn.jsdelivr.net/npm/@bbki.ng/bb-msg-history@latest/dist/index.js"></script>
|
|
95
200
|
</head>
|
|
96
201
|
<body>
|
|
97
|
-
<bb-msg-history>
|
|
202
|
+
<bb-msg-history id="chat">
|
|
98
203
|
alice: Hey, are you free this weekend?
|
|
99
204
|
bob: Yeah, what's up?
|
|
100
205
|
alice: Want to grab coffee?
|
|
101
206
|
bob: Sounds good! Saturday morning?
|
|
102
207
|
alice: Perfect, see you then!
|
|
103
208
|
</bb-msg-history>
|
|
209
|
+
|
|
210
|
+
<script>
|
|
211
|
+
const el = document.getElementById('chat');
|
|
212
|
+
el.setAuthor('alice', { avatar: '👩', side: 'right' });
|
|
213
|
+
el.setAuthor('bob', { avatar: '👨', side: 'left', bubbleColor: '#ecfdf5' });
|
|
214
|
+
</script>
|
|
104
215
|
</body>
|
|
105
216
|
</html>
|
|
106
217
|
```
|
package/dist/index.d.ts
CHANGED
|
@@ -1,18 +1,44 @@
|
|
|
1
|
+
interface AuthorOptions {
|
|
2
|
+
/** Avatar HTML string: SVG, <img>, emoji, or plain text */
|
|
3
|
+
avatar?: string;
|
|
4
|
+
/** Which side the messages appear on */
|
|
5
|
+
side?: 'left' | 'right';
|
|
6
|
+
/** Bubble background color */
|
|
7
|
+
bubbleColor?: string;
|
|
8
|
+
/** Text color inside bubble */
|
|
9
|
+
textColor?: string;
|
|
10
|
+
}
|
|
1
11
|
declare class BBMsgHistory extends HTMLElement {
|
|
2
|
-
private
|
|
12
|
+
private _mutationObserver?;
|
|
13
|
+
private _userAuthors;
|
|
3
14
|
constructor();
|
|
15
|
+
/**
|
|
16
|
+
* Configure an author's avatar, side, and colors.
|
|
17
|
+
* Call before or after rendering — the component re-renders automatically.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* el.setAuthor('alice', { avatar: '🐱', side: 'right', bubbleColor: '#e0f2fe' });
|
|
21
|
+
* el.setAuthor('bob', { avatar: '<img src="bob.png" />', side: 'left' });
|
|
22
|
+
*/
|
|
23
|
+
setAuthor(name: string, options: AuthorOptions): this;
|
|
24
|
+
/**
|
|
25
|
+
* Remove a previously set author config.
|
|
26
|
+
*/
|
|
27
|
+
removeAuthor(name: string): this;
|
|
4
28
|
connectedCallback(): void;
|
|
5
29
|
disconnectedCallback(): void;
|
|
6
|
-
private
|
|
7
|
-
private _adjustLayout;
|
|
30
|
+
private _setupMutationObserver;
|
|
8
31
|
private parseMessages;
|
|
9
32
|
private getAuthorConfig;
|
|
33
|
+
private _wrapAvatarHtml;
|
|
10
34
|
private _generateLetterAvatar;
|
|
11
35
|
private escapeHtml;
|
|
12
36
|
private render;
|
|
13
37
|
private _renderEmpty;
|
|
14
38
|
}
|
|
15
|
-
|
|
39
|
+
declare function define(tagName?: string): void;
|
|
40
|
+
export { BBMsgHistory, define };
|
|
41
|
+
export type { AuthorOptions };
|
|
16
42
|
declare global {
|
|
17
43
|
interface HTMLElementTagNameMap {
|
|
18
44
|
'bb-msg-history': BBMsgHistory;
|
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
// 主题色板
|
|
2
|
+
const THEME = {
|
|
3
|
+
gray: {
|
|
4
|
+
50: '#f9fafb',
|
|
5
|
+
100: '#f3f4f6',
|
|
6
|
+
200: '#e5e7eb',
|
|
7
|
+
300: '#d1d5db',
|
|
8
|
+
400: '#9ca3af',
|
|
9
|
+
500: '#6b7280',
|
|
10
|
+
600: '#4b5563',
|
|
11
|
+
700: '#374151',
|
|
12
|
+
800: '#1f2937',
|
|
13
|
+
900: '#111827',
|
|
14
|
+
},
|
|
15
|
+
red: {
|
|
16
|
+
50: '#fef2f2',
|
|
17
|
+
100: '#fee2e2',
|
|
18
|
+
200: '#fecaca',
|
|
19
|
+
300: '#fca5a5',
|
|
20
|
+
400: '#f87171',
|
|
21
|
+
500: '#ef4444',
|
|
22
|
+
600: '#dc2626',
|
|
23
|
+
},
|
|
24
|
+
yyPink: {
|
|
25
|
+
50: '#fdf4f4',
|
|
26
|
+
100: '#fbd1d2',
|
|
27
|
+
150: '#f8babc',
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
// 作者配置映射
|
|
31
|
+
const AUTHOR_CONFIG = {
|
|
32
|
+
'bbki.ng': {
|
|
33
|
+
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>`,
|
|
34
|
+
bubbleColor: THEME.gray[100],
|
|
35
|
+
textColor: THEME.gray[900],
|
|
36
|
+
side: 'right'
|
|
37
|
+
},
|
|
38
|
+
'xwy': {
|
|
39
|
+
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>`,
|
|
40
|
+
bubbleColor: THEME.yyPink[50], // 淡红色背景
|
|
41
|
+
textColor: THEME.gray[900],
|
|
42
|
+
side: 'left'
|
|
43
|
+
},
|
|
44
|
+
'小乌鸦': {
|
|
45
|
+
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>`,
|
|
46
|
+
bubbleColor: THEME.yyPink[50], // 淡红色背景
|
|
47
|
+
textColor: THEME.gray[900],
|
|
48
|
+
side: 'left'
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
// 使用首字符头像的作者(非 bbki.ng / xwy)
|
|
52
|
+
const FIRST_CHAR_AVATAR_AUTHORS = new Set(['小乌鸦']);
|
|
53
|
+
class BBMsgHistory extends HTMLElement {
|
|
54
|
+
constructor() {
|
|
55
|
+
super();
|
|
56
|
+
this._userAuthors = new Map();
|
|
57
|
+
this.attachShadow({ mode: 'open' });
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Configure an author's avatar, side, and colors.
|
|
61
|
+
* Call before or after rendering — the component re-renders automatically.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* el.setAuthor('alice', { avatar: '🐱', side: 'right', bubbleColor: '#e0f2fe' });
|
|
65
|
+
* el.setAuthor('bob', { avatar: '<img src="bob.png" />', side: 'left' });
|
|
66
|
+
*/
|
|
67
|
+
setAuthor(name, options) {
|
|
68
|
+
this._userAuthors.set(name, options);
|
|
69
|
+
this.render();
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Remove a previously set author config.
|
|
74
|
+
*/
|
|
75
|
+
removeAuthor(name) {
|
|
76
|
+
this._userAuthors.delete(name);
|
|
77
|
+
this.render();
|
|
78
|
+
return this;
|
|
79
|
+
}
|
|
80
|
+
connectedCallback() {
|
|
81
|
+
this.render();
|
|
82
|
+
this._setupMutationObserver();
|
|
83
|
+
}
|
|
84
|
+
disconnectedCallback() {
|
|
85
|
+
this._mutationObserver?.disconnect();
|
|
86
|
+
}
|
|
87
|
+
_setupMutationObserver() {
|
|
88
|
+
let debounceTimer;
|
|
89
|
+
this._mutationObserver = new MutationObserver(() => {
|
|
90
|
+
clearTimeout(debounceTimer);
|
|
91
|
+
debounceTimer = setTimeout(() => this.render(), 50);
|
|
92
|
+
});
|
|
93
|
+
this._mutationObserver.observe(this, {
|
|
94
|
+
childList: true,
|
|
95
|
+
characterData: true,
|
|
96
|
+
subtree: true,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
parseMessages() {
|
|
100
|
+
const raw = this.textContent || '';
|
|
101
|
+
const messages = [];
|
|
102
|
+
for (const line of raw.split('\n')) {
|
|
103
|
+
const trimmed = line.trim();
|
|
104
|
+
if (!trimmed)
|
|
105
|
+
continue;
|
|
106
|
+
const colonIdx = trimmed.indexOf(':');
|
|
107
|
+
if (colonIdx <= 0)
|
|
108
|
+
continue;
|
|
109
|
+
const author = trimmed.slice(0, colonIdx).trim();
|
|
110
|
+
const text = trimmed.slice(colonIdx + 1).trim();
|
|
111
|
+
if (author && text) {
|
|
112
|
+
messages.push({ author, text });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return messages;
|
|
116
|
+
}
|
|
117
|
+
getAuthorConfig(author) {
|
|
118
|
+
// 1. 用户自定义配置(精确匹配)
|
|
119
|
+
const userConfig = this._userAuthors.get(author);
|
|
120
|
+
if (userConfig) {
|
|
121
|
+
return {
|
|
122
|
+
avatar: userConfig.avatar
|
|
123
|
+
? this._wrapAvatarHtml(userConfig.avatar)
|
|
124
|
+
: this._generateLetterAvatar(author.charAt(0).toUpperCase()),
|
|
125
|
+
bubbleColor: userConfig.bubbleColor || THEME.gray[50],
|
|
126
|
+
textColor: userConfig.textColor || THEME.gray[900],
|
|
127
|
+
side: userConfig.side || 'left',
|
|
128
|
+
isCustomAvatar: !!userConfig.avatar,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
// 2. 用户自定义配置(模糊匹配:作者名包含已配置的 key)
|
|
132
|
+
for (const [key, cfg] of this._userAuthors.entries()) {
|
|
133
|
+
if (author.includes(key)) {
|
|
134
|
+
return {
|
|
135
|
+
avatar: cfg.avatar
|
|
136
|
+
? this._wrapAvatarHtml(cfg.avatar)
|
|
137
|
+
: this._generateLetterAvatar(author.charAt(0).toUpperCase()),
|
|
138
|
+
bubbleColor: cfg.bubbleColor || THEME.gray[50],
|
|
139
|
+
textColor: cfg.textColor || THEME.gray[900],
|
|
140
|
+
side: cfg.side || 'left',
|
|
141
|
+
isCustomAvatar: !!cfg.avatar,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// 3. 内置首字符头像作者(secret)
|
|
146
|
+
if (FIRST_CHAR_AVATAR_AUTHORS.has(author)) {
|
|
147
|
+
const config = AUTHOR_CONFIG[author];
|
|
148
|
+
const firstChar = author.charAt(0);
|
|
149
|
+
return {
|
|
150
|
+
...(config || { bubbleColor: THEME.gray[50], textColor: THEME.gray[900], side: 'left' }),
|
|
151
|
+
avatar: this._generateLetterAvatar(firstChar),
|
|
152
|
+
isCustomAvatar: false
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
// 4. 内置配置精确匹配(secret)
|
|
156
|
+
if (AUTHOR_CONFIG[author]) {
|
|
157
|
+
return { ...AUTHOR_CONFIG[author], isCustomAvatar: true };
|
|
158
|
+
}
|
|
159
|
+
// 5. 内置配置模糊匹配(secret)
|
|
160
|
+
for (const [key, config] of Object.entries(AUTHOR_CONFIG)) {
|
|
161
|
+
if (author.includes(key)) {
|
|
162
|
+
return { ...config, isCustomAvatar: true };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// 6. 默认:首字母头像,左侧
|
|
166
|
+
const firstChar = author.charAt(0).toUpperCase();
|
|
167
|
+
return {
|
|
168
|
+
avatar: this._generateLetterAvatar(firstChar),
|
|
169
|
+
bubbleColor: THEME.gray[50],
|
|
170
|
+
textColor: THEME.gray[900],
|
|
171
|
+
side: 'left',
|
|
172
|
+
isCustomAvatar: false
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
_wrapAvatarHtml(html) {
|
|
176
|
+
// If it looks like a single emoji or short text (no HTML tags), wrap in a styled div
|
|
177
|
+
if (!html.includes('<')) {
|
|
178
|
+
return `<div style="
|
|
179
|
+
width: 100%;
|
|
180
|
+
height: 100%;
|
|
181
|
+
display: flex;
|
|
182
|
+
align-items: center;
|
|
183
|
+
justify-content: center;
|
|
184
|
+
font-size: 18px;
|
|
185
|
+
line-height: 1;
|
|
186
|
+
">${html}</div>`;
|
|
187
|
+
}
|
|
188
|
+
return html;
|
|
189
|
+
}
|
|
190
|
+
_generateLetterAvatar(letter) {
|
|
191
|
+
return `<div style="
|
|
192
|
+
width: 100%;
|
|
193
|
+
height: 100%;
|
|
194
|
+
display: flex;
|
|
195
|
+
align-items: center;
|
|
196
|
+
justify-content: center;
|
|
197
|
+
background: #ffffff;
|
|
198
|
+
color: ${THEME.gray[600]};
|
|
199
|
+
font-size: 14px;
|
|
200
|
+
font-weight: 600;
|
|
201
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
|
|
202
|
+
">${letter}</div>`;
|
|
203
|
+
}
|
|
204
|
+
escapeHtml(str) {
|
|
205
|
+
return str
|
|
206
|
+
.replace(/&/g, '&')
|
|
207
|
+
.replace(/</g, '<')
|
|
208
|
+
.replace(/>/g, '>')
|
|
209
|
+
.replace(/"/g, '"')
|
|
210
|
+
.replace(/'/g, ''');
|
|
211
|
+
}
|
|
212
|
+
render() {
|
|
213
|
+
const messages = this.parseMessages();
|
|
214
|
+
if (messages.length === 0) {
|
|
215
|
+
this._renderEmpty();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
let lastAuthor = '';
|
|
219
|
+
const messagesHtml = messages
|
|
220
|
+
.map(({ author, text }) => {
|
|
221
|
+
const config = this.getAuthorConfig(author);
|
|
222
|
+
const isFirstFromAuthor = author !== lastAuthor;
|
|
223
|
+
lastAuthor = author;
|
|
224
|
+
const showAvatar = isFirstFromAuthor;
|
|
225
|
+
const side = config.side;
|
|
226
|
+
const isSubsequent = !isFirstFromAuthor;
|
|
227
|
+
const avatarHtml = `
|
|
228
|
+
<div class="avatar-wrapper ${showAvatar ? '' : 'avatar-wrapper--hidden'}"
|
|
229
|
+
data-author="${this.escapeHtml(author)}">
|
|
230
|
+
<div class="avatar">${config.avatar}</div>
|
|
231
|
+
<div class="avatar-tooltip">${this.escapeHtml(author)}</div>
|
|
232
|
+
</div>
|
|
233
|
+
`;
|
|
234
|
+
return `
|
|
235
|
+
<div class="msg-row msg-row--${side} ${isSubsequent ? 'msg-row--subsequent' : 'msg-row--new-author'}">
|
|
236
|
+
${side === 'left' ? avatarHtml : ''}
|
|
237
|
+
|
|
238
|
+
<div class="msg-content">
|
|
239
|
+
<div class="msg-bubble msg-bubble--${side}"
|
|
240
|
+
style="background-color: ${config.bubbleColor}; color: ${config.textColor};">
|
|
241
|
+
${this.escapeHtml(text)}
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
${side === 'right' ? avatarHtml : ''}
|
|
246
|
+
</div>
|
|
247
|
+
`;
|
|
248
|
+
})
|
|
249
|
+
.join('');
|
|
250
|
+
this.shadowRoot.innerHTML = `
|
|
251
|
+
<style>
|
|
252
|
+
:host {
|
|
253
|
+
display: block;
|
|
254
|
+
font-family: "PT Sans", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
|
255
|
+
"Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
|
|
256
|
+
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
|
|
257
|
+
"Noto Color Emoji";
|
|
258
|
+
--bb-bg-color: ${THEME.gray[50]};
|
|
259
|
+
--bb-max-height: 600px;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.history {
|
|
263
|
+
max-width: 640px;
|
|
264
|
+
margin: 0 auto;
|
|
265
|
+
display: flex;
|
|
266
|
+
flex-direction: column;
|
|
267
|
+
gap: 0.25rem;
|
|
268
|
+
max-height: var(--bb-max-height, 600px);
|
|
269
|
+
overflow-y: auto;
|
|
270
|
+
scroll-behavior: smooth;
|
|
271
|
+
background-color: transparent;
|
|
272
|
+
border-radius: 0.5rem;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/* 消息行布局 */
|
|
276
|
+
.msg-row {
|
|
277
|
+
display: flex;
|
|
278
|
+
align-items: flex-end;
|
|
279
|
+
gap: 0.5rem;
|
|
280
|
+
max-width: 80%;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.msg-row--left {
|
|
284
|
+
align-self: flex-start;
|
|
285
|
+
margin-right: auto;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.msg-row--right {
|
|
289
|
+
align-self: flex-end;
|
|
290
|
+
margin-left: auto;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.msg-row--subsequent {
|
|
294
|
+
margin-top: 0.125rem;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.msg-row--new-author {
|
|
298
|
+
margin-top: 0.75rem;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.msg-row--new-author:first-child {
|
|
302
|
+
margin-top: 0;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/* 头像容器 */
|
|
306
|
+
.avatar-wrapper {
|
|
307
|
+
position: relative;
|
|
308
|
+
flex-shrink: 0;
|
|
309
|
+
width: 1.75rem;
|
|
310
|
+
height: 1.75rem;
|
|
311
|
+
background: #ffffff;
|
|
312
|
+
border-radius: 50%;
|
|
313
|
+
overflow: hidden;
|
|
314
|
+
cursor: help;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.avatar-wrapper--hidden {
|
|
318
|
+
opacity: 0;
|
|
319
|
+
pointer-events: none;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.avatar {
|
|
323
|
+
width: 100%;
|
|
324
|
+
height: 100%;
|
|
325
|
+
display: flex;
|
|
326
|
+
align-items: center;
|
|
327
|
+
justify-content: center;
|
|
328
|
+
border-radius: 50%;
|
|
329
|
+
overflow: hidden;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.avatar svg {
|
|
333
|
+
width: 100%;
|
|
334
|
+
height: 100%;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/* 悬浮显示名字 */
|
|
338
|
+
.avatar-tooltip {
|
|
339
|
+
position: fixed;
|
|
340
|
+
padding: 0.25rem 0.5rem;
|
|
341
|
+
background: ${THEME.gray[800]};
|
|
342
|
+
color: ${THEME.gray[50]};
|
|
343
|
+
font-size: 0.75rem;
|
|
344
|
+
border-radius: 0.25rem;
|
|
345
|
+
white-space: nowrap;
|
|
346
|
+
opacity: 0;
|
|
347
|
+
visibility: hidden;
|
|
348
|
+
pointer-events: none;
|
|
349
|
+
z-index: 10;
|
|
350
|
+
font-weight: 500;
|
|
351
|
+
letter-spacing: 0.02em;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.avatar-tooltip::after {
|
|
355
|
+
content: '';
|
|
356
|
+
position: absolute;
|
|
357
|
+
top: calc(100% - 1px);
|
|
358
|
+
left: 50%;
|
|
359
|
+
transform: translateX(-50%);
|
|
360
|
+
border: 4px solid transparent;
|
|
361
|
+
border-top-color: ${THEME.gray[800]};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.avatar-wrapper:hover .avatar-tooltip {
|
|
365
|
+
opacity: 1;
|
|
366
|
+
visibility: visible;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/* 消息内容区 */
|
|
370
|
+
.msg-content {
|
|
371
|
+
display: flex;
|
|
372
|
+
flex-direction: column;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.msg-bubble {
|
|
376
|
+
padding: 0.625rem 0.875rem;
|
|
377
|
+
font-size: 0.9375rem;
|
|
378
|
+
line-height: 1.5;
|
|
379
|
+
word-wrap: break-word;
|
|
380
|
+
overflow-wrap: anywhere;
|
|
381
|
+
word-break: break-word;
|
|
382
|
+
border-radius: 1rem;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/* 左侧气泡 */
|
|
386
|
+
.msg-bubble--left {
|
|
387
|
+
border-bottom-left-radius: 0.25rem;
|
|
388
|
+
background-color: ${THEME.gray[200]};
|
|
389
|
+
color: ${THEME.gray[900]};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/* 右侧气泡 */
|
|
393
|
+
.msg-bubble--right {
|
|
394
|
+
border-bottom-right-radius: 0.25rem;
|
|
395
|
+
/* 移除边框 */
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/* 空状态 */
|
|
399
|
+
.empty-state {
|
|
400
|
+
text-align: center;
|
|
401
|
+
padding: 2rem;
|
|
402
|
+
color: ${THEME.gray[400]};
|
|
403
|
+
font-size: 0.875rem;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/* 移动端 */
|
|
407
|
+
@media (max-width: 480px) {
|
|
408
|
+
.history {
|
|
409
|
+
max-height: var(--bb-max-height, 70vh);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.msg-row {
|
|
413
|
+
max-width: 85%;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.msg-bubble {
|
|
417
|
+
font-size: 0.9375rem;
|
|
418
|
+
padding: 0.5rem 0.75rem;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.avatar-wrapper {
|
|
422
|
+
width: 1.5rem;
|
|
423
|
+
height: 1.5rem;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/* Dark mode */
|
|
428
|
+
@media (prefers-color-scheme: dark) {
|
|
429
|
+
:host {
|
|
430
|
+
--bb-bg-color: ${THEME.gray[900]};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
.history {
|
|
434
|
+
background-color: transparent;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.msg-bubble {
|
|
438
|
+
color: ${THEME.gray[100]};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.msg-bubble--left {
|
|
442
|
+
background-color: ${THEME.gray[700]};
|
|
443
|
+
color: ${THEME.gray[100]};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.avatar-wrapper {
|
|
447
|
+
background: ${THEME.gray[800]};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.empty-state {
|
|
451
|
+
color: ${THEME.gray[500]};
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/* Reduced motion */
|
|
456
|
+
@media (prefers-reduced-motion: reduce) {
|
|
457
|
+
.history {
|
|
458
|
+
scroll-behavior: auto;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
</style>
|
|
463
|
+
<div class="history" role="log" aria-live="polite" aria-label="Message history">
|
|
464
|
+
${messagesHtml}
|
|
465
|
+
</div>
|
|
466
|
+
`;
|
|
467
|
+
requestAnimationFrame(() => {
|
|
468
|
+
const container = this.shadowRoot.querySelector('.history');
|
|
469
|
+
if (container) {
|
|
470
|
+
container.scrollTop = container.scrollHeight;
|
|
471
|
+
}
|
|
472
|
+
// Position tooltips dynamically on hover to avoid overflow clipping
|
|
473
|
+
this.shadowRoot.querySelectorAll('.avatar-wrapper').forEach(wrapper => {
|
|
474
|
+
wrapper.addEventListener('mouseenter', () => {
|
|
475
|
+
const tooltip = wrapper.querySelector('.avatar-tooltip');
|
|
476
|
+
if (!tooltip)
|
|
477
|
+
return;
|
|
478
|
+
const rect = wrapper.getBoundingClientRect();
|
|
479
|
+
const tooltipRect = tooltip.getBoundingClientRect();
|
|
480
|
+
tooltip.style.left = `${rect.left + rect.width / 2 - tooltipRect.width / 2}px`;
|
|
481
|
+
tooltip.style.top = `${rect.top - tooltipRect.height - 8}px`;
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
_renderEmpty() {
|
|
487
|
+
this.shadowRoot.innerHTML = `
|
|
488
|
+
<style>
|
|
489
|
+
:host { display: block; }
|
|
490
|
+
.empty-state {
|
|
491
|
+
text-align: center;
|
|
492
|
+
padding: 2rem;
|
|
493
|
+
color: ${THEME.gray[400]};
|
|
494
|
+
font-size: 0.875rem;
|
|
495
|
+
font-family: inherit;
|
|
496
|
+
}
|
|
497
|
+
</style>
|
|
498
|
+
<div class="empty-state">No messages</div>
|
|
499
|
+
`;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
// 手动注册
|
|
503
|
+
function define(tagName = 'bb-msg-history') {
|
|
504
|
+
if (!customElements.get(tagName)) {
|
|
505
|
+
customElements.define(tagName, tagName === 'bb-msg-history'
|
|
506
|
+
? BBMsgHistory
|
|
507
|
+
: class extends BBMsgHistory {
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
// 降级处理
|
|
512
|
+
function initBBMsgHistory() {
|
|
513
|
+
try {
|
|
514
|
+
define();
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
console.warn('BBMsgHistory registration failed, falling back to plain text:', error);
|
|
518
|
+
document.querySelectorAll('bb-msg-history').forEach(el => {
|
|
519
|
+
const pre = document.createElement('pre');
|
|
520
|
+
pre.style.cssText = `
|
|
521
|
+
background: ${THEME.gray[100]};
|
|
522
|
+
padding: 1rem;
|
|
523
|
+
border-radius: 0.5rem;
|
|
524
|
+
overflow-x: auto;
|
|
525
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
|
|
526
|
+
font-size: 0.875rem;
|
|
527
|
+
line-height: 1.5;
|
|
528
|
+
color: ${THEME.gray[900]};
|
|
529
|
+
margin: 0;
|
|
530
|
+
white-space: pre-wrap;
|
|
531
|
+
word-wrap: break-word;
|
|
532
|
+
border: 1px solid ${THEME.gray[200]};
|
|
533
|
+
`;
|
|
534
|
+
pre.textContent = el.textContent || '';
|
|
535
|
+
el.replaceWith(pre);
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
// 自动初始化
|
|
540
|
+
if (document.readyState === 'loading') {
|
|
541
|
+
document.addEventListener('DOMContentLoaded', initBBMsgHistory);
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
initBBMsgHistory();
|
|
545
|
+
}
|
|
546
|
+
export { BBMsgHistory, define };
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const THEME={gray:{50:"#f9fafb",100:"#f3f4f6",200:"#e5e7eb",300:"#d1d5db",400:"#9ca3af",500:"#6b7280",600:"#4b5563",700:"#374151",800:"#1f2937",900:"#111827"},blue:{600:"#000000"},red:{50:"#fef2f2",100:"#fee2e2",200:"#fecaca",300:"#fca5a5",400:"#f87171",500:"#ef4444",600:"#dc2626"},yyPink:{50:"#fdf4f4",100:"#fbd1d2",150:"#f8babc"}},AUTHOR_CONFIG={"bbki.ng":{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>`,bubbleColor:THEME.gray[100],textColor:THEME.gray[900],side:"right"},xwy:{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>`,bubbleColor:THEME.yyPink[50],textColor:THEME.gray[900],side:"left"},"小乌鸦":{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>`,bubbleColor:THEME.yyPink[50],textColor:THEME.gray[900],side:"left"}},FIRST_CHAR_AVATAR_AUTHORS=new Set(["小乌鸦"]);class BBMsgHistory extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"})}connectedCallback(){this.render(),this._setupResizeObserver()}disconnectedCallback(){this._resizeObserver?.disconnect()}_setupResizeObserver(){"ResizeObserver"in window&&(this._resizeObserver=new ResizeObserver(()=>{this._adjustLayout()}),this._resizeObserver.observe(this))}_adjustLayout(){}parseMessages(){const n=this.textContent||"",e=[];for(const t of n.split("\n")){const n=t.trim();if(!n)continue;const r=n.indexOf(":");if(r<=0)continue;const o=n.slice(0,r).trim(),a=n.slice(r+1).trim();o&&a&&e.push({author:o,text:a})}return e}getAuthorConfig(n){if(FIRST_CHAR_AVATAR_AUTHORS.has(n)){const e=AUTHOR_CONFIG[n],t=n.charAt(0);return{...e||{bubbleColor:THEME.gray[50],textColor:THEME.gray[900],side:"left"},avatar:this._generateLetterAvatar(t),isCustomAvatar:!1}}if(AUTHOR_CONFIG[n])return{...AUTHOR_CONFIG[n],isCustomAvatar:!0};for(const[e,t]of Object.entries(AUTHOR_CONFIG))if(n.includes(e)||e.includes(n))return{...t,isCustomAvatar:!0};const e=n.charAt(0).toUpperCase();return{avatar:this._generateLetterAvatar(e),bubbleColor:THEME.gray[50],textColor:THEME.gray[900],side:"left",isCustomAvatar:!1}}_generateLetterAvatar(n){return`<div style="\n width: 100%; \n height: 100%; \n display: flex; \n align-items: center; \n justify-content: center; \n background: #ffffff; \n color: ${THEME.gray[600]}; \n font-size: 14px; \n font-weight: 600;\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;\n ">${n}</div>`}escapeHtml(n){const e=document.createElement("div");return e.textContent=n,e.innerHTML}render(){const n=this.parseMessages();if(0===n.length)return void this._renderEmpty();let e="";const t=n.map(({author:n,text:t})=>{const r=this.getAuthorConfig(n),o=n!==e;e=n;const a=o,i=r.side,s=!o,l=`\n <div class="avatar-wrapper ${a?"":"avatar-wrapper--hidden"}" \n data-author="${this.escapeHtml(n)}">\n <div class="avatar">${r.avatar}</div>\n <div class="avatar-tooltip">${this.escapeHtml(n)}</div>\n </div>\n `;return`\n <div class="msg-row msg-row--${i} ${s?"msg-row--subsequent":"msg-row--new-author"}">\n ${"left"===i?l:""}\n \n <div class="msg-content">\n <div class="msg-bubble msg-bubble--${i}" \n style="background-color: ${r.bubbleColor}; color: ${r.textColor};">\n ${this.escapeHtml(t)}\n </div>\n </div>\n \n ${"right"===i?l:""}\n </div>\n `}).join("");this.shadowRoot.innerHTML=`\n <style>\n :host {\n display: block;\n font-family: "PT Sans", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,\n "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,\n "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",\n "Noto Color Emoji";\n --bb-bg-color: ${THEME.gray[50]};\n }\n\n .history {\n max-width: 640px;\n margin: 0 auto;\n display: flex;\n flex-direction: column;\n gap: 0.25rem;\n max-height: 600px;\n overflow-y: auto;\n scroll-behavior: smooth;\n background-color: transparent;\n border-radius: 0.5rem;\n }\n\n .history::-webkit-scrollbar {\n width: 0.5rem;\n }\n .history::-webkit-scrollbar-track {\n background: transparent;\n }\n .history::-webkit-scrollbar-thumb {\n background-color: ${THEME.gray[300]};\n border-radius: 0.25rem;\n }\n\n /* 消息行布局 */\n .msg-row {\n display: flex;\n align-items: flex-end;\n gap: 0.5rem;\n max-width: 80%;\n animation: fadeIn 0.2s ease-out;\n }\n\n @keyframes fadeIn {\n from { opacity: 0; transform: translateY(4px); }\n to { opacity: 1; transform: translateY(0); }\n }\n\n .msg-row--left {\n align-self: flex-start;\n margin-right: auto;\n }\n\n .msg-row--right {\n align-self: flex-end;\n margin-left: auto;\n }\n\n .msg-row--subsequent {\n margin-top: 0.125rem;\n }\n\n .msg-row--new-author {\n margin-top: 0.75rem;\n }\n\n .msg-row--new-author:first-child {\n margin-top: 0;\n }\n\n /* 头像容器 */\n .avatar-wrapper {\n position: relative;\n flex-shrink: 0;\n width: 1.75rem;\n height: 1.75rem;\n border-radius: 50%;\n overflow: hidden;\n background: #ffffff;\n cursor: help;\n transition: transform 0.15s ease;\n }\n\n .avatar-wrapper:hover {\n transform: scale(1.1);\n }\n\n .avatar-wrapper--hidden {\n opacity: 0;\n pointer-events: none;\n }\n\n .avatar {\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n .avatar svg {\n width: 100%;\n height: 100%;\n }\n\n /* 悬浮显示名字 */\n .avatar-tooltip {\n position: absolute;\n bottom: calc(100% + 0.5rem);\n left: 50%;\n transform: translateX(-50%) scale(0.9);\n padding: 0.25rem 0.5rem;\n background: ${THEME.gray[800]};\n color: ${THEME.gray[50]};\n font-size: 0.75rem;\n border-radius: 0.25rem;\n white-space: nowrap;\n opacity: 0;\n visibility: hidden;\n transition: all 0.15s ease;\n pointer-events: none;\n z-index: 10;\n font-weight: 500;\n letter-spacing: 0.02em;\n }\n\n .avatar-tooltip::after {\n content: '';\n position: absolute;\n top: 100%;\n left: 50%;\n transform: translateX(-50%);\n border: 4px solid transparent;\n border-top-color: ${THEME.gray[800]};\n }\n\n .avatar-wrapper:hover .avatar-tooltip {\n opacity: 1;\n visibility: visible;\n transform: translateX(-50%) scale(1);\n }\n\n /* 消息内容区 */\n .msg-content {\n display: flex;\n flex-direction: column;\n }\n\n .msg-bubble {\n padding: 0.625rem 0.875rem;\n font-size: 0.9375rem;\n line-height: 1.5;\n word-wrap: break-word;\n overflow-wrap: anywhere;\n word-break: break-word;\n border-radius: 1rem;\n }\n\n /* 左侧气泡 */\n .msg-bubble--left {\n border-bottom-left-radius: 0.25rem;\n background-color: ${THEME.gray[200]};\n color: ${THEME.gray[900]};\n }\n\n /* 右侧气泡 */\n .msg-bubble--right {\n border-bottom-right-radius: 0.25rem;\n /* 移除边框 */\n }\n\n /* 空状态 */\n .empty-state {\n text-align: center;\n padding: 2rem;\n color: ${THEME.gray[400]};\n font-size: 0.875rem;\n }\n\n /* 移动端 */\n @media (max-width: 480px) {\n .history {\n max-height: 70vh;\n }\n \n .msg-row {\n max-width: 85%;\n }\n \n .msg-bubble {\n font-size: 0.9375rem;\n padding: 0.5rem 0.75rem;\n }\n \n .avatar-wrapper {\n width: 1.5rem;\n height: 1.5rem;\n }\n }\n\n /* 暗色模式 */\n @media (prefers-color-scheme: dark) {\n :host {\n --bb-bg-color: ${THEME.gray[900]};\n }\n \n .msg-bubble--left {\n background-color: ${THEME.gray[700]} !important;\n color: ${THEME.gray[100]} !important;\n }\n \n .msg-bubble--right {\n background-color: ${THEME.gray[800]} !important;\n color: ${THEME.gray[100]} !important;\n border-color: ${THEME.gray[700]};\n }\n \n .avatar-wrapper {\n background: #ffffff;\n }\n \n .avatar-tooltip {\n background: ${THEME.gray[200]};\n color: ${THEME.gray[900]};\n }\n \n .avatar-tooltip::after {\n border-top-color: ${THEME.gray[200]};\n }\n }\n\n /* 减少动画 */\n @media (prefers-reduced-motion: reduce) {\n .msg-row {\n animation: none;\n }\n }\n </style>\n <div class="history" role="log" aria-live="polite" aria-label="Message history">\n ${t}\n </div>\n `,requestAnimationFrame(()=>{const n=this.shadowRoot.querySelector(".history");n&&(n.scrollTop=n.scrollHeight)})}_renderEmpty(){this.shadowRoot.innerHTML=`\n <style>\n :host { display: block; }\n .empty-state {\n text-align: center;\n padding: 2rem;\n color: ${THEME.gray[400]};\n font-size: 0.875rem;\n font-family: inherit;\n }\n </style>\n <div class="empty-state">No messages</div>\n `}}function initBBMsgHistory(){try{customElements.get("bb-msg-history")||customElements.define("bb-msg-history",BBMsgHistory)}catch(n){console.warn("BBMsgHistory registration failed, falling back to plain text:",n),document.querySelectorAll("bb-msg-history").forEach(n=>{const e=document.createElement("pre");e.style.cssText=`\n background: ${THEME.gray[100]};\n padding: 1rem;\n border-radius: 0.5rem;\n overflow-x: auto;\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;\n font-size: 0.875rem;\n line-height: 1.5;\n color: ${THEME.gray[900]};\n margin: 0;\n white-space: pre-wrap;\n word-wrap: break-word;\n border: 1px solid ${THEME.gray[200]};\n `,e.textContent=n.textContent||"",n.replaceWith(e)})}}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",initBBMsgHistory):initBBMsgHistory();export{BBMsgHistory};
|
|
1
|
+
const THEME={gray:{50:"#f9fafb",100:"#f3f4f6",200:"#e5e7eb",300:"#d1d5db",400:"#9ca3af",500:"#6b7280",600:"#4b5563",700:"#374151",800:"#1f2937",900:"#111827"},red:{50:"#fef2f2",100:"#fee2e2",200:"#fecaca",300:"#fca5a5",400:"#f87171",500:"#ef4444",600:"#dc2626"},yyPink:{50:"#fdf4f4",100:"#fbd1d2",150:"#f8babc"}},AUTHOR_CONFIG={"bbki.ng":{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>`,bubbleColor:THEME.gray[100],textColor:THEME.gray[900],side:"right"},xwy:{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>`,bubbleColor:THEME.yyPink[50],textColor:THEME.gray[900],side:"left"},"小乌鸦":{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>`,bubbleColor:THEME.yyPink[50],textColor:THEME.gray[900],side:"left"}},FIRST_CHAR_AVATAR_AUTHORS=new Set(["小乌鸦"]);class BBMsgHistory extends HTMLElement{constructor(){super(),this._userAuthors=new Map,this.attachShadow({mode:"open"})}setAuthor(e,n){return this._userAuthors.set(e,n),this.render(),this}removeAuthor(e){return this._userAuthors.delete(e),this.render(),this}connectedCallback(){this.render(),this._setupMutationObserver()}disconnectedCallback(){this._mutationObserver?.disconnect()}_setupMutationObserver(){let e;this._mutationObserver=new MutationObserver(()=>{clearTimeout(e),e=setTimeout(()=>this.render(),50)}),this._mutationObserver.observe(this,{childList:!0,characterData:!0,subtree:!0})}parseMessages(){const e=this.textContent||"",n=[];for(const t of e.split("\n")){const e=t.trim();if(!e)continue;const r=e.indexOf(":");if(r<=0)continue;const a=e.slice(0,r).trim(),o=e.slice(r+1).trim();a&&o&&n.push({author:a,text:o})}return n}getAuthorConfig(e){const n=this._userAuthors.get(e);if(n)return{avatar:n.avatar?this._wrapAvatarHtml(n.avatar):this._generateLetterAvatar(e.charAt(0).toUpperCase()),bubbleColor:n.bubbleColor||THEME.gray[50],textColor:n.textColor||THEME.gray[900],side:n.side||"left",isCustomAvatar:!!n.avatar};for(const[n,t]of this._userAuthors.entries())if(e.includes(n))return{avatar:t.avatar?this._wrapAvatarHtml(t.avatar):this._generateLetterAvatar(e.charAt(0).toUpperCase()),bubbleColor:t.bubbleColor||THEME.gray[50],textColor:t.textColor||THEME.gray[900],side:t.side||"left",isCustomAvatar:!!t.avatar};if(FIRST_CHAR_AVATAR_AUTHORS.has(e)){const n=AUTHOR_CONFIG[e],t=e.charAt(0);return{...n||{bubbleColor:THEME.gray[50],textColor:THEME.gray[900],side:"left"},avatar:this._generateLetterAvatar(t),isCustomAvatar:!1}}if(AUTHOR_CONFIG[e])return{...AUTHOR_CONFIG[e],isCustomAvatar:!0};for(const[n,t]of Object.entries(AUTHOR_CONFIG))if(e.includes(n))return{...t,isCustomAvatar:!0};const t=e.charAt(0).toUpperCase();return{avatar:this._generateLetterAvatar(t),bubbleColor:THEME.gray[50],textColor:THEME.gray[900],side:"left",isCustomAvatar:!1}}_wrapAvatarHtml(e){return e.includes("<")?e:`<div style="\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 18px;\n line-height: 1;\n ">${e}</div>`}_generateLetterAvatar(e){return`<div style="\n width: 100%; \n height: 100%; \n display: flex; \n align-items: center; \n justify-content: center; \n background: #ffffff; \n color: ${THEME.gray[600]}; \n font-size: 14px; \n font-weight: 600;\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;\n ">${e}</div>`}escapeHtml(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}render(){const e=this.parseMessages();if(0===e.length)return void this._renderEmpty();let n="";const t=e.map(({author:e,text:t})=>{const r=this.getAuthorConfig(e),a=e!==n;n=e;const o=a,i=r.side,s=!a,l=`\n <div class="avatar-wrapper ${o?"":"avatar-wrapper--hidden"}" \n data-author="${this.escapeHtml(e)}">\n <div class="avatar">${r.avatar}</div>\n <div class="avatar-tooltip">${this.escapeHtml(e)}</div>\n </div>\n `;return`\n <div class="msg-row msg-row--${i} ${s?"msg-row--subsequent":"msg-row--new-author"}">\n ${"left"===i?l:""}\n \n <div class="msg-content">\n <div class="msg-bubble msg-bubble--${i}" \n style="background-color: ${r.bubbleColor}; color: ${r.textColor};">\n ${this.escapeHtml(t)}\n </div>\n </div>\n \n ${"right"===i?l:""}\n </div>\n `}).join("");this.shadowRoot.innerHTML=`\n <style>\n :host {\n display: block;\n font-family: "PT Sans", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,\n "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,\n "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",\n "Noto Color Emoji";\n --bb-bg-color: ${THEME.gray[50]};\n --bb-max-height: 600px;\n }\n\n .history {\n max-width: 640px;\n margin: 0 auto;\n display: flex;\n flex-direction: column;\n gap: 0.25rem;\n max-height: var(--bb-max-height, 600px);\n overflow-y: auto;\n scroll-behavior: smooth;\n background-color: transparent;\n border-radius: 0.5rem;\n }\n\n /* 消息行布局 */\n .msg-row {\n display: flex;\n align-items: flex-end;\n gap: 0.5rem;\n max-width: 80%;\n }\n\n .msg-row--left {\n align-self: flex-start;\n margin-right: auto;\n }\n\n .msg-row--right {\n align-self: flex-end;\n margin-left: auto;\n }\n\n .msg-row--subsequent {\n margin-top: 0.125rem;\n }\n\n .msg-row--new-author {\n margin-top: 0.75rem;\n }\n\n .msg-row--new-author:first-child {\n margin-top: 0;\n }\n\n /* 头像容器 */\n .avatar-wrapper {\n position: relative;\n flex-shrink: 0;\n width: 1.75rem;\n height: 1.75rem;\n background: #ffffff;\n border-radius: 50%;\n overflow: hidden;\n cursor: help;\n }\n\n .avatar-wrapper--hidden {\n opacity: 0;\n pointer-events: none;\n }\n\n .avatar {\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 50%;\n overflow: hidden;\n }\n\n .avatar svg {\n width: 100%;\n height: 100%;\n }\n\n /* 悬浮显示名字 */\n .avatar-tooltip {\n position: fixed;\n padding: 0.25rem 0.5rem;\n background: ${THEME.gray[800]};\n color: ${THEME.gray[50]};\n font-size: 0.75rem;\n border-radius: 0.25rem;\n white-space: nowrap;\n opacity: 0;\n visibility: hidden;\n pointer-events: none;\n z-index: 10;\n font-weight: 500;\n letter-spacing: 0.02em;\n }\n\n .avatar-tooltip::after {\n content: '';\n position: absolute;\n top: calc(100% - 1px);\n left: 50%;\n transform: translateX(-50%);\n border: 4px solid transparent;\n border-top-color: ${THEME.gray[800]};\n }\n\n .avatar-wrapper:hover .avatar-tooltip {\n opacity: 1;\n visibility: visible;\n }\n\n /* 消息内容区 */\n .msg-content {\n display: flex;\n flex-direction: column;\n }\n\n .msg-bubble {\n padding: 0.625rem 0.875rem;\n font-size: 0.9375rem;\n line-height: 1.5;\n word-wrap: break-word;\n overflow-wrap: anywhere;\n word-break: break-word;\n border-radius: 1rem;\n }\n\n /* 左侧气泡 */\n .msg-bubble--left {\n border-bottom-left-radius: 0.25rem;\n background-color: ${THEME.gray[200]};\n color: ${THEME.gray[900]};\n }\n\n /* 右侧气泡 */\n .msg-bubble--right {\n border-bottom-right-radius: 0.25rem;\n /* 移除边框 */\n }\n\n /* 空状态 */\n .empty-state {\n text-align: center;\n padding: 2rem;\n color: ${THEME.gray[400]};\n font-size: 0.875rem;\n }\n\n /* 移动端 */\n @media (max-width: 480px) {\n .history {\n max-height: var(--bb-max-height, 70vh);\n }\n \n .msg-row {\n max-width: 85%;\n }\n \n .msg-bubble {\n font-size: 0.9375rem;\n padding: 0.5rem 0.75rem;\n }\n \n .avatar-wrapper {\n width: 1.5rem;\n height: 1.5rem;\n }\n }\n\n /* Dark mode */\n @media (prefers-color-scheme: dark) {\n :host {\n --bb-bg-color: ${THEME.gray[900]};\n }\n\n .history {\n background-color: transparent;\n }\n\n .msg-bubble {\n color: ${THEME.gray[100]};\n }\n\n .msg-bubble--left {\n background-color: ${THEME.gray[700]};\n color: ${THEME.gray[100]};\n }\n\n .avatar-wrapper {\n background: ${THEME.gray[800]};\n }\n\n .empty-state {\n color: ${THEME.gray[500]};\n }\n }\n\n /* Reduced motion */\n @media (prefers-reduced-motion: reduce) {\n .history {\n scroll-behavior: auto;\n }\n }\n\n </style>\n <div class="history" role="log" aria-live="polite" aria-label="Message history">\n ${t}\n </div>\n `,requestAnimationFrame(()=>{const e=this.shadowRoot.querySelector(".history");e&&(e.scrollTop=e.scrollHeight),this.shadowRoot.querySelectorAll(".avatar-wrapper").forEach(e=>{e.addEventListener("mouseenter",()=>{const n=e.querySelector(".avatar-tooltip");if(!n)return;const t=e.getBoundingClientRect(),r=n.getBoundingClientRect();n.style.left=t.left+t.width/2-r.width/2+"px",n.style.top=t.top-r.height-8+"px"})})})}_renderEmpty(){this.shadowRoot.innerHTML=`\n <style>\n :host { display: block; }\n .empty-state {\n text-align: center;\n padding: 2rem;\n color: ${THEME.gray[400]};\n font-size: 0.875rem;\n font-family: inherit;\n }\n </style>\n <div class="empty-state">No messages</div>\n `}}function define(e="bb-msg-history"){customElements.get(e)||customElements.define(e,"bb-msg-history"===e?BBMsgHistory:class extends BBMsgHistory{})}function initBBMsgHistory(){try{define()}catch(e){console.warn("BBMsgHistory registration failed, falling back to plain text:",e),document.querySelectorAll("bb-msg-history").forEach(e=>{const n=document.createElement("pre");n.style.cssText=`\n background: ${THEME.gray[100]};\n padding: 1rem;\n border-radius: 0.5rem;\n overflow-x: auto;\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;\n font-size: 0.875rem;\n line-height: 1.5;\n color: ${THEME.gray[900]};\n margin: 0;\n white-space: pre-wrap;\n word-wrap: break-word;\n border: 1px solid ${THEME.gray[200]};\n `,n.textContent=e.textContent||"",e.replaceWith(n)})}}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",initBBMsgHistory):initBBMsgHistory();export{BBMsgHistory,define};
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -11,6 +11,17 @@ interface AuthorConfig {
|
|
|
11
11
|
isCustomAvatar: boolean;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
interface AuthorOptions {
|
|
15
|
+
/** Avatar HTML string: SVG, <img>, emoji, or plain text */
|
|
16
|
+
avatar?: string;
|
|
17
|
+
/** Which side the messages appear on */
|
|
18
|
+
side?: 'left' | 'right';
|
|
19
|
+
/** Bubble background color */
|
|
20
|
+
bubbleColor?: string;
|
|
21
|
+
/** Text color inside bubble */
|
|
22
|
+
textColor?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
14
25
|
// 主题色板
|
|
15
26
|
const THEME = {
|
|
16
27
|
gray: {
|
|
@@ -25,9 +36,6 @@ const THEME = {
|
|
|
25
36
|
800: '#1f2937',
|
|
26
37
|
900: '#111827',
|
|
27
38
|
},
|
|
28
|
-
blue: {
|
|
29
|
-
600: '#000000',
|
|
30
|
-
},
|
|
31
39
|
red: {
|
|
32
40
|
50: '#fef2f2',
|
|
33
41
|
100: '#fee2e2',
|
|
@@ -70,33 +78,59 @@ const AUTHOR_CONFIG: Record<string, Omit<AuthorConfig, 'isCustomAvatar'>> = {
|
|
|
70
78
|
const FIRST_CHAR_AVATAR_AUTHORS = new Set(['小乌鸦']);
|
|
71
79
|
|
|
72
80
|
class BBMsgHistory extends HTMLElement {
|
|
73
|
-
private
|
|
81
|
+
private _mutationObserver?: MutationObserver;
|
|
82
|
+
private _userAuthors = new Map<string, AuthorOptions>();
|
|
74
83
|
|
|
75
84
|
constructor() {
|
|
76
85
|
super();
|
|
77
86
|
this.attachShadow({ mode: 'open' });
|
|
78
87
|
}
|
|
79
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Configure an author's avatar, side, and colors.
|
|
91
|
+
* Call before or after rendering — the component re-renders automatically.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* el.setAuthor('alice', { avatar: '🐱', side: 'right', bubbleColor: '#e0f2fe' });
|
|
95
|
+
* el.setAuthor('bob', { avatar: '<img src="bob.png" />', side: 'left' });
|
|
96
|
+
*/
|
|
97
|
+
setAuthor(name: string, options: AuthorOptions): this {
|
|
98
|
+
this._userAuthors.set(name, options);
|
|
99
|
+
this.render();
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Remove a previously set author config.
|
|
105
|
+
*/
|
|
106
|
+
removeAuthor(name: string): this {
|
|
107
|
+
this._userAuthors.delete(name);
|
|
108
|
+
this.render();
|
|
109
|
+
return this;
|
|
110
|
+
}
|
|
111
|
+
|
|
80
112
|
connectedCallback() {
|
|
81
113
|
this.render();
|
|
82
|
-
this.
|
|
114
|
+
this._setupMutationObserver();
|
|
83
115
|
}
|
|
84
116
|
|
|
85
117
|
disconnectedCallback() {
|
|
86
|
-
this.
|
|
118
|
+
this._mutationObserver?.disconnect();
|
|
87
119
|
}
|
|
88
120
|
|
|
89
|
-
private
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
121
|
+
private _setupMutationObserver() {
|
|
122
|
+
let debounceTimer: ReturnType<typeof setTimeout>;
|
|
123
|
+
this._mutationObserver = new MutationObserver(() => {
|
|
124
|
+
clearTimeout(debounceTimer);
|
|
125
|
+
debounceTimer = setTimeout(() => this.render(), 50);
|
|
126
|
+
});
|
|
127
|
+
this._mutationObserver.observe(this, {
|
|
128
|
+
childList: true,
|
|
129
|
+
characterData: true,
|
|
130
|
+
subtree: true,
|
|
131
|
+
});
|
|
96
132
|
}
|
|
97
133
|
|
|
98
|
-
private _adjustLayout() {}
|
|
99
|
-
|
|
100
134
|
private parseMessages(): Message[] {
|
|
101
135
|
const raw = this.textContent || '';
|
|
102
136
|
const messages: Message[] = [];
|
|
@@ -120,7 +154,36 @@ class BBMsgHistory extends HTMLElement {
|
|
|
120
154
|
}
|
|
121
155
|
|
|
122
156
|
private getAuthorConfig(author: string): AuthorConfig {
|
|
123
|
-
//
|
|
157
|
+
// 1. 用户自定义配置(精确匹配)
|
|
158
|
+
const userConfig = this._userAuthors.get(author);
|
|
159
|
+
if (userConfig) {
|
|
160
|
+
return {
|
|
161
|
+
avatar: userConfig.avatar
|
|
162
|
+
? this._wrapAvatarHtml(userConfig.avatar)
|
|
163
|
+
: this._generateLetterAvatar(author.charAt(0).toUpperCase()),
|
|
164
|
+
bubbleColor: userConfig.bubbleColor || THEME.gray[50],
|
|
165
|
+
textColor: userConfig.textColor || THEME.gray[900],
|
|
166
|
+
side: userConfig.side || 'left',
|
|
167
|
+
isCustomAvatar: !!userConfig.avatar,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 2. 用户自定义配置(模糊匹配:作者名包含已配置的 key)
|
|
172
|
+
for (const [key, cfg] of this._userAuthors.entries()) {
|
|
173
|
+
if (author.includes(key)) {
|
|
174
|
+
return {
|
|
175
|
+
avatar: cfg.avatar
|
|
176
|
+
? this._wrapAvatarHtml(cfg.avatar)
|
|
177
|
+
: this._generateLetterAvatar(author.charAt(0).toUpperCase()),
|
|
178
|
+
bubbleColor: cfg.bubbleColor || THEME.gray[50],
|
|
179
|
+
textColor: cfg.textColor || THEME.gray[900],
|
|
180
|
+
side: cfg.side || 'left',
|
|
181
|
+
isCustomAvatar: !!cfg.avatar,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 3. 内置首字符头像作者(secret)
|
|
124
187
|
if (FIRST_CHAR_AVATAR_AUTHORS.has(author)) {
|
|
125
188
|
const config = AUTHOR_CONFIG[author];
|
|
126
189
|
const firstChar = author.charAt(0);
|
|
@@ -130,30 +193,46 @@ class BBMsgHistory extends HTMLElement {
|
|
|
130
193
|
isCustomAvatar: false
|
|
131
194
|
};
|
|
132
195
|
}
|
|
133
|
-
|
|
196
|
+
|
|
197
|
+
// 4. 内置配置精确匹配(secret)
|
|
134
198
|
if (AUTHOR_CONFIG[author]) {
|
|
135
199
|
return { ...AUTHOR_CONFIG[author], isCustomAvatar: true };
|
|
136
200
|
}
|
|
137
|
-
|
|
201
|
+
|
|
202
|
+
// 5. 内置配置模糊匹配(secret)
|
|
138
203
|
for (const [key, config] of Object.entries(AUTHOR_CONFIG)) {
|
|
139
|
-
if (author.includes(key)
|
|
204
|
+
if (author.includes(key)) {
|
|
140
205
|
return { ...config, isCustomAvatar: true };
|
|
141
206
|
}
|
|
142
207
|
}
|
|
143
208
|
|
|
144
|
-
//
|
|
209
|
+
// 6. 默认:首字母头像,左侧
|
|
145
210
|
const firstChar = author.charAt(0).toUpperCase();
|
|
146
|
-
const side = 'left'; // 默认左侧
|
|
147
|
-
|
|
148
211
|
return {
|
|
149
212
|
avatar: this._generateLetterAvatar(firstChar),
|
|
150
213
|
bubbleColor: THEME.gray[50],
|
|
151
214
|
textColor: THEME.gray[900],
|
|
152
|
-
side,
|
|
215
|
+
side: 'left',
|
|
153
216
|
isCustomAvatar: false
|
|
154
217
|
};
|
|
155
218
|
}
|
|
156
219
|
|
|
220
|
+
private _wrapAvatarHtml(html: string): string {
|
|
221
|
+
// If it looks like a single emoji or short text (no HTML tags), wrap in a styled div
|
|
222
|
+
if (!html.includes('<')) {
|
|
223
|
+
return `<div style="
|
|
224
|
+
width: 100%;
|
|
225
|
+
height: 100%;
|
|
226
|
+
display: flex;
|
|
227
|
+
align-items: center;
|
|
228
|
+
justify-content: center;
|
|
229
|
+
font-size: 18px;
|
|
230
|
+
line-height: 1;
|
|
231
|
+
">${html}</div>`;
|
|
232
|
+
}
|
|
233
|
+
return html;
|
|
234
|
+
}
|
|
235
|
+
|
|
157
236
|
private _generateLetterAvatar(letter: string): string {
|
|
158
237
|
return `<div style="
|
|
159
238
|
width: 100%;
|
|
@@ -170,9 +249,12 @@ class BBMsgHistory extends HTMLElement {
|
|
|
170
249
|
}
|
|
171
250
|
|
|
172
251
|
private escapeHtml(str: string): string {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
252
|
+
return str
|
|
253
|
+
.replace(/&/g, '&')
|
|
254
|
+
.replace(/</g, '<')
|
|
255
|
+
.replace(/>/g, '>')
|
|
256
|
+
.replace(/"/g, '"')
|
|
257
|
+
.replace(/'/g, ''');
|
|
176
258
|
}
|
|
177
259
|
|
|
178
260
|
private render() {
|
|
@@ -228,6 +310,7 @@ class BBMsgHistory extends HTMLElement {
|
|
|
228
310
|
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
|
|
229
311
|
"Noto Color Emoji";
|
|
230
312
|
--bb-bg-color: ${THEME.gray[50]};
|
|
313
|
+
--bb-max-height: 600px;
|
|
231
314
|
}
|
|
232
315
|
|
|
233
316
|
.history {
|
|
@@ -236,36 +319,19 @@ class BBMsgHistory extends HTMLElement {
|
|
|
236
319
|
display: flex;
|
|
237
320
|
flex-direction: column;
|
|
238
321
|
gap: 0.25rem;
|
|
239
|
-
max-height: 600px;
|
|
322
|
+
max-height: var(--bb-max-height, 600px);
|
|
240
323
|
overflow-y: auto;
|
|
241
324
|
scroll-behavior: smooth;
|
|
242
325
|
background-color: transparent;
|
|
243
326
|
border-radius: 0.5rem;
|
|
244
327
|
}
|
|
245
328
|
|
|
246
|
-
.history::-webkit-scrollbar {
|
|
247
|
-
width: 0.5rem;
|
|
248
|
-
}
|
|
249
|
-
.history::-webkit-scrollbar-track {
|
|
250
|
-
background: transparent;
|
|
251
|
-
}
|
|
252
|
-
.history::-webkit-scrollbar-thumb {
|
|
253
|
-
background-color: ${THEME.gray[300]};
|
|
254
|
-
border-radius: 0.25rem;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
329
|
/* 消息行布局 */
|
|
258
330
|
.msg-row {
|
|
259
331
|
display: flex;
|
|
260
332
|
align-items: flex-end;
|
|
261
333
|
gap: 0.5rem;
|
|
262
334
|
max-width: 80%;
|
|
263
|
-
animation: fadeIn 0.2s ease-out;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
@keyframes fadeIn {
|
|
267
|
-
from { opacity: 0; transform: translateY(4px); }
|
|
268
|
-
to { opacity: 1; transform: translateY(0); }
|
|
269
335
|
}
|
|
270
336
|
|
|
271
337
|
.msg-row--left {
|
|
@@ -296,15 +362,10 @@ class BBMsgHistory extends HTMLElement {
|
|
|
296
362
|
flex-shrink: 0;
|
|
297
363
|
width: 1.75rem;
|
|
298
364
|
height: 1.75rem;
|
|
365
|
+
background: #ffffff;
|
|
299
366
|
border-radius: 50%;
|
|
300
367
|
overflow: hidden;
|
|
301
|
-
background: #ffffff;
|
|
302
368
|
cursor: help;
|
|
303
|
-
transition: transform 0.15s ease;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
.avatar-wrapper:hover {
|
|
307
|
-
transform: scale(1.1);
|
|
308
369
|
}
|
|
309
370
|
|
|
310
371
|
.avatar-wrapper--hidden {
|
|
@@ -318,6 +379,8 @@ class BBMsgHistory extends HTMLElement {
|
|
|
318
379
|
display: flex;
|
|
319
380
|
align-items: center;
|
|
320
381
|
justify-content: center;
|
|
382
|
+
border-radius: 50%;
|
|
383
|
+
overflow: hidden;
|
|
321
384
|
}
|
|
322
385
|
|
|
323
386
|
.avatar svg {
|
|
@@ -327,10 +390,7 @@ class BBMsgHistory extends HTMLElement {
|
|
|
327
390
|
|
|
328
391
|
/* 悬浮显示名字 */
|
|
329
392
|
.avatar-tooltip {
|
|
330
|
-
position:
|
|
331
|
-
bottom: calc(100% + 0.5rem);
|
|
332
|
-
left: 50%;
|
|
333
|
-
transform: translateX(-50%) scale(0.9);
|
|
393
|
+
position: fixed;
|
|
334
394
|
padding: 0.25rem 0.5rem;
|
|
335
395
|
background: ${THEME.gray[800]};
|
|
336
396
|
color: ${THEME.gray[50]};
|
|
@@ -339,7 +399,6 @@ class BBMsgHistory extends HTMLElement {
|
|
|
339
399
|
white-space: nowrap;
|
|
340
400
|
opacity: 0;
|
|
341
401
|
visibility: hidden;
|
|
342
|
-
transition: all 0.15s ease;
|
|
343
402
|
pointer-events: none;
|
|
344
403
|
z-index: 10;
|
|
345
404
|
font-weight: 500;
|
|
@@ -349,7 +408,7 @@ class BBMsgHistory extends HTMLElement {
|
|
|
349
408
|
.avatar-tooltip::after {
|
|
350
409
|
content: '';
|
|
351
410
|
position: absolute;
|
|
352
|
-
top: 100
|
|
411
|
+
top: calc(100% - 1px);
|
|
353
412
|
left: 50%;
|
|
354
413
|
transform: translateX(-50%);
|
|
355
414
|
border: 4px solid transparent;
|
|
@@ -359,7 +418,6 @@ class BBMsgHistory extends HTMLElement {
|
|
|
359
418
|
.avatar-wrapper:hover .avatar-tooltip {
|
|
360
419
|
opacity: 1;
|
|
361
420
|
visibility: visible;
|
|
362
|
-
transform: translateX(-50%) scale(1);
|
|
363
421
|
}
|
|
364
422
|
|
|
365
423
|
/* 消息内容区 */
|
|
@@ -402,7 +460,7 @@ class BBMsgHistory extends HTMLElement {
|
|
|
402
460
|
/* 移动端 */
|
|
403
461
|
@media (max-width: 480px) {
|
|
404
462
|
.history {
|
|
405
|
-
max-height: 70vh;
|
|
463
|
+
max-height: var(--bb-max-height, 70vh);
|
|
406
464
|
}
|
|
407
465
|
|
|
408
466
|
.msg-row {
|
|
@@ -420,43 +478,41 @@ class BBMsgHistory extends HTMLElement {
|
|
|
420
478
|
}
|
|
421
479
|
}
|
|
422
480
|
|
|
423
|
-
/*
|
|
481
|
+
/* Dark mode */
|
|
424
482
|
@media (prefers-color-scheme: dark) {
|
|
425
483
|
:host {
|
|
426
484
|
--bb-bg-color: ${THEME.gray[900]};
|
|
427
485
|
}
|
|
428
|
-
|
|
429
|
-
.
|
|
430
|
-
background-color:
|
|
431
|
-
color: ${THEME.gray[100]} !important;
|
|
486
|
+
|
|
487
|
+
.history {
|
|
488
|
+
background-color: transparent;
|
|
432
489
|
}
|
|
433
|
-
|
|
434
|
-
.msg-bubble
|
|
435
|
-
|
|
436
|
-
color: ${THEME.gray[100]} !important;
|
|
437
|
-
border-color: ${THEME.gray[700]};
|
|
490
|
+
|
|
491
|
+
.msg-bubble {
|
|
492
|
+
color: ${THEME.gray[100]};
|
|
438
493
|
}
|
|
439
|
-
|
|
440
|
-
.
|
|
441
|
-
background:
|
|
494
|
+
|
|
495
|
+
.msg-bubble--left {
|
|
496
|
+
background-color: ${THEME.gray[700]};
|
|
497
|
+
color: ${THEME.gray[100]};
|
|
442
498
|
}
|
|
443
|
-
|
|
444
|
-
.avatar-
|
|
445
|
-
background: ${THEME.gray[
|
|
446
|
-
color: ${THEME.gray[900]};
|
|
499
|
+
|
|
500
|
+
.avatar-wrapper {
|
|
501
|
+
background: ${THEME.gray[800]};
|
|
447
502
|
}
|
|
448
|
-
|
|
449
|
-
.
|
|
450
|
-
|
|
503
|
+
|
|
504
|
+
.empty-state {
|
|
505
|
+
color: ${THEME.gray[500]};
|
|
451
506
|
}
|
|
452
507
|
}
|
|
453
508
|
|
|
454
|
-
/*
|
|
509
|
+
/* Reduced motion */
|
|
455
510
|
@media (prefers-reduced-motion: reduce) {
|
|
456
|
-
.
|
|
457
|
-
|
|
511
|
+
.history {
|
|
512
|
+
scroll-behavior: auto;
|
|
458
513
|
}
|
|
459
514
|
}
|
|
515
|
+
|
|
460
516
|
</style>
|
|
461
517
|
<div class="history" role="log" aria-live="polite" aria-label="Message history">
|
|
462
518
|
${messagesHtml}
|
|
@@ -468,6 +524,18 @@ class BBMsgHistory extends HTMLElement {
|
|
|
468
524
|
if (container) {
|
|
469
525
|
container.scrollTop = container.scrollHeight;
|
|
470
526
|
}
|
|
527
|
+
|
|
528
|
+
// Position tooltips dynamically on hover to avoid overflow clipping
|
|
529
|
+
this.shadowRoot!.querySelectorAll('.avatar-wrapper').forEach(wrapper => {
|
|
530
|
+
wrapper.addEventListener('mouseenter', () => {
|
|
531
|
+
const tooltip = wrapper.querySelector('.avatar-tooltip') as HTMLElement;
|
|
532
|
+
if (!tooltip) return;
|
|
533
|
+
const rect = wrapper.getBoundingClientRect();
|
|
534
|
+
const tooltipRect = tooltip.getBoundingClientRect();
|
|
535
|
+
tooltip.style.left = `${rect.left + rect.width / 2 - tooltipRect.width / 2}px`;
|
|
536
|
+
tooltip.style.top = `${rect.top - tooltipRect.height - 8}px`;
|
|
537
|
+
});
|
|
538
|
+
});
|
|
471
539
|
});
|
|
472
540
|
}
|
|
473
541
|
|
|
@@ -488,12 +556,20 @@ class BBMsgHistory extends HTMLElement {
|
|
|
488
556
|
}
|
|
489
557
|
}
|
|
490
558
|
|
|
559
|
+
// 手动注册
|
|
560
|
+
function define(tagName = 'bb-msg-history') {
|
|
561
|
+
if (!customElements.get(tagName)) {
|
|
562
|
+
customElements.define(tagName, tagName === 'bb-msg-history'
|
|
563
|
+
? BBMsgHistory
|
|
564
|
+
: class extends BBMsgHistory {}
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
491
569
|
// 降级处理
|
|
492
570
|
function initBBMsgHistory() {
|
|
493
571
|
try {
|
|
494
|
-
|
|
495
|
-
customElements.define('bb-msg-history', BBMsgHistory);
|
|
496
|
-
}
|
|
572
|
+
define();
|
|
497
573
|
} catch (error) {
|
|
498
574
|
console.warn('BBMsgHistory registration failed, falling back to plain text:', error);
|
|
499
575
|
|
|
@@ -526,7 +602,8 @@ if (document.readyState === 'loading') {
|
|
|
526
602
|
initBBMsgHistory();
|
|
527
603
|
}
|
|
528
604
|
|
|
529
|
-
export { BBMsgHistory };
|
|
605
|
+
export { BBMsgHistory, define };
|
|
606
|
+
export type { AuthorOptions };
|
|
530
607
|
|
|
531
608
|
declare global {
|
|
532
609
|
interface HTMLElementTagNameMap {
|