@bbki.ng/bb-msg-history 1.0.0 → 2.0.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/dist/components/bb-custom-avatar.d.ts +20 -0
- package/dist/components/bb-custom-avatar.js +145 -0
- package/dist/components/bb-letter-avatar.d.ts +14 -0
- package/dist/components/bb-letter-avatar.js +61 -0
- package/dist/components/bb-loading-overlay.d.ts +14 -0
- package/dist/components/bb-loading-overlay.js +89 -0
- package/dist/components/bb-message-bubble.d.ts +19 -0
- package/dist/components/bb-message-bubble.js +116 -0
- package/dist/components/bb-message.d.ts +27 -0
- package/dist/components/bb-message.js +174 -0
- package/dist/components/bb-msg-history.d.ts +111 -0
- package/dist/components/bb-msg-history.js +473 -0
- package/dist/components/bb-scroll-button.d.ts +16 -0
- package/dist/components/bb-scroll-button.js +161 -0
- package/dist/components/bb-timestamp.d.ts +15 -0
- package/dist/components/bb-timestamp.js +59 -0
- package/dist/components/index.d.ts +7 -0
- package/dist/components/index.js +7 -0
- package/dist/const/styles.js +0 -33
- package/dist/contexts/author-context.d.ts +8 -0
- package/dist/contexts/author-context.js +6 -0
- package/dist/controllers/scroll-controller.d.ts +52 -0
- package/dist/controllers/scroll-controller.js +138 -0
- package/dist/core/renderer.js +1 -9
- package/dist/parsers/base.d.ts +21 -0
- package/dist/parsers/base.js +1 -0
- package/dist/parsers/default-parser.d.ts +10 -0
- package/dist/parsers/default-parser.js +40 -0
- package/dist/parsers/index.d.ts +2 -0
- package/dist/parsers/index.js +1 -0
- package/dist/utils/message-builder.d.ts +0 -4
- package/dist/utils/message-builder.js +0 -15
- package/dist/utils/tooltip.d.ts +11 -2
- package/dist/utils/tooltip.js +56 -13
- package/package.json +1 -1
- package/src/const/styles.ts +0 -33
- package/src/core/renderer.ts +1 -11
- package/src/utils/message-builder.ts +0 -15
- package/src/utils/tooltip.ts +0 -16
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { LitElement, html, css, unsafeCSS } from 'lit';
|
|
8
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
9
|
+
import { THEME } from '../const/theme.js';
|
|
10
|
+
/**
|
|
11
|
+
* Scroll-to-bottom button component
|
|
12
|
+
* Emits bb-scroll-to-bottom event when clicked
|
|
13
|
+
*/
|
|
14
|
+
let BBScrollButton = class BBScrollButton extends LitElement {
|
|
15
|
+
constructor() {
|
|
16
|
+
super(...arguments);
|
|
17
|
+
this.visible = false;
|
|
18
|
+
}
|
|
19
|
+
static { this.styles = css `
|
|
20
|
+
:host {
|
|
21
|
+
display: block;
|
|
22
|
+
position: absolute;
|
|
23
|
+
bottom: 16px;
|
|
24
|
+
left: 50%;
|
|
25
|
+
transform: translateX(-50%);
|
|
26
|
+
z-index: 10;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.scroll-btn {
|
|
30
|
+
width: 36px;
|
|
31
|
+
height: 36px;
|
|
32
|
+
border-radius: 50%;
|
|
33
|
+
background: #ffffff;
|
|
34
|
+
border: none;
|
|
35
|
+
color: ${unsafeCSS(THEME.gray[500])};
|
|
36
|
+
cursor: pointer;
|
|
37
|
+
display: flex;
|
|
38
|
+
align-items: center;
|
|
39
|
+
justify-content: center;
|
|
40
|
+
opacity: 0;
|
|
41
|
+
visibility: hidden;
|
|
42
|
+
transform: translateY(10px) scale(0);
|
|
43
|
+
transition:
|
|
44
|
+
opacity 0.2s ease,
|
|
45
|
+
transform 0.2s ease,
|
|
46
|
+
visibility 0.2s ease,
|
|
47
|
+
box-shadow 0.2s ease;
|
|
48
|
+
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.scroll-btn.visible {
|
|
52
|
+
opacity: 1;
|
|
53
|
+
visibility: visible;
|
|
54
|
+
transform: translateY(0) scale(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.scroll-btn:hover {
|
|
58
|
+
color: ${unsafeCSS(THEME.gray[700])};
|
|
59
|
+
transform: translateY(-2px) scale(1.05);
|
|
60
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.scroll-btn:active {
|
|
64
|
+
transform: translateY(-1px) scale(0.95);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.scroll-btn svg {
|
|
68
|
+
width: 20px;
|
|
69
|
+
height: 20px;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@media (max-width: 480px) {
|
|
73
|
+
:host {
|
|
74
|
+
bottom: 12px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.scroll-btn {
|
|
78
|
+
width: 32px;
|
|
79
|
+
height: 32px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.scroll-btn svg {
|
|
83
|
+
width: 18px;
|
|
84
|
+
height: 18px;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@media (prefers-reduced-motion: reduce) {
|
|
89
|
+
.scroll-btn {
|
|
90
|
+
transition:
|
|
91
|
+
opacity 0.15s ease,
|
|
92
|
+
visibility 0.15s ease;
|
|
93
|
+
transform: translateY(10px) scale(0);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.scroll-btn.visible {
|
|
97
|
+
transform: translateY(0) scale(1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.scroll-btn:hover {
|
|
101
|
+
transform: translateY(-2px) scale(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.scroll-btn:active {
|
|
105
|
+
transform: translateY(0) scale(0.95);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* Dark mode */
|
|
110
|
+
@media (prefers-color-scheme: dark) {
|
|
111
|
+
.scroll-btn {
|
|
112
|
+
background: ${unsafeCSS(THEME.slate[800])};
|
|
113
|
+
color: ${unsafeCSS(THEME.slate[300])};
|
|
114
|
+
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.scroll-btn:hover {
|
|
118
|
+
color: ${unsafeCSS(THEME.slate[200])};
|
|
119
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
`; }
|
|
123
|
+
_handleClick() {
|
|
124
|
+
this.dispatchEvent(new CustomEvent('bb-scroll-to-bottom', {
|
|
125
|
+
bubbles: true,
|
|
126
|
+
composed: true,
|
|
127
|
+
detail: { behavior: 'smooth' },
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
render() {
|
|
131
|
+
return html `
|
|
132
|
+
<button
|
|
133
|
+
class="scroll-btn ${this.visible ? 'visible' : ''}"
|
|
134
|
+
@click=${this._handleClick}
|
|
135
|
+
aria-label="Scroll to bottom"
|
|
136
|
+
title="Scroll to bottom"
|
|
137
|
+
>
|
|
138
|
+
<svg
|
|
139
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
140
|
+
width="20"
|
|
141
|
+
height="20"
|
|
142
|
+
viewBox="0 0 24 24"
|
|
143
|
+
fill="none"
|
|
144
|
+
stroke="currentColor"
|
|
145
|
+
stroke-width="2"
|
|
146
|
+
stroke-linecap="round"
|
|
147
|
+
stroke-linejoin="round"
|
|
148
|
+
>
|
|
149
|
+
<polyline points="6 9 12 15 18 9"></polyline>
|
|
150
|
+
</svg>
|
|
151
|
+
</button>
|
|
152
|
+
`;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
__decorate([
|
|
156
|
+
property({ type: Boolean, reflect: true })
|
|
157
|
+
], BBScrollButton.prototype, "visible", void 0);
|
|
158
|
+
BBScrollButton = __decorate([
|
|
159
|
+
customElement('bb-scroll-button')
|
|
160
|
+
], BBScrollButton);
|
|
161
|
+
export { BBScrollButton };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { LitElement } from 'lit';
|
|
2
|
+
/**
|
|
3
|
+
* Timestamp component - displays message timestamp
|
|
4
|
+
*/
|
|
5
|
+
export declare class BBTimestamp extends LitElement {
|
|
6
|
+
static styles: import("lit").CSSResult;
|
|
7
|
+
value: string;
|
|
8
|
+
side: 'left' | 'right';
|
|
9
|
+
render(): import("lit").TemplateResult<1>;
|
|
10
|
+
}
|
|
11
|
+
declare global {
|
|
12
|
+
interface HTMLElementTagNameMap {
|
|
13
|
+
'bb-timestamp': BBTimestamp;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { LitElement, html, css, unsafeCSS } from 'lit';
|
|
8
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
9
|
+
import { THEME } from '../const/theme.js';
|
|
10
|
+
/**
|
|
11
|
+
* Timestamp component - displays message timestamp
|
|
12
|
+
*/
|
|
13
|
+
let BBTimestamp = class BBTimestamp extends LitElement {
|
|
14
|
+
constructor() {
|
|
15
|
+
super(...arguments);
|
|
16
|
+
this.value = '';
|
|
17
|
+
this.side = 'left';
|
|
18
|
+
}
|
|
19
|
+
static { this.styles = css `
|
|
20
|
+
:host {
|
|
21
|
+
display: block;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.timestamp {
|
|
25
|
+
font-size: 11px;
|
|
26
|
+
color: ${unsafeCSS(THEME.gray[400])};
|
|
27
|
+
white-space: nowrap;
|
|
28
|
+
line-height: 1;
|
|
29
|
+
pointer-events: none;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
:host([side='left']) .timestamp {
|
|
33
|
+
text-align: left;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
:host([side='right']) .timestamp {
|
|
37
|
+
text-align: right;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@media (prefers-color-scheme: dark) {
|
|
41
|
+
.timestamp {
|
|
42
|
+
color: ${unsafeCSS(THEME.gray[500])};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
`; }
|
|
46
|
+
render() {
|
|
47
|
+
return html `<div class="timestamp">${this.value}</div>`;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
__decorate([
|
|
51
|
+
property()
|
|
52
|
+
], BBTimestamp.prototype, "value", void 0);
|
|
53
|
+
__decorate([
|
|
54
|
+
property({ reflect: true })
|
|
55
|
+
], BBTimestamp.prototype, "side", void 0);
|
|
56
|
+
BBTimestamp = __decorate([
|
|
57
|
+
customElement('bb-timestamp')
|
|
58
|
+
], BBTimestamp);
|
|
59
|
+
export { BBTimestamp };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { BBLetterAvatar } from './bb-letter-avatar.js';
|
|
2
|
+
export { BBCustomAvatar } from './bb-custom-avatar.js';
|
|
3
|
+
export { BBMessageBubble } from './bb-message-bubble.js';
|
|
4
|
+
export { BBTimestamp } from './bb-timestamp.js';
|
|
5
|
+
export { BBMessage } from './bb-message.js';
|
|
6
|
+
export { BBScrollButton } from './bb-scroll-button.js';
|
|
7
|
+
export { BBLoadingOverlay } from './bb-loading-overlay.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { BBLetterAvatar } from './bb-letter-avatar.js';
|
|
2
|
+
export { BBCustomAvatar } from './bb-custom-avatar.js';
|
|
3
|
+
export { BBMessageBubble } from './bb-message-bubble.js';
|
|
4
|
+
export { BBTimestamp } from './bb-timestamp.js';
|
|
5
|
+
export { BBMessage } from './bb-message.js';
|
|
6
|
+
export { BBScrollButton } from './bb-scroll-button.js';
|
|
7
|
+
export { BBLoadingOverlay } from './bb-loading-overlay.js';
|
package/dist/const/styles.js
CHANGED
|
@@ -159,7 +159,6 @@ export const MAIN_STYLES = `
|
|
|
159
159
|
background: #ffffff;
|
|
160
160
|
border-radius: 50%;
|
|
161
161
|
overflow: hidden;
|
|
162
|
-
cursor: help;
|
|
163
162
|
}
|
|
164
163
|
|
|
165
164
|
.avatar-wrapper--hidden {
|
|
@@ -182,38 +181,6 @@ export const MAIN_STYLES = `
|
|
|
182
181
|
height: 100%;
|
|
183
182
|
}
|
|
184
183
|
|
|
185
|
-
/* Hover tooltip */
|
|
186
|
-
.avatar-tooltip {
|
|
187
|
-
position: fixed;
|
|
188
|
-
padding: 0.25rem 0.5rem;
|
|
189
|
-
background: ${THEME.gray[800]};
|
|
190
|
-
color: ${THEME.gray[50]};
|
|
191
|
-
font-size: 0.75rem;
|
|
192
|
-
border-radius: 0.25rem;
|
|
193
|
-
white-space: nowrap;
|
|
194
|
-
opacity: 0;
|
|
195
|
-
visibility: hidden;
|
|
196
|
-
pointer-events: none;
|
|
197
|
-
z-index: 10;
|
|
198
|
-
font-weight: 500;
|
|
199
|
-
letter-spacing: 0.02em;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
.avatar-tooltip::after {
|
|
203
|
-
content: '';
|
|
204
|
-
position: absolute;
|
|
205
|
-
top: calc(100% - 1px);
|
|
206
|
-
left: 50%;
|
|
207
|
-
transform: translateX(-50%);
|
|
208
|
-
border: 4px solid transparent;
|
|
209
|
-
border-top-color: ${THEME.gray[800]};
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
.avatar-wrapper:hover .avatar-tooltip {
|
|
213
|
-
opacity: 1;
|
|
214
|
-
visibility: visible;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
184
|
/* Message content area */
|
|
218
185
|
.msg-content {
|
|
219
186
|
display: flex;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AuthorOptions } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Lit Context for author configuration sharing
|
|
4
|
+
* Allows sub-components to access author configs without prop drilling
|
|
5
|
+
*/
|
|
6
|
+
export declare const authorContext: {
|
|
7
|
+
__context__: Map<string, AuthorOptions>;
|
|
8
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ReactiveController } from 'lit';
|
|
2
|
+
import type { LitElement } from 'lit';
|
|
3
|
+
/**
|
|
4
|
+
* ScrollController - Reactive controller for scroll behavior
|
|
5
|
+
*
|
|
6
|
+
* Encapsulates scroll-related logic:
|
|
7
|
+
* - Scroll to bottom with smooth animation
|
|
8
|
+
* - Detect when user is near/at bottom of content
|
|
9
|
+
* - Control scroll button visibility based on scroll position
|
|
10
|
+
*/
|
|
11
|
+
export declare class ScrollController implements ReactiveController {
|
|
12
|
+
private host;
|
|
13
|
+
private _container?;
|
|
14
|
+
private _intersectionObserver?;
|
|
15
|
+
private _resizeObserver?;
|
|
16
|
+
private _isVisible;
|
|
17
|
+
private readonly BOTTOM_THRESHOLD;
|
|
18
|
+
constructor(host: LitElement);
|
|
19
|
+
hostConnected(): void;
|
|
20
|
+
hostDisconnected(): void;
|
|
21
|
+
hostUpdated(): void;
|
|
22
|
+
private _init;
|
|
23
|
+
private _cleanup;
|
|
24
|
+
private _initIntersectionObserver;
|
|
25
|
+
private _initResizeObserver;
|
|
26
|
+
/**
|
|
27
|
+
* Check current scroll position and update visibility state
|
|
28
|
+
*/
|
|
29
|
+
checkPosition(): void;
|
|
30
|
+
/**
|
|
31
|
+
* Scroll the container to the bottom
|
|
32
|
+
* @param behavior - Scroll behavior: 'smooth' or 'auto'
|
|
33
|
+
*/
|
|
34
|
+
scrollToBottom(behavior?: ScrollBehavior): void;
|
|
35
|
+
/**
|
|
36
|
+
* Check if the container is currently at or near the bottom
|
|
37
|
+
* @returns true if within threshold pixels of bottom
|
|
38
|
+
*/
|
|
39
|
+
isAtBottom(): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Get the current button visibility state
|
|
42
|
+
*/
|
|
43
|
+
get isVisible(): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Get the scrollable container element
|
|
46
|
+
*/
|
|
47
|
+
get container(): HTMLElement | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Update the observed last message (call when messages change)
|
|
50
|
+
*/
|
|
51
|
+
updateObservedMessage(): void;
|
|
52
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScrollController - Reactive controller for scroll behavior
|
|
3
|
+
*
|
|
4
|
+
* Encapsulates scroll-related logic:
|
|
5
|
+
* - Scroll to bottom with smooth animation
|
|
6
|
+
* - Detect when user is near/at bottom of content
|
|
7
|
+
* - Control scroll button visibility based on scroll position
|
|
8
|
+
*/
|
|
9
|
+
export class ScrollController {
|
|
10
|
+
constructor(host) {
|
|
11
|
+
this._isVisible = false;
|
|
12
|
+
// Threshold in pixels from bottom to consider "at bottom"
|
|
13
|
+
this.BOTTOM_THRESHOLD = 50;
|
|
14
|
+
this.host = host;
|
|
15
|
+
host.addController(this);
|
|
16
|
+
}
|
|
17
|
+
hostConnected() {
|
|
18
|
+
// Wait for render to complete before querying DOM
|
|
19
|
+
this.host.updateComplete.then(() => {
|
|
20
|
+
this._init();
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
hostDisconnected() {
|
|
24
|
+
this._cleanup();
|
|
25
|
+
}
|
|
26
|
+
hostUpdated() {
|
|
27
|
+
// Re-initialize if container becomes available
|
|
28
|
+
if (!this._container) {
|
|
29
|
+
this._init();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
_init() {
|
|
33
|
+
const host = this.host;
|
|
34
|
+
this._container = host.renderRoot?.querySelector('.history-container');
|
|
35
|
+
if (this._container) {
|
|
36
|
+
this._initIntersectionObserver();
|
|
37
|
+
this._initResizeObserver();
|
|
38
|
+
this.checkPosition();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
_cleanup() {
|
|
42
|
+
this._intersectionObserver?.disconnect();
|
|
43
|
+
this._resizeObserver?.disconnect();
|
|
44
|
+
this._container = undefined;
|
|
45
|
+
}
|
|
46
|
+
_initIntersectionObserver() {
|
|
47
|
+
if (!this._container)
|
|
48
|
+
return;
|
|
49
|
+
// Observe the last message to detect if we're at bottom
|
|
50
|
+
const lastMsg = this._container.lastElementChild;
|
|
51
|
+
if (!lastMsg)
|
|
52
|
+
return;
|
|
53
|
+
this._intersectionObserver = new IntersectionObserver(entries => {
|
|
54
|
+
entries.forEach(entry => {
|
|
55
|
+
const wasVisible = this._isVisible;
|
|
56
|
+
this._isVisible = !entry.isIntersecting;
|
|
57
|
+
if (wasVisible !== this._isVisible) {
|
|
58
|
+
this.host.requestUpdate();
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}, {
|
|
62
|
+
root: this._container,
|
|
63
|
+
threshold: 0.1,
|
|
64
|
+
rootMargin: '0px 0px 50px 0px',
|
|
65
|
+
});
|
|
66
|
+
this._intersectionObserver.observe(lastMsg);
|
|
67
|
+
}
|
|
68
|
+
_initResizeObserver() {
|
|
69
|
+
if (!this._container)
|
|
70
|
+
return;
|
|
71
|
+
this._resizeObserver = new ResizeObserver(() => {
|
|
72
|
+
this.checkPosition();
|
|
73
|
+
});
|
|
74
|
+
this._resizeObserver.observe(this._container);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Check current scroll position and update visibility state
|
|
78
|
+
*/
|
|
79
|
+
checkPosition() {
|
|
80
|
+
if (!this._container)
|
|
81
|
+
return;
|
|
82
|
+
const isAtBottom = this.isAtBottom();
|
|
83
|
+
const hasOverflow = this._container.scrollHeight > this._container.clientHeight;
|
|
84
|
+
// Show button when not at bottom and content has overflow
|
|
85
|
+
const shouldShow = !isAtBottom && hasOverflow;
|
|
86
|
+
if (shouldShow !== this._isVisible) {
|
|
87
|
+
this._isVisible = shouldShow;
|
|
88
|
+
this.host.requestUpdate();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Scroll the container to the bottom
|
|
93
|
+
* @param behavior - Scroll behavior: 'smooth' or 'auto'
|
|
94
|
+
*/
|
|
95
|
+
scrollToBottom(behavior = 'smooth') {
|
|
96
|
+
if (!this._container)
|
|
97
|
+
return;
|
|
98
|
+
this._container.scrollTo({
|
|
99
|
+
top: this._container.scrollHeight,
|
|
100
|
+
behavior,
|
|
101
|
+
});
|
|
102
|
+
// Hide button since we're scrolling to bottom
|
|
103
|
+
if (this._isVisible) {
|
|
104
|
+
this._isVisible = false;
|
|
105
|
+
this.host.requestUpdate();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Check if the container is currently at or near the bottom
|
|
110
|
+
* @returns true if within threshold pixels of bottom
|
|
111
|
+
*/
|
|
112
|
+
isAtBottom() {
|
|
113
|
+
if (!this._container)
|
|
114
|
+
return true;
|
|
115
|
+
const distanceFromBottom = this._container.scrollHeight - this._container.scrollTop - this._container.clientHeight;
|
|
116
|
+
return distanceFromBottom < this.BOTTOM_THRESHOLD;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Get the current button visibility state
|
|
120
|
+
*/
|
|
121
|
+
get isVisible() {
|
|
122
|
+
return this._isVisible;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Get the scrollable container element
|
|
126
|
+
*/
|
|
127
|
+
get container() {
|
|
128
|
+
return this._container;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Update the observed last message (call when messages change)
|
|
132
|
+
*/
|
|
133
|
+
updateObservedMessage() {
|
|
134
|
+
this._intersectionObserver?.disconnect();
|
|
135
|
+
this._initIntersectionObserver();
|
|
136
|
+
this.checkPosition();
|
|
137
|
+
}
|
|
138
|
+
}
|
package/dist/core/renderer.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { EMPTY_STYLES, LOADING_STYLES, MAIN_STYLES } from '../const/styles.js';
|
|
2
2
|
import { resolveAuthorConfig } from '../utils/author-resolver.js';
|
|
3
|
-
import { buildMessageRowHtml
|
|
3
|
+
import { buildMessageRowHtml } from '../utils/message-builder.js';
|
|
4
4
|
import { buildScrollButtonHtml } from '../utils/scroll-button.js';
|
|
5
|
-
import { setupTooltips } from '../utils/tooltip.js';
|
|
6
5
|
/**
|
|
7
6
|
* Renderer - Manages all DOM rendering operations
|
|
8
7
|
*
|
|
@@ -88,8 +87,6 @@ export class Renderer {
|
|
|
88
87
|
if (wasAtBottom) {
|
|
89
88
|
scrollContainer.scrollTop = scrollContainer.scrollHeight;
|
|
90
89
|
}
|
|
91
|
-
// Re-setup tooltips for new content
|
|
92
|
-
setupTooltips(this.shadowRoot);
|
|
93
90
|
return { wasAtBottom };
|
|
94
91
|
}
|
|
95
92
|
/**
|
|
@@ -126,11 +123,6 @@ export class Renderer {
|
|
|
126
123
|
lastGroupTimestamp, true // isLastInGroup - when appending, this is always last (for now)
|
|
127
124
|
);
|
|
128
125
|
container.insertAdjacentHTML('beforeend', msgHtml);
|
|
129
|
-
// Setup tooltip for new element
|
|
130
|
-
const newWrapper = container.lastElementChild?.querySelector('.avatar-wrapper');
|
|
131
|
-
if (newWrapper) {
|
|
132
|
-
setupTooltipForElement(newWrapper);
|
|
133
|
-
}
|
|
134
126
|
return {
|
|
135
127
|
success: true,
|
|
136
128
|
lastAuthor: message.author,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Message } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parser interface for message parsing
|
|
4
|
+
* Allows custom parsers to be plugged in
|
|
5
|
+
*/
|
|
6
|
+
export interface MessageParser {
|
|
7
|
+
/**
|
|
8
|
+
* Parse text content into message array
|
|
9
|
+
* @param textContent - Raw text content to parse
|
|
10
|
+
* @returns Array of parsed messages
|
|
11
|
+
*/
|
|
12
|
+
parse(textContent: string | null): Message[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Input type for appending messages
|
|
16
|
+
*/
|
|
17
|
+
export interface MessageInput {
|
|
18
|
+
author: string;
|
|
19
|
+
text: string;
|
|
20
|
+
timestamp?: string;
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Message } from '../types/index.js';
|
|
2
|
+
import type { MessageParser } from './base.js';
|
|
3
|
+
/**
|
|
4
|
+
* Default message parser implementation
|
|
5
|
+
* Format: `[timestamp] author: text` or `author: text` (one message per line)
|
|
6
|
+
* Timestamp is optional for backward compatibility
|
|
7
|
+
*/
|
|
8
|
+
export declare class DefaultMessageParser implements MessageParser {
|
|
9
|
+
parse(textContent: string | null): Message[];
|
|
10
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default message parser implementation
|
|
3
|
+
* Format: `[timestamp] author: text` or `author: text` (one message per line)
|
|
4
|
+
* Timestamp is optional for backward compatibility
|
|
5
|
+
*/
|
|
6
|
+
export class DefaultMessageParser {
|
|
7
|
+
parse(textContent) {
|
|
8
|
+
const raw = textContent || '';
|
|
9
|
+
const messages = [];
|
|
10
|
+
// Pattern: [timestamp] author: text (space between timestamp and author is optional)
|
|
11
|
+
const timestampPattern = /^\[([^\]]+)\]\s*(.+)$/;
|
|
12
|
+
for (const line of raw.split('\n')) {
|
|
13
|
+
const trimmed = line.trim();
|
|
14
|
+
if (!trimmed)
|
|
15
|
+
continue;
|
|
16
|
+
let remainingLine = trimmed;
|
|
17
|
+
let timestamp;
|
|
18
|
+
// Try to extract timestamp
|
|
19
|
+
const timestampMatch = trimmed.match(timestampPattern);
|
|
20
|
+
if (timestampMatch) {
|
|
21
|
+
timestamp = timestampMatch[1].trim();
|
|
22
|
+
remainingLine = timestampMatch[2];
|
|
23
|
+
}
|
|
24
|
+
// Find the author:text separator (colon followed by space)
|
|
25
|
+
const colonIdx = remainingLine.indexOf(':');
|
|
26
|
+
if (colonIdx <= 0)
|
|
27
|
+
continue;
|
|
28
|
+
const author = remainingLine.slice(0, colonIdx).trim();
|
|
29
|
+
const text = remainingLine.slice(colonIdx + 1).trim();
|
|
30
|
+
if (author && text) {
|
|
31
|
+
const message = { author, text };
|
|
32
|
+
if (timestamp) {
|
|
33
|
+
message.timestamp = timestamp;
|
|
34
|
+
}
|
|
35
|
+
messages.push(message);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return messages;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DefaultMessageParser } from './default-parser.js';
|
|
@@ -11,7 +11,3 @@ export declare function buildTimestampHtml(timestamp: string, side: 'left' | 'ri
|
|
|
11
11
|
* Build a single message row HTML string
|
|
12
12
|
*/
|
|
13
13
|
export declare function buildMessageRowHtml(author: string, text: string, config: AuthorConfig, isSubsequent: boolean, timestamp?: string, isLastInGroup?: boolean): string;
|
|
14
|
-
/**
|
|
15
|
-
* Setup tooltip for a single avatar wrapper element
|
|
16
|
-
*/
|
|
17
|
-
export declare function setupTooltipForElement(wrapper: Element): void;
|
|
@@ -8,7 +8,6 @@ export function buildAvatarHtml(author, config, showAvatar) {
|
|
|
8
8
|
<div class="avatar-wrapper ${showAvatar ? '' : 'avatar-wrapper--hidden'}"
|
|
9
9
|
data-author="${escapeHtml(author)}">
|
|
10
10
|
<div class="avatar">${config.avatar}</div>
|
|
11
|
-
<div class="avatar-tooltip">${escapeHtml(author)}</div>
|
|
12
11
|
</div>
|
|
13
12
|
`;
|
|
14
13
|
}
|
|
@@ -53,17 +52,3 @@ export function buildMessageRowHtml(author, text, config, isSubsequent, timestam
|
|
|
53
52
|
</div>
|
|
54
53
|
`;
|
|
55
54
|
}
|
|
56
|
-
/**
|
|
57
|
-
* Setup tooltip for a single avatar wrapper element
|
|
58
|
-
*/
|
|
59
|
-
export function setupTooltipForElement(wrapper) {
|
|
60
|
-
wrapper.addEventListener('mouseenter', () => {
|
|
61
|
-
const tooltip = wrapper.querySelector('.avatar-tooltip');
|
|
62
|
-
if (!tooltip)
|
|
63
|
-
return;
|
|
64
|
-
const rect = wrapper.getBoundingClientRect();
|
|
65
|
-
const tooltipRect = tooltip.getBoundingClientRect();
|
|
66
|
-
tooltip.style.left = `${rect.left + rect.width / 2 - tooltipRect.width / 2}px`;
|
|
67
|
-
tooltip.style.top = `${rect.top - tooltipRect.height - 8}px`;
|
|
68
|
-
});
|
|
69
|
-
}
|
package/dist/utils/tooltip.d.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Tooltip utilities for avatar hover effects
|
|
3
|
+
*
|
|
4
|
+
* Tooltips use position: fixed to escape overflow clipping from scrollable containers.
|
|
5
|
+
* Position is calculated on mouseenter and updated on scroll to prevent staleness.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Setup tooltip positioning for a single avatar wrapper element
|
|
9
|
+
*/
|
|
10
|
+
export declare function setupTooltipForElement(wrapper: Element): void;
|
|
11
|
+
/**
|
|
12
|
+
* Setup tooltips for all avatar wrappers in the shadow root
|
|
4
13
|
*/
|
|
5
14
|
export declare function setupTooltips(shadowRoot: ShadowRoot): void;
|