@bbki.ng/bb-msg-history 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/component.d.ts +2 -0
- package/dist/component.js +37 -0
- package/dist/const/styles.js +85 -0
- package/dist/utils/scroll-button.d.ts +4 -0
- package/dist/utils/scroll-button.js +12 -0
- package/package.json +1 -1
- package/src/component.ts +45 -0
- package/src/const/styles.ts +85 -0
- package/src/utils/scroll-button.ts +12 -0
package/dist/component.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export declare class BBMsgHistory extends HTMLElement {
|
|
|
3
3
|
private _mutationObserver?;
|
|
4
4
|
private _userAuthors;
|
|
5
5
|
private _lastAuthor;
|
|
6
|
+
private _scrollButtonVisible;
|
|
6
7
|
constructor();
|
|
7
8
|
/**
|
|
8
9
|
* Configure an author's avatar, side, and colors.
|
|
@@ -32,4 +33,5 @@ export declare class BBMsgHistory extends HTMLElement {
|
|
|
32
33
|
private _setupMutationObserver;
|
|
33
34
|
private render;
|
|
34
35
|
private _renderEmpty;
|
|
36
|
+
private _setupScrollTracking;
|
|
35
37
|
}
|
package/dist/component.js
CHANGED
|
@@ -3,11 +3,13 @@ import { parseMessages } from './utils/message-parser.js';
|
|
|
3
3
|
import { resolveAuthorConfig } from './utils/author-resolver.js';
|
|
4
4
|
import { setupTooltips } from './utils/tooltip.js';
|
|
5
5
|
import { buildMessageRowHtml, setupTooltipForElement } from './utils/message-builder.js';
|
|
6
|
+
import { buildScrollButtonHtml } from './utils/scroll-button.js';
|
|
6
7
|
export class BBMsgHistory extends HTMLElement {
|
|
7
8
|
constructor() {
|
|
8
9
|
super();
|
|
9
10
|
this._userAuthors = new Map();
|
|
10
11
|
this._lastAuthor = '';
|
|
12
|
+
this._scrollButtonVisible = false;
|
|
11
13
|
this.attachShadow({ mode: 'open' });
|
|
12
14
|
}
|
|
13
15
|
/**
|
|
@@ -79,6 +81,12 @@ export class BBMsgHistory extends HTMLElement {
|
|
|
79
81
|
top: container.scrollHeight,
|
|
80
82
|
behavior: 'smooth'
|
|
81
83
|
});
|
|
84
|
+
// Hide scroll button since we're scrolling to bottom
|
|
85
|
+
const scrollButton = this.shadowRoot.querySelector('.scroll-to-bottom');
|
|
86
|
+
if (scrollButton && this._scrollButtonVisible) {
|
|
87
|
+
this._scrollButtonVisible = false;
|
|
88
|
+
scrollButton.classList.remove('visible');
|
|
89
|
+
}
|
|
82
90
|
}
|
|
83
91
|
connectedCallback() {
|
|
84
92
|
this.render();
|
|
@@ -123,11 +131,22 @@ export class BBMsgHistory extends HTMLElement {
|
|
|
123
131
|
<div class="history" role="log" aria-live="polite" aria-label="Message history">
|
|
124
132
|
${messagesHtml}
|
|
125
133
|
</div>
|
|
134
|
+
${buildScrollButtonHtml()}
|
|
126
135
|
`;
|
|
127
136
|
requestAnimationFrame(() => {
|
|
128
137
|
const container = this.shadowRoot.querySelector('.history');
|
|
138
|
+
const scrollButton = this.shadowRoot.querySelector('.scroll-to-bottom');
|
|
129
139
|
if (container) {
|
|
130
140
|
container.scrollTop = container.scrollHeight;
|
|
141
|
+
this._setupScrollTracking(container, scrollButton);
|
|
142
|
+
}
|
|
143
|
+
if (scrollButton) {
|
|
144
|
+
scrollButton.addEventListener('click', () => {
|
|
145
|
+
container?.scrollTo({
|
|
146
|
+
top: container.scrollHeight,
|
|
147
|
+
behavior: 'smooth'
|
|
148
|
+
});
|
|
149
|
+
});
|
|
131
150
|
}
|
|
132
151
|
setupTooltips(this.shadowRoot);
|
|
133
152
|
});
|
|
@@ -138,4 +157,22 @@ export class BBMsgHistory extends HTMLElement {
|
|
|
138
157
|
<div class="empty-state">No messages</div>
|
|
139
158
|
`;
|
|
140
159
|
}
|
|
160
|
+
_setupScrollTracking(container, button) {
|
|
161
|
+
const checkScrollPosition = () => {
|
|
162
|
+
const threshold = 50; // pixels from bottom
|
|
163
|
+
const isAtBottom = container.scrollHeight - container.scrollTop - container.clientHeight < threshold;
|
|
164
|
+
const hasOverflow = container.scrollHeight > container.clientHeight;
|
|
165
|
+
const shouldShow = !isAtBottom && hasOverflow;
|
|
166
|
+
if (shouldShow !== this._scrollButtonVisible) {
|
|
167
|
+
this._scrollButtonVisible = shouldShow;
|
|
168
|
+
button.classList.toggle('visible', shouldShow);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
// Check initial state
|
|
172
|
+
checkScrollPosition();
|
|
173
|
+
// Listen for scroll events with passive listener for performance
|
|
174
|
+
container.addEventListener('scroll', checkScrollPosition, { passive: true });
|
|
175
|
+
// Also check on resize
|
|
176
|
+
window.addEventListener('resize', checkScrollPosition, { passive: true });
|
|
177
|
+
}
|
|
141
178
|
}
|
package/dist/const/styles.js
CHANGED
|
@@ -5,6 +5,7 @@ import { THEME } from './theme.js';
|
|
|
5
5
|
export const MAIN_STYLES = `
|
|
6
6
|
:host {
|
|
7
7
|
display: block;
|
|
8
|
+
position: relative;
|
|
8
9
|
font-family: "PT Sans", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
|
9
10
|
"Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
|
|
10
11
|
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
|
|
@@ -47,6 +48,50 @@ export const MAIN_STYLES = `
|
|
|
47
48
|
background: ${THEME.gray[500]};
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
/* Scroll to bottom button */
|
|
52
|
+
.scroll-to-bottom {
|
|
53
|
+
position: absolute;
|
|
54
|
+
bottom: 16px;
|
|
55
|
+
left: 50%;
|
|
56
|
+
width: 36px;
|
|
57
|
+
height: 36px;
|
|
58
|
+
border-radius: 50%;
|
|
59
|
+
background: transparent;
|
|
60
|
+
border: none;
|
|
61
|
+
color: ${THEME.gray[500]};
|
|
62
|
+
cursor: pointer;
|
|
63
|
+
display: flex;
|
|
64
|
+
align-items: center;
|
|
65
|
+
justify-content: center;
|
|
66
|
+
opacity: 0;
|
|
67
|
+
visibility: hidden;
|
|
68
|
+
transform: translateX(-50%) translateY(10px) scale(0.9);
|
|
69
|
+
transition: opacity 0.3s ease, transform 0.3s ease, visibility 0.3s ease;
|
|
70
|
+
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
|
|
71
|
+
z-index: 10;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.scroll-to-bottom.visible {
|
|
75
|
+
opacity: 1;
|
|
76
|
+
visibility: visible;
|
|
77
|
+
transform: translateX(-50%) translateY(0) scale(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.scroll-to-bottom:hover {
|
|
81
|
+
color: ${THEME.gray[700]};
|
|
82
|
+
transform: translateX(-50%) translateY(-2px);
|
|
83
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.scroll-to-bottom:active {
|
|
87
|
+
transform: translateX(-50%) translateY(0) scale(0.95);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.scroll-to-bottom svg {
|
|
91
|
+
width: 20px;
|
|
92
|
+
height: 20px;
|
|
93
|
+
}
|
|
94
|
+
|
|
50
95
|
/* Message row layout */
|
|
51
96
|
.msg-row {
|
|
52
97
|
display: flex;
|
|
@@ -196,6 +241,17 @@ export const MAIN_STYLES = `
|
|
|
196
241
|
width: 1.5rem;
|
|
197
242
|
height: 1.5rem;
|
|
198
243
|
}
|
|
244
|
+
|
|
245
|
+
.scroll-to-bottom {
|
|
246
|
+
width: 32px;
|
|
247
|
+
height: 32px;
|
|
248
|
+
bottom: 12px;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.scroll-to-bottom svg {
|
|
252
|
+
width: 18px;
|
|
253
|
+
height: 18px;
|
|
254
|
+
}
|
|
199
255
|
}
|
|
200
256
|
|
|
201
257
|
/* Dark mode */
|
|
@@ -234,6 +290,18 @@ export const MAIN_STYLES = `
|
|
|
234
290
|
.empty-state {
|
|
235
291
|
color: ${THEME.gray[500]};
|
|
236
292
|
}
|
|
293
|
+
|
|
294
|
+
.scroll-to-bottom {
|
|
295
|
+
background: transparent;
|
|
296
|
+
border: none;
|
|
297
|
+
color: ${THEME.gray[400]};
|
|
298
|
+
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.scroll-to-bottom:hover {
|
|
302
|
+
color: ${THEME.gray[300]};
|
|
303
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
|
|
304
|
+
}
|
|
237
305
|
}
|
|
238
306
|
|
|
239
307
|
/* Reduced motion */
|
|
@@ -241,6 +309,23 @@ export const MAIN_STYLES = `
|
|
|
241
309
|
.history {
|
|
242
310
|
scroll-behavior: auto;
|
|
243
311
|
}
|
|
312
|
+
|
|
313
|
+
.scroll-to-bottom {
|
|
314
|
+
transition: opacity 0.15s ease, visibility 0.15s ease;
|
|
315
|
+
transform: translateX(-50%);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.scroll-to-bottom.visible {
|
|
319
|
+
transform: translateX(-50%);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.scroll-to-bottom:hover {
|
|
323
|
+
transform: translateX(-50%);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.scroll-to-bottom:active {
|
|
327
|
+
transform: translateX(-50%);
|
|
328
|
+
}
|
|
244
329
|
}
|
|
245
330
|
`;
|
|
246
331
|
/**
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build scroll-to-bottom button HTML string
|
|
3
|
+
*/
|
|
4
|
+
export function buildScrollButtonHtml() {
|
|
5
|
+
return `
|
|
6
|
+
<button class="scroll-to-bottom" aria-label="Scroll to bottom" title="Scroll to bottom">
|
|
7
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
8
|
+
<polyline points="6 9 12 15 18 9"></polyline>
|
|
9
|
+
</svg>
|
|
10
|
+
</button>
|
|
11
|
+
`;
|
|
12
|
+
}
|
package/package.json
CHANGED
package/src/component.ts
CHANGED
|
@@ -4,11 +4,13 @@ import { parseMessages } from './utils/message-parser.js';
|
|
|
4
4
|
import { resolveAuthorConfig } from './utils/author-resolver.js';
|
|
5
5
|
import { setupTooltips } from './utils/tooltip.js';
|
|
6
6
|
import { buildMessageRowHtml, setupTooltipForElement } from './utils/message-builder.js';
|
|
7
|
+
import { buildScrollButtonHtml } from './utils/scroll-button.js';
|
|
7
8
|
|
|
8
9
|
export class BBMsgHistory extends HTMLElement {
|
|
9
10
|
private _mutationObserver?: MutationObserver;
|
|
10
11
|
private _userAuthors = new Map<string, AuthorOptions>();
|
|
11
12
|
private _lastAuthor = '';
|
|
13
|
+
private _scrollButtonVisible = false;
|
|
12
14
|
|
|
13
15
|
constructor() {
|
|
14
16
|
super();
|
|
@@ -98,6 +100,13 @@ export class BBMsgHistory extends HTMLElement {
|
|
|
98
100
|
top: container.scrollHeight,
|
|
99
101
|
behavior: 'smooth'
|
|
100
102
|
});
|
|
103
|
+
|
|
104
|
+
// Hide scroll button since we're scrolling to bottom
|
|
105
|
+
const scrollButton = this.shadowRoot!.querySelector('.scroll-to-bottom') as HTMLButtonElement;
|
|
106
|
+
if (scrollButton && this._scrollButtonVisible) {
|
|
107
|
+
this._scrollButtonVisible = false;
|
|
108
|
+
scrollButton.classList.remove('visible');
|
|
109
|
+
}
|
|
101
110
|
}
|
|
102
111
|
|
|
103
112
|
connectedCallback() {
|
|
@@ -151,12 +160,25 @@ export class BBMsgHistory extends HTMLElement {
|
|
|
151
160
|
<div class="history" role="log" aria-live="polite" aria-label="Message history">
|
|
152
161
|
${messagesHtml}
|
|
153
162
|
</div>
|
|
163
|
+
${buildScrollButtonHtml()}
|
|
154
164
|
`;
|
|
155
165
|
|
|
156
166
|
requestAnimationFrame(() => {
|
|
157
167
|
const container = this.shadowRoot!.querySelector('.history') as HTMLElement;
|
|
168
|
+
const scrollButton = this.shadowRoot!.querySelector('.scroll-to-bottom') as HTMLButtonElement;
|
|
169
|
+
|
|
158
170
|
if (container) {
|
|
159
171
|
container.scrollTop = container.scrollHeight;
|
|
172
|
+
this._setupScrollTracking(container, scrollButton);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (scrollButton) {
|
|
176
|
+
scrollButton.addEventListener('click', () => {
|
|
177
|
+
container?.scrollTo({
|
|
178
|
+
top: container.scrollHeight,
|
|
179
|
+
behavior: 'smooth'
|
|
180
|
+
});
|
|
181
|
+
});
|
|
160
182
|
}
|
|
161
183
|
|
|
162
184
|
setupTooltips(this.shadowRoot!);
|
|
@@ -169,4 +191,27 @@ export class BBMsgHistory extends HTMLElement {
|
|
|
169
191
|
<div class="empty-state">No messages</div>
|
|
170
192
|
`;
|
|
171
193
|
}
|
|
194
|
+
|
|
195
|
+
private _setupScrollTracking(container: HTMLElement, button: HTMLButtonElement): void {
|
|
196
|
+
const checkScrollPosition = () => {
|
|
197
|
+
const threshold = 50; // pixels from bottom
|
|
198
|
+
const isAtBottom = container.scrollHeight - container.scrollTop - container.clientHeight < threshold;
|
|
199
|
+
const hasOverflow = container.scrollHeight > container.clientHeight;
|
|
200
|
+
const shouldShow = !isAtBottom && hasOverflow;
|
|
201
|
+
|
|
202
|
+
if (shouldShow !== this._scrollButtonVisible) {
|
|
203
|
+
this._scrollButtonVisible = shouldShow;
|
|
204
|
+
button.classList.toggle('visible', shouldShow);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// Check initial state
|
|
209
|
+
checkScrollPosition();
|
|
210
|
+
|
|
211
|
+
// Listen for scroll events with passive listener for performance
|
|
212
|
+
container.addEventListener('scroll', checkScrollPosition, { passive: true });
|
|
213
|
+
|
|
214
|
+
// Also check on resize
|
|
215
|
+
window.addEventListener('resize', checkScrollPosition, { passive: true });
|
|
216
|
+
}
|
|
172
217
|
}
|
package/src/const/styles.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { THEME } from './theme.js';
|
|
|
6
6
|
export const MAIN_STYLES = `
|
|
7
7
|
:host {
|
|
8
8
|
display: block;
|
|
9
|
+
position: relative;
|
|
9
10
|
font-family: "PT Sans", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
|
10
11
|
"Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
|
|
11
12
|
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
|
|
@@ -48,6 +49,50 @@ export const MAIN_STYLES = `
|
|
|
48
49
|
background: ${THEME.gray[500]};
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
/* Scroll to bottom button */
|
|
53
|
+
.scroll-to-bottom {
|
|
54
|
+
position: absolute;
|
|
55
|
+
bottom: 16px;
|
|
56
|
+
left: 50%;
|
|
57
|
+
width: 36px;
|
|
58
|
+
height: 36px;
|
|
59
|
+
border-radius: 50%;
|
|
60
|
+
background: transparent;
|
|
61
|
+
border: none;
|
|
62
|
+
color: ${THEME.gray[500]};
|
|
63
|
+
cursor: pointer;
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
justify-content: center;
|
|
67
|
+
opacity: 0;
|
|
68
|
+
visibility: hidden;
|
|
69
|
+
transform: translateX(-50%) translateY(10px) scale(0.9);
|
|
70
|
+
transition: opacity 0.3s ease, transform 0.3s ease, visibility 0.3s ease;
|
|
71
|
+
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
|
|
72
|
+
z-index: 10;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.scroll-to-bottom.visible {
|
|
76
|
+
opacity: 1;
|
|
77
|
+
visibility: visible;
|
|
78
|
+
transform: translateX(-50%) translateY(0) scale(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.scroll-to-bottom:hover {
|
|
82
|
+
color: ${THEME.gray[700]};
|
|
83
|
+
transform: translateX(-50%) translateY(-2px);
|
|
84
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.scroll-to-bottom:active {
|
|
88
|
+
transform: translateX(-50%) translateY(0) scale(0.95);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.scroll-to-bottom svg {
|
|
92
|
+
width: 20px;
|
|
93
|
+
height: 20px;
|
|
94
|
+
}
|
|
95
|
+
|
|
51
96
|
/* Message row layout */
|
|
52
97
|
.msg-row {
|
|
53
98
|
display: flex;
|
|
@@ -197,6 +242,17 @@ export const MAIN_STYLES = `
|
|
|
197
242
|
width: 1.5rem;
|
|
198
243
|
height: 1.5rem;
|
|
199
244
|
}
|
|
245
|
+
|
|
246
|
+
.scroll-to-bottom {
|
|
247
|
+
width: 32px;
|
|
248
|
+
height: 32px;
|
|
249
|
+
bottom: 12px;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.scroll-to-bottom svg {
|
|
253
|
+
width: 18px;
|
|
254
|
+
height: 18px;
|
|
255
|
+
}
|
|
200
256
|
}
|
|
201
257
|
|
|
202
258
|
/* Dark mode */
|
|
@@ -235,6 +291,18 @@ export const MAIN_STYLES = `
|
|
|
235
291
|
.empty-state {
|
|
236
292
|
color: ${THEME.gray[500]};
|
|
237
293
|
}
|
|
294
|
+
|
|
295
|
+
.scroll-to-bottom {
|
|
296
|
+
background: transparent;
|
|
297
|
+
border: none;
|
|
298
|
+
color: ${THEME.gray[400]};
|
|
299
|
+
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.scroll-to-bottom:hover {
|
|
303
|
+
color: ${THEME.gray[300]};
|
|
304
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
|
|
305
|
+
}
|
|
238
306
|
}
|
|
239
307
|
|
|
240
308
|
/* Reduced motion */
|
|
@@ -242,6 +310,23 @@ export const MAIN_STYLES = `
|
|
|
242
310
|
.history {
|
|
243
311
|
scroll-behavior: auto;
|
|
244
312
|
}
|
|
313
|
+
|
|
314
|
+
.scroll-to-bottom {
|
|
315
|
+
transition: opacity 0.15s ease, visibility 0.15s ease;
|
|
316
|
+
transform: translateX(-50%);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.scroll-to-bottom.visible {
|
|
320
|
+
transform: translateX(-50%);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.scroll-to-bottom:hover {
|
|
324
|
+
transform: translateX(-50%);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.scroll-to-bottom:active {
|
|
328
|
+
transform: translateX(-50%);
|
|
329
|
+
}
|
|
245
330
|
}
|
|
246
331
|
`;
|
|
247
332
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build scroll-to-bottom button HTML string
|
|
3
|
+
*/
|
|
4
|
+
export function buildScrollButtonHtml(): string {
|
|
5
|
+
return `
|
|
6
|
+
<button class="scroll-to-bottom" aria-label="Scroll to bottom" title="Scroll to bottom">
|
|
7
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
8
|
+
<polyline points="6 9 12 15 18 9"></polyline>
|
|
9
|
+
</svg>
|
|
10
|
+
</button>
|
|
11
|
+
`;
|
|
12
|
+
}
|