@nyaruka/temba-components 0.138.4 → 0.139.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 (69) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/locales/es.js +5 -5
  3. package/dist/locales/es.js.map +1 -1
  4. package/dist/locales/fr.js +5 -5
  5. package/dist/locales/fr.js.map +1 -1
  6. package/dist/locales/locale-codes.js +2 -11
  7. package/dist/locales/locale-codes.js.map +1 -1
  8. package/dist/locales/pt.js +5 -5
  9. package/dist/locales/pt.js.map +1 -1
  10. package/dist/temba-components.js +816 -852
  11. package/dist/temba-components.js.map +1 -1
  12. package/out-tsc/src/display/FloatingTab.js +23 -30
  13. package/out-tsc/src/display/FloatingTab.js.map +1 -1
  14. package/out-tsc/src/flow/CanvasMenu.js +5 -3
  15. package/out-tsc/src/flow/CanvasMenu.js.map +1 -1
  16. package/out-tsc/src/flow/CanvasNode.js +6 -7
  17. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  18. package/out-tsc/src/flow/Editor.js +152 -235
  19. package/out-tsc/src/flow/Editor.js.map +1 -1
  20. package/out-tsc/src/flow/Plumber.js +757 -403
  21. package/out-tsc/src/flow/Plumber.js.map +1 -1
  22. package/out-tsc/src/flow/utils.js +138 -66
  23. package/out-tsc/src/flow/utils.js.map +1 -1
  24. package/out-tsc/src/interfaces.js +1 -0
  25. package/out-tsc/src/interfaces.js.map +1 -1
  26. package/out-tsc/src/list/TicketList.js +4 -1
  27. package/out-tsc/src/list/TicketList.js.map +1 -1
  28. package/out-tsc/src/live/ContactChat.js +18 -1
  29. package/out-tsc/src/live/ContactChat.js.map +1 -1
  30. package/out-tsc/src/locales/es.js +5 -5
  31. package/out-tsc/src/locales/es.js.map +1 -1
  32. package/out-tsc/src/locales/fr.js +5 -5
  33. package/out-tsc/src/locales/fr.js.map +1 -1
  34. package/out-tsc/src/locales/locale-codes.js +2 -11
  35. package/out-tsc/src/locales/locale-codes.js.map +1 -1
  36. package/out-tsc/src/locales/pt.js +5 -5
  37. package/out-tsc/src/locales/pt.js.map +1 -1
  38. package/out-tsc/src/simulator/Simulator.js +1 -0
  39. package/out-tsc/src/simulator/Simulator.js.map +1 -1
  40. package/out-tsc/test/temba-floating-tab.test.js +4 -6
  41. package/out-tsc/test/temba-floating-tab.test.js.map +1 -1
  42. package/out-tsc/test/temba-flow-collision.test.js +221 -223
  43. package/out-tsc/test/temba-flow-collision.test.js.map +1 -1
  44. package/out-tsc/test/temba-flow-editor.test.js +0 -2
  45. package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
  46. package/out-tsc/test/temba-flow-plumber-connections.test.js +83 -84
  47. package/out-tsc/test/temba-flow-plumber-connections.test.js.map +1 -1
  48. package/out-tsc/test/temba-flow-plumber.test.js +102 -93
  49. package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
  50. package/package.json +1 -1
  51. package/src/display/FloatingTab.ts +22 -31
  52. package/src/flow/CanvasMenu.ts +8 -3
  53. package/src/flow/CanvasNode.ts +6 -7
  54. package/src/flow/Editor.ts +184 -279
  55. package/src/flow/Plumber.ts +1011 -457
  56. package/src/flow/utils.ts +162 -84
  57. package/src/interfaces.ts +2 -1
  58. package/src/list/TicketList.ts +4 -1
  59. package/src/live/ContactChat.ts +19 -1
  60. package/src/locales/es.ts +13 -18
  61. package/src/locales/fr.ts +13 -18
  62. package/src/locales/locale-codes.ts +2 -11
  63. package/src/locales/pt.ts +13 -18
  64. package/src/simulator/Simulator.ts +1 -0
  65. package/test/temba-floating-tab.test.ts +4 -6
  66. package/test/temba-flow-collision.test.ts +225 -303
  67. package/test/temba-flow-editor.test.ts +0 -2
  68. package/test/temba-flow-plumber-connections.test.ts +97 -97
  69. package/test/temba-flow-plumber.test.ts +116 -103
package/src/flow/utils.ts CHANGED
@@ -1,6 +1,13 @@
1
1
  import { html } from 'lit-html';
2
2
  import { NamedObject, FlowPosition } from '../store/flow-definition';
3
3
 
4
+ const GRID_SIZE = 20;
5
+
6
+ export function snapToGrid(value: number): number {
7
+ const snapped = Math.round(value / GRID_SIZE) * GRID_SIZE;
8
+ return Math.max(snapped, 0);
9
+ }
10
+
4
11
  /**
5
12
  * Renders a single line item with optional icon
6
13
  */
@@ -264,110 +271,181 @@ export const detectCollisions = (
264
271
  );
265
272
  };
266
273
 
274
+ type Direction = 'down' | 'up' | 'right' | 'left';
275
+
276
+ const DIRECTIONS: Direction[] = ['down', 'up', 'right', 'left'];
277
+
278
+ /**
279
+ * Creates a new NodeBounds at a different position
280
+ */
281
+ const makeBoundsAt = (
282
+ original: NodeBounds,
283
+ left: number,
284
+ top: number
285
+ ): NodeBounds => ({
286
+ ...original,
287
+ left,
288
+ top,
289
+ right: left + original.width,
290
+ bottom: top + original.height
291
+ });
292
+
293
+ /**
294
+ * Computes the minimum position needed to clear all fixed nodes in a given direction.
295
+ * Returns null if the direction is not viable (e.g., would require negative coordinates
296
+ * and still overlap).
297
+ */
298
+ const computeDirectionalClearance = (
299
+ collider: NodeBounds,
300
+ fixedNodes: NodeBounds[],
301
+ direction: Direction
302
+ ): { left: number; top: number } | null => {
303
+ switch (direction) {
304
+ case 'down': {
305
+ const maxBottom = Math.max(...fixedNodes.map((f) => f.bottom));
306
+ const newTop = snapToGrid(maxBottom + MIN_NODE_SPACING);
307
+ return { left: collider.left, top: newTop };
308
+ }
309
+ case 'up': {
310
+ const minTop = Math.min(...fixedNodes.map((f) => f.top));
311
+ const newTop = snapToGrid(minTop - collider.height - MIN_NODE_SPACING);
312
+ if (newTop < 0) return { left: collider.left, top: 0 };
313
+ return { left: collider.left, top: newTop };
314
+ }
315
+ case 'right': {
316
+ const maxRight = Math.max(...fixedNodes.map((f) => f.right));
317
+ const newLeft = snapToGrid(maxRight + MIN_NODE_SPACING);
318
+ return { left: newLeft, top: collider.top };
319
+ }
320
+ case 'left': {
321
+ const minLeft = Math.min(...fixedNodes.map((f) => f.left));
322
+ const newLeft = snapToGrid(minLeft - collider.width - MIN_NODE_SPACING);
323
+ if (newLeft < 0) return { left: 0, top: collider.top };
324
+ return { left: newLeft, top: collider.top };
325
+ }
326
+ }
327
+ };
328
+
267
329
  /**
268
- * Calculates the new positions needed to resolve all collisions
269
- * Nodes are only moved downward, never up, left, or right
270
- * Returns a map of node UUIDs to their new positions
330
+ * Calculates new positions to resolve all collisions using multi-directional reflow.
331
+ *
332
+ * Sacred nodes (the ones just dropped/created) keep their positions. All other
333
+ * colliding nodes are moved in whichever direction requires the least displacement
334
+ * and causes the fewest cascading collisions.
271
335
  */
272
336
  export const calculateReflowPositions = (
273
- movedNodeUuid: string,
274
- movedNodeBounds: NodeBounds,
275
- allBounds: NodeBounds[],
276
- droppedBelowMidpoint: boolean = false
337
+ sacredNodeUuids: string[],
338
+ allBounds: NodeBounds[]
277
339
  ): Map<string, FlowPosition> => {
278
340
  const newPositions = new Map<string, FlowPosition>();
341
+ const sacredSet = new Set(sacredNodeUuids);
279
342
 
280
- // If dropped below midpoint, the moved node should move down instead
281
- if (droppedBelowMidpoint) {
282
- // Find all nodes that collide with the moved node
283
- const collisions = detectCollisions(movedNodeBounds, allBounds);
284
-
285
- if (collisions.length > 0) {
286
- // Find the highest bottom position of all colliding nodes
287
- const maxBottom = Math.max(...collisions.map((b) => b.bottom));
288
-
289
- // Move the dropped node below all colliding nodes
290
- const newTop = maxBottom + MIN_NODE_SPACING;
291
- newPositions.set(movedNodeUuid, {
292
- left: movedNodeBounds.left,
293
- top: newTop
294
- });
295
-
296
- // Update the moved node bounds for further collision checks
297
- movedNodeBounds = {
298
- ...movedNodeBounds,
299
- top: newTop,
300
- bottom: newTop + movedNodeBounds.height
301
- };
302
- }
343
+ // Mutable map of current bounds, updated as collisions are resolved
344
+ const currentBounds = new Map<string, NodeBounds>();
345
+ for (const b of allBounds) {
346
+ currentBounds.set(b.uuid, { ...b });
303
347
  }
304
348
 
305
- // Now check for any remaining collisions and move other nodes down
306
- const processedNodes = new Set<string>();
307
- processedNodes.add(movedNodeUuid);
349
+ // Seed the queue with non-sacred nodes that overlap any sacred node
350
+ const queue: string[] = [];
351
+ const inQueue = new Set<string>();
352
+
353
+ for (const sacredUuid of sacredSet) {
354
+ const sacred = currentBounds.get(sacredUuid);
355
+ if (!sacred) continue;
356
+ for (const [uuid, bounds] of currentBounds) {
357
+ if (sacredSet.has(uuid) || inQueue.has(uuid)) continue;
358
+ if (nodesOverlap(sacred, bounds)) {
359
+ queue.push(uuid);
360
+ inQueue.add(uuid);
361
+ }
362
+ }
363
+ }
308
364
 
309
- // Keep checking for collisions until none remain
310
- let hasCollisions = true;
365
+ const resolved = new Set<string>();
311
366
  let iterations = 0;
312
- const maxIterations = 100; // Prevent infinite loops
367
+ const maxIterations = 200;
313
368
 
314
- while (hasCollisions && iterations < maxIterations) {
315
- hasCollisions = false;
369
+ while (queue.length > 0 && iterations < maxIterations) {
316
370
  iterations++;
371
+ const uuid = queue.shift()!;
317
372
 
318
- // Check all nodes for collisions
319
- for (const bounds of allBounds) {
320
- if (processedNodes.has(bounds.uuid)) {
321
- continue;
322
- }
323
-
324
- // Use original bounds since we skip already processed nodes
325
- const currentBounds = bounds;
373
+ if (resolved.has(uuid)) continue;
326
374
 
327
- // Check if this node collides with the moved node or any already repositioned nodes
328
- let collisionFound = false;
329
- let maxCollisionBottom = 0;
375
+ const collider = currentBounds.get(uuid)!;
330
376
 
331
- // Check against moved node
332
- if (nodesOverlap(currentBounds, movedNodeBounds)) {
333
- collisionFound = true;
334
- maxCollisionBottom = Math.max(
335
- maxCollisionBottom,
336
- movedNodeBounds.bottom
337
- );
377
+ // Find all fixed nodes (sacred + already-resolved) that overlap this node
378
+ const fixedOverlaps: NodeBounds[] = [];
379
+ for (const [otherUuid, otherBounds] of currentBounds) {
380
+ if (otherUuid === uuid) continue;
381
+ if (sacredSet.has(otherUuid) || resolved.has(otherUuid)) {
382
+ if (nodesOverlap(collider, otherBounds)) {
383
+ fixedOverlaps.push(otherBounds);
384
+ }
338
385
  }
386
+ }
339
387
 
340
- // Check against other repositioned nodes
341
- for (const [otherUuid, otherPosition] of newPositions.entries()) {
342
- if (otherUuid === bounds.uuid) continue;
343
-
344
- const otherBounds = allBounds.find((b) => b.uuid === otherUuid);
345
- if (!otherBounds) continue;
346
-
347
- const otherUpdatedBounds = {
348
- ...otherBounds,
349
- top: otherPosition.top,
350
- bottom: otherPosition.top + otherBounds.height
351
- };
352
-
353
- if (nodesOverlap(currentBounds, otherUpdatedBounds)) {
354
- collisionFound = true;
355
- maxCollisionBottom = Math.max(
356
- maxCollisionBottom,
357
- otherUpdatedBounds.bottom
358
- );
388
+ if (fixedOverlaps.length === 0) continue;
389
+
390
+ // Try each direction, pick the one with least disruption
391
+ let bestPos: { left: number; top: number } | null = null;
392
+ let bestScore = Infinity;
393
+
394
+ for (const dir of DIRECTIONS) {
395
+ const candidate = computeDirectionalClearance(
396
+ collider,
397
+ fixedOverlaps,
398
+ dir
399
+ );
400
+ if (!candidate) continue;
401
+
402
+ const candidateBounds = makeBoundsAt(
403
+ collider,
404
+ candidate.left,
405
+ candidate.top
406
+ );
407
+
408
+ // Verify no overlap with any sacred or resolved node
409
+ let stillOverlaps = false;
410
+ let cascadeCount = 0;
411
+ for (const [otherUuid, otherBounds] of currentBounds) {
412
+ if (otherUuid === uuid) continue;
413
+ if (!nodesOverlap(candidateBounds, otherBounds)) continue;
414
+
415
+ if (sacredSet.has(otherUuid) || resolved.has(otherUuid)) {
416
+ stillOverlaps = true;
417
+ break;
359
418
  }
419
+ cascadeCount++;
420
+ }
421
+ if (stillOverlaps) continue;
422
+
423
+ const distance =
424
+ Math.abs(candidate.left - collider.left) +
425
+ Math.abs(candidate.top - collider.top);
426
+ const score = cascadeCount * 10000 + distance;
427
+
428
+ if (score < bestScore) {
429
+ bestScore = score;
430
+ bestPos = candidate;
360
431
  }
432
+ }
361
433
 
362
- if (collisionFound) {
363
- // Move this node down below the collision
364
- const newTop = maxCollisionBottom + MIN_NODE_SPACING;
365
- newPositions.set(bounds.uuid, {
366
- left: bounds.left,
367
- top: newTop
368
- });
369
- hasCollisions = true;
370
- processedNodes.add(bounds.uuid);
434
+ if (bestPos) {
435
+ newPositions.set(uuid, { left: bestPos.left, top: bestPos.top });
436
+ const newBounds = makeBoundsAt(collider, bestPos.left, bestPos.top);
437
+ currentBounds.set(uuid, newBounds);
438
+ resolved.add(uuid);
439
+
440
+ // Enqueue any new cascading collisions
441
+ for (const [otherUuid, otherBounds] of currentBounds) {
442
+ if (otherUuid === uuid) continue;
443
+ if (sacredSet.has(otherUuid) || resolved.has(otherUuid)) continue;
444
+ if (inQueue.has(otherUuid)) continue;
445
+ if (nodesOverlap(newBounds, otherBounds)) {
446
+ queue.push(otherUuid);
447
+ inQueue.add(otherUuid);
448
+ }
371
449
  }
372
450
  }
373
451
  }
package/src/interfaces.ts CHANGED
@@ -303,5 +303,6 @@ export enum CustomEventType {
303
303
  NodeEditRequested = 'temba-node-edit-requested',
304
304
  NodeSaved = 'temba-node-saved',
305
305
  NodeEditCancelled = 'temba-node-edit-cancelled',
306
- FollowSimulation = 'temba-follow-simulation'
306
+ FollowSimulation = 'temba-follow-simulation',
307
+ ContactClicked = 'temba-contact-clicked'
307
308
  }
@@ -13,7 +13,10 @@ export class TicketList extends TembaList {
13
13
  const lastActivity = this.items[0].ticket.last_activity_on;
14
14
  const separator = this.endpoint.includes('?') ? '&' : '?';
15
15
  return (
16
- this.endpoint + separator + 'after=' + new Date(lastActivity).getTime() * 1000
16
+ this.endpoint +
17
+ separator +
18
+ 'after=' +
19
+ new Date(lastActivity).getTime() * 1000
17
20
  );
18
21
  }
19
22
  return this.endpoint;
@@ -271,6 +271,12 @@ export class ContactChat extends ContactStoreElement {
271
271
  @property({ type: Boolean })
272
272
  showInterrupt = false;
273
273
 
274
+ @property({ type: Boolean })
275
+ disableAssign = false;
276
+
277
+ @property({ type: Boolean })
278
+ disableReply = false;
279
+
274
280
  @property({ type: String })
275
281
  avatar = DEFAULT_AVATAR;
276
282
 
@@ -607,6 +613,14 @@ export class ContactChat extends ContactStoreElement {
607
613
  return null;
608
614
  } else {
609
615
  if (!this.currentTicket.closed_on) {
616
+ // hide compose if agent can't reply to unassigned tickets
617
+ if (
618
+ this.disableReply &&
619
+ (!this.currentTicket.assignee ||
620
+ this.currentTicket.assignee.email !== this.agent)
621
+ ) {
622
+ return null;
623
+ }
610
624
  //chatbox for active contacts with an open ticket
611
625
  return this.getCompose();
612
626
  } else {
@@ -676,6 +690,10 @@ export class ContactChat extends ContactStoreElement {
676
690
  }
677
691
 
678
692
  public assignTicket(email: string) {
693
+ if (this.disableAssign) {
694
+ return;
695
+ }
696
+
679
697
  // if its already assigned to use, it's a noop
680
698
  if (
681
699
  (this.currentTicket.assignee &&
@@ -803,7 +821,7 @@ export class ContactChat extends ContactStoreElement {
803
821
  ? [this.currentTicket.assignee]
804
822
  : []}
805
823
  @change=${this.handleAssignmentChanged}
806
- ?disabled=${ticketClosed}
824
+ ?disabled=${ticketClosed || this.disableAssign}
807
825
  ></temba-user-select>
808
826
 
809
827
  <temba-select
package/src/locales/es.ts CHANGED
@@ -1,18 +1,13 @@
1
-
2
- // Do not modify this file by hand!
3
- // Re-generate this file by running lit-localize
4
-
5
-
6
-
7
-
8
- /* eslint-disable no-irregular-whitespace */
9
- /* eslint-disable @typescript-eslint/no-explicit-any */
10
-
11
- export const templates = {
12
- 'scf1453991c986b25': `Tab para completar, enter para seleccionar`,
13
- 's73b4d70c02f4b4e0': `No options`,
14
- 's8f02e3a18ffc083a': `Are not currently in a flow`,
15
- 's638236250662c6b3': `Have sent a message in the last`,
16
- 's4788ee206c4570c7': `Have not started this flow in the last 90 days`,
17
- };
18
-
1
+ // Do not modify this file by hand!
2
+ // Re-generate this file by running lit-localize
3
+
4
+ /* eslint-disable no-irregular-whitespace */
5
+ /* eslint-disable @typescript-eslint/no-explicit-any */
6
+
7
+ export const templates = {
8
+ scf1453991c986b25: `Tab para completar, enter para seleccionar`,
9
+ s73b4d70c02f4b4e0: `No options`,
10
+ s8f02e3a18ffc083a: `Are not currently in a flow`,
11
+ s638236250662c6b3: `Have sent a message in the last`,
12
+ s4788ee206c4570c7: `Have not started this flow in the last 90 days`
13
+ };
package/src/locales/fr.ts CHANGED
@@ -1,18 +1,13 @@
1
-
2
- // Do not modify this file by hand!
3
- // Re-generate this file by running lit-localize
4
-
5
-
6
-
7
-
8
- /* eslint-disable no-irregular-whitespace */
9
- /* eslint-disable @typescript-eslint/no-explicit-any */
10
-
11
- export const templates = {
12
- 's73b4d70c02f4b4e0': `No options`,
13
- 'scf1453991c986b25': `Tab to complete, enter to select`,
14
- 's8f02e3a18ffc083a': `Are not currently in a flow`,
15
- 's638236250662c6b3': `Have sent a message in the last`,
16
- 's4788ee206c4570c7': `Have not started this flow in the last 90 days`,
17
- };
18
-
1
+ // Do not modify this file by hand!
2
+ // Re-generate this file by running lit-localize
3
+
4
+ /* eslint-disable no-irregular-whitespace */
5
+ /* eslint-disable @typescript-eslint/no-explicit-any */
6
+
7
+ export const templates = {
8
+ s73b4d70c02f4b4e0: `No options`,
9
+ scf1453991c986b25: `Tab to complete, enter to select`,
10
+ s8f02e3a18ffc083a: `Are not currently in a flow`,
11
+ s638236250662c6b3: `Have sent a message in the last`,
12
+ s4788ee206c4570c7: `Have not started this flow in the last 90 days`
13
+ };
@@ -10,18 +10,9 @@ export const sourceLocale = `en`;
10
10
  * The other locale codes that this application is localized into. Sorted
11
11
  * lexicographically.
12
12
  */
13
- export const targetLocales = [
14
- `es`,
15
- `fr`,
16
- `pt`,
17
- ] as const;
13
+ export const targetLocales = [`es`, `fr`, `pt`] as const;
18
14
 
19
15
  /**
20
16
  * All valid project locale codes. Sorted lexicographically.
21
17
  */
22
- export const allLocales = [
23
- `en`,
24
- `es`,
25
- `fr`,
26
- `pt`,
27
- ] as const;
18
+ export const allLocales = [`en`, `es`, `fr`, `pt`] as const;
package/src/locales/pt.ts CHANGED
@@ -1,18 +1,13 @@
1
-
2
- // Do not modify this file by hand!
3
- // Re-generate this file by running lit-localize
4
-
5
-
6
-
7
-
8
- /* eslint-disable no-irregular-whitespace */
9
- /* eslint-disable @typescript-eslint/no-explicit-any */
10
-
11
- export const templates = {
12
- 's73b4d70c02f4b4e0': `No options`,
13
- 'scf1453991c986b25': `Tab to complete, enter to select`,
14
- 's8f02e3a18ffc083a': `Are not currently in a flow`,
15
- 's638236250662c6b3': `Have sent a message in the last`,
16
- 's4788ee206c4570c7': `Have not started this flow in the last 90 days`,
17
- };
18
-
1
+ // Do not modify this file by hand!
2
+ // Re-generate this file by running lit-localize
3
+
4
+ /* eslint-disable no-irregular-whitespace */
5
+ /* eslint-disable @typescript-eslint/no-explicit-any */
6
+
7
+ export const templates = {
8
+ s73b4d70c02f4b4e0: `No options`,
9
+ scf1453991c986b25: `Tab to complete, enter to select`,
10
+ s8f02e3a18ffc083a: `Are not currently in a flow`,
11
+ s638236250662c6b3: `Have sent a message in the last`,
12
+ s4788ee206c4570c7: `Have not started this flow in the last 90 days`
13
+ };
@@ -1934,6 +1934,7 @@ export class Simulator extends RapidElement {
1934
1934
  icon="simulator"
1935
1935
  label="Phone Simulator"
1936
1936
  color="#10b981"
1937
+ order="3"
1937
1938
  .hidden=${this.isVisible}
1938
1939
  @temba-button-clicked=${this.handleShow}
1939
1940
  ></temba-floating-tab>
@@ -7,15 +7,13 @@ describe('temba-floating-tab', () => {
7
7
  const tab = (await getComponent('temba-floating-tab', {
8
8
  icon: 'phone',
9
9
  label: 'Phone Simulator',
10
- color: '#10b981',
11
- top: 100
10
+ color: '#10b981'
12
11
  })) as FloatingTab;
13
12
 
14
13
  assert.instanceOf(tab, FloatingTab);
15
14
  expect(tab.icon).to.equal('phone');
16
15
  expect(tab.label).to.equal('Phone Simulator');
17
16
  expect(tab.color).to.equal('#10b981');
18
- expect(tab.top).to.equal(100);
19
17
  expect(tab.hidden).to.equal(false);
20
18
 
21
19
  await assertScreenshot('floating-tab/default', getClip(tab));
@@ -75,21 +73,21 @@ describe('temba-floating-tab', () => {
75
73
  icon: 'phone',
76
74
  label: 'Phone',
77
75
  color: '#10b981',
78
- top: 100
76
+ order: 1
79
77
  })) as FloatingTab;
80
78
 
81
79
  const tab2 = (await getComponent('temba-floating-tab', {
82
80
  icon: 'globe',
83
81
  label: 'Translation',
84
82
  color: '#6b7280',
85
- top: 200
83
+ order: 2
86
84
  })) as FloatingTab;
87
85
 
88
86
  const tab3 = (await getComponent('temba-floating-tab', {
89
87
  icon: 'clock',
90
88
  label: 'History',
91
89
  color: '#8b5cf6',
92
- top: 300
90
+ order: 3
93
91
  })) as FloatingTab;
94
92
 
95
93
  await assertScreenshot('floating-tab/green', getClip(tab1));