@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.
@@ -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
  }
@@ -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,4 @@
1
+ /**
2
+ * Build scroll-to-bottom button HTML string
3
+ */
4
+ export declare function buildScrollButtonHtml(): string;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bbki.ng/bb-msg-history",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "A chat-style message history web component",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
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
  }
@@ -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
+ }