@nyaruka/temba-components 0.123.0 → 0.124.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 (87) hide show
  1. package/.github/copilot-instructions.md +22 -4
  2. package/CHANGELOG.md +11 -0
  3. package/demo/drag-drop-demo.html +141 -0
  4. package/demo/index.html +15 -0
  5. package/demo/test-drag-drop.html +94 -0
  6. package/dist/temba-components.js +323 -191
  7. package/dist/temba-components.js.map +1 -1
  8. package/out-tsc/src/fields/FieldManager.js +27 -34
  9. package/out-tsc/src/fields/FieldManager.js.map +1 -1
  10. package/out-tsc/src/list/SortableList.js +257 -60
  11. package/out-tsc/src/list/SortableList.js.map +1 -1
  12. package/out-tsc/src/omnibox/Omnibox.js +1 -1
  13. package/out-tsc/src/omnibox/Omnibox.js.map +1 -1
  14. package/out-tsc/src/select/Select.js +198 -38
  15. package/out-tsc/src/select/Select.js.map +1 -1
  16. package/out-tsc/src/webchat/WebChat.js +5 -2
  17. package/out-tsc/src/webchat/WebChat.js.map +1 -1
  18. package/out-tsc/test/temba-flow-editor-node.test.js +273 -0
  19. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -0
  20. package/out-tsc/test/temba-flow-editor.test.js +244 -0
  21. package/out-tsc/test/temba-flow-editor.test.js.map +1 -0
  22. package/out-tsc/test/temba-flow-plumber.test.js +145 -0
  23. package/out-tsc/test/temba-flow-plumber.test.js.map +1 -0
  24. package/out-tsc/test/temba-flow-render.test.js +171 -0
  25. package/out-tsc/test/temba-flow-render.test.js.map +1 -0
  26. package/out-tsc/test/temba-omnibox.test.js +2 -3
  27. package/out-tsc/test/temba-omnibox.test.js.map +1 -1
  28. package/out-tsc/test/temba-select.test.js +134 -53
  29. package/out-tsc/test/temba-select.test.js.map +1 -1
  30. package/out-tsc/test/temba-sortable-list.test.js +91 -15
  31. package/out-tsc/test/temba-sortable-list.test.js.map +1 -1
  32. package/out-tsc/test/temba-webchat-lightbox-fix.test.js +42 -0
  33. package/out-tsc/test/temba-webchat-lightbox-fix.test.js.map +1 -0
  34. package/out-tsc/test/utils.test.js +30 -0
  35. package/out-tsc/test/utils.test.js.map +1 -1
  36. package/package.json +1 -1
  37. package/screenshots/truth/flow/editor-basic.png +0 -0
  38. package/screenshots/truth/list/fields-dragging.png +0 -0
  39. package/screenshots/truth/list/sortable-dragging.png +0 -0
  40. package/screenshots/truth/list/sortable-dropped.png +0 -0
  41. package/screenshots/truth/list/sortable.png +0 -0
  42. package/screenshots/truth/omnibox/selected.png +0 -0
  43. package/screenshots/truth/select/disabled-multi-selection.png +0 -0
  44. package/screenshots/truth/select/disabled-selection.png +0 -0
  45. package/screenshots/truth/select/disabled.png +0 -0
  46. package/screenshots/truth/select/embedded.png +0 -0
  47. package/screenshots/truth/select/empty-options.png +0 -0
  48. package/screenshots/truth/select/expression-selected.png +0 -0
  49. package/screenshots/truth/select/expressions.png +0 -0
  50. package/screenshots/truth/select/functions.png +0 -0
  51. package/screenshots/truth/select/local-options.png +0 -0
  52. package/screenshots/truth/select/multi-reorder-final.png +0 -0
  53. package/screenshots/truth/select/multi-reorder-initial.png +0 -0
  54. package/screenshots/truth/select/multi-with-endpoint.png +0 -0
  55. package/screenshots/truth/select/multiple-initial-values.png +0 -0
  56. package/screenshots/truth/select/remote-options.png +0 -0
  57. package/screenshots/truth/select/search-enabled.png +0 -0
  58. package/screenshots/truth/select/search-multi-no-matches.png +0 -0
  59. package/screenshots/truth/select/search-selected-focus.png +0 -0
  60. package/screenshots/truth/select/search-selected.png +0 -0
  61. package/screenshots/truth/select/search-with-selected.png +0 -0
  62. package/screenshots/truth/select/searching.png +0 -0
  63. package/screenshots/truth/select/selected-multi-maxitems-reached.png +0 -0
  64. package/screenshots/truth/select/selected-multi.png +0 -0
  65. package/screenshots/truth/select/selected-single.png +0 -0
  66. package/screenshots/truth/select/selection-clearable.png +0 -0
  67. package/screenshots/truth/select/static-initial-value.png +0 -0
  68. package/screenshots/truth/select/static-initial-via-selected.png +0 -0
  69. package/screenshots/truth/select/truncated-selection.png +0 -0
  70. package/screenshots/truth/select/with-placeholder.png +0 -0
  71. package/screenshots/truth/select/without-placeholder.png +0 -0
  72. package/screenshots/truth/templates/default.png +0 -0
  73. package/screenshots/truth/templates/unapproved.png +0 -0
  74. package/src/fields/FieldManager.ts +30 -38
  75. package/src/list/SortableList.ts +291 -67
  76. package/src/omnibox/Omnibox.ts +1 -1
  77. package/src/select/Select.ts +213 -42
  78. package/src/webchat/WebChat.ts +5 -2
  79. package/test/temba-flow-editor-node.test.ts +344 -0
  80. package/test/temba-flow-editor.test.ts +301 -0
  81. package/test/temba-flow-plumber.test.ts +189 -0
  82. package/test/temba-flow-render.test.ts +220 -0
  83. package/test/temba-omnibox.test.ts +2 -3
  84. package/test/temba-select.test.ts +180 -79
  85. package/test/temba-sortable-list.test.ts +108 -15
  86. package/test/temba-webchat-lightbox-fix.test.ts +57 -0
  87. package/test/utils.test.ts +52 -0
@@ -8,7 +8,7 @@ import { RapidElement } from '../RapidElement';
8
8
  */
9
9
 
10
10
  // how far we have to drag before it starts
11
- const DRAG_THRESHOLD = 5;
11
+ const DRAG_THRESHOLD = 2;
12
12
  export class SortableList extends RapidElement {
13
13
  static get styles() {
14
14
  return css`
@@ -18,18 +18,54 @@ export class SortableList extends RapidElement {
18
18
 
19
19
  .container {
20
20
  user-select: none;
21
+ position: relative;
22
+ }
23
+
24
+ .container.horizontal {
25
+ display: flex;
26
+ flex-wrap: wrap;
27
+ align-items: center;
21
28
  }
22
29
 
23
30
  .dragging {
24
31
  background: var(--color-selection);
25
32
  }
26
33
 
34
+ .dragged-item {
35
+ opacity: 0;
36
+ pointer-events: none;
37
+ }
38
+
27
39
  .sortable {
28
40
  transition: all 300ms ease-in-out;
29
41
  display: flex;
30
42
  padding: 0.4em 0;
31
43
  }
32
44
 
45
+ .container.horizontal .sortable {
46
+ padding: 0;
47
+ margin-right: 0.25em;
48
+ margin-bottom: 0.25em;
49
+ }
50
+
51
+ .drop-indicator {
52
+ position: absolute;
53
+ background: var(--color-primary-dark, #1c7cd6);
54
+ z-index: 1000;
55
+ pointer-events: none;
56
+ }
57
+
58
+ .container.horizontal .drop-indicator {
59
+ width: 2px;
60
+ margin-top: -5px;
61
+ padding-bottom: 10px;
62
+ }
63
+
64
+ .container:not(.horizontal) .drop-indicator {
65
+ height: 2px;
66
+ left: 0;
67
+ }
68
+
33
69
  .sortable:hover temba-icon {
34
70
  opacity: 1;
35
71
  cursor: move;
@@ -37,8 +73,14 @@ export class SortableList extends RapidElement {
37
73
 
38
74
  .ghost {
39
75
  position: absolute;
40
- opacity: 0.5;
76
+ opacity: 0.7;
41
77
  transition: none;
78
+ background: var(--color-background, white);
79
+ border: 1px solid var(--color-primary, #1c7cd6);
80
+ border-radius: 4px;
81
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
82
+ pointer-events: none;
83
+ z-index: 999;
42
84
  }
43
85
 
44
86
  .slot {
@@ -60,14 +102,33 @@ export class SortableList extends RapidElement {
60
102
  @property({ type: String })
61
103
  draggingId: string;
62
104
 
105
+ @property({ type: Boolean })
106
+ horizontal: boolean = false;
107
+
108
+ @property({ type: String })
109
+ dropTargetId: string;
110
+
111
+ /**
112
+ * Optional callback to allow parent components to customize the ghost node.
113
+ * Called after the ghost node is cloned but before it is appended to the DOM.
114
+ */
115
+ @property({ attribute: false })
116
+ prepareGhost?: (ghost: HTMLElement) => void;
117
+
63
118
  ghostElement: HTMLDivElement = null;
64
119
  downEle: HTMLDivElement = null;
65
120
  xOffset = 0;
66
121
  yOffset = 0;
67
122
  yDown = 0;
123
+ xDown = 0;
68
124
 
69
125
  draggingIdx = -1;
70
126
  draggingEle = null;
127
+ dropIndicator: HTMLDivElement = null;
128
+ pendingDropIndex = -1;
129
+ pendingTargetElement: HTMLElement = null;
130
+
131
+ private clickBlocker: ((e: MouseEvent) => void) | null = null;
71
132
 
72
133
  public constructor() {
73
134
  super();
@@ -82,57 +143,139 @@ export class SortableList extends RapidElement {
82
143
  super.firstUpdated(_changedProperties);
83
144
  }
84
145
 
85
- public getIds() {
146
+ private getSortableElements(): Element[] {
86
147
  return this.shadowRoot
87
148
  .querySelector('slot')
88
149
  .assignedElements()
89
- .map((ele) => ele.id);
150
+ .filter((ele) => ele.classList.contains('sortable'));
151
+ }
152
+
153
+ public getIds() {
154
+ return this.getSortableElements().map((ele) => ele.id);
90
155
  }
91
156
 
92
157
  private getRowIndex(id: string): number {
93
- return this.shadowRoot
94
- .querySelector('slot')
95
- .assignedElements()
96
- .findIndex((ele) => ele.id === id);
158
+ return this.getSortableElements().findIndex((ele) => ele.id === id);
97
159
  }
98
160
 
99
- private getOverlappingElement(mouseY: number): HTMLDivElement {
100
- const ghostRect = this.ghostElement.getBoundingClientRect();
161
+ private getDropTargetInfo(
162
+ mouseX: number,
163
+ mouseY: number
164
+ ): { element: HTMLDivElement; insertAfter: boolean } | null {
165
+ const elements = this.getSortableElements().filter(
166
+ (ele) => ele.id !== this.draggingEle?.id
167
+ );
168
+
169
+ if (elements.length === 0) return null;
170
+
171
+ if (this.horizontal) {
172
+ // For horizontal layout, find the insertion point based on mouse X position
173
+ for (let i = 0; i < elements.length; i++) {
174
+ const ele = elements[i];
175
+ const rect = ele.getBoundingClientRect();
176
+ const centerX = rect.left + rect.width / 2;
177
+
178
+ if (mouseX < centerX) {
179
+ // Insert before this element
180
+ return { element: ele as HTMLDivElement, insertAfter: false };
181
+ }
182
+ }
183
+ // If we're past all elements, insert after the last one
184
+ return {
185
+ element: elements[elements.length - 1] as HTMLDivElement,
186
+ insertAfter: true
187
+ };
188
+ } else {
189
+ // For vertical layout, find the insertion point based on mouse Y position
190
+ for (let i = 0; i < elements.length; i++) {
191
+ const ele = elements[i];
192
+ const rect = ele.getBoundingClientRect();
193
+ const centerY = rect.top + rect.height / 2;
194
+
195
+ if (mouseY < centerY) {
196
+ // Insert before this element
197
+ return { element: ele as HTMLDivElement, insertAfter: false };
198
+ }
199
+ }
200
+ // If we're past all elements, insert after the last one
201
+ return {
202
+ element: elements[elements.length - 1] as HTMLDivElement,
203
+ insertAfter: true
204
+ };
205
+ }
206
+ }
101
207
 
102
- const ele = this.shadowRoot
103
- .querySelector('slot')
104
- .assignedElements()
105
- .find((otherEle) => {
106
- const rect = otherEle.getBoundingClientRect();
208
+ private showDropIndicator(targetElement: HTMLElement, insertAfter: boolean) {
209
+ this.hideDropIndicator();
107
210
 
108
- // don't return ourselves
109
- if (otherEle.id === this.ghostElement.id) {
110
- return false;
111
- }
211
+ if (!targetElement) return;
112
212
 
113
- if (mouseY > this.yDown) {
114
- // moving down
115
- return ghostRect.top < rect.bottom && ghostRect.bottom > rect.bottom;
116
- } else {
117
- // moving up
118
- return rect.top < ghostRect.bottom && rect.bottom > ghostRect.bottom;
119
- }
120
- });
121
- return ele as HTMLDivElement;
213
+ const container = this.shadowRoot.querySelector('.container');
214
+ this.dropIndicator = document.createElement('div');
215
+ this.dropIndicator.className = 'drop-indicator';
216
+
217
+ const targetRect = targetElement.getBoundingClientRect();
218
+ const containerRect = container.getBoundingClientRect();
219
+
220
+ if (this.horizontal) {
221
+ // For horizontal layout, show vertical line
222
+ this.dropIndicator.style.height = targetRect.height + 'px';
223
+ this.dropIndicator.style.top = targetRect.top - containerRect.top + 'px';
224
+
225
+ if (insertAfter) {
226
+ // Show line after target
227
+ this.dropIndicator.style.left =
228
+ targetRect.right - containerRect.left + 'px';
229
+ } else {
230
+ // Show line before target
231
+ this.dropIndicator.style.left =
232
+ targetRect.left - containerRect.left + 'px';
233
+ }
234
+ } else {
235
+ // For vertical layout, show horizontal line
236
+ this.dropIndicator.style.width = targetRect.width + 'px';
237
+ this.dropIndicator.style.left =
238
+ targetRect.left - containerRect.left + 'px';
239
+
240
+ if (insertAfter) {
241
+ // Show line after target
242
+ this.dropIndicator.style.top =
243
+ targetRect.bottom - containerRect.top + 'px';
244
+ } else {
245
+ // Show line before target
246
+ this.dropIndicator.style.top =
247
+ targetRect.top - containerRect.top + 'px';
248
+ }
249
+ }
250
+
251
+ container.appendChild(this.dropIndicator);
252
+ }
253
+
254
+ private hideDropIndicator() {
255
+ if (this.dropIndicator) {
256
+ this.dropIndicator.remove();
257
+ this.dropIndicator = null;
258
+ }
122
259
  }
123
260
 
124
261
  private handleMouseDown(event: MouseEvent) {
125
262
  let ele = event.target as HTMLDivElement;
126
263
  ele = ele.closest('.sortable');
127
264
  if (ele) {
265
+ event.preventDefault();
266
+ event.stopPropagation();
267
+
128
268
  this.downEle = ele;
129
269
  this.draggingId = ele.id;
130
270
  this.draggingIdx = this.getRowIndex(ele.id);
131
271
  this.draggingEle = ele;
132
272
 
133
- this.xOffset = event.clientX - ele.offsetLeft;
134
- this.yOffset = event.clientY - ele.offsetTop;
273
+ // Use getBoundingClientRect for accurate offsets
274
+ const rect = ele.getBoundingClientRect();
275
+ this.xOffset = event.clientX - rect.left;
276
+ this.yOffset = event.clientY - rect.top;
135
277
  this.yDown = event.clientY;
278
+ this.xDown = event.clientX;
136
279
 
137
280
  document.addEventListener('mousemove', this.handleMouseMove);
138
281
  document.addEventListener('mouseup', this.handleMouseUp);
@@ -140,14 +283,11 @@ export class SortableList extends RapidElement {
140
283
  }
141
284
 
142
285
  private handleMouseMove(event: MouseEvent) {
143
- const scrollTop = this.shadowRoot
144
- .querySelector('slot')
145
- .assignedElements()[0].parentElement.scrollTop;
146
-
147
286
  if (
148
287
  !this.ghostElement &&
149
288
  this.downEle &&
150
- Math.abs(event.clientY - this.yDown) > DRAG_THRESHOLD
289
+ (Math.abs(event.clientY - this.yDown) > DRAG_THRESHOLD ||
290
+ Math.abs(event.clientX - this.xDown) > DRAG_THRESHOLD)
151
291
  ) {
152
292
  this.fireCustomEvent(CustomEventType.DragStart, {
153
293
  id: this.downEle.id
@@ -156,58 +296,142 @@ export class SortableList extends RapidElement {
156
296
  this.ghostElement = this.downEle.cloneNode(true) as HTMLDivElement;
157
297
  this.ghostElement.classList.add('ghost');
158
298
 
159
- const computedStyle = getComputedStyle(this.downEle);
299
+ // dim the original element while dragging
300
+ this.downEle.style.pointerEvents = 'none';
301
+ this.downEle.style.opacity = '0.5';
160
302
 
161
- this.ghostElement.style.width =
162
- this.downEle.clientWidth -
163
- parseFloat(computedStyle.paddingLeft) -
164
- parseFloat(computedStyle.paddingRight) +
165
- 'px';
166
- const container = this.shadowRoot.querySelector('.container');
303
+ const rect = this.downEle.getBoundingClientRect();
304
+ this.ghostElement.style.transition = 'transform 300ms linear';
167
305
 
168
- container.appendChild(this.ghostElement);
306
+ this.ghostElement.style.width = rect.width + 'px';
307
+ this.ghostElement.style.height = rect.height + 'px';
308
+ this.ghostElement.style.position = 'fixed';
309
+ this.ghostElement.style.left = event.clientX - this.xOffset + 'px';
310
+ this.ghostElement.style.top = event.clientY - this.yOffset + 'px';
311
+ this.ghostElement.style.pointerEvents = 'none';
312
+ this.ghostElement.style.border =
313
+ '1px solid var(--color-primary, #1c7cd6)';
314
+ this.ghostElement.style.zIndex = '99999';
315
+ this.ghostElement.style.background = '#fff';
316
+ this.ghostElement.style.opacity = '0.7';
317
+ this.ghostElement.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.2)';
318
+ this.ghostElement.style.borderRadius = 'var(--curvature)';
319
+
320
+ // allow component to customize the ghost node
321
+ if (this.prepareGhost) {
322
+ this.prepareGhost(this.ghostElement);
323
+ }
169
324
 
170
- this.downEle = null;
325
+ document.body.appendChild(this.ghostElement);
326
+
327
+ // this.downEle = null;
328
+
329
+ // Add global click blocker when drag starts
330
+ if (!this.clickBlocker) {
331
+ this.clickBlocker = (e: MouseEvent) => {
332
+ e.stopPropagation();
333
+ e.preventDefault();
334
+ };
335
+ // Use capture phase to intercept clicks before they reach any elements
336
+ document.addEventListener('click', this.clickBlocker, true);
337
+ }
171
338
  }
172
339
 
173
340
  if (this.ghostElement) {
174
341
  this.ghostElement.style.left = event.clientX - this.xOffset + 'px';
175
- this.ghostElement.style.top =
176
- event.clientY - this.yOffset - scrollTop + 'px';
177
-
178
- const other = this.getOverlappingElement(event.clientY);
179
- if (other) {
180
- const otherIdx = this.getRowIndex(other.id);
181
- const dragId = this.ghostElement.id;
182
- const otherId = other.id;
183
-
184
- this.fireCustomEvent(CustomEventType.OrderChanged, {
185
- from: dragId,
186
- to: otherId,
187
- fromIdx: this.draggingIdx,
188
- toIdx: otherIdx
189
- });
190
-
191
- // TODO: Dont do swapping, just send the full order?
192
- this.draggingIdx = otherIdx;
193
- this.draggingId = otherId;
342
+ this.ghostElement.style.top = event.clientY - this.yOffset + 'px';
343
+
344
+ const targetInfo = this.getDropTargetInfo(event.clientX, event.clientY);
345
+ if (targetInfo) {
346
+ const { element: targetElement, insertAfter } = targetInfo;
347
+ const targetIdx = this.getRowIndex(targetElement.id);
348
+ const originalDragIdx = this.getRowIndex(this.draggingEle.id);
349
+
350
+ // Calculate the intended drop index
351
+ let dropIdx = targetIdx;
352
+ if (insertAfter) {
353
+ dropIdx += 1;
354
+ }
355
+
356
+ // Adjust dropIdx if dragging forward in the list
357
+ if (originalDragIdx < dropIdx) {
358
+ dropIdx -= 1;
359
+ }
360
+
361
+ // Store pending drop info but don't fire event yet
362
+ this.dropTargetId = targetElement.id;
363
+ this.pendingDropIndex = dropIdx;
364
+ this.pendingTargetElement = targetElement;
365
+
366
+ // Show drop indicator
367
+ this.showDropIndicator(targetElement, insertAfter);
368
+ } else {
369
+ this.hideDropIndicator();
370
+ this.dropTargetId = null;
371
+ this.pendingDropIndex = -1;
372
+ this.pendingTargetElement = null;
194
373
  }
195
374
  }
196
375
  }
197
376
 
198
- private handleMouseUp() {
199
- if (this.draggingId) {
377
+ private handleMouseUp(evt: MouseEvent) {
378
+ if (this.draggingId && this.ghostElement) {
379
+ evt.preventDefault();
380
+ evt.stopPropagation();
381
+
382
+ // restore visibility of the dragged item
383
+
384
+ if (this.downEle) {
385
+ this.downEle.style.pointerEvents = '';
386
+ this.downEle.style.opacity = '1';
387
+ }
388
+
389
+ // fire the order changed event only when dropped if we have a valid drop position
390
+ if (this.pendingDropIndex >= 0 && this.pendingTargetElement) {
391
+ const originalDragIdx = this.getRowIndex(this.draggingEle.id);
392
+
393
+ // use swap-based logic - report which indexes need to be swapped
394
+ const fromIdx = originalDragIdx;
395
+ const toIdx = this.pendingDropIndex;
396
+
397
+ // only fire if the position actually changed
398
+ if (fromIdx !== toIdx) {
399
+ this.fireCustomEvent(CustomEventType.OrderChanged, {
400
+ swap: [fromIdx, toIdx]
401
+ });
402
+ }
403
+ }
404
+
200
405
  this.fireCustomEvent(CustomEventType.DragStop, {
201
406
  id: this.draggingId
202
407
  });
203
408
 
204
409
  this.draggingId = null;
410
+ this.dropTargetId = null;
205
411
  this.downEle = null;
412
+ this.pendingDropIndex = -1;
413
+ this.pendingTargetElement = null;
206
414
 
207
415
  if (this.ghostElement) {
208
- this.ghostElement.remove();
416
+ // Remove from body if present
417
+ if (this.ghostElement.parentNode) {
418
+ this.ghostElement.parentNode.removeChild(this.ghostElement);
419
+ }
209
420
  this.ghostElement = null;
210
421
  }
422
+
423
+ this.hideDropIndicator();
424
+
425
+ // Keep the click blocker active for a short time after drop
426
+ if (this.clickBlocker) {
427
+ // We'll clean it up after a timeout
428
+ setTimeout(() => {
429
+ if (this.clickBlocker) {
430
+ document.removeEventListener('click', this.clickBlocker, true);
431
+ this.clickBlocker = null;
432
+ }
433
+ }, 100);
434
+ }
211
435
  }
212
436
  document.removeEventListener('mousemove', this.handleMouseMove);
213
437
  document.removeEventListener('mouseup', this.handleMouseUp);
@@ -216,7 +440,7 @@ export class SortableList extends RapidElement {
216
440
 
217
441
  public render(): TemplateResult {
218
442
  return html`
219
- <div class="container">
443
+ <div class="container ${this.horizontal ? 'horizontal' : ''}">
220
444
  <slot @mousedown=${this.handleMouseDown}></slot>
221
445
  </div>
222
446
  `;
@@ -107,7 +107,7 @@ export class Omnibox extends Select<OmniOption> {
107
107
  public renderSelectedItemDefault(option: OmniOption): TemplateResult {
108
108
  return html`
109
109
  <div
110
- style="flex:1 1 auto; display: flex; align-items: stretch; color: var(--color-text-dark); font-size: 12px;"
110
+ style="flex:1 1 auto; text-overflow:ellipsis; overflow:hidden; white-space:nowrap; display: flex; align-items: stretch; color: var(--color-text-dark); font-size: 12px;"
111
111
  >
112
112
  <div style="align-self: center; padding: 0px 7px; color: #bbb">
113
113
  ${this.getIcon(option)}