@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.
Files changed (39) hide show
  1. package/dist/components/bb-custom-avatar.d.ts +20 -0
  2. package/dist/components/bb-custom-avatar.js +145 -0
  3. package/dist/components/bb-letter-avatar.d.ts +14 -0
  4. package/dist/components/bb-letter-avatar.js +61 -0
  5. package/dist/components/bb-loading-overlay.d.ts +14 -0
  6. package/dist/components/bb-loading-overlay.js +89 -0
  7. package/dist/components/bb-message-bubble.d.ts +19 -0
  8. package/dist/components/bb-message-bubble.js +116 -0
  9. package/dist/components/bb-message.d.ts +27 -0
  10. package/dist/components/bb-message.js +174 -0
  11. package/dist/components/bb-msg-history.d.ts +111 -0
  12. package/dist/components/bb-msg-history.js +473 -0
  13. package/dist/components/bb-scroll-button.d.ts +16 -0
  14. package/dist/components/bb-scroll-button.js +161 -0
  15. package/dist/components/bb-timestamp.d.ts +15 -0
  16. package/dist/components/bb-timestamp.js +59 -0
  17. package/dist/components/index.d.ts +7 -0
  18. package/dist/components/index.js +7 -0
  19. package/dist/const/styles.js +0 -33
  20. package/dist/contexts/author-context.d.ts +8 -0
  21. package/dist/contexts/author-context.js +6 -0
  22. package/dist/controllers/scroll-controller.d.ts +52 -0
  23. package/dist/controllers/scroll-controller.js +138 -0
  24. package/dist/core/renderer.js +1 -9
  25. package/dist/parsers/base.d.ts +21 -0
  26. package/dist/parsers/base.js +1 -0
  27. package/dist/parsers/default-parser.d.ts +10 -0
  28. package/dist/parsers/default-parser.js +40 -0
  29. package/dist/parsers/index.d.ts +2 -0
  30. package/dist/parsers/index.js +1 -0
  31. package/dist/utils/message-builder.d.ts +0 -4
  32. package/dist/utils/message-builder.js +0 -15
  33. package/dist/utils/tooltip.d.ts +11 -2
  34. package/dist/utils/tooltip.js +56 -13
  35. package/package.json +1 -1
  36. package/src/const/styles.ts +0 -33
  37. package/src/core/renderer.ts +1 -11
  38. package/src/utils/message-builder.ts +0 -15
  39. package/src/utils/tooltip.ts +0 -16
@@ -0,0 +1,111 @@
1
+ import { LitElement, type PropertyValues } from 'lit';
2
+ import type { AuthorOptions } from '../types/index.js';
3
+ import type { MessageParser, MessageInput } from '../parsers/base.js';
4
+ import './bb-message.js';
5
+ import './bb-scroll-button.js';
6
+ import './bb-loading-overlay.js';
7
+ /**
8
+ * BBMsgHistory - A chat-style message history web component
9
+ *
10
+ * Uses Lit for reactive rendering with a compositional architecture.
11
+ * Preserves backward compatibility with the lightweight textContent mode.
12
+ *
13
+ * @example
14
+ * ```html
15
+ * <!-- Lightweight mode -->
16
+ * <bb-msg-history>
17
+ * alice: Hello!
18
+ * bob: Hi there!
19
+ * </bb-msg-history>
20
+ *
21
+ * <!-- With custom authors -->
22
+ * <bb-msg-history id="chat"></bb-msg-history>
23
+ * <script>
24
+ * document.getElementById('chat')
25
+ * .setAuthor('alice', { avatar: '🐱', side: 'right' });
26
+ * </script>
27
+ * ```
28
+ */
29
+ export declare class BBMsgHistory extends LitElement {
30
+ static styles: import("lit").CSSResult;
31
+ loading: boolean;
32
+ hideScrollBar: boolean;
33
+ infinite: boolean;
34
+ hideScrollButton: boolean;
35
+ theme: 'light' | 'dark' | null;
36
+ private _messages;
37
+ private _processedMessages;
38
+ private _userAuthors;
39
+ private _mutationObserver?;
40
+ private _isParsing;
41
+ private _parser;
42
+ private _scrollController;
43
+ connectedCallback(): void;
44
+ disconnectedCallback(): void;
45
+ willUpdate(changedProps: PropertyValues<BBMsgHistory>): void;
46
+ updated(changedProps: PropertyValues<BBMsgHistory>): void;
47
+ /**
48
+ * Configure an author's avatar, side, and colors.
49
+ * Call before or after rendering — the component re-renders automatically.
50
+ */
51
+ setAuthor(name: string, options: AuthorOptions): this;
52
+ /**
53
+ * Remove a previously set author config.
54
+ */
55
+ removeAuthor(name: string): this;
56
+ /**
57
+ * Show or hide the loading overlay.
58
+ */
59
+ setLoading(isLoading: boolean): this;
60
+ /**
61
+ * Append a message to the history.
62
+ * Automatically scrolls to the new message with smooth animation.
63
+ */
64
+ appendMessage(input: MessageInput): this;
65
+ /**
66
+ * Scroll to the bottom of the message history.
67
+ */
68
+ scrollToBottom(): this;
69
+ /**
70
+ * Set a custom parser for message parsing
71
+ */
72
+ setParser(parser: MessageParser): this;
73
+ /**
74
+ * Initialize MutationObserver for Light DOM observation
75
+ */
76
+ private _initLightDOMObserver;
77
+ /**
78
+ * Parse Light DOM textContent into messages
79
+ */
80
+ private _parseLightDOM;
81
+ /**
82
+ * Check if messages have changed
83
+ */
84
+ private _messagesChanged;
85
+ /**
86
+ * Hide light DOM content visually while keeping it for accessibility/parsing
87
+ */
88
+ private _hideLightDOMContent;
89
+ /**
90
+ * Sync current messages back to light DOM
91
+ */
92
+ private _syncToLightDOM;
93
+ /**
94
+ * Compute message groups for rendering
95
+ */
96
+ private _computeGroups;
97
+ /**
98
+ * Check if two messages can be grouped together
99
+ */
100
+ private _canGroup;
101
+ /**
102
+ * Handle scroll button click
103
+ */
104
+ private _onScrollButtonClick;
105
+ render(): import("lit").TemplateResult<1>;
106
+ }
107
+ declare global {
108
+ interface HTMLElementTagNameMap {
109
+ 'bb-msg-history': BBMsgHistory;
110
+ }
111
+ }
@@ -0,0 +1,473 @@
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, nothing, unsafeCSS } from 'lit';
8
+ import { customElement, property, state } from 'lit/decorators.js';
9
+ import { provide } from '@lit/context';
10
+ import { authorContext } from '../contexts/author-context.js';
11
+ import { ScrollController } from '../controllers/scroll-controller.js';
12
+ import { DefaultMessageParser } from '../parsers/default-parser.js';
13
+ import { THEME } from '../const/theme.js';
14
+ import './bb-message.js';
15
+ import './bb-scroll-button.js';
16
+ import './bb-loading-overlay.js';
17
+ /**
18
+ * BBMsgHistory - A chat-style message history web component
19
+ *
20
+ * Uses Lit for reactive rendering with a compositional architecture.
21
+ * Preserves backward compatibility with the lightweight textContent mode.
22
+ *
23
+ * @example
24
+ * ```html
25
+ * <!-- Lightweight mode -->
26
+ * <bb-msg-history>
27
+ * alice: Hello!
28
+ * bob: Hi there!
29
+ * </bb-msg-history>
30
+ *
31
+ * <!-- With custom authors -->
32
+ * <bb-msg-history id="chat"></bb-msg-history>
33
+ * <script>
34
+ * document.getElementById('chat')
35
+ * .setAuthor('alice', { avatar: '🐱', side: 'right' });
36
+ * </script>
37
+ * ```
38
+ */
39
+ let BBMsgHistory = class BBMsgHistory extends LitElement {
40
+ constructor() {
41
+ super(...arguments);
42
+ // Public properties
43
+ this.loading = false;
44
+ this.hideScrollBar = false;
45
+ this.infinite = false;
46
+ this.hideScrollButton = false;
47
+ this.theme = null;
48
+ // Private state
49
+ this._messages = [];
50
+ this._processedMessages = [];
51
+ // Context provider for author configurations
52
+ this._userAuthors = new Map();
53
+ this._isParsing = false;
54
+ this._parser = new DefaultMessageParser();
55
+ // Scroll controller
56
+ this._scrollController = new ScrollController(this);
57
+ }
58
+ static { this.styles = css `
59
+ :host {
60
+ display: block;
61
+ position: relative;
62
+ font-family:
63
+ 'PT Sans',
64
+ ui-sans-serif,
65
+ system-ui,
66
+ -apple-system,
67
+ BlinkMacSystemFont,
68
+ 'Segoe UI',
69
+ Roboto,
70
+ 'Helvetica Neue',
71
+ Arial,
72
+ 'Noto Sans',
73
+ sans-serif,
74
+ 'Apple Color Emoji',
75
+ 'Segoe UI Emoji',
76
+ 'Segoe UI Symbol',
77
+ 'Noto Color Emoji';
78
+ --bb-bg-color: ${unsafeCSS(THEME.gray[50])};
79
+ --bb-max-height: 600px;
80
+ --bb-avatar-bg: #ffffff;
81
+ --bb-avatar-color: ${unsafeCSS(THEME.gray[600])};
82
+ }
83
+
84
+ :host([theme='dark']) {
85
+ --bb-bg-color: ${unsafeCSS(THEME.gray[900])};
86
+ --bb-avatar-bg: ${unsafeCSS(THEME.slate[600])};
87
+ --bb-avatar-color: ${unsafeCSS(THEME.slate[200])};
88
+ }
89
+
90
+ @media (prefers-color-scheme: dark) {
91
+ :host {
92
+ --bb-bg-color: ${unsafeCSS(THEME.gray[900])};
93
+ --bb-avatar-bg: ${unsafeCSS(THEME.slate[600])};
94
+ --bb-avatar-color: ${unsafeCSS(THEME.slate[200])};
95
+ }
96
+ }
97
+
98
+ .history-container {
99
+ margin: 0 auto;
100
+ display: flex;
101
+ flex-direction: column;
102
+ gap: 0.25rem;
103
+ max-height: var(--bb-max-height, 600px);
104
+ overflow-y: auto;
105
+ scroll-behavior: smooth;
106
+ background-color: transparent;
107
+ border-radius: 0.5rem;
108
+ /* Firefox scrollbar */
109
+ scrollbar-width: thin;
110
+ scrollbar-color: ${unsafeCSS(THEME.gray[400])} transparent;
111
+ }
112
+
113
+ /* Custom scrollbar for webkit browsers */
114
+ .history-container::-webkit-scrollbar {
115
+ width: 6px;
116
+ }
117
+
118
+ .history-container::-webkit-scrollbar-track {
119
+ background: transparent;
120
+ border-radius: 3px;
121
+ }
122
+
123
+ .history-container::-webkit-scrollbar-thumb {
124
+ background: ${unsafeCSS(THEME.gray[400])};
125
+ border-radius: 3px;
126
+ }
127
+
128
+ .history-container::-webkit-scrollbar-thumb:hover {
129
+ background: ${unsafeCSS(THEME.gray[500])};
130
+ }
131
+
132
+ /* Hide scrollbar */
133
+ :host([hide-scroll-bar]) .history-container {
134
+ scrollbar-width: none;
135
+ -ms-overflow-style: none;
136
+ }
137
+
138
+ :host([hide-scroll-bar]) .history-container::-webkit-scrollbar {
139
+ display: none;
140
+ }
141
+
142
+ /* Infinite mode */
143
+ :host([infinite]) .history-container {
144
+ max-height: none;
145
+ overflow-y: visible;
146
+ }
147
+
148
+ /* Empty state */
149
+ .empty-state {
150
+ text-align: center;
151
+ padding: 2rem;
152
+ color: ${unsafeCSS(THEME.gray[400])};
153
+ font-size: 0.875rem;
154
+ }
155
+
156
+ @media (prefers-color-scheme: dark) {
157
+ .empty-state {
158
+ color: ${unsafeCSS(THEME.gray[500])};
159
+ }
160
+
161
+ .history-container {
162
+ scrollbar-color: ${unsafeCSS(THEME.gray[600])} transparent;
163
+ }
164
+
165
+ .history-container::-webkit-scrollbar-thumb {
166
+ background: ${unsafeCSS(THEME.gray[600])};
167
+ }
168
+
169
+ .history-container::-webkit-scrollbar-thumb:hover {
170
+ background: ${unsafeCSS(THEME.gray[500])};
171
+ }
172
+ }
173
+
174
+ @media (max-width: 480px) {
175
+ .history-container {
176
+ max-height: var(--bb-max-height, 70vh);
177
+ }
178
+ }
179
+
180
+ @media (prefers-reduced-motion: reduce) {
181
+ .history-container {
182
+ scroll-behavior: auto;
183
+ }
184
+ }
185
+ `; }
186
+ connectedCallback() {
187
+ super.connectedCallback();
188
+ this._initLightDOMObserver();
189
+ // Initial parse if content exists (for SSR or static HTML)
190
+ if (this.textContent?.trim()) {
191
+ this._parseLightDOM();
192
+ }
193
+ }
194
+ disconnectedCallback() {
195
+ super.disconnectedCallback();
196
+ this._mutationObserver?.disconnect();
197
+ }
198
+ willUpdate(changedProps) {
199
+ if (changedProps.has('_messages') ||
200
+ changedProps.has('_userAuthors')) {
201
+ this._processedMessages = this._computeGroups(this._messages);
202
+ }
203
+ }
204
+ updated(changedProps) {
205
+ if (changedProps.has('_processedMessages')) {
206
+ // Update scroll observer when messages change
207
+ this._scrollController.updateObservedMessage();
208
+ // Auto-scroll if not in infinite mode
209
+ if (!this.infinite) {
210
+ this._scrollController.scrollToBottom('auto');
211
+ }
212
+ }
213
+ }
214
+ /**
215
+ * Configure an author's avatar, side, and colors.
216
+ * Call before or after rendering — the component re-renders automatically.
217
+ */
218
+ setAuthor(name, options) {
219
+ this._userAuthors = new Map([...this._userAuthors, [name, options]]);
220
+ return this;
221
+ }
222
+ /**
223
+ * Remove a previously set author config.
224
+ */
225
+ removeAuthor(name) {
226
+ const newMap = new Map(this._userAuthors);
227
+ newMap.delete(name);
228
+ this._userAuthors = newMap;
229
+ return this;
230
+ }
231
+ /**
232
+ * Show or hide the loading overlay.
233
+ */
234
+ setLoading(isLoading) {
235
+ this.loading = isLoading;
236
+ return this;
237
+ }
238
+ /**
239
+ * Append a message to the history.
240
+ * Automatically scrolls to the new message with smooth animation.
241
+ */
242
+ appendMessage(input) {
243
+ const msg = {
244
+ author: input.author,
245
+ text: input.text,
246
+ timestamp: input.timestamp,
247
+ };
248
+ // Pause observation to avoid loop
249
+ this._mutationObserver?.disconnect();
250
+ // Update messages array
251
+ this._messages = [...this._messages, msg];
252
+ // Optionally sync back to light DOM (for consistency)
253
+ this._syncToLightDOM();
254
+ // Restore observation after current event loop
255
+ requestAnimationFrame(() => {
256
+ this._initLightDOMObserver();
257
+ });
258
+ // Scroll to bottom (skip in infinite mode)
259
+ if (!this.infinite) {
260
+ this._scrollController.scrollToBottom();
261
+ }
262
+ return this;
263
+ }
264
+ /**
265
+ * Scroll to the bottom of the message history.
266
+ */
267
+ scrollToBottom() {
268
+ this._scrollController.scrollToBottom();
269
+ return this;
270
+ }
271
+ /**
272
+ * Set a custom parser for message parsing
273
+ */
274
+ setParser(parser) {
275
+ this._parser = parser;
276
+ this._parseLightDOM();
277
+ return this;
278
+ }
279
+ /**
280
+ * Initialize MutationObserver for Light DOM observation
281
+ */
282
+ _initLightDOMObserver() {
283
+ if (this._mutationObserver) {
284
+ this._mutationObserver.disconnect();
285
+ }
286
+ this._mutationObserver = new MutationObserver(records => {
287
+ // Only react to light DOM changes, not shadow DOM
288
+ const hasLightDOMChange = records.some(r => {
289
+ const root = r.target.getRootNode();
290
+ return root === document || root === this;
291
+ });
292
+ if (hasLightDOMChange && !this._isParsing) {
293
+ this._parseLightDOM();
294
+ }
295
+ });
296
+ this._mutationObserver.observe(this, {
297
+ childList: true,
298
+ subtree: true,
299
+ characterData: true,
300
+ });
301
+ }
302
+ /**
303
+ * Parse Light DOM textContent into messages
304
+ */
305
+ _parseLightDOM() {
306
+ const rawText = this.textContent || '';
307
+ if (!rawText.trim()) {
308
+ this._messages = [];
309
+ return;
310
+ }
311
+ this._isParsing = true;
312
+ const parsed = this._parser.parse(rawText);
313
+ // Only update if messages actually changed (avoid unnecessary re-renders)
314
+ if (this._messagesChanged(parsed)) {
315
+ this._messages = parsed;
316
+ }
317
+ // Hide light DOM content visually but keep it accessible
318
+ this._hideLightDOMContent();
319
+ requestAnimationFrame(() => {
320
+ this._isParsing = false;
321
+ });
322
+ }
323
+ /**
324
+ * Check if messages have changed
325
+ */
326
+ _messagesChanged(newMessages) {
327
+ if (newMessages.length !== this._messages.length)
328
+ return true;
329
+ return newMessages.some((msg, i) => {
330
+ const old = this._messages[i];
331
+ return (msg.author !== old?.author || msg.text !== old?.text || msg.timestamp !== old?.timestamp);
332
+ });
333
+ }
334
+ /**
335
+ * Hide light DOM content visually while keeping it for accessibility/parsing
336
+ */
337
+ _hideLightDOMContent() {
338
+ // The content is parsed and rendered in shadow DOM,
339
+ // so we don't need to hide it explicitly - the shadow DOM overlays it.
340
+ // But we can ensure proper accessibility by marking it.
341
+ this.setAttribute('aria-live', 'polite');
342
+ this.setAttribute('role', 'log');
343
+ this.setAttribute('aria-label', 'Message history');
344
+ }
345
+ /**
346
+ * Sync current messages back to light DOM
347
+ */
348
+ _syncToLightDOM() {
349
+ const content = this._messages
350
+ .map(m => {
351
+ const timestamp = m.timestamp ? `[${m.timestamp}] ` : '';
352
+ return `${timestamp}${m.author}: ${m.text}`;
353
+ })
354
+ .join('\n');
355
+ // Temporarily disconnect observer to prevent loop
356
+ this._mutationObserver?.disconnect();
357
+ this.textContent = content;
358
+ this._initLightDOMObserver();
359
+ }
360
+ /**
361
+ * Compute message groups for rendering
362
+ */
363
+ _computeGroups(messages) {
364
+ if (messages.length === 0)
365
+ return [];
366
+ const processed = [];
367
+ let currentGroupTimestamp;
368
+ for (let i = 0; i < messages.length; i++) {
369
+ const msg = messages[i];
370
+ const prev = i > 0 ? messages[i - 1] : null;
371
+ const next = i < messages.length - 1 ? messages[i + 1] : null;
372
+ // Determine if this is first from author
373
+ const isFirstFromAuthor = !this._canGroup(prev, msg);
374
+ // Start of new group - initialize group timestamp
375
+ if (isFirstFromAuthor) {
376
+ currentGroupTimestamp = msg.timestamp;
377
+ }
378
+ else if (!currentGroupTimestamp && msg.timestamp) {
379
+ currentGroupTimestamp = msg.timestamp;
380
+ }
381
+ // Determine if this is last in group
382
+ const isLastInGroup = !next || !this._canGroup(msg, next);
383
+ processed.push({
384
+ ...msg,
385
+ isFirstFromAuthor,
386
+ isLastInGroup,
387
+ groupTimestamp: isLastInGroup ? currentGroupTimestamp : undefined,
388
+ });
389
+ // Reset group timestamp at end of group
390
+ if (isLastInGroup) {
391
+ currentGroupTimestamp = undefined;
392
+ }
393
+ }
394
+ return processed;
395
+ }
396
+ /**
397
+ * Check if two messages can be grouped together
398
+ */
399
+ _canGroup(prev, curr) {
400
+ if (!prev)
401
+ return false;
402
+ if (prev.author !== curr.author)
403
+ return false;
404
+ if (prev.timestamp && curr.timestamp && prev.timestamp !== curr.timestamp) {
405
+ return false;
406
+ }
407
+ return true;
408
+ }
409
+ /**
410
+ * Handle scroll button click
411
+ */
412
+ _onScrollButtonClick() {
413
+ this._scrollController.scrollToBottom();
414
+ }
415
+ render() {
416
+ const hasMessages = this._processedMessages.length > 0;
417
+ return html `
418
+ <div class="history-container" role="log" aria-live="polite" aria-label="Message history">
419
+ ${hasMessages
420
+ ? this._processedMessages.map((msg, index) => html `
421
+ <bb-message
422
+ .author=${msg.author}
423
+ .text=${msg.text}
424
+ .timestamp=${msg.groupTimestamp || ''}
425
+ ?subsequent=${!msg.isFirstFromAuthor}
426
+ ?lastInGroup=${msg.isLastInGroup}
427
+ data-index="${index}"
428
+ ></bb-message>
429
+ `)
430
+ : html ` <div class="empty-state">${this.loading ? '' : 'No messages'}</div> `}
431
+ </div>
432
+
433
+ ${!this.hideScrollButton && !this.infinite
434
+ ? html `
435
+ <bb-scroll-button
436
+ .visible=${this._scrollController.isVisible}
437
+ @bb-scroll-to-bottom=${this._onScrollButtonClick}
438
+ ></bb-scroll-button>
439
+ `
440
+ : nothing}
441
+ ${this.loading ? html `<bb-loading-overlay visible></bb-loading-overlay>` : nothing}
442
+ `;
443
+ }
444
+ };
445
+ __decorate([
446
+ property({ type: Boolean, reflect: true })
447
+ ], BBMsgHistory.prototype, "loading", void 0);
448
+ __decorate([
449
+ property({ type: Boolean, reflect: true, attribute: 'hide-scroll-bar' })
450
+ ], BBMsgHistory.prototype, "hideScrollBar", void 0);
451
+ __decorate([
452
+ property({ type: Boolean, reflect: true })
453
+ ], BBMsgHistory.prototype, "infinite", void 0);
454
+ __decorate([
455
+ property({ type: Boolean, reflect: true, attribute: 'hide-scroll-button' })
456
+ ], BBMsgHistory.prototype, "hideScrollButton", void 0);
457
+ __decorate([
458
+ property({ reflect: true })
459
+ ], BBMsgHistory.prototype, "theme", void 0);
460
+ __decorate([
461
+ state()
462
+ ], BBMsgHistory.prototype, "_messages", void 0);
463
+ __decorate([
464
+ state()
465
+ ], BBMsgHistory.prototype, "_processedMessages", void 0);
466
+ __decorate([
467
+ provide({ context: authorContext }),
468
+ state()
469
+ ], BBMsgHistory.prototype, "_userAuthors", void 0);
470
+ BBMsgHistory = __decorate([
471
+ customElement('bb-msg-history')
472
+ ], BBMsgHistory);
473
+ export { BBMsgHistory };
@@ -0,0 +1,16 @@
1
+ import { LitElement } from 'lit';
2
+ /**
3
+ * Scroll-to-bottom button component
4
+ * Emits bb-scroll-to-bottom event when clicked
5
+ */
6
+ export declare class BBScrollButton extends LitElement {
7
+ static styles: import("lit").CSSResult;
8
+ visible: boolean;
9
+ private _handleClick;
10
+ render(): import("lit").TemplateResult<1>;
11
+ }
12
+ declare global {
13
+ interface HTMLElementTagNameMap {
14
+ 'bb-scroll-button': BBScrollButton;
15
+ }
16
+ }