@limetech/lime-crm-building-blocks 1.118.0 → 1.119.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 (207) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cjs/lime-crm-building-blocks.cjs.js +1 -1
  3. package/dist/cjs/limebb-alert-dialog.cjs.entry.js +1 -1
  4. package/dist/cjs/limebb-browser.cjs.entry.js +2 -2
  5. package/dist/cjs/limebb-chat-icon-list_3.cjs.entry.js +4 -4
  6. package/dist/cjs/limebb-chat-list.cjs.entry.js +336 -68
  7. package/dist/cjs/limebb-composer-toolbar.cjs.entry.js +2 -2
  8. package/dist/cjs/limebb-currency-picker.cjs.entry.js +1 -1
  9. package/dist/cjs/limebb-dashboard-widget.cjs.entry.js +1 -1
  10. package/dist/cjs/limebb-data-cells.cjs.entry.js +22 -1
  11. package/dist/cjs/limebb-date-picker.cjs.entry.js +1 -1
  12. package/dist/cjs/limebb-date-range.cjs.entry.js +1 -1
  13. package/dist/cjs/limebb-document-chips.cjs.entry.js +1 -1
  14. package/dist/cjs/limebb-document-item.cjs.entry.js +2 -2
  15. package/dist/cjs/limebb-document-picker.cjs.entry.js +1 -1
  16. package/dist/cjs/limebb-feed-item-thumbnail-file-info.cjs.entry.js +1 -1
  17. package/dist/cjs/limebb-feed-timeline-item.cjs.entry.js +1 -1
  18. package/dist/cjs/limebb-feed.cjs.entry.js +1 -1
  19. package/dist/cjs/limebb-icon-picker.cjs.entry.js +1 -1
  20. package/dist/cjs/limebb-info-tile-format.cjs.entry.js +1 -1
  21. package/dist/cjs/limebb-info-tile.cjs.entry.js +1 -1
  22. package/dist/cjs/limebb-kanban-group.cjs.entry.js +1 -1
  23. package/dist/cjs/limebb-kanban-item.cjs.entry.js +1 -1
  24. package/dist/cjs/limebb-lime-query-builder.cjs.entry.js +1 -1
  25. package/dist/cjs/limebb-lime-query-filter-builder_3.cjs.entry.js +2 -2
  26. package/dist/cjs/limebb-lime-query-filter-comparison_2.cjs.entry.js +1 -1
  27. package/dist/cjs/limebb-lime-query-filter-group_3.cjs.entry.js +3 -3
  28. package/dist/cjs/limebb-lime-query-order-by-item.cjs.entry.js +2 -2
  29. package/dist/cjs/limebb-lime-query-response-format-builder.cjs.entry.js +1 -1
  30. package/dist/cjs/limebb-lime-query-response-format-editor_2.cjs.entry.js +1 -1
  31. package/dist/cjs/limebb-live-docs-info.cjs.entry.js +2 -2
  32. package/dist/cjs/limebb-locale-picker.cjs.entry.js +1 -1
  33. package/dist/cjs/limebb-mention-group-counter.cjs.entry.js +2 -2
  34. package/dist/cjs/limebb-navigation-button_2.cjs.entry.js +2 -2
  35. package/dist/cjs/limebb-notification-item.cjs.entry.js +1 -1
  36. package/dist/cjs/limebb-percentage-visualizer.cjs.entry.js +2 -2
  37. package/dist/cjs/limebb-text-editor.cjs.entry.js +1 -1
  38. package/dist/cjs/limebb-trend-indicator.cjs.entry.js +1 -1
  39. package/dist/cjs/loader.cjs.js +1 -1
  40. package/dist/collection/components/alert-dialog/alert-dialog.js +1 -1
  41. package/dist/collection/components/browser/browser.js +2 -2
  42. package/dist/collection/components/chat-list/chat-icon-list/chat-icon-list.js +1 -1
  43. package/dist/collection/components/chat-list/chat-item/chat-item.js +2 -2
  44. package/dist/collection/components/chat-list/chat-list.css +10 -10
  45. package/dist/collection/components/chat-list/chat-list.js +93 -25
  46. package/dist/collection/components/chat-list/process-chat-items.js +111 -45
  47. package/dist/collection/components/chat-list/scroll-anchor.js +93 -0
  48. package/dist/collection/components/chat-list/typing-indicator/typing-indicator.js +1 -1
  49. package/dist/collection/components/chat-list/typing-indicator-scroll.js +41 -0
  50. package/dist/collection/components/composer-toolbar/composer-toolbar.js +2 -2
  51. package/dist/collection/components/currency-picker/currency-picker.js +1 -1
  52. package/dist/collection/components/dashboard-widget/dashboard-widget.js +1 -1
  53. package/dist/collection/components/data-cells/data-cells.js +22 -1
  54. package/dist/collection/components/date-picker/date-picker.js +1 -1
  55. package/dist/collection/components/date-range/date-range.js +1 -1
  56. package/dist/collection/components/document-chips/document-chips.js +1 -1
  57. package/dist/collection/components/document-picker/document-item/document-item.js +2 -2
  58. package/dist/collection/components/document-picker/document-picker.js +1 -1
  59. package/dist/collection/components/feed/feed-item/feed-timeline-item.js +1 -1
  60. package/dist/collection/components/feed/feed-item-thumbnail-file-info/feed-item-thumbnail-file-info.js +1 -1
  61. package/dist/collection/components/feed/feed.js +1 -1
  62. package/dist/collection/components/icon-picker/icon-picker.js +1 -1
  63. package/dist/collection/components/info-tile/format/config/info-tile-format.js +1 -1
  64. package/dist/collection/components/info-tile/info-tile.js +1 -1
  65. package/dist/collection/components/kanban/kanban-group/kanban-group.js +1 -1
  66. package/dist/collection/components/kanban/kanban-item/kanban-item.js +1 -1
  67. package/dist/collection/components/lime-query-builder/expressions/lime-query-filter-comparison.js +1 -1
  68. package/dist/collection/components/lime-query-builder/expressions/lime-query-filter-group.js +2 -2
  69. package/dist/collection/components/lime-query-builder/expressions/lime-query-filter-not.js +1 -1
  70. package/dist/collection/components/lime-query-builder/lime-query-builder.js +1 -1
  71. package/dist/collection/components/lime-query-builder/lime-query-response-format-builder.js +1 -1
  72. package/dist/collection/components/lime-query-builder/limetype-field/limetype-field.js +1 -1
  73. package/dist/collection/components/lime-query-builder/order-by/order-by-editor.js +1 -1
  74. package/dist/collection/components/lime-query-builder/order-by/order-by-item.js +2 -2
  75. package/dist/collection/components/lime-query-builder/response-format/response-format-item.js +1 -1
  76. package/dist/collection/components/limeobject/file-viewer/live-docs-info.js +2 -2
  77. package/dist/collection/components/locale-picker/locale-picker.js +1 -1
  78. package/dist/collection/components/notification-list/notification-item/notification-item.js +1 -1
  79. package/dist/collection/components/percentage-visualizer/percentage-visualizer.js +2 -2
  80. package/dist/collection/components/summary-popover/summary-popover.js +2 -2
  81. package/dist/collection/components/text-editor/mention-group-counter.js +2 -2
  82. package/dist/collection/components/text-editor/text-editor.js +1 -1
  83. package/dist/collection/components/trend-indicator/trend-indicator.js +1 -1
  84. package/dist/components/chat-icon-list.js +1 -1
  85. package/dist/components/chat-item.js +1 -1
  86. package/dist/components/currency-picker.js +1 -1
  87. package/dist/components/date-picker.js +1 -1
  88. package/dist/components/document-item.js +1 -1
  89. package/dist/components/feed-item-thumbnail-file-info.js +1 -1
  90. package/dist/components/feed-timeline-item.js +1 -1
  91. package/dist/components/kanban-group.js +1 -1
  92. package/dist/components/kanban-item.js +1 -1
  93. package/dist/components/lime-query-filter-comparison.js +1 -1
  94. package/dist/components/lime-query-filter-expression.js +1 -1
  95. package/dist/components/limebb-alert-dialog.js +1 -1
  96. package/dist/components/limebb-browser.js +1 -1
  97. package/dist/components/limebb-chat-list.js +1 -1
  98. package/dist/components/limebb-composer-toolbar.js +1 -1
  99. package/dist/components/limebb-dashboard-widget.js +1 -1
  100. package/dist/components/limebb-data-cells.js +1 -1
  101. package/dist/components/limebb-date-range.js +1 -1
  102. package/dist/components/limebb-document-chips.js +1 -1
  103. package/dist/components/limebb-document-picker.js +1 -1
  104. package/dist/components/limebb-feed.js +1 -1
  105. package/dist/components/limebb-icon-picker.js +1 -1
  106. package/dist/components/limebb-info-tile-format.js +1 -1
  107. package/dist/components/limebb-info-tile.js +1 -1
  108. package/dist/components/limebb-lime-query-builder.js +1 -1
  109. package/dist/components/limebb-lime-query-response-format-builder.js +1 -1
  110. package/dist/components/limebb-locale-picker.js +1 -1
  111. package/dist/components/limebb-mention-group-counter.js +1 -1
  112. package/dist/components/limebb-text-editor.js +1 -1
  113. package/dist/components/limebb-trend-indicator.js +1 -1
  114. package/dist/components/limetype-field.js +1 -1
  115. package/dist/components/live-docs-info.js +1 -1
  116. package/dist/components/notification-item.js +1 -1
  117. package/dist/components/order-by-editor.js +1 -1
  118. package/dist/components/order-by-item.js +1 -1
  119. package/dist/components/percentage-visualizer.js +1 -1
  120. package/dist/components/response-format-item.js +1 -1
  121. package/dist/components/summary-popover.js +1 -1
  122. package/dist/components/typing-indicator.js +1 -1
  123. package/dist/esm/lime-crm-building-blocks.js +1 -1
  124. package/dist/esm/limebb-alert-dialog.entry.js +1 -1
  125. package/dist/esm/limebb-browser.entry.js +2 -2
  126. package/dist/esm/limebb-chat-icon-list_3.entry.js +4 -4
  127. package/dist/esm/limebb-chat-list.entry.js +337 -69
  128. package/dist/esm/limebb-composer-toolbar.entry.js +2 -2
  129. package/dist/esm/limebb-currency-picker.entry.js +1 -1
  130. package/dist/esm/limebb-dashboard-widget.entry.js +1 -1
  131. package/dist/esm/limebb-data-cells.entry.js +22 -1
  132. package/dist/esm/limebb-date-picker.entry.js +1 -1
  133. package/dist/esm/limebb-date-range.entry.js +1 -1
  134. package/dist/esm/limebb-document-chips.entry.js +1 -1
  135. package/dist/esm/limebb-document-item.entry.js +2 -2
  136. package/dist/esm/limebb-document-picker.entry.js +1 -1
  137. package/dist/esm/limebb-feed-item-thumbnail-file-info.entry.js +1 -1
  138. package/dist/esm/limebb-feed-timeline-item.entry.js +1 -1
  139. package/dist/esm/limebb-feed.entry.js +1 -1
  140. package/dist/esm/limebb-icon-picker.entry.js +1 -1
  141. package/dist/esm/limebb-info-tile-format.entry.js +1 -1
  142. package/dist/esm/limebb-info-tile.entry.js +1 -1
  143. package/dist/esm/limebb-kanban-group.entry.js +1 -1
  144. package/dist/esm/limebb-kanban-item.entry.js +1 -1
  145. package/dist/esm/limebb-lime-query-builder.entry.js +1 -1
  146. package/dist/esm/limebb-lime-query-filter-builder_3.entry.js +2 -2
  147. package/dist/esm/limebb-lime-query-filter-comparison_2.entry.js +1 -1
  148. package/dist/esm/limebb-lime-query-filter-group_3.entry.js +3 -3
  149. package/dist/esm/limebb-lime-query-order-by-item.entry.js +2 -2
  150. package/dist/esm/limebb-lime-query-response-format-builder.entry.js +1 -1
  151. package/dist/esm/limebb-lime-query-response-format-editor_2.entry.js +1 -1
  152. package/dist/esm/limebb-live-docs-info.entry.js +2 -2
  153. package/dist/esm/limebb-locale-picker.entry.js +1 -1
  154. package/dist/esm/limebb-mention-group-counter.entry.js +2 -2
  155. package/dist/esm/limebb-navigation-button_2.entry.js +2 -2
  156. package/dist/esm/limebb-notification-item.entry.js +1 -1
  157. package/dist/esm/limebb-percentage-visualizer.entry.js +2 -2
  158. package/dist/esm/limebb-text-editor.entry.js +1 -1
  159. package/dist/esm/limebb-trend-indicator.entry.js +1 -1
  160. package/dist/esm/loader.js +1 -1
  161. package/dist/lime-crm-building-blocks/lime-crm-building-blocks.esm.js +1 -1
  162. package/dist/lime-crm-building-blocks/{p-685dd8a1.entry.js → p-02a3530f.entry.js} +1 -1
  163. package/dist/lime-crm-building-blocks/{p-52fb2a66.entry.js → p-16ad65dc.entry.js} +1 -1
  164. package/dist/lime-crm-building-blocks/{p-6cb2d9dd.entry.js → p-2079ae59.entry.js} +1 -1
  165. package/dist/lime-crm-building-blocks/{p-c3b13800.entry.js → p-2aae332a.entry.js} +1 -1
  166. package/dist/lime-crm-building-blocks/{p-b3ee6683.entry.js → p-4edfa72a.entry.js} +1 -1
  167. package/dist/lime-crm-building-blocks/{p-b36ad4e8.entry.js → p-5058bc60.entry.js} +1 -1
  168. package/dist/lime-crm-building-blocks/{p-8477cb5d.entry.js → p-52fd0169.entry.js} +1 -1
  169. package/dist/lime-crm-building-blocks/{p-21ec697f.entry.js → p-54ae7814.entry.js} +1 -1
  170. package/dist/lime-crm-building-blocks/{p-35a51259.entry.js → p-54bb577b.entry.js} +1 -1
  171. package/dist/lime-crm-building-blocks/p-59b992b3.entry.js +1 -0
  172. package/dist/lime-crm-building-blocks/{p-1a61674f.entry.js → p-5ef41ad0.entry.js} +1 -1
  173. package/dist/lime-crm-building-blocks/{p-f375d69a.entry.js → p-640c41a3.entry.js} +1 -1
  174. package/dist/lime-crm-building-blocks/{p-528f3635.entry.js → p-65485470.entry.js} +1 -1
  175. package/dist/lime-crm-building-blocks/{p-4ddd75bc.entry.js → p-655968ce.entry.js} +1 -1
  176. package/dist/lime-crm-building-blocks/{p-8a05a0e5.entry.js → p-67551f9d.entry.js} +1 -1
  177. package/dist/lime-crm-building-blocks/{p-152edb10.entry.js → p-750dbb18.entry.js} +1 -1
  178. package/dist/lime-crm-building-blocks/{p-bac3d599.entry.js → p-83e93ebd.entry.js} +1 -1
  179. package/dist/lime-crm-building-blocks/{p-fa41a9e6.entry.js → p-8a820ede.entry.js} +1 -1
  180. package/dist/lime-crm-building-blocks/p-8da95b3a.entry.js +1 -0
  181. package/dist/lime-crm-building-blocks/{p-943c8589.entry.js → p-999c1628.entry.js} +1 -1
  182. package/dist/lime-crm-building-blocks/{p-62437a1d.entry.js → p-aea85d91.entry.js} +1 -1
  183. package/dist/lime-crm-building-blocks/{p-9ae55a96.entry.js → p-b17327da.entry.js} +1 -1
  184. package/dist/lime-crm-building-blocks/{p-71191041.entry.js → p-b3db6a41.entry.js} +1 -1
  185. package/dist/lime-crm-building-blocks/{p-6fdee70a.entry.js → p-b6895996.entry.js} +1 -1
  186. package/dist/lime-crm-building-blocks/{p-cff9bccd.entry.js → p-b92eedb5.entry.js} +1 -1
  187. package/dist/lime-crm-building-blocks/{p-aaeecca6.entry.js → p-c019f7f2.entry.js} +1 -1
  188. package/dist/lime-crm-building-blocks/{p-fdf269f7.entry.js → p-c3648986.entry.js} +1 -1
  189. package/dist/lime-crm-building-blocks/{p-ebe6040f.entry.js → p-cc07285b.entry.js} +1 -1
  190. package/dist/lime-crm-building-blocks/{p-ffb847d0.entry.js → p-cee4e6a6.entry.js} +1 -1
  191. package/dist/lime-crm-building-blocks/{p-18fc1a06.entry.js → p-d153e06d.entry.js} +1 -1
  192. package/dist/lime-crm-building-blocks/{p-be8dc8ae.entry.js → p-e33feb37.entry.js} +1 -1
  193. package/dist/lime-crm-building-blocks/{p-fcedbc77.entry.js → p-e4005988.entry.js} +1 -1
  194. package/dist/lime-crm-building-blocks/{p-a48a6ff8.entry.js → p-e8a0bbb8.entry.js} +1 -1
  195. package/dist/lime-crm-building-blocks/{p-eec0a0c8.entry.js → p-ebeddc29.entry.js} +1 -1
  196. package/dist/lime-crm-building-blocks/p-ece5c8ad.entry.js +1 -0
  197. package/dist/lime-crm-building-blocks/{p-5a1f0a2b.entry.js → p-fa808d3a.entry.js} +1 -1
  198. package/dist/types/components/chat-list/chat-list.d.ts +13 -3
  199. package/dist/types/components/chat-list/process-chat-items.d.ts +20 -4
  200. package/dist/types/components/chat-list/scroll-anchor.d.ts +46 -0
  201. package/dist/types/components/chat-list/typing-indicator-scroll.d.ts +18 -0
  202. package/dist/types/components/data-cells/data-cells.d.ts +1 -0
  203. package/dist/types/components.d.ts +4 -4
  204. package/package.json +1 -1
  205. package/dist/lime-crm-building-blocks/p-5fc88ece.entry.js +0 -1
  206. package/dist/lime-crm-building-blocks/p-b10faec5.entry.js +0 -1
  207. package/dist/lime-crm-building-blocks/p-f23ea0e7.entry.js +0 -1
@@ -15,7 +15,7 @@ function getProcessedItemTimestamp(item) {
15
15
  /**
16
16
  * Processes a list of chat list inputs by adding date headers and a "new items" indicator.
17
17
  *
18
- * @param items - The list of items to process. Pre-sorted by `timestamp` in descending order.
18
+ * @param items - The list of items to process. Pre-sorted by `timestamp` in ascending order.
19
19
  * @param dateTimeFormatter - A service used to format dates for the date headers.
20
20
  * @param lastVisitedTimestamp - The timestamp of the user's last visit. Used to position the
21
21
  * "new items" indicator.
@@ -29,16 +29,18 @@ function processChatItems(items, dateTimeFormatter, lastVisitedTimestamp) {
29
29
  processedItems: [],
30
30
  lastVisitedTimestamp: lastVisitedTimestamp,
31
31
  dateTimeFormatter: dateTimeFormatter,
32
- newItemIndicatorInserted: false,
33
- lastProcessedDate: null,
34
- hasUnseenNotifications: items.some((item) => lastVisitedTimestamp &&
35
- new Date(item.timestamp) > lastVisitedTimestamp),
36
32
  };
37
- // Add processors here, in the order they should be executed
33
+ // Add processors here, in the order they should be executed.
34
+ // `insertNewItemIndicator` runs after `insertDateGroups` so the
35
+ // indicator can be placed at top level when the seen/unseen
36
+ // boundary falls between two groups, and only inside a group when
37
+ // the boundary falls within a single day. That placement makes
38
+ // `reverseProcessedItems` a pure two-level reverse — no group-
39
+ // boundary hoisting needed.
38
40
  const processors = [
39
41
  insertNotifications,
40
- insertNewItemIndicator,
41
42
  insertDateGroups,
43
+ insertNewItemIndicator,
42
44
  ];
43
45
  const finalContext = createPipeline(initialContext, processors);
44
46
  return finalContext.processedItems;
@@ -47,6 +49,32 @@ function createPipeline(initialContext, processors) {
47
49
  // eslint-disable-next-line unicorn/no-array-reduce
48
50
  return processors.reduce((context, processor) => processor(context), initialContext);
49
51
  }
52
+ /**
53
+ * Reverses the chronological order of a processed-items array.
54
+ *
55
+ * Pure two-level reverse: the top-level array is reversed, and each
56
+ * group's items are reversed. `insertNewItemIndicator` places the
57
+ * indicator at top level when the seen/unseen boundary falls
58
+ * between groups, and only nests it inside a group when the
59
+ * boundary falls within a single day — so reversal never needs to
60
+ * move an indicator across levels to preserve its separator role.
61
+ *
62
+ * Used by `limebb-chat-list` when rendering in `newest-on-top`
63
+ * layout, so that DOM order continues to match visual order
64
+ * (newest-first in that layout), keeping copy/paste, screen-reader,
65
+ * and tab order aligned with what the user sees.
66
+ *
67
+ * @param items - processed items in ascending-by-timestamp order
68
+ * @returns processed items in descending-by-timestamp order
69
+ */
70
+ function reverseProcessedItems(items) {
71
+ return items.toReversed().map((item) => {
72
+ if (item.type !== 'group') {
73
+ return item;
74
+ }
75
+ return Object.assign(Object.assign({}, item), { items: item.items.toReversed() });
76
+ });
77
+ }
50
78
  /**
51
79
  *
52
80
  * @param context
@@ -67,61 +95,99 @@ function insertNotifications(context) {
67
95
  * @param context
68
96
  */
69
97
  function insertNewItemIndicator(context) {
70
- const processedWithIndicator = [];
71
- let newItemIndicatorInserted = context.newItemIndicatorInserted;
72
- for (const item of context.processedItems) {
73
- const timestamp = getProcessedItemTimestamp(item);
74
- if (timestamp !== undefined &&
75
- context.lastVisitedTimestamp &&
76
- !newItemIndicatorInserted &&
77
- new Date(timestamp) <= context.lastVisitedTimestamp &&
78
- context.hasUnseenNotifications) {
79
- processedWithIndicator.push({ type: 'newItemIndicator' });
80
- newItemIndicatorInserted = true;
98
+ if (!context.lastVisitedTimestamp) {
99
+ return context;
100
+ }
101
+ const groups = context.processedItems;
102
+ const lastVisited = context.lastVisitedTimestamp;
103
+ // Walk groups newest → oldest. Items and groups are sorted
104
+ // ascending, so at most one group straddles the seen/unseen
105
+ // boundary; the rest are entirely seen or entirely unseen. In an
106
+ // active chat where the newest item is already seen, the loop
107
+ // exits in O(1).
108
+ for (let g = groups.length - 1; g >= 0; g--) {
109
+ const group = groups[g];
110
+ if (group.type !== 'group') {
111
+ continue;
81
112
  }
82
- processedWithIndicator.push(item);
113
+ const seenIndex = findNewestSeenIndex(group.items, lastVisited);
114
+ if (seenIndex === -1) {
115
+ continue;
116
+ }
117
+ return placeIndicatorAtBoundary(context, groups, g, group, seenIndex);
83
118
  }
84
- context.processedItems = processedWithIndicator;
85
- context.newItemIndicatorInserted = newItemIndicatorInserted;
119
+ // No seen item found — everything is unseen, no boundary to mark.
86
120
  return context;
87
121
  }
122
+ function findNewestSeenIndex(items, lastVisited) {
123
+ for (let i = items.length - 1; i >= 0; i--) {
124
+ const timestamp = getProcessedItemTimestamp(items[i]);
125
+ if (timestamp === undefined) {
126
+ continue;
127
+ }
128
+ if (new Date(timestamp) <= lastVisited) {
129
+ return i;
130
+ }
131
+ }
132
+ return -1;
133
+ }
134
+ function placeIndicatorAtBoundary(context, groups, groupIndex, group, seenIndex) {
135
+ const isLastItemInGroup = seenIndex === group.items.length - 1;
136
+ const isLastGroup = groupIndex === groups.length - 1;
137
+ // Newest seen item is the very last item overall → everything is
138
+ // seen, nothing to mark.
139
+ if (isLastItemInGroup && isLastGroup) {
140
+ return context;
141
+ }
142
+ // Seen item is last in its group → boundary is between groups.
143
+ // Place the indicator at top level so reversal stays a pure
144
+ // two-level reverse.
145
+ if (isLastItemInGroup) {
146
+ return Object.assign(Object.assign({}, context), { processedItems: [
147
+ ...groups.slice(0, groupIndex + 1),
148
+ { type: 'newItemIndicator' },
149
+ ...groups.slice(groupIndex + 1),
150
+ ] });
151
+ }
152
+ // Boundary falls inside a single day — indicator stays nested.
153
+ const newGroupItems = [
154
+ ...group.items.slice(0, seenIndex + 1),
155
+ { type: 'newItemIndicator' },
156
+ ...group.items.slice(seenIndex + 1),
157
+ ];
158
+ return Object.assign(Object.assign({}, context), { processedItems: [
159
+ ...groups.slice(0, groupIndex),
160
+ Object.assign(Object.assign({}, group), { items: newGroupItems }),
161
+ ...groups.slice(groupIndex + 1),
162
+ ] });
163
+ }
88
164
  /**
89
165
  *
90
166
  * @param context
91
167
  */
92
- // eslint-disable-next-line sonarjs/cognitive-complexity
93
168
  function insertDateGroups(context) {
94
169
  const newProcessedItems = [];
95
170
  let currentGroup = null;
96
171
  let lastProcessedDate = null;
97
172
  for (const item of context.processedItems) {
98
- if (item.type === 'chat' || item.type === 'iconList') {
99
- const currentTimestamp = new Date(item.item.timestamp);
100
- const currentDate = context.dateTimeFormatter.format(currentTimestamp, 'date');
101
- if (currentDate !== lastProcessedDate) {
102
- if (currentGroup) {
103
- newProcessedItems.push(currentGroup);
104
- }
105
- currentGroup = {
106
- type: 'group',
107
- groupType: 'dateGroup',
108
- heading: currentDate,
109
- items: [],
110
- };
111
- lastProcessedDate = currentDate;
112
- }
113
- if (currentGroup) {
114
- currentGroup.items.push(item);
115
- }
173
+ if (item.type !== 'chat' && item.type !== 'iconList') {
174
+ continue;
116
175
  }
117
- else if (item.type === 'newItemIndicator') {
176
+ const currentDate = context.dateTimeFormatter.format(new Date(item.item.timestamp), 'date');
177
+ if (currentDate !== lastProcessedDate) {
118
178
  if (currentGroup) {
119
- currentGroup.items.push(item);
120
- }
121
- else {
122
- newProcessedItems.push(item);
179
+ newProcessedItems.push(currentGroup);
123
180
  }
181
+ currentGroup = {
182
+ type: 'group',
183
+ groupType: 'dateGroup',
184
+ heading: currentDate,
185
+ items: [item],
186
+ };
187
+ lastProcessedDate = currentDate;
188
+ continue;
124
189
  }
190
+ currentGroup === null || currentGroup === void 0 ? void 0 : currentGroup.items.push(item);
125
191
  }
126
192
  if (currentGroup) {
127
193
  newProcessedItems.push(currentGroup);
@@ -129,14 +195,153 @@ function insertDateGroups(context) {
129
195
  return Object.assign(Object.assign({}, context), { processedItems: newProcessedItems });
130
196
  }
131
197
 
132
- const chatListCss = () => `@charset "UTF-8";:host(limebb-chat-list){--limel-top-edge-fade-height:0.75rem;isolation:isolate;--limel-overflow-mask-vertical:linear-gradient( to bottom, transparent 0%, black calc(0% + var(--limel-top-edge-fade-height, 1rem)), black calc(100% - var(--limel-bottom-edge-fade-height, 1rem)), transparent 100% );-webkit-mask-image:var(--limel-overflow-mask-vertical);mask-image:var(--limel-overflow-mask-vertical);padding-top:var(--limel-top-edge-fade-height, 1rem);padding-bottom:var(--limel-bottom-edge-fade-height, 1rem);box-sizing:border-box;display:flex;gap:0.5rem;height:100%;width:100%;min-width:0;min-height:0;padding:var(--limel-top-edge-fade-height) 0.5rem;overflow:hidden auto}:host(limebb-chat-list.is-empty){padding:0}:host(limebb-chat-list[order=oldest-on-top]){flex-direction:column-reverse}:host(limebb-chat-list[order=newest-on-top]){flex-direction:column}*{box-sizing:border-box}limel-spinner{align-self:center;justify-self:center;margin:auto}.date-group{isolation:isolate;display:flex;flex-direction:inherit;gap:0.5rem}.date-heading{position:sticky;z-index:1;top:0.25rem;display:flex;border-radius:9rem;padding:0.25rem 0.5rem;margin:0 auto;width:fit-content;font-size:0.75rem;line-height:1;color:rgb(var(--contrast-900));backdrop-filter:blur(0.5rem);-webkit-backdrop-filter:blur(0.5rem);transition:color 0.2s ease, border-color 0.4s ease}.date-heading:hover{color:rgb(var(--contrast-1000));border-color:rgb(var(--contrast-500))}.date-heading:hover:before{opacity:1}.date-heading:before{transition:opacity 0.2s ease;content:"";position:absolute;z-index:-1;inset:0;opacity:0.6;border-radius:inherit;background-color:rgb(var(--contrast-100))}.new-items-indicator{position:relative;isolation:isolate;display:flex;align-items:center;justify-content:flex-end;margin:0.25rem 0}.new-items-indicator hr{border:none;position:absolute;width:100%;height:1px;background-color:rgb(var(--color-red-lighter))}.new-items-indicator h3{z-index:1;padding:0 0.5rem;border-radius:1rem;margin:0 1rem;font-size:small;line-height:0.75rem;text-transform:lowercase;color:rgb(var(--color-red-default));background-color:rgb(var(--contrast-100))}.typing-indicator-height-animation{--limebb-chat-typing-opacity-transition-speed:0.1s;--limebb-chat-typing-opacity-transition-delay:0s;--limebb-chat-typing-grid-template-rows-transition-speed:0.3s;position:sticky;top:0;z-index:1;transition:grid-template-rows var(--limebb-chat-typing-grid-template-rows-transition-speed) ease;display:grid;grid-template-rows:0}.typing-indicator-height-animation div{transition:opacity var(--limebb-chat-typing-opacity-transition-speed) ease var(--limebb-chat-typing-opacity-transition-delay);overflow:hidden;opacity:0}:host(limebb-chat-list[is-typing-indicator-visible]) .typing-indicator-height-animation{--limebb-chat-typing-opacity-transition-speed:0.4s;--limebb-chat-typing-opacity-transition-delay:0.3s;--limebb-chat-typing-grid-template-rows-transition-speed:0.46s;grid-template-rows:1.5rem}:host(limebb-chat-list[is-typing-indicator-visible]) .typing-indicator-height-animation div{opacity:1}.date-group limebb-chat-item{--limebb-promoted-action-bar-grid-template-rows:0fr}:host(limebb-chat-list[order=oldest-on-top]) .date-group:first-of-type limebb-chat-item:first-of-type{--limebb-promoted-action-bar-grid-template-rows:1fr}:host(limebb-chat-list[order=newest-on-top]) .date-group:first-of-type limebb-chat-item:first-of-type{--limebb-promoted-action-bar-grid-template-rows:1fr}`;
198
+ // "Near enough the newest edge to count as sticky." 50px is roughly
199
+ // two message-bubble margins; a user who is within that distance
200
+ // clearly wasn't scrolling back through history, so we treat their
201
+ // position as "stuck to newest" and re-pin on each items update.
202
+ const NEAR_NEWEST_THRESHOLD_PX = 50;
203
+ /**
204
+ * Keeps a scroll container stuck to its "newest" edge as items are
205
+ * appended, so long as the user hasn't scrolled away.
206
+ *
207
+ * We use imperative scrolling rather than `flex-direction: column-reverse`
208
+ * so DOM order matches visual order (copy/paste, screen readers, tab
209
+ * order all align with what the user sees). CSS `overflow-anchor: auto`
210
+ * — the default and left enabled here — solves a different problem:
211
+ * preserving scroll position when content is inserted *above* the
212
+ * viewport. It doesn't stick the scroll to the newest edge when items
213
+ * arrive below the viewport, which is what chat typically needs; that
214
+ * requires the imperative approach in this class.
215
+ *
216
+ * Lifecycle:
217
+ * - `beforeRender()` in `componentWillRender` — snapshot whether the
218
+ * user is near the newest edge, using the order the scroll position
219
+ * was last measured against. Capturing here (synchronously, before
220
+ * the new DOM lands) avoids the race a scroll-event listener would
221
+ * have with `componentDidRender`.
222
+ * - `afterRender()` in `componentDidRender` — if the snapshot said
223
+ * "near newest", re-pin to the newest edge (new `scrollHeight`).
224
+ * - `repinIfSticky()` for out-of-band re-pins (e.g. after the typing
225
+ * indicator's height transition finishes — its `scrollHeight`
226
+ * change lands after `componentDidRender`).
227
+ *
228
+ * Scroll-container contract: the passed element must itself be the
229
+ * scrolling ancestor (the element whose `scrollTop` / `scrollHeight`
230
+ * / `clientHeight` reflect the chat list's scroll state). In practice
231
+ * that is the component's host, which sets `overflow: hidden auto` on
232
+ * itself. If future refactors introduce an inner scrolling wrapper,
233
+ * pass that wrapper instead.
234
+ */
235
+ class ScrollAnchor {
236
+ constructor(scrollContainer, getOrder, initialOrder) {
237
+ this.scrollContainer = scrollContainer;
238
+ this.getOrder = getOrder;
239
+ this.isSticky = true;
240
+ this.paintedOrder = initialOrder;
241
+ }
242
+ beforeRender() {
243
+ if (!this.scrollContainer.isConnected) {
244
+ return;
245
+ }
246
+ // Measure against the order the scroll position was last painted
247
+ // with. If `order` just changed, the scroll position still refers
248
+ // to the previous layout until this render commits.
249
+ this.isSticky =
250
+ this.distanceFromNewestEdge(this.paintedOrder) <
251
+ NEAR_NEWEST_THRESHOLD_PX;
252
+ }
253
+ afterRender() {
254
+ if (!this.scrollContainer.isConnected) {
255
+ return;
256
+ }
257
+ this.paintedOrder = this.getOrder();
258
+ if (this.isSticky) {
259
+ this.scrollToNewest();
260
+ }
261
+ }
262
+ repinIfSticky() {
263
+ if (!this.scrollContainer.isConnected) {
264
+ return;
265
+ }
266
+ if (this.isSticky) {
267
+ this.scrollToNewest();
268
+ }
269
+ }
270
+ scrollToNewest() {
271
+ const target = this.getOrder() === 'oldest-on-top'
272
+ ? this.scrollContainer.scrollHeight
273
+ : 0;
274
+ // Reading `scrollTop` is a cheap (already-measured) lookup, and
275
+ // short-circuiting avoids forcing a layout when we'd write the
276
+ // same value — important under high-frequency re-renders
277
+ // (streaming AI output).
278
+ if (this.scrollContainer.scrollTop === target) {
279
+ return;
280
+ }
281
+ this.scrollContainer.scrollTop = target;
282
+ }
283
+ distanceFromNewestEdge(order) {
284
+ return order === 'oldest-on-top'
285
+ ? this.scrollContainer.scrollHeight -
286
+ this.scrollContainer.scrollTop -
287
+ this.scrollContainer.clientHeight
288
+ : this.scrollContainer.scrollTop;
289
+ }
290
+ }
291
+
292
+ // The CSS property the typing-indicator container transitions.
293
+ // Mirrored from chat-list.scss `.typing-indicator-height-animation
294
+ // { transition: grid-template-rows ... }`. The runtime verifier below
295
+ // guards against the SCSS and the handler-side filter silently
296
+ // disagreeing.
297
+ const HEIGHT_ANIMATION_PROPERTY = 'grid-template-rows';
298
+ /**
299
+ * Returns a `transitionend` listener for the typing indicator's
300
+ * height transition. Filters by property name so unrelated
301
+ * transitions on the same element (e.g. opacity) don't trigger the
302
+ * callback.
303
+ * @param onHeightTransitionEnd
304
+ */
305
+ function createHeightTransitionEndHandler(onHeightTransitionEnd) {
306
+ return (event) => {
307
+ if (event.propertyName !== HEIGHT_ANIMATION_PROPERTY) {
308
+ return;
309
+ }
310
+ onHeightTransitionEnd();
311
+ };
312
+ }
313
+ /**
314
+ * Verifies that the passed element actually transitions on the
315
+ * property the height-transition handler filters on. Surfaces SCSS
316
+ * drift loudly instead of silently breaking auto-scroll-on-indicator-
317
+ * appearance.
318
+ * @param element
319
+ */
320
+ function verifyHeightAnimationProperty(element) {
321
+ if (!element) {
322
+ return;
323
+ }
324
+ const transitionProperty = globalThis.getComputedStyle(element).transitionProperty;
325
+ if (!transitionProperty) {
326
+ return;
327
+ }
328
+ const properties = new Set(transitionProperty.split(',').map((p) => p.trim()));
329
+ if (!properties.has('all') && !properties.has(HEIGHT_ANIMATION_PROPERTY)) {
330
+ console.warn(`[limebb-chat-list] Typing indicator is transitioning \`${transitionProperty}\` instead of \`${HEIGHT_ANIMATION_PROPERTY}\`. The auto-scroll on indicator appearance will not fire. Update the CSS or the handler so they match.`);
331
+ }
332
+ }
333
+
334
+ const chatListCss = () => `@charset "UTF-8";:host(limebb-chat-list){--limel-top-edge-fade-height:0.75rem;isolation:isolate;--limel-overflow-mask-vertical:linear-gradient( to bottom, transparent 0%, black calc(0% + var(--limel-top-edge-fade-height, 1rem)), black calc(100% - var(--limel-bottom-edge-fade-height, 1rem)), transparent 100% );-webkit-mask-image:var(--limel-overflow-mask-vertical);mask-image:var(--limel-overflow-mask-vertical);padding-top:var(--limel-top-edge-fade-height, 1rem);padding-bottom:var(--limel-bottom-edge-fade-height, 1rem);box-sizing:border-box;display:flex;flex-direction:column;gap:0.5rem;height:100%;width:100%;min-width:0;min-height:0;padding:var(--limel-top-edge-fade-height) 0.5rem;overflow:hidden auto}:host(limebb-chat-list.is-empty){padding:0}*{box-sizing:border-box}limel-spinner{align-self:center;justify-self:center;margin:auto}.date-group{isolation:isolate;display:flex;flex-direction:inherit;gap:0.5rem}.date-heading{position:sticky;z-index:1;top:0.25rem;display:flex;border-radius:9rem;padding:0.25rem 0.5rem;margin:0 auto;width:fit-content;font-size:0.75rem;line-height:1;color:rgb(var(--contrast-900));backdrop-filter:blur(0.5rem);-webkit-backdrop-filter:blur(0.5rem);transition:color 0.2s ease, border-color 0.4s ease}.date-heading:hover{color:rgb(var(--contrast-1000));border-color:rgb(var(--contrast-500))}.date-heading:hover:before{opacity:1}.date-heading:before{transition:opacity 0.2s ease;content:"";position:absolute;z-index:-1;inset:0;opacity:0.6;border-radius:inherit;background-color:rgb(var(--contrast-100))}.new-items-indicator{position:relative;isolation:isolate;display:flex;align-items:center;justify-content:flex-end;margin:0.25rem 0}.new-items-indicator hr{border:none;position:absolute;width:100%;height:1px;background-color:rgb(var(--color-red-lighter))}.new-items-indicator h3{z-index:1;padding:0 0.5rem;border-radius:1rem;margin:0 1rem;font-size:small;line-height:0.75rem;text-transform:lowercase;color:rgb(var(--color-red-default));background-color:rgb(var(--contrast-100))}.typing-indicator-height-animation{--limebb-chat-typing-opacity-transition-speed:0.1s;--limebb-chat-typing-opacity-transition-delay:0s;--limebb-chat-typing-grid-template-rows-transition-speed:0.3s;position:sticky;z-index:1;transition:grid-template-rows var(--limebb-chat-typing-grid-template-rows-transition-speed) ease;display:grid;grid-template-rows:0}.typing-indicator-height-animation div{transition:opacity var(--limebb-chat-typing-opacity-transition-speed) ease var(--limebb-chat-typing-opacity-transition-delay);overflow:hidden;opacity:0}:host(limebb-chat-list[order=oldest-on-top]) .typing-indicator-height-animation{bottom:0}:host(limebb-chat-list[order=newest-on-top]) .typing-indicator-height-animation{top:0}:host(limebb-chat-list[is-typing-indicator-visible]) .typing-indicator-height-animation{--limebb-chat-typing-opacity-transition-speed:0.4s;--limebb-chat-typing-opacity-transition-delay:0.3s;--limebb-chat-typing-grid-template-rows-transition-speed:0.46s;grid-template-rows:1.5rem}:host(limebb-chat-list[is-typing-indicator-visible]) .typing-indicator-height-animation div{opacity:1}.date-group limebb-chat-item{--limebb-promoted-action-bar-grid-template-rows:0fr}:host(limebb-chat-list[order=oldest-on-top]) .date-group:last-of-type limebb-chat-item:last-of-type{--limebb-promoted-action-bar-grid-template-rows:1fr}:host(limebb-chat-list[order=newest-on-top]) .date-group:first-of-type limebb-chat-item:first-of-type{--limebb-promoted-action-bar-grid-template-rows:1fr}`;
133
335
 
134
336
  const ChatList = class {
135
337
  constructor(hostRef) {
136
338
  index.registerInstance(this, hostRef);
137
339
  /**
138
340
  * List of items to display in the feed.
139
- * These items must be sorted by their `timestamp` in descending order.
341
+ * These items must be sorted by their `timestamp` in ascending order
342
+ * (oldest first, newest last). This matches the natural visual order
343
+ * for the default `oldest-on-top` layout, keeping DOM order aligned
344
+ * with visual order for copy/paste, screen readers, and tab order.
140
345
  *
141
346
  * See the [ChatListItem](#/type/ChatListItem/) type for the kinds of
142
347
  * items that can appear in the list.
@@ -150,29 +355,56 @@ const ChatList = class {
150
355
  *
151
356
  * :::important
152
357
  * Using this prop will _not_ automatically sort the chat items for you!
153
- * You still need to pass the items in the correct order (descending order by timestamp).
154
- * This prop will only affect the direction of the elements in the user interface,
358
+ * You still need to pass the items in the correct order (ascending order by timestamp — oldest first).
359
+ * This prop only affects the direction of the elements in the user interface,
155
360
  * such as the location of the typing indicator or the scrollbars.
156
361
  * :::
157
362
  */
158
363
  this.order = 'oldest-on-top';
159
364
  this.processedItems = [];
365
+ this.handleHeightTransitionEnd = createHeightTransitionEndHandler(() => { var _a; return (_a = this.scrollAnchor) === null || _a === void 0 ? void 0 : _a.repinIfSticky(); });
160
366
  }
161
367
  handleItemsChange() {
162
368
  let lastVisited = null;
163
369
  if (this.lastVisitedTimestamp) {
164
370
  lastVisited = parseToUTCDate(this.lastVisitedTimestamp);
165
371
  }
166
- this.processedItems = processChatItems(this.items, this.dateTimeFormatter, lastVisited);
372
+ const processed = processChatItems(this.items, this.dateTimeFormatter, lastVisited);
373
+ this.processedItems =
374
+ this.order === 'newest-on-top'
375
+ ? reverseProcessedItems(processed)
376
+ : processed;
167
377
  }
168
378
  componentWillLoad() {
169
379
  this.handleItemsChange();
380
+ this.scrollAnchor = new ScrollAnchor(this.host, () => this.order, this.order);
381
+ }
382
+ componentDidLoad() {
383
+ var _a;
384
+ verifyHeightAnimationProperty((_a = this.host.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.typing-indicator-height-animation'));
385
+ }
386
+ componentWillRender() {
387
+ if (this.items.length === 0) {
388
+ // Nothing to measure or scroll against. `isSticky` retains
389
+ // its last value (default `true`), so the first non-empty
390
+ // render still pins correctly.
391
+ return;
392
+ }
393
+ this.scrollAnchor.beforeRender();
394
+ }
395
+ componentDidRender() {
396
+ if (this.items.length === 0) {
397
+ return;
398
+ }
399
+ this.scrollAnchor.afterRender();
170
400
  }
171
401
  render() {
172
- return (index.h(index.Host, { key: 'b042ac219b98d38deb63cd862fc6aeb565bf3064', class: {
402
+ return (index.h(index.Host, { key: '002369dc1f47978696b4eb8ac7189f794568b0b4', class: {
173
403
  'is-empty': this.items.length === 0 &&
174
404
  !this.isTypingIndicatorVisible,
175
- }, "aria-busy": this.isBusy(), role: "log" }, this.renderSpinner(), index.h("div", { key: 'dd245b7fced3824abbc97671204310f51f0ba603', class: "typing-indicator-height-animation" }, index.h("div", { key: 'da9f1fdb805cda959b707598694070ce8b765965' }, this.renderTypingIndicator())), this.renderChatItems()));
405
+ }, "aria-busy": this.isBusy(), role: "log" }, this.renderSpinner(), this.order === 'newest-on-top' &&
406
+ this.renderTypingIndicatorBlock(), this.renderChatItems(), this.order === 'oldest-on-top' &&
407
+ this.renderTypingIndicatorBlock()));
176
408
  }
177
409
  renderSpinner() {
178
410
  if (!this.loading) {
@@ -180,6 +412,14 @@ const ChatList = class {
180
412
  }
181
413
  return index.h("limel-spinner", null);
182
414
  }
415
+ renderTypingIndicatorBlock() {
416
+ // The indicator's grid-template-rows transition grows the
417
+ // container over ~0.46s. `componentDidRender` fires before the
418
+ // transition starts, so `scrollHeight` at that moment doesn't
419
+ // include the final height. `handleHeightTransitionEnd` re-pins
420
+ // once the transition lands.
421
+ return (index.h("div", { class: "typing-indicator-height-animation", onTransitionEnd: this.handleHeightTransitionEnd }, index.h("div", null, this.renderTypingIndicator())));
422
+ }
183
423
  renderTypingIndicator() {
184
424
  if (!this.isTypingIndicatorVisible) {
185
425
  return;
@@ -195,33 +435,54 @@ const ChatList = class {
195
435
  case 'newItemIndicator': {
196
436
  return this.renderIndicator();
197
437
  }
198
- default: {
438
+ case 'chat':
439
+ case 'iconList': {
440
+ // `insertDateGroups` wraps every `chat` / `iconList`
441
+ // in a `group`, so these variants shouldn't appear
442
+ // at top level. If the processor pipeline is ever
443
+ // reordered, render nothing rather than double-
444
+ // render once the wrapping group includes them.
199
445
  return null;
200
446
  }
447
+ default: {
448
+ return assertNever(item);
449
+ }
201
450
  }
202
451
  });
203
452
  }
204
453
  renderGroup(group) {
205
454
  const chatItems = group.items.map((item) => {
206
- if (item.type === 'chat') {
207
- return this.renderChatItem(item.item);
208
- }
209
- else if (item.type === 'iconList') {
210
- return this.renderIconList(item.item);
211
- }
212
- else if (item.type === 'newItemIndicator') {
213
- return this.renderIndicator();
455
+ switch (item.type) {
456
+ case 'chat': {
457
+ return this.renderChatItem(item.item);
458
+ }
459
+ case 'iconList': {
460
+ return this.renderIconList(item.item);
461
+ }
462
+ case 'newItemIndicator': {
463
+ return this.renderIndicator();
464
+ }
465
+ case 'group': {
466
+ // Date groups don't nest. A compile-time `never`
467
+ // here would require a separate nested-item type;
468
+ // this runtime null return is the pragmatic
469
+ // middle ground.
470
+ return null;
471
+ }
472
+ default: {
473
+ return assertNever(item);
474
+ }
214
475
  }
215
476
  });
216
- const groupChatItems = group.items.filter((i) => i.type === 'chat');
217
- const lastChatItem = groupChatItems.at(-1);
218
- const stableId = (lastChatItem === null || lastChatItem === void 0 ? void 0 : lastChatItem.type) === 'chat' ? lastChatItem.item.id : '';
219
- const sectionKey = 'date-group-' + group.heading + '-' + stableId;
477
+ // Key the section by the group's heading (unique per date, and
478
+ // stable across item additions). Must not vary with group
479
+ // content: if it did, appending a new message would change the
480
+ // key and cause Stencil to remount the whole section, briefly
481
+ // collapsing scrollHeight and scrolling the list to the top
482
+ // before `componentDidRender` scrolls it back.
483
+ const sectionKey = 'date-group-' + group.heading;
220
484
  const headingId = `heading-${sectionKey}`.replaceAll(/[^a-zA-Z0-9_-]/g, '_');
221
485
  const dateHeading = (index.h("a", { id: headingId, class: "date-heading", role: "heading", "aria-level": 2 }, group.heading));
222
- if (this.order === 'oldest-on-top') {
223
- return (index.h("section", { class: "date-group", key: sectionKey, "aria-labelledby": headingId }, chatItems, dateHeading));
224
- }
225
486
  return (index.h("section", { class: "date-group", key: sectionKey, "aria-labelledby": headingId }, dateHeading, chatItems));
226
487
  }
227
488
  renderChatItem(item) {
@@ -242,9 +503,13 @@ const ChatList = class {
242
503
  get translator() {
243
504
  return this.platform.get(index_esm.n.Translate);
244
505
  }
506
+ get host() { return index.getElement(this); }
245
507
  static get watchers() { return {
246
508
  "items": [{
247
509
  "handleItemsChange": 0
510
+ }],
511
+ "order": [{
512
+ "handleItemsChange": 0
248
513
  }]
249
514
  }; }
250
515
  };
@@ -254,6 +519,9 @@ function parseToUTCDate(date) {
254
519
  }
255
520
  return new Date(date); // This will automatically be UTC
256
521
  }
522
+ function assertNever(value) {
523
+ throw new Error(`Unexpected value in exhaustive switch: ${JSON.stringify(value)}`);
524
+ }
257
525
  ChatList.style = chatListCss();
258
526
 
259
527
  exports.limebb_chat_list = ChatList;
@@ -66,8 +66,8 @@ const ComposerToolbar = class {
66
66
  }
67
67
  render() {
68
68
  return [
69
- index.h("div", { key: '62fa9154e6593b46509a4b4185c425c416fbf1de', class: "actions" }, index.h("slot", { key: '97476b607eb583b8c26e0164836174b9aeb8faa9', name: "leading" }), this.renderFileInput(), this.renderTriggerActions(), index.h("slot", { key: '14c2388e0cb0d1ec2a1cd548d11994475da5e949', name: "trailing" })),
70
- index.h("slot", { key: 'bc290147db27a9a6454cb32031bfe0990fe59e77', name: "primary-action" }),
69
+ index.h("div", { key: '6034b2945edfd9b613f606f8bb4fb0db81c1390f', class: "actions" }, index.h("slot", { key: '82b9bc14d80aedd3320af70bc63d3a908db6a7b2', name: "leading" }), this.renderFileInput(), this.renderTriggerActions(), index.h("slot", { key: '338fc1b7c7ec3f7dfedb123b7e09dd05f75fc45c', name: "trailing" })),
70
+ index.h("slot", { key: '57b277b772cbaf702e619ecbd6ff9032718b87b7', name: "primary-action" }),
71
71
  ];
72
72
  }
73
73
  get translator() {
@@ -1970,7 +1970,7 @@ const CurrencyPicker = class {
1970
1970
  };
1971
1971
  }
1972
1972
  render() {
1973
- return (index.h("limel-picker", { key: '75278107cede6773fb098f52183b8c3d291d12fe', label: this.label || this.defaultLabel, value: this.createListItem(this.value), helperText: this.helperText, onChange: this.handleChange, badgeIcons: false, disabled: this.disabled, required: this.required, readonly: this.readonly, invalid: this.readonly, searcher: this.search }));
1973
+ return (index.h("limel-picker", { key: '12d166b930a2864eefbb16c4d66706dd02b54112', label: this.label || this.defaultLabel, value: this.createListItem(this.value), helperText: this.helperText, onChange: this.handleChange, badgeIcons: false, disabled: this.disabled, required: this.required, readonly: this.readonly, invalid: this.readonly, searcher: this.search }));
1974
1974
  }
1975
1975
  getCurrencyName(code) {
1976
1976
  var _a;
@@ -18,7 +18,7 @@ const DashboardComponent = class {
18
18
  };
19
19
  }
20
20
  render() {
21
- return [this.renderHeader(), index.h("slot", { key: '3c5a1725b0cf3887074d0be4b1b7086126447d3b', name: "content" })];
21
+ return [this.renderHeader(), index.h("slot", { key: 'f5202c8c5d6ceb3e01420c6045ecb5589fed915a', name: "content" })];
22
22
  }
23
23
  };
24
24
  DashboardComponent.style = dashboardWidgetCss();
@@ -2,6 +2,7 @@
2
2
 
3
3
  var index = require('./index-C_OnxXhP.js');
4
4
  var index_esm = require('./index.esm-DhBVH875.js');
5
+ var randomString = require('./random-string-BdZGIsGg.js');
5
6
 
6
7
  const SECOND = 1000;
7
8
  const MINUTE = 60 * SECOND;
@@ -100,7 +101,11 @@ const DataCells = class {
100
101
  if (Number.isNaN(date.getTime())) {
101
102
  return item.value;
102
103
  }
103
- return formatRelativeDate(date, this.language);
104
+ const id = randomString.createRandomString();
105
+ return [
106
+ index.h("span", { id: id }, formatRelativeDate(date, this.language)),
107
+ index.h("limel-tooltip", { elementId: id, label: this.formatAbsoluteDate(date, item.type) }),
108
+ ];
104
109
  }
105
110
  if (item.type === 'percent') {
106
111
  return (index.h("limebb-percentage-visualizer", { value: Number(item.value), multiplier: 100, displayPercentageColors: true, reducePresence: false }));
@@ -148,6 +153,22 @@ const DataCells = class {
148
153
  return navigator.language;
149
154
  }
150
155
  }
156
+ formatAbsoluteDate(date, type) {
157
+ var _a;
158
+ try {
159
+ const formatter = (_a = this.platform) === null || _a === void 0 ? void 0 : _a.get(index_esm.n.DateTimeFormatter);
160
+ if (formatter) {
161
+ return formatter.format(date, type);
162
+ }
163
+ }
164
+ catch (_b) {
165
+ // Fall through to default formatting.
166
+ }
167
+ return date.toLocaleString(this.language, {
168
+ dateStyle: 'long',
169
+ timeStyle: type === 'time' ? 'short' : undefined,
170
+ });
171
+ }
151
172
  };
152
173
  DataCells.style = dataCellsCss();
153
174
 
@@ -75,7 +75,7 @@ const DatePicker = class {
75
75
  if (this.value) {
76
76
  this.shouldEmitValueAsString = typeof this.value === 'string';
77
77
  }
78
- return (index.h("limel-date-picker", { key: 'd3f3229f134e51ae94abbdb63da0e88988eaa474', disabled: this.disabled, readonly: this.readonly, invalid: this.invalid, label: this.label, placeholder: this.placeholder, helperText: this.helperText, required: this.required, value: this.parseDateValue(), type: this.type, language: this.language, formatter: this.formatter, onChange: this.handleChange }));
78
+ return (index.h("limel-date-picker", { key: 'f3276cfc57128c566248a97e5fea9a3dcc107888', disabled: this.disabled, readonly: this.readonly, invalid: this.invalid, label: this.label, placeholder: this.placeholder, helperText: this.helperText, required: this.required, value: this.parseDateValue(), type: this.type, language: this.language, formatter: this.formatter, onChange: this.handleChange }));
79
79
  }
80
80
  parseDateValue() {
81
81
  const value = this.value;
@@ -23,7 +23,7 @@ const DateRange = class {
23
23
  this.originalEndTime = this.endTime;
24
24
  }
25
25
  render() {
26
- return (index.h("div", { key: '0eab8abe9007384af9e7fcd56f8ac37c4aeb6b39', class: "date-pickers" }, this.renderStartDatePicker(), this.renderEndDatePicker()));
26
+ return (index.h("div", { key: 'e00abfd31fcae2a4279ae2194696b5cffb76dd79', class: "date-pickers" }, this.renderStartDatePicker(), this.renderEndDatePicker()));
27
27
  }
28
28
  renderStartDatePicker() {
29
29
  if (!this.originalStartTime) {
@@ -347,7 +347,7 @@ const DocumentChips = class {
347
347
  this.revokeAllBlobUrls();
348
348
  }
349
349
  render() {
350
- return (index.h(index.Host, { key: 'd5bdfcc1c2895820c4613bf907799063e1118899', role: "list", "aria-label": this.accessibleLabel }, this.files.map((file) => this.renderFile(file))));
350
+ return (index.h(index.Host, { key: '93d00770917014b1de6e380db6b102859210fded', role: "list", "aria-label": this.accessibleLabel }, this.files.map((file) => this.renderFile(file))));
351
351
  }
352
352
  renderFile(file) {
353
353
  const isReady = !file.uploadState || file.uploadState === 'done';