@conduction/nextcloud-vue 2.85.0 → 2.87.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 (301) hide show
  1. package/dist/esm/components/CnAdvancedFormDialog/CnMetadataTab.vue.js +1 -1
  2. package/dist/esm/components/CnAdvancedFormDialog/CnMetadataTab.vue2.js +3 -2
  3. package/dist/esm/components/CnAdvancedFormDialog/CnMetadataTab.vue2.js.map +1 -1
  4. package/dist/esm/components/CnAdvancedFormDialog/CnMetadataTab.vue3.js.map +1 -1
  5. package/dist/esm/components/CnAdvancedFormDialog/CnPropertyValueCell.vue.js +1 -1
  6. package/dist/esm/components/CnAdvancedFormDialog/CnPropertyValueCell.vue2.js +5 -4
  7. package/dist/esm/components/CnAdvancedFormDialog/CnPropertyValueCell.vue2.js.map +1 -1
  8. package/dist/esm/components/CnAdvancedFormDialog/CnPropertyValueCell.vue3.js.map +1 -1
  9. package/dist/esm/components/CnAnalyticsReportPicker/CnAnalyticsReportPicker.vue.js +1 -1
  10. package/dist/esm/components/CnAnalyticsReportPicker/CnAnalyticsReportPicker.vue2.js +2 -1
  11. package/dist/esm/components/CnAnalyticsReportPicker/CnAnalyticsReportPicker.vue2.js.map +1 -1
  12. package/dist/esm/components/CnAnalyticsReportPicker/CnAnalyticsReportPicker.vue3.js.map +1 -1
  13. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
  14. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +2 -1
  15. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  16. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  17. package/dist/esm/components/CnBoardView/CnBoardView.vue.js +1 -1
  18. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +156 -14
  19. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
  20. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +105 -74
  21. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
  22. package/dist/esm/components/CnCalendarEventPicker/CnCalendarEventPicker.vue.js +1 -1
  23. package/dist/esm/components/CnCalendarEventPicker/CnCalendarEventPicker.vue2.js +2 -1
  24. package/dist/esm/components/CnCalendarEventPicker/CnCalendarEventPicker.vue2.js.map +1 -1
  25. package/dist/esm/components/CnCalendarEventPicker/CnCalendarEventPicker.vue3.js.map +1 -1
  26. package/dist/esm/components/CnCalendarWidget/CnCalendarWidget.vue.js +1 -1
  27. package/dist/esm/components/CnCalendarWidget/CnCalendarWidget.vue2.js +2 -1
  28. package/dist/esm/components/CnCalendarWidget/CnCalendarWidget.vue2.js.map +1 -1
  29. package/dist/esm/components/CnCalendarWidget/CnCalendarWidget.vue3.js.map +1 -1
  30. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  31. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +6 -5
  32. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  33. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  34. package/dist/esm/components/CnCollectivePagePicker/CnCollectivePagePicker.vue.js +1 -1
  35. package/dist/esm/components/CnCollectivePagePicker/CnCollectivePagePicker.vue2.js +2 -1
  36. package/dist/esm/components/CnCollectivePagePicker/CnCollectivePagePicker.vue2.js.map +1 -1
  37. package/dist/esm/components/CnCollectivePagePicker/CnCollectivePagePicker.vue3.js.map +1 -1
  38. package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +1 -1
  39. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js +2 -1
  40. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -1
  41. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js.map +1 -1
  42. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue.js +1 -1
  43. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js +2 -1
  44. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js.map +1 -1
  45. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js.map +1 -1
  46. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  47. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +2 -1
  48. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  49. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  50. package/dist/esm/components/CnDataMatrix/CnDataMatrix.vue.js +1 -1
  51. package/dist/esm/components/CnDataMatrix/CnDataMatrix.vue2.js +3 -1
  52. package/dist/esm/components/CnDataMatrix/CnDataMatrix.vue2.js.map +1 -1
  53. package/dist/esm/components/CnDataMatrix/CnDataMatrix.vue3.js.map +1 -1
  54. package/dist/esm/components/CnDetailPage/CnDetailHeaderChips.vue2.js +2 -1
  55. package/dist/esm/components/CnDetailPage/CnDetailHeaderChips.vue2.js.map +1 -1
  56. package/dist/esm/components/CnDetailPage/CnDetailHeaderChips.vue3.js.map +1 -1
  57. package/dist/esm/components/CnEmailPicker/CnEmailPicker.vue.js +1 -1
  58. package/dist/esm/components/CnEmailPicker/CnEmailPicker.vue2.js +2 -1
  59. package/dist/esm/components/CnEmailPicker/CnEmailPicker.vue2.js.map +1 -1
  60. package/dist/esm/components/CnEmailPicker/CnEmailPicker.vue3.js.map +1 -1
  61. package/dist/esm/components/CnFederationStatus/CnFederationStatus.vue.js +1 -1
  62. package/dist/esm/components/CnFederationStatus/CnFederationStatus.vue2.js +3 -1
  63. package/dist/esm/components/CnFederationStatus/CnFederationStatus.vue2.js.map +1 -1
  64. package/dist/esm/components/CnFederationStatus/CnFederationStatus.vue3.js.map +1 -1
  65. package/dist/esm/components/CnFileManager/CnFileManager.vue.js +1 -1
  66. package/dist/esm/components/CnFileManager/CnFileManager.vue2.js +3 -1
  67. package/dist/esm/components/CnFileManager/CnFileManager.vue2.js.map +1 -1
  68. package/dist/esm/components/CnFileManager/CnFileManager.vue3.js.map +1 -1
  69. package/dist/esm/components/CnFilePreview/CnFilePreview.vue.js +1 -1
  70. package/dist/esm/components/CnFilePreview/CnFilePreview.vue2.js +2 -1
  71. package/dist/esm/components/CnFilePreview/CnFilePreview.vue2.js.map +1 -1
  72. package/dist/esm/components/CnFilePreview/CnFilePreview.vue3.js.map +1 -1
  73. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  74. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +49 -1
  75. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  76. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +3 -3
  77. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  78. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue.js +1 -1
  79. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue2.js +2 -1
  80. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue2.js.map +1 -1
  81. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue3.js.map +1 -1
  82. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +30 -5
  83. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  84. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  85. package/dist/esm/components/CnNotesCard/CnNotesCard.vue.js +1 -1
  86. package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js +2 -1
  87. package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js.map +1 -1
  88. package/dist/esm/components/CnNotesCard/CnNotesCard.vue3.js.map +1 -1
  89. package/dist/esm/components/CnObjectCalendar/CnObjectCalendar.vue.js +1 -1
  90. package/dist/esm/components/CnObjectCalendar/CnObjectCalendar.vue2.js +3 -2
  91. package/dist/esm/components/CnObjectCalendar/CnObjectCalendar.vue2.js.map +1 -1
  92. package/dist/esm/components/CnObjectCalendar/CnObjectCalendar.vue3.js.map +1 -1
  93. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
  94. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +28 -0
  95. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  96. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +82 -75
  97. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  98. package/dist/esm/components/CnObjectMetadataWidget/CnObjectMetadataWidget.vue.js +1 -1
  99. package/dist/esm/components/CnObjectMetadataWidget/CnObjectMetadataWidget.vue2.js +5 -4
  100. package/dist/esm/components/CnObjectMetadataWidget/CnObjectMetadataWidget.vue2.js.map +1 -1
  101. package/dist/esm/components/CnObjectMetadataWidget/CnObjectMetadataWidget.vue3.js.map +1 -1
  102. package/dist/esm/components/CnObjectSidebar/CnAuditTrailTab.vue.js +1 -1
  103. package/dist/esm/components/CnObjectSidebar/CnAuditTrailTab.vue2.js +2 -1
  104. package/dist/esm/components/CnObjectSidebar/CnAuditTrailTab.vue2.js.map +1 -1
  105. package/dist/esm/components/CnObjectSidebar/CnAuditTrailTab.vue3.js.map +1 -1
  106. package/dist/esm/components/CnObjectSidebar/CnFlowTasksPanel.vue.js +1 -1
  107. package/dist/esm/components/CnObjectSidebar/CnFlowTasksPanel.vue2.js +2 -1
  108. package/dist/esm/components/CnObjectSidebar/CnFlowTasksPanel.vue2.js.map +1 -1
  109. package/dist/esm/components/CnObjectSidebar/CnFlowTasksPanel.vue3.js.map +1 -1
  110. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue.js +1 -1
  111. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js +2 -1
  112. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js.map +1 -1
  113. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue3.js.map +1 -1
  114. package/dist/esm/components/CnObjectSidebar/CnTasksTab.vue.js +1 -1
  115. package/dist/esm/components/CnObjectSidebar/CnTasksTab.vue2.js +2 -1
  116. package/dist/esm/components/CnObjectSidebar/CnTasksTab.vue2.js.map +1 -1
  117. package/dist/esm/components/CnObjectSidebar/CnTasksTab.vue3.js.map +1 -1
  118. package/dist/esm/components/CnPropertySourceField/CnPropertySourceField.vue.js +1 -1
  119. package/dist/esm/components/CnPropertySourceField/CnPropertySourceField.vue2.js +2 -1
  120. package/dist/esm/components/CnPropertySourceField/CnPropertySourceField.vue2.js.map +1 -1
  121. package/dist/esm/components/CnPropertySourceField/CnPropertySourceField.vue3.js.map +1 -1
  122. package/dist/esm/components/CnRoadmapItem/CnRoadmapItem.vue.js +1 -1
  123. package/dist/esm/components/CnRoadmapItem/CnRoadmapItem.vue2.js +2 -1
  124. package/dist/esm/components/CnRoadmapItem/CnRoadmapItem.vue2.js.map +1 -1
  125. package/dist/esm/components/CnRoadmapItem/CnRoadmapItem.vue3.js.map +1 -1
  126. package/dist/esm/components/CnSchemaFormDialog/CnSchemaFormDialog.vue2.js +2 -0
  127. package/dist/esm/components/CnSchemaFormDialog/CnSchemaFormDialog.vue2.js.map +1 -1
  128. package/dist/esm/components/CnSchemaFormDialog/CnSchemaFormDialog.vue3.js +2 -2
  129. package/dist/esm/components/CnSchemaFormDialog/CnSchemaFormDialog.vue3.js.map +1 -1
  130. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue.js +1 -1
  131. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue2.js +2 -1
  132. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue2.js.map +1 -1
  133. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue3.js.map +1 -1
  134. package/dist/esm/components/CnSummaryAggregates/CnSummaryAggregates.vue.js +1 -1
  135. package/dist/esm/components/CnSummaryAggregates/CnSummaryAggregates.vue2.js +2 -1
  136. package/dist/esm/components/CnSummaryAggregates/CnSummaryAggregates.vue2.js.map +1 -1
  137. package/dist/esm/components/CnSummaryAggregates/CnSummaryAggregates.vue3.js.map +1 -1
  138. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  139. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +36 -0
  140. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  141. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +55 -40
  142. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  143. package/dist/esm/components/CnTasksCard/CnTasksCard.vue.js +1 -1
  144. package/dist/esm/components/CnTasksCard/CnTasksCard.vue2.js +2 -1
  145. package/dist/esm/components/CnTasksCard/CnTasksCard.vue2.js.map +1 -1
  146. package/dist/esm/components/CnTasksCard/CnTasksCard.vue3.js.map +1 -1
  147. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js +1 -1
  148. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +2 -1
  149. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -1
  150. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -1
  151. package/dist/esm/components/CnTranslatedBadge/CnTranslatedBadge.vue.js +1 -1
  152. package/dist/esm/components/CnTranslatedBadge/CnTranslatedBadge.vue2.js +2 -1
  153. package/dist/esm/components/CnTranslatedBadge/CnTranslatedBadge.vue2.js.map +1 -1
  154. package/dist/esm/components/CnVersionHistory/CnVersionHistory.vue.js +1 -1
  155. package/dist/esm/components/CnVersionHistory/CnVersionHistory.vue2.js +2 -1
  156. package/dist/esm/components/CnVersionHistory/CnVersionHistory.vue2.js.map +1 -1
  157. package/dist/esm/components/CnVersionHistory/CnVersionHistory.vue3.js.map +1 -1
  158. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
  159. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +3 -26
  160. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
  161. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
  162. package/dist/esm/dialogs/CnNoteHistoryDialog.vue.js +1 -1
  163. package/dist/esm/dialogs/CnNoteHistoryDialog.vue2.js +2 -1
  164. package/dist/esm/dialogs/CnNoteHistoryDialog.vue2.js.map +1 -1
  165. package/dist/esm/dialogs/CnNoteHistoryDialog.vue3.js.map +1 -1
  166. package/dist/esm/integrations/builtin/activity/CnActivityCard.vue.js +1 -1
  167. package/dist/esm/integrations/builtin/activity/CnActivityCard.vue2.js +3 -2
  168. package/dist/esm/integrations/builtin/activity/CnActivityCard.vue2.js.map +1 -1
  169. package/dist/esm/integrations/builtin/activity/CnActivityCard.vue3.js.map +1 -1
  170. package/dist/esm/integrations/builtin/activity/CnActivityTab.vue.js +1 -1
  171. package/dist/esm/integrations/builtin/activity/CnActivityTab.vue2.js +4 -3
  172. package/dist/esm/integrations/builtin/activity/CnActivityTab.vue2.js.map +1 -1
  173. package/dist/esm/integrations/builtin/activity/CnActivityTab.vue3.js.map +1 -1
  174. package/dist/esm/integrations/builtin/analytics/CnAnalyticsTab.vue.js +1 -1
  175. package/dist/esm/integrations/builtin/analytics/CnAnalyticsTab.vue2.js +3 -2
  176. package/dist/esm/integrations/builtin/analytics/CnAnalyticsTab.vue2.js.map +1 -1
  177. package/dist/esm/integrations/builtin/analytics/CnAnalyticsTab.vue3.js.map +1 -1
  178. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue.js +1 -1
  179. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue2.js +2 -1
  180. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue2.js.map +1 -1
  181. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue3.js.map +1 -1
  182. package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue.js +1 -1
  183. package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue2.js +6 -5
  184. package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue2.js.map +1 -1
  185. package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue3.js.map +1 -1
  186. package/dist/esm/integrations/builtin/contactmoment/CnContactmomentCard.vue.js +1 -1
  187. package/dist/esm/integrations/builtin/contactmoment/CnContactmomentCard.vue2.js +2 -1
  188. package/dist/esm/integrations/builtin/contactmoment/CnContactmomentCard.vue2.js.map +1 -1
  189. package/dist/esm/integrations/builtin/contactmoment/CnContactmomentCard.vue3.js.map +1 -1
  190. package/dist/esm/integrations/builtin/cospend/CnCospendTab.vue.js +1 -1
  191. package/dist/esm/integrations/builtin/cospend/CnCospendTab.vue2.js +2 -1
  192. package/dist/esm/integrations/builtin/cospend/CnCospendTab.vue2.js.map +1 -1
  193. package/dist/esm/integrations/builtin/cospend/CnCospendTab.vue3.js.map +1 -1
  194. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue.js +1 -1
  195. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue2.js +2 -1
  196. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue2.js.map +1 -1
  197. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue3.js.map +1 -1
  198. package/dist/esm/integrations/builtin/email/CnEmailCard.vue.js +1 -1
  199. package/dist/esm/integrations/builtin/email/CnEmailCard.vue2.js +2 -1
  200. package/dist/esm/integrations/builtin/email/CnEmailCard.vue2.js.map +1 -1
  201. package/dist/esm/integrations/builtin/email/CnEmailCard.vue3.js.map +1 -1
  202. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue.js +1 -1
  203. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js +2 -1
  204. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js.map +1 -1
  205. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue3.js.map +1 -1
  206. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionTab.vue.js +1 -1
  207. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionTab.vue2.js +2 -1
  208. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionTab.vue2.js.map +1 -1
  209. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionTab.vue3.js.map +1 -1
  210. package/dist/esm/integrations/builtin/photos/CnPhotosTab.vue.js +1 -1
  211. package/dist/esm/integrations/builtin/photos/CnPhotosTab.vue2.js +2 -1
  212. package/dist/esm/integrations/builtin/photos/CnPhotosTab.vue2.js.map +1 -1
  213. package/dist/esm/integrations/builtin/photos/CnPhotosTab.vue3.js.map +1 -1
  214. package/dist/esm/l10n/en.json.js +35 -2
  215. package/dist/esm/l10n/en.json.js.map +1 -1
  216. package/dist/esm/l10n/nl.json.js +38 -2
  217. package/dist/esm/l10n/nl.json.js.map +1 -1
  218. package/dist/esm/library-style.js +1 -1
  219. package/dist/esm/nextcloud-vue.css +1068 -962
  220. package/dist/esm/store/useObjectStore.js +7 -2
  221. package/dist/esm/store/useObjectStore.js.map +1 -1
  222. package/dist/esm/utils/boardColumns.js +50 -1
  223. package/dist/esm/utils/boardColumns.js.map +1 -1
  224. package/dist/esm/utils/boardReferenceColumns.js +115 -4
  225. package/dist/esm/utils/boardReferenceColumns.js.map +1 -1
  226. package/dist/esm/utils/contentHeaderLink.js +48 -1
  227. package/dist/esm/utils/contentHeaderLink.js.map +1 -1
  228. package/dist/esm/utils/schema.js +5 -4
  229. package/dist/esm/utils/schema.js.map +1 -1
  230. package/dist/esm/utils/userLocale.js +30 -0
  231. package/dist/esm/utils/userLocale.js.map +1 -0
  232. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  233. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  234. package/dist/nextcloud-vue.cjs.js +1088 -476
  235. package/dist/nextcloud-vue.cjs.js.map +2 -2
  236. package/dist/nextcloud-vue.css +1068 -962
  237. package/l10n/en.json +35 -2
  238. package/l10n/nl.json +38 -2
  239. package/package.json +1 -1
  240. package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +3 -2
  241. package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +5 -4
  242. package/src/components/CnAnalyticsReportPicker/CnAnalyticsReportPicker.vue +2 -1
  243. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +2 -1
  244. package/src/components/CnBoardView/CnBoardView.vue +189 -24
  245. package/src/components/CnCalendarEventPicker/CnCalendarEventPicker.vue +2 -1
  246. package/src/components/CnCalendarWidget/CnCalendarWidget.vue +2 -1
  247. package/src/components/CnChartWidget/CnChartWidget.vue +6 -5
  248. package/src/components/CnCollectivePagePicker/CnCollectivePagePicker.vue +2 -1
  249. package/src/components/CnConversationThread/CnConversationThread.vue +2 -1
  250. package/src/components/CnCountdownWidget/CnCountdownWidget.vue +2 -1
  251. package/src/components/CnDashboardPage/CnDashboardPage.vue +2 -1
  252. package/src/components/CnDataMatrix/CnDataMatrix.vue +2 -1
  253. package/src/components/CnDetailPage/CnDetailHeaderChips.vue +2 -1
  254. package/src/components/CnEmailPicker/CnEmailPicker.vue +2 -1
  255. package/src/components/CnFederationStatus/CnFederationStatus.vue +3 -1
  256. package/src/components/CnFileManager/CnFileManager.vue +2 -1
  257. package/src/components/CnFilePreview/CnFilePreview.vue +2 -1
  258. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +54 -6
  259. package/src/components/CnFollowToggle/CnFollowToggle.vue +2 -1
  260. package/src/components/CnIndexPage/CnIndexPage.vue +30 -4
  261. package/src/components/CnNotesCard/CnNotesCard.vue +2 -1
  262. package/src/components/CnObjectCalendar/CnObjectCalendar.vue +3 -2
  263. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +36 -0
  264. package/src/components/CnObjectMetadataWidget/CnObjectMetadataWidget.vue +5 -4
  265. package/src/components/CnObjectSidebar/CnAuditTrailTab.vue +2 -1
  266. package/src/components/CnObjectSidebar/CnFlowTasksPanel.vue +2 -1
  267. package/src/components/CnObjectSidebar/CnNotesTab.vue +2 -1
  268. package/src/components/CnObjectSidebar/CnTasksTab.vue +2 -1
  269. package/src/components/CnPropertySourceField/CnPropertySourceField.vue +2 -1
  270. package/src/components/CnRoadmapItem/CnRoadmapItem.vue +2 -1
  271. package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +4 -2
  272. package/src/components/CnStatsBlock/CnStatsBlock.vue +2 -1
  273. package/src/components/CnSummaryAggregates/CnSummaryAggregates.vue +2 -1
  274. package/src/components/CnTabsWidget/CnTabsWidget.vue +71 -0
  275. package/src/components/CnTasksCard/CnTasksCard.vue +2 -1
  276. package/src/components/CnTimelineWidget/CnTimelineWidget.vue +2 -1
  277. package/src/components/CnTranslatedBadge/CnTranslatedBadge.vue +2 -1
  278. package/src/components/CnVersionHistory/CnVersionHistory.vue +2 -1
  279. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +3 -26
  280. package/src/css/board.css +40 -0
  281. package/src/dialogs/CnNoteHistoryDialog.vue +2 -1
  282. package/src/integrations/builtin/activity/CnActivityCard.vue +3 -2
  283. package/src/integrations/builtin/activity/CnActivityTab.vue +4 -3
  284. package/src/integrations/builtin/analytics/CnAnalyticsTab.vue +3 -2
  285. package/src/integrations/builtin/calendar/CnCalendarCard.vue +2 -1
  286. package/src/integrations/builtin/calendar/CnCalendarTab.vue +6 -5
  287. package/src/integrations/builtin/contactmoment/CnContactmomentCard.vue +2 -1
  288. package/src/integrations/builtin/cospend/CnCospendTab.vue +2 -1
  289. package/src/integrations/builtin/deck/CnDeckTab.vue +2 -1
  290. package/src/integrations/builtin/email/CnEmailCard.vue +2 -1
  291. package/src/integrations/builtin/field-inspection/CnFieldInspectionCard.vue +2 -1
  292. package/src/integrations/builtin/field-inspection/CnFieldInspectionTab.vue +2 -1
  293. package/src/integrations/builtin/photos/CnPhotosTab.vue +2 -1
  294. package/src/schemas/app-manifest-v2.schema.json +4 -1
  295. package/src/store/useObjectStore.js +7 -2
  296. package/src/utils/boardColumns.js +49 -0
  297. package/src/utils/boardReferenceColumns.js +114 -3
  298. package/src/utils/contentHeaderLink.js +47 -0
  299. package/src/utils/schema.js +5 -4
  300. package/src/utils/userLocale.js +26 -0
  301. package/src/utils/validateManifestV2.compiled.js +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"CnEmailPicker.vue3.js","sources":["../../../../src/components/CnEmailPicker/CnEmailPicker.vue?vue&type=template&id=d228f484&lang.js"],"sourcesContent":["<!--\n CnEmailPicker — multi-step modal for picking an existing NC Mail\n message to link to the parent OR object.\n\n Flow (three steps + confirm):\n 1. Choose an account — GET /api/integrations/email/accounts\n 2. Choose a mailbox — GET /api/integrations/email/accounts/{id}/mailboxes\n 3. Browse messages — GET /api/integrations/email/accounts/{id}/messages?mailbox=\n (cursor pagination + client-side search filter)\n 4. Confirm — emit `link` with `{ mailAccountId, messageId, messageUid }`\n\n All API calls are wrapped in best-effort try/catch so a transient\n Mail failure surfaces a user-visible inline error rather than a modal\n crash. The modal stays open across errors so the user can retry\n without losing context.\n\n ADR-004: modal lives in its own .vue file under\n `src/components/CnEmailPicker/`. Picker UX is dialog-shaped so this\n variant uses NcDialog (matches CnDeckCardPicker).\n\n ADR-019: drives the `email` integration leaf's \"link existing\"\n surface; emits `link` so the parent (CnEmailTab) can POST the\n selection to the OR endpoint.\n\n SPDX-License-Identifier: EUPL-1.2\n SPDX-FileCopyrightText: 2026 Conduction B.V.\n-->\n<template>\n\t<NcDialog\n\t\t:name=\"dialogTitle\"\n\t\tsize=\"normal\"\n\t\tdata-testid=\"cn-email-picker\"\n\t\t@closing=\"onClose\">\n\t\t<div class=\"cn-email-picker\">\n\t\t\t<ol class=\"cn-email-picker__steps\">\n\t\t\t\t<li :class=\"stepClass(1)\">\n\t\t\t\t\t{{ t('nextcloud-vue', '1. Account') }}\n\t\t\t\t</li>\n\t\t\t\t<li :class=\"stepClass(2)\">\n\t\t\t\t\t{{ t('nextcloud-vue', '2. Mailbox') }}\n\t\t\t\t</li>\n\t\t\t\t<li :class=\"stepClass(3)\">\n\t\t\t\t\t{{ t('nextcloud-vue', '3. Message') }}\n\t\t\t\t</li>\n\t\t\t</ol>\n\n\t\t\t<NcNoteCard v-if=\"error\" type=\"error\" class=\"cn-email-picker__error\">\n\t\t\t\t{{ error }}\n\t\t\t</NcNoteCard>\n\n\t\t\t<section v-if=\"step === 1\" class=\"cn-email-picker__panel\">\n\t\t\t\t<NcLoadingIcon v-if=\"loading\" />\n\t\t\t\t<NcEmptyContent\n\t\t\t\t\tv-else-if=\"accounts.length === 0\"\n\t\t\t\t\t:name=\"t('nextcloud-vue', 'No mail accounts')\"\n\t\t\t\t\t:description=\"t('nextcloud-vue', 'NC Mail has no accounts configured for you yet.')\" />\n\t\t\t\t<ul v-else class=\"cn-email-picker__list\">\n\t\t\t\t\t<li\n\t\t\t\t\t\tv-for=\"account in accounts\"\n\t\t\t\t\t\t:key=\"account.id\"\n\t\t\t\t\t\tclass=\"cn-email-picker__row\"\n\t\t\t\t\t\t:class=\"{ 'cn-email-picker__row--selected': selectedAccountId === account.id }\">\n\t\t\t\t\t\t<button type=\"button\" class=\"cn-email-picker__row-button\" @click=\"pickAccount(account)\">\n\t\t\t\t\t\t\t<EmailOutline :size=\"20\" />\n\t\t\t\t\t\t\t<span class=\"cn-email-picker__row-label\">\n\t\t\t\t\t\t\t\t<span class=\"cn-email-picker__row-primary\">{{ account.label }}</span>\n\t\t\t\t\t\t\t\t<span v-if=\"account.email && account.email !== account.label\" class=\"cn-email-picker__row-secondary\">{{ account.email }}</span>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t</section>\n\n\t\t\t<section v-else-if=\"step === 2\" class=\"cn-email-picker__panel\">\n\t\t\t\t<NcLoadingIcon v-if=\"loading\" />\n\t\t\t\t<NcEmptyContent\n\t\t\t\t\tv-else-if=\"mailboxes.length === 0\"\n\t\t\t\t\t:name=\"t('nextcloud-vue', 'No mailboxes')\"\n\t\t\t\t\t:description=\"t('nextcloud-vue', 'This account has no mailboxes to browse.')\" />\n\t\t\t\t<ul v-else class=\"cn-email-picker__list\">\n\t\t\t\t\t<li\n\t\t\t\t\t\tv-for=\"mb in mailboxes\"\n\t\t\t\t\t\t:key=\"mb.id\"\n\t\t\t\t\t\tclass=\"cn-email-picker__row\"\n\t\t\t\t\t\t:class=\"{ 'cn-email-picker__row--selected': selectedMailbox === mb.name }\">\n\t\t\t\t\t\t<button type=\"button\" class=\"cn-email-picker__row-button\" @click=\"pickMailbox(mb)\">\n\t\t\t\t\t\t\t<FolderOutline :size=\"20\" />\n\t\t\t\t\t\t\t<span class=\"cn-email-picker__row-label\">\n\t\t\t\t\t\t\t\t<span class=\"cn-email-picker__row-primary\">{{ mb.displayName }}</span>\n\t\t\t\t\t\t\t\t<span v-if=\"mb.name !== mb.displayName\" class=\"cn-email-picker__row-secondary\">{{ mb.name }}</span>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t</section>\n\n\t\t\t<section v-else-if=\"step === 3\" class=\"cn-email-picker__panel\">\n\t\t\t\t<NcTextField\n\t\t\t\t\tv-model=\"filterText\"\n\t\t\t\t\t:label=\"t('nextcloud-vue', 'Filter messages')\"\n\t\t\t\t\tclass=\"cn-email-picker__filter\" />\n\n\t\t\t\t<NcLoadingIcon v-if=\"loading && messages.length === 0\" />\n\t\t\t\t<NcEmptyContent\n\t\t\t\t\tv-else-if=\"filteredMessages.length === 0\"\n\t\t\t\t\t:name=\"t('nextcloud-vue', 'No messages')\"\n\t\t\t\t\t:description=\"t('nextcloud-vue', 'This mailbox has no messages matching your filter.')\" />\n\t\t\t\t<ul v-else class=\"cn-email-picker__list cn-email-picker__list--messages\">\n\t\t\t\t\t<li\n\t\t\t\t\t\tv-for=\"msg in filteredMessages\"\n\t\t\t\t\t\t:key=\"msg.id\"\n\t\t\t\t\t\tclass=\"cn-email-picker__row\"\n\t\t\t\t\t\t:class=\"{ 'cn-email-picker__row--selected': selectedMessageId === msg.id }\">\n\t\t\t\t\t\t<button type=\"button\" class=\"cn-email-picker__row-button cn-email-picker__row-button--message\" @click=\"pickMessage(msg)\">\n\t\t\t\t\t\t\t<EmailOutline :size=\"18\" />\n\t\t\t\t\t\t\t<span class=\"cn-email-picker__message\">\n\t\t\t\t\t\t\t\t<span class=\"cn-email-picker__message-subject\">{{ msg.subject || t('nextcloud-vue', '(no subject)') }}</span>\n\t\t\t\t\t\t\t\t<span class=\"cn-email-picker__message-meta\">\n\t\t\t\t\t\t\t\t\t<span class=\"cn-email-picker__message-sender\">{{ msg.sender || t('nextcloud-vue', 'Unknown sender') }}</span>\n\t\t\t\t\t\t\t\t\t<span v-if=\"msg.date\" class=\"cn-email-picker__message-date\">{{ formatDate(msg.date) }}</span>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\n\t\t\t\t<NcButton\n\t\t\t\t\tv-if=\"nextCursor !== null\"\n\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t:disabled=\"loadingMore\"\n\t\t\t\t\tclass=\"cn-email-picker__load-more\"\n\t\t\t\t\t@click=\"loadMoreMessages\">\n\t\t\t\t\t<template v-if=\"loadingMore\" #icon>\n\t\t\t\t\t\t<NcLoadingIcon :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ t('nextcloud-vue', 'Load more') }}\n\t\t\t\t</NcButton>\n\t\t\t</section>\n\t\t</div>\n\n\t\t<template #actions>\n\t\t\t<NcButton v-if=\"step > 1\" @click=\"goBack\">\n\t\t\t\t{{ t('nextcloud-vue', 'Back') }}\n\t\t\t</NcButton>\n\t\t\t<NcButton @click=\"onClose\">\n\t\t\t\t{{ t('nextcloud-vue', 'Cancel') }}\n\t\t\t</NcButton>\n\t\t\t<NcButton\n\t\t\t\tv-if=\"step === 3\"\n\t\t\t\tvariant=\"primary\"\n\t\t\t\t:disabled=\"!selectedMessageId\"\n\t\t\t\t@click=\"confirm\">\n\t\t\t\t{{ t('nextcloud-vue', 'Link message') }}\n\t\t\t</NcButton>\n\t\t</template>\n\t</NcDialog>\n</template>\n\n<script>\n/**\n * SPDX-License-Identifier: EUPL-1.2\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n *\n * CnEmailPicker — pick an existing Mail message via account, mailbox,\n * message. Emits `link` with `{ mailAccountId, messageId, messageUid }`.\n *\n * @see ADR-019 (pluggable integrations) and ADR-022 (sidebar tabs)\n */\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcDialog, NcEmptyContent, NcLoadingIcon, NcNoteCard, NcTextField } from '@nextcloud/vue'\nimport EmailOutline from 'vue-material-design-icons/EmailOutline.vue'\nimport FolderOutline from 'vue-material-design-icons/FolderOutline.vue'\nimport { buildHeaders, prefixUrl } from '../../utils/index.js'\n\nexport default {\n\tname: 'CnEmailPicker',\n\n\tcomponents: { NcButton, NcDialog, NcEmptyContent, NcLoadingIcon, NcNoteCard, NcTextField, EmailOutline, FolderOutline },\n\n\tprops: {\n\t\t/** Base API URL for OR. */\n\t\tapiBase: { type: String, default: '/apps/openregister/api' },\n\t\t/** Pre-translated dialog title. */\n\t\tdialogTitle: { type: String, default: () => t('nextcloud-vue', 'Link an existing email') },\n\t\t/** Page size for the message list (load-more pagination). */\n\t\tpageSize: { type: Number, default: 50 },\n\t},\n\n\temits: ['close', 'link'],\n\n\tdata() {\n\t\treturn {\n\t\t\tstep: 1,\n\t\t\tloading: false,\n\t\t\tloadingMore: false,\n\t\t\terror: '',\n\t\t\taccounts: [],\n\t\t\tmailboxes: [],\n\t\t\tmessages: [],\n\t\t\tselectedAccountId: null,\n\t\t\tselectedMailbox: null,\n\t\t\tselectedMessageId: null,\n\t\t\tselectedMessageUid: null,\n\t\t\tnextCursor: null,\n\t\t\tfilterText: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\tfilteredMessages() {\n\t\t\tconst needle = this.filterText.trim().toLowerCase()\n\t\t\tif (needle === '') {\n\t\t\t\treturn this.messages\n\t\t\t}\n\t\t\treturn this.messages.filter((msg) => {\n\t\t\t\tconst subj = (msg.subject || '').toLowerCase()\n\t\t\t\tconst sender = (msg.sender || '').toLowerCase()\n\t\t\t\treturn subj.includes(needle) || sender.includes(needle)\n\t\t\t})\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.fetchAccounts()\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\tstepClass(step) {\n\t\t\treturn {\n\t\t\t\t'cn-email-picker__step': true,\n\t\t\t\t'cn-email-picker__step--active': this.step === step,\n\t\t\t\t'cn-email-picker__step--done': this.step > step,\n\t\t\t}\n\t\t},\n\n\t\tasync fetchAccounts() {\n\t\t\tthis.loading = true\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(`${this.apiBase}/integrations/email/accounts`), {\n\t\t\t\t\theaders: buildHeaders(),\n\t\t\t\t})\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tconst data = await response.json()\n\t\t\t\t\tthis.accounts = data.results || []\n\t\t\t\t} else if (response.status === 501) {\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'Nextcloud Mail is not installed.')\n\t\t\t\t} else {\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'Could not load mail accounts.')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnEmailPicker] fetch accounts failed', err)\n\t\t\t\tthis.error = t('nextcloud-vue', 'Could not load mail accounts.')\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\tasync fetchMailboxes(accountId) {\n\t\t\tthis.loading = true\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(`${this.apiBase}/integrations/email/accounts/${accountId}/mailboxes`), {\n\t\t\t\t\theaders: buildHeaders(),\n\t\t\t\t})\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tconst data = await response.json()\n\t\t\t\t\tthis.mailboxes = data.results || []\n\t\t\t\t} else {\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'Could not load mailboxes.')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnEmailPicker] fetch mailboxes failed', err)\n\t\t\t\tthis.error = t('nextcloud-vue', 'Could not load mailboxes.')\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\tasync fetchMessages(accountId, mailbox, append = false) {\n\t\t\tif (append === true) {\n\t\t\t\tthis.loadingMore = true\n\t\t\t} else {\n\t\t\t\tthis.loading = true\n\t\t\t\tthis.messages = []\n\t\t\t\tthis.nextCursor = null\n\t\t\t}\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tconst params = new URLSearchParams({\n\t\t\t\t\tmailbox,\n\t\t\t\t\tlimit: String(this.pageSize),\n\t\t\t\t})\n\t\t\t\tif (append === true && this.nextCursor !== null) {\n\t\t\t\t\tparams.set('cursor', String(this.nextCursor))\n\t\t\t\t}\n\t\t\t\tconst response = await fetch(\n\t\t\t\t\tprefixUrl(`${this.apiBase}/integrations/email/accounts/${accountId}/messages?${params.toString()}`),\n\t\t\t\t\t{ headers: buildHeaders() },\n\t\t\t\t)\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tconst data = await response.json()\n\t\t\t\t\tconst items = Array.isArray(data.items) ? data.items : []\n\t\t\t\t\tthis.messages = append === true ? [...this.messages, ...items] : items\n\t\t\t\t\tthis.nextCursor = data.nextCursor !== undefined ? data.nextCursor : null\n\t\t\t\t} else {\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'Could not load messages.')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnEmailPicker] fetch messages failed', err)\n\t\t\t\tthis.error = t('nextcloud-vue', 'Could not load messages.')\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t\tthis.loadingMore = false\n\t\t\t}\n\t\t},\n\n\t\tpickAccount(account) {\n\t\t\tthis.selectedAccountId = account.id\n\t\t\tthis.selectedMailbox = null\n\t\t\tthis.selectedMessageId = null\n\t\t\tthis.selectedMessageUid = null\n\t\t\tthis.mailboxes = []\n\t\t\tthis.messages = []\n\t\t\tthis.step = 2\n\t\t\tthis.fetchMailboxes(account.id)\n\t\t},\n\n\t\tpickMailbox(mailbox) {\n\t\t\tthis.selectedMailbox = mailbox.name\n\t\t\tthis.selectedMessageId = null\n\t\t\tthis.selectedMessageUid = null\n\t\t\tthis.messages = []\n\t\t\tthis.filterText = ''\n\t\t\tthis.step = 3\n\t\t\tthis.fetchMessages(this.selectedAccountId, mailbox.name)\n\t\t},\n\n\t\tpickMessage(msg) {\n\t\t\tthis.selectedMessageId = msg.id\n\t\t\tthis.selectedMessageUid = msg.uid || ''\n\t\t},\n\n\t\tloadMoreMessages() {\n\t\t\tif (this.nextCursor === null || this.selectedAccountId === null || this.selectedMailbox === null) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.fetchMessages(this.selectedAccountId, this.selectedMailbox, true)\n\t\t},\n\n\t\tgoBack() {\n\t\t\tif (this.step === 3) {\n\t\t\t\tthis.selectedMessageId = null\n\t\t\t\tthis.selectedMessageUid = null\n\t\t\t\tthis.messages = []\n\t\t\t\tthis.nextCursor = null\n\t\t\t\tthis.filterText = ''\n\t\t\t\tthis.step = 2\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.step === 2) {\n\t\t\t\tthis.selectedMailbox = null\n\t\t\t\tthis.mailboxes = []\n\t\t\t\tthis.step = 1\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Dismiss the dialog.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonClose() {\n\t\t\t/**\n\t\t\t * @event close Emitted when the dialog should be closed (cancel or close button).\n\t\t\t */\n\t\t\tthis.$emit('close')\n\t\t},\n\n\t\tconfirm() {\n\t\t\tif (this.selectedMessageId === null || this.selectedAccountId === null) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event link Emitted when the user confirms the selection. Payload: `{ mailAccountId, messageId, messageUid }`.\n\t\t\t */\n\t\t\tthis.$emit('link', {\n\t\t\t\tmailAccountId: this.selectedAccountId,\n\t\t\t\tmessageId: String(this.selectedMessageId),\n\t\t\t\tmessageUid: String(this.selectedMessageUid || ''),\n\t\t\t})\n\t\t},\n\n\t\tformatDate(raw) {\n\t\t\tif (!raw) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst d = new Date(raw)\n\t\t\tif (Number.isNaN(d.getTime())) {\n\t\t\t\treturn String(raw)\n\t\t\t}\n\t\t\treturn d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-email-picker {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n\tpadding: 4px 0;\n}\n\n.cn-email-picker__steps {\n\tdisplay: flex;\n\tgap: 16px;\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0 4px 8px;\n\tborder-bottom: 1px solid var(--color-border);\n}\n\n.cn-email-picker__step {\n\tfont-size: 0.85em;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-email-picker__step--active {\n\tcolor: var(--color-main-text);\n\tfont-weight: 600;\n}\n\n.cn-email-picker__step--done {\n\tcolor: var(--color-success, #46ba61);\n}\n\n.cn-email-picker__error {\n\tmargin: 4px 0;\n}\n\n.cn-email-picker__panel {\n\tmin-height: 180px;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n}\n\n.cn-email-picker__filter {\n\twidth: 100%;\n}\n\n.cn-email-picker__list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n\tmax-height: 360px;\n\toverflow-y: auto;\n}\n\n.cn-email-picker__list--messages {\n\tmax-height: 320px;\n}\n\n.cn-email-picker__row {\n\tborder-radius: var(--border-radius);\n}\n\n.cn-email-picker__row--selected {\n\tbackground: var(--color-primary-element-light);\n}\n\n.cn-email-picker__row-button {\n\tdisplay: flex;\n\twidth: 100%;\n\talign-items: center;\n\tgap: 10px;\n\tpadding: 8px 10px;\n\tbackground: transparent;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tcolor: var(--color-main-text);\n\tcursor: pointer;\n\ttext-align: left;\n}\n\n.cn-email-picker__row-button:hover {\n\tbackground: var(--color-background-hover);\n}\n\n.cn-email-picker__row--selected .cn-email-picker__row-button {\n\tborder-color: var(--color-primary-element);\n}\n\n.cn-email-picker__row-button--message {\n\talign-items: flex-start;\n}\n\n.cn-email-picker__row-label {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tflex: 1 1 auto;\n\toverflow: hidden;\n}\n\n.cn-email-picker__row-primary {\n\tfont-weight: 500;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n}\n\n.cn-email-picker__row-secondary {\n\tfont-size: 0.8em;\n\tcolor: var(--color-text-maxcontrast);\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n}\n\n.cn-email-picker__message {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tflex: 1 1 auto;\n\toverflow: hidden;\n}\n\n.cn-email-picker__message-subject {\n\tfont-weight: 500;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n}\n\n.cn-email-picker__message-meta {\n\tdisplay: flex;\n\tgap: 8px;\n\tjustify-content: space-between;\n\tfont-size: 0.8em;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-email-picker__message-sender {\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n}\n\n.cn-email-picker__message-date {\n\tflex-shrink: 0;\n}\n\n.cn-email-picker__load-more {\n\tmargin-top: 4px;\n}\n</style>\n"],"names":["_createBlock","_createElementVNode","_openBlock","_createElementBlock","_Fragment","_renderList","_createVNode","_toDisplayString"],"mappings":";;AA4DO,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,iBAAiB,GAAA;AACvB,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,wBAAwB,GAAA;;;AAgBN,EAAA,KAAI,EAAC,wBAAU;;;;AAM/B,EAAA,KAAK,EAAC,uBAAY;;;AAQpB,MAAA,UAAA,GAAA,EAAA,KAAC,EAAA,4BAAA,GAAA;AACL,MAAA,UAAA,GAAA,EAAA,KAAA,EAAA,8BAAA,GAAA;;;AACD,EAAA,KAAA,EAAA,gCAAA;;;;AAO2B,EAAA,KAAG,EAAA,wBAAA;;;;AAMvB,EAAA,KAAK,EAAC,uBAAuB;;;AAQ/B,MAAA,WAAA,GAAA,EAAA,KAAK,EAAC,4BAAc,GAAA;AACnB,MAAA,WAAA,GAAA,EAAA,KAAK,EAAC,8BAAyB,GAAA;;;AACG,EAAA,KAAK,EAAC,gCAAgC;;;;AAO7E,EAAA,KAAA,EAAA,wBAAA;;;;AAWM,EAAA,KAAA,EAAA,uDAAA;;;AAQF,MAAA,WAAA,GAAA,EAAA,KAAK,EAAC,0BAAgB,GAAA;AACtB,MAAA,WAAA,GAAA,EAAA,KAAA,EAAA,kCAAA,GAAA;AACC,MAAA,WAAA,GAAA,EAAA,KAAK,EAAC,+BAAS,GAAA;AACd,MAAA,WAAA,GAAA,EAAA,KAAK,EAAC,iCAAgB,GAAA;;;AACxB,EAAA,KAAA,EAAA,+BAAA;;;;;;;;;;;;;uBA3FRA;AACD,IAAA,IAAI,EAAC,MAAW,CAAA,WAAA;AACf,IAAA,IAAC,EAAC,QAAA;iBACM,EAAC,iBAAiB;AACzB,IAAA,SAAI,EAAE,QAAO,CAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAEb,QAAAC,kBAAA,CAUE,MAVF,UAUE,EAAA;UATFA,kBAEG,CAAA,IAAA,EAAA;AAFA,YAAA,KAAK,iBAAE,QAAS,CAAA,SAAA,CAAA,CAAA,CAAA,CAAA;6BACjB,QAAC,CAAA,CAAA,CAAA,eAAA,EAAA,YAAA,CAAA,CAAA,EAAA,CAAA,mBAAA;UAEDA,kBAEC,CAAA,IAAA,EAAA;AAFA,YAAA,KAAK,iBAAE,QAAS,CAAA,SAAA,CAAA,CAAA,CAAA,CAAA;6BACjB,QAAC,CAAA,CAAA,CAAA,eAAA,EAAA,YAAA,CAAA,CAAA,EAAA,CAAA,mBAAA;UAEHA,kBAEE,CAAA,IAAA,EAAA;AAFC,YAAA,KAAK,iBAAA,QAAA,CAAA,SAAA,CAAA,CAAA,CAAA,CAAA;6BACN,QAAC,CAAA,CAAA,CAAA,eAAA,EAAA,YAAA,CAAA,CAAA,EAAA,CAAA,mBAAA;;SAIY,KAAK,CAAA,KAAA;0BAAvBD,WAEa,CAAA,qBAAA,EAAA;;AAFY,cAAA,IAAI,EAAC,OAAO;AAAC,cAAA,KAAK,EAAC,wBAAkB;;+BAC7D,MAAW;gDAAR,KAAK,CAAA,KAAA,CAAA,EAAA,CAAA,YAAA;;;;;SAGM,KAAI,CAAA,IAAA,KAAA,CAAA;AAAjB,aAAAE,SAAA,EAAA,EAAAC,kBAAA,CAqBQ,WArBR,UAqBQ,EAAA;eApBY,KAAO,CAAA,OAAA;gCAA3BH,WAA+B,CAAA,wBAAA,EAAA,EAAA,GAAA,EAAA,CAAA,EAAA,CAAA;AAE7B,kBAAA,CAAA,KAAA,CAAA,QAAA,CAAA,MAAA,KAAA,CAAA;kCADHA,WAGa,CAAA,yBAAA,EAAA;;AADX,sBAAA,IAAI,EAAE,QAAC,CAAA,CAAA,CAAA,eAAA,EAAA,kBAAA,CAAA;AACP,sBAAA,WAAW,EAAA,QAAA,CAAA,CAAA,CAAA,eAAA,EAAA,iDAAA,CAAA;;qDAeR,gBAAA,EAAA;wCAbHG,kBAYI,CAAAC,QAAA,EAAA,IAAA,EAAAC,UAAA,CAXc,KAAQ,CAAA,QAAA,EAAA,QAAZ,KAAA;6CADdF,kBAYI,CAAA,IAAA,EAAA;0BAVH,GAAG,EAAE,OAAO,CAAC,EAAE;AACf,0BAAA,KAAI,kBAAE,sBAAqB,EAAA,EAAA,gCAAA,EACgB,uBAAiB,KAAK,OAAK,CAAA,EAAA,EAAA,CAAA,CAAA;;0BACrEF;AAAM,4BAAA,IAAI,EAAC,QAAS;AAAA,4BAAA,KAAK,EAAC,6BAA6B;4BAAE,OAAK,EAAA,MAAA,KAAE,QAAW,CAAA,WAAA,CAAC,OAAO,CAAA,CAAA;;4BACpFK,WAAM,CAAA,uBAAA,EAAA,EAAA,IAAA,EAAA,EAAA,EAAA,CAAA;AACN,4BAAAL,kBAAA,CAGK,QAHL,UAGK,EAAA;AAFJ,8BAAAA,kBAAA,CAAC,MAAD,EAAA,UAAC,EAAAM,eAAA,CAAA,OAAA,CAAA,KAAA,CAAA,EAAA,CAAA,YAAA;+BACD,OAAA,CAAA,KAAA,IAAA,OAAA,CAAA,KAAA,KAAA,OAAA,CAAA,KAAA;AAAA,mCAAAL,SAAA,EAAA,EAAAC,kBAAA,CAAA,MAAA,EAAA,UAAA,EAAAI,eAAA,CAAA,OAAA,CAAA,KAAA,CAAA,EAAA,CAAA,YAAA;;;;;;;;aAOe,KAAI,CAAA,IAAA,KAAA,CAAA;AAAtB,eAAAL,SAAA,EAAA,EAAAC,kBAAA,CAqBQ,WArBR,UAqBQ,EAAA;;;AAlBG,oBAAA,CAAA,KAAA,CAAA,SAAE,CAAA,MAAA,KAAA,CAAA;oCADdH,WAGyC,CAAA,yBAAA,EAAA;;AADvC,wBAAA,IAAI,EAAE,QAAC,CAAA,CAAA,CAAA,eAAA,EAAA,cAAA,CAAA;AACP,wBAAA,WAAW,EAAE,QAAC,CAAA,CAAA,CAAA,eAAA,EAAA,0CAAA,CAAA;;AACf,uBAAAE,SAAA,EAAA,EAAAC,kBAAA,CAcI,MAdJ,WAcI,EAAA;0CAbJA,kBAYK,CAAAC,QAAA,EAAA,IAAA,EAAAC,UAAA,CAXH,KAAA,CAAA,SAAA,EAAA,CAAA,EAAA,KAAA;+CADFF,kBAYK,CAAA,IAAA,EAAA;4BAVH,GAAG,EAAE,EAAE,CAAC,EAAE;AACX,4BAAA,KAAK,kBAAC,sBAAM,EAAA,EAAA,gCAAA,EACe,qBAAA,KAAA,EAAA,CAAA,IAAA,EAAA,CAAA,CAAA;;4BAC3BF,kBAMS,CAAA,QAAA,EAAA;AAND,8BAAA,IAAI,EAAC,QAAQ;AAAC,8BAAA,KAAK,EAAC,6BAA6B;8BAAE,OAAK,EAAA,MAAA,KAAE,QAAS,CAAA,WAAA,CAAA,EAAA,CAAA,CAAA;;8BAC1EK,WAA4B,CAAA,wBAAA,EAAA,EAAZ,IAAG,EAAG,EAAE,EAAA,CAAA;AACxB,8BAAAL,kBAAA,CAGO,QAHP,WAGO,EAAA;AAFN,gCAAAA,kBAAA,CAAqC,MAArC,EAAA,WAAqC,EAAAM,eAAA,CAAA,EAAA,CAAA,WAAA,CAAA,EAAA,CAAA,YAAA;AACzB,gCAAA,CAAA,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,WAAW;AAAtC,qCAAAL,SAAA,EAAA,EAAAC,kBAAA,OAAA,EAAA,aAAkFI,eAAA,CAAA,EAAE,CAAC,IAAI,CAAA,EAAA,CAAA,YAAA;;;;;;;;eAOxF,KAAA,CAAA,IAAA,KAAA,CAAA;;kBACLD,WAG0B,CAAA,sBAAA,EAAA;;;AADxB,oBAAA,KAAK,EAAC,QAAA,CAAA,CAAA,CAAA,eAAA,EAAA,iBAAA,CAAA;AACP,oBAAA,KAAK,EAAC,yBAAmB;;mBAEL,KAAE,CAAA,OAAA,IAAA,KAAA,CAAA,QAAA,CAAA,MAAA,KAAA,CAAA;+CAAA,CAAA,wBAAA,EAAA,EAAA,GAAA,EAAA,CAAA,EAAA,CAAA;AAEX,sBAAA,CAAA,QAAA,CAAA,gBAAc,CAAA,MAAA,KAAA,CAAA;sCADzBN,WAG4B,CAAA,yBAAA,EAAA;;AAD3B,0BAAA,IAAI,EAAE,QAAC,CAAA,CAAA,CAAA,eAAA,EAAA,aAAA,CAAA;AACP,0BAAA,WAAW,EAAE,QAAE,CAAA,CAAA,CAAA,eAAA,EAAA,oDAAA,CAAA;;yDAkBZ,iBAAA,EAAA;4CAhBJG,kBAeK,CAAAC,QAAA,EAAA,IAAA,EAAAC,UAAA,CAdI,QAAA,CAAA,gBAAA,EAAA,CAAD,GAAC,KAAA;iDADTF,kBAeK,CAAA,IAAA,EAAA;8BAbH,GAAG,EAAE,GAAA,CAAA,EAAA;AACN,8BAAA,KAAC,kBAAA,sBAAA,EAAA,EAAA,gCAAA;;8BAEDF,kBASS,CAAA,QAAA,EAAA;AATD,gCAAA,IAAI,EAAC,QAAC;AAAA,gCAAA,KAAA,EAAA,kEAAA;gCAAA,OAAA,EAAA,MAAA,KAAA,QAAA,CAAA,WAAA,CAAA,GAAA,CAAA,CAAA;;gCACbK,WAA2B,CAAA,uBAAA,EAAA,EAAZ,IAAI,EAAG,EAAC,EAAA,CAAA;AACvB,gCAAAL,kBAAA,CAMQ,QANR,WAMQ,EAAA;AALP,kCAAAA,kBAAA,CAAK,QAAL,WAAK,EAAAM,eAAA,CAAA,GAAA,CAAA,OAAA,IAAA,QAAA,CAAA,CAAA,CAAA,eAAA,EAAA,cAAA,CAAA,CAAA,EAAA,CAAA,YAAA;AACL,kCAAAN,kBAAA,CAGG,QAHH,WAGG,EAAA;AAFF,oCAAAA,kBAAA,CAA4B,QAA5B,WAA4B,EAAAM,eAAA,CAAA,GAAA,CAAA,MAAA,IAAA,QAAA,CAAA,CAAA,CAAA,eAAA,EAAA,gBAAA,CAAA,CAAA,EAAA,CAAA,YAAA;AACxB,oCAAA,CAAA,GAAA,CAAA,IAAA;AAAJ,yCAAAL,SAAA,EAAA,EAAAC,kBAAA,CAAI,QAAJ,WAAI,EAAAI,eAAA,CAAA,mBAAA,CAAA,GAAA,CAAA,IAAA,CAAA,CAAA,EAAA,CAAA,YAAA;;;;;;;;mBAQF,KAAC,CAAA,UAAA,KAAA,IAAA;oCADRP;;AAEC,wBAAA,OAAM,EAAA,UAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"CnEmailPicker.vue3.js","sources":["../../../../src/components/CnEmailPicker/CnEmailPicker.vue?vue&type=template&id=25ee69f0&lang.js"],"sourcesContent":["<!--\n CnEmailPicker — multi-step modal for picking an existing NC Mail\n message to link to the parent OR object.\n\n Flow (three steps + confirm):\n 1. Choose an account — GET /api/integrations/email/accounts\n 2. Choose a mailbox — GET /api/integrations/email/accounts/{id}/mailboxes\n 3. Browse messages — GET /api/integrations/email/accounts/{id}/messages?mailbox=\n (cursor pagination + client-side search filter)\n 4. Confirm — emit `link` with `{ mailAccountId, messageId, messageUid }`\n\n All API calls are wrapped in best-effort try/catch so a transient\n Mail failure surfaces a user-visible inline error rather than a modal\n crash. The modal stays open across errors so the user can retry\n without losing context.\n\n ADR-004: modal lives in its own .vue file under\n `src/components/CnEmailPicker/`. Picker UX is dialog-shaped so this\n variant uses NcDialog (matches CnDeckCardPicker).\n\n ADR-019: drives the `email` integration leaf's \"link existing\"\n surface; emits `link` so the parent (CnEmailTab) can POST the\n selection to the OR endpoint.\n\n SPDX-License-Identifier: EUPL-1.2\n SPDX-FileCopyrightText: 2026 Conduction B.V.\n-->\n<template>\n\t<NcDialog\n\t\t:name=\"dialogTitle\"\n\t\tsize=\"normal\"\n\t\tdata-testid=\"cn-email-picker\"\n\t\t@closing=\"onClose\">\n\t\t<div class=\"cn-email-picker\">\n\t\t\t<ol class=\"cn-email-picker__steps\">\n\t\t\t\t<li :class=\"stepClass(1)\">\n\t\t\t\t\t{{ t('nextcloud-vue', '1. Account') }}\n\t\t\t\t</li>\n\t\t\t\t<li :class=\"stepClass(2)\">\n\t\t\t\t\t{{ t('nextcloud-vue', '2. Mailbox') }}\n\t\t\t\t</li>\n\t\t\t\t<li :class=\"stepClass(3)\">\n\t\t\t\t\t{{ t('nextcloud-vue', '3. Message') }}\n\t\t\t\t</li>\n\t\t\t</ol>\n\n\t\t\t<NcNoteCard v-if=\"error\" type=\"error\" class=\"cn-email-picker__error\">\n\t\t\t\t{{ error }}\n\t\t\t</NcNoteCard>\n\n\t\t\t<section v-if=\"step === 1\" class=\"cn-email-picker__panel\">\n\t\t\t\t<NcLoadingIcon v-if=\"loading\" />\n\t\t\t\t<NcEmptyContent\n\t\t\t\t\tv-else-if=\"accounts.length === 0\"\n\t\t\t\t\t:name=\"t('nextcloud-vue', 'No mail accounts')\"\n\t\t\t\t\t:description=\"t('nextcloud-vue', 'NC Mail has no accounts configured for you yet.')\" />\n\t\t\t\t<ul v-else class=\"cn-email-picker__list\">\n\t\t\t\t\t<li\n\t\t\t\t\t\tv-for=\"account in accounts\"\n\t\t\t\t\t\t:key=\"account.id\"\n\t\t\t\t\t\tclass=\"cn-email-picker__row\"\n\t\t\t\t\t\t:class=\"{ 'cn-email-picker__row--selected': selectedAccountId === account.id }\">\n\t\t\t\t\t\t<button type=\"button\" class=\"cn-email-picker__row-button\" @click=\"pickAccount(account)\">\n\t\t\t\t\t\t\t<EmailOutline :size=\"20\" />\n\t\t\t\t\t\t\t<span class=\"cn-email-picker__row-label\">\n\t\t\t\t\t\t\t\t<span class=\"cn-email-picker__row-primary\">{{ account.label }}</span>\n\t\t\t\t\t\t\t\t<span v-if=\"account.email && account.email !== account.label\" class=\"cn-email-picker__row-secondary\">{{ account.email }}</span>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t</section>\n\n\t\t\t<section v-else-if=\"step === 2\" class=\"cn-email-picker__panel\">\n\t\t\t\t<NcLoadingIcon v-if=\"loading\" />\n\t\t\t\t<NcEmptyContent\n\t\t\t\t\tv-else-if=\"mailboxes.length === 0\"\n\t\t\t\t\t:name=\"t('nextcloud-vue', 'No mailboxes')\"\n\t\t\t\t\t:description=\"t('nextcloud-vue', 'This account has no mailboxes to browse.')\" />\n\t\t\t\t<ul v-else class=\"cn-email-picker__list\">\n\t\t\t\t\t<li\n\t\t\t\t\t\tv-for=\"mb in mailboxes\"\n\t\t\t\t\t\t:key=\"mb.id\"\n\t\t\t\t\t\tclass=\"cn-email-picker__row\"\n\t\t\t\t\t\t:class=\"{ 'cn-email-picker__row--selected': selectedMailbox === mb.name }\">\n\t\t\t\t\t\t<button type=\"button\" class=\"cn-email-picker__row-button\" @click=\"pickMailbox(mb)\">\n\t\t\t\t\t\t\t<FolderOutline :size=\"20\" />\n\t\t\t\t\t\t\t<span class=\"cn-email-picker__row-label\">\n\t\t\t\t\t\t\t\t<span class=\"cn-email-picker__row-primary\">{{ mb.displayName }}</span>\n\t\t\t\t\t\t\t\t<span v-if=\"mb.name !== mb.displayName\" class=\"cn-email-picker__row-secondary\">{{ mb.name }}</span>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t</section>\n\n\t\t\t<section v-else-if=\"step === 3\" class=\"cn-email-picker__panel\">\n\t\t\t\t<NcTextField\n\t\t\t\t\tv-model=\"filterText\"\n\t\t\t\t\t:label=\"t('nextcloud-vue', 'Filter messages')\"\n\t\t\t\t\tclass=\"cn-email-picker__filter\" />\n\n\t\t\t\t<NcLoadingIcon v-if=\"loading && messages.length === 0\" />\n\t\t\t\t<NcEmptyContent\n\t\t\t\t\tv-else-if=\"filteredMessages.length === 0\"\n\t\t\t\t\t:name=\"t('nextcloud-vue', 'No messages')\"\n\t\t\t\t\t:description=\"t('nextcloud-vue', 'This mailbox has no messages matching your filter.')\" />\n\t\t\t\t<ul v-else class=\"cn-email-picker__list cn-email-picker__list--messages\">\n\t\t\t\t\t<li\n\t\t\t\t\t\tv-for=\"msg in filteredMessages\"\n\t\t\t\t\t\t:key=\"msg.id\"\n\t\t\t\t\t\tclass=\"cn-email-picker__row\"\n\t\t\t\t\t\t:class=\"{ 'cn-email-picker__row--selected': selectedMessageId === msg.id }\">\n\t\t\t\t\t\t<button type=\"button\" class=\"cn-email-picker__row-button cn-email-picker__row-button--message\" @click=\"pickMessage(msg)\">\n\t\t\t\t\t\t\t<EmailOutline :size=\"18\" />\n\t\t\t\t\t\t\t<span class=\"cn-email-picker__message\">\n\t\t\t\t\t\t\t\t<span class=\"cn-email-picker__message-subject\">{{ msg.subject || t('nextcloud-vue', '(no subject)') }}</span>\n\t\t\t\t\t\t\t\t<span class=\"cn-email-picker__message-meta\">\n\t\t\t\t\t\t\t\t\t<span class=\"cn-email-picker__message-sender\">{{ msg.sender || t('nextcloud-vue', 'Unknown sender') }}</span>\n\t\t\t\t\t\t\t\t\t<span v-if=\"msg.date\" class=\"cn-email-picker__message-date\">{{ formatDate(msg.date) }}</span>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\n\t\t\t\t<NcButton\n\t\t\t\t\tv-if=\"nextCursor !== null\"\n\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t:disabled=\"loadingMore\"\n\t\t\t\t\tclass=\"cn-email-picker__load-more\"\n\t\t\t\t\t@click=\"loadMoreMessages\">\n\t\t\t\t\t<template v-if=\"loadingMore\" #icon>\n\t\t\t\t\t\t<NcLoadingIcon :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ t('nextcloud-vue', 'Load more') }}\n\t\t\t\t</NcButton>\n\t\t\t</section>\n\t\t</div>\n\n\t\t<template #actions>\n\t\t\t<NcButton v-if=\"step > 1\" @click=\"goBack\">\n\t\t\t\t{{ t('nextcloud-vue', 'Back') }}\n\t\t\t</NcButton>\n\t\t\t<NcButton @click=\"onClose\">\n\t\t\t\t{{ t('nextcloud-vue', 'Cancel') }}\n\t\t\t</NcButton>\n\t\t\t<NcButton\n\t\t\t\tv-if=\"step === 3\"\n\t\t\t\tvariant=\"primary\"\n\t\t\t\t:disabled=\"!selectedMessageId\"\n\t\t\t\t@click=\"confirm\">\n\t\t\t\t{{ t('nextcloud-vue', 'Link message') }}\n\t\t\t</NcButton>\n\t\t</template>\n\t</NcDialog>\n</template>\n\n<script>\n/**\n * SPDX-License-Identifier: EUPL-1.2\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n *\n * CnEmailPicker — pick an existing Mail message via account, mailbox,\n * message. Emits `link` with `{ mailAccountId, messageId, messageUid }`.\n *\n * @see ADR-019 (pluggable integrations) and ADR-022 (sidebar tabs)\n */\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcDialog, NcEmptyContent, NcLoadingIcon, NcNoteCard, NcTextField } from '@nextcloud/vue'\nimport EmailOutline from 'vue-material-design-icons/EmailOutline.vue'\nimport FolderOutline from 'vue-material-design-icons/FolderOutline.vue'\nimport { buildHeaders, prefixUrl } from '../../utils/index.js'\nimport { userLocale } from '../../utils/userLocale.js'\n\nexport default {\n\tname: 'CnEmailPicker',\n\n\tcomponents: { NcButton, NcDialog, NcEmptyContent, NcLoadingIcon, NcNoteCard, NcTextField, EmailOutline, FolderOutline },\n\n\tprops: {\n\t\t/** Base API URL for OR. */\n\t\tapiBase: { type: String, default: '/apps/openregister/api' },\n\t\t/** Pre-translated dialog title. */\n\t\tdialogTitle: { type: String, default: () => t('nextcloud-vue', 'Link an existing email') },\n\t\t/** Page size for the message list (load-more pagination). */\n\t\tpageSize: { type: Number, default: 50 },\n\t},\n\n\temits: ['close', 'link'],\n\n\tdata() {\n\t\treturn {\n\t\t\tstep: 1,\n\t\t\tloading: false,\n\t\t\tloadingMore: false,\n\t\t\terror: '',\n\t\t\taccounts: [],\n\t\t\tmailboxes: [],\n\t\t\tmessages: [],\n\t\t\tselectedAccountId: null,\n\t\t\tselectedMailbox: null,\n\t\t\tselectedMessageId: null,\n\t\t\tselectedMessageUid: null,\n\t\t\tnextCursor: null,\n\t\t\tfilterText: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\tfilteredMessages() {\n\t\t\tconst needle = this.filterText.trim().toLowerCase()\n\t\t\tif (needle === '') {\n\t\t\t\treturn this.messages\n\t\t\t}\n\t\t\treturn this.messages.filter((msg) => {\n\t\t\t\tconst subj = (msg.subject || '').toLowerCase()\n\t\t\t\tconst sender = (msg.sender || '').toLowerCase()\n\t\t\t\treturn subj.includes(needle) || sender.includes(needle)\n\t\t\t})\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.fetchAccounts()\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\tstepClass(step) {\n\t\t\treturn {\n\t\t\t\t'cn-email-picker__step': true,\n\t\t\t\t'cn-email-picker__step--active': this.step === step,\n\t\t\t\t'cn-email-picker__step--done': this.step > step,\n\t\t\t}\n\t\t},\n\n\t\tasync fetchAccounts() {\n\t\t\tthis.loading = true\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(`${this.apiBase}/integrations/email/accounts`), {\n\t\t\t\t\theaders: buildHeaders(),\n\t\t\t\t})\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tconst data = await response.json()\n\t\t\t\t\tthis.accounts = data.results || []\n\t\t\t\t} else if (response.status === 501) {\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'Nextcloud Mail is not installed.')\n\t\t\t\t} else {\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'Could not load mail accounts.')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnEmailPicker] fetch accounts failed', err)\n\t\t\t\tthis.error = t('nextcloud-vue', 'Could not load mail accounts.')\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\tasync fetchMailboxes(accountId) {\n\t\t\tthis.loading = true\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(`${this.apiBase}/integrations/email/accounts/${accountId}/mailboxes`), {\n\t\t\t\t\theaders: buildHeaders(),\n\t\t\t\t})\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tconst data = await response.json()\n\t\t\t\t\tthis.mailboxes = data.results || []\n\t\t\t\t} else {\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'Could not load mailboxes.')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnEmailPicker] fetch mailboxes failed', err)\n\t\t\t\tthis.error = t('nextcloud-vue', 'Could not load mailboxes.')\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\tasync fetchMessages(accountId, mailbox, append = false) {\n\t\t\tif (append === true) {\n\t\t\t\tthis.loadingMore = true\n\t\t\t} else {\n\t\t\t\tthis.loading = true\n\t\t\t\tthis.messages = []\n\t\t\t\tthis.nextCursor = null\n\t\t\t}\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tconst params = new URLSearchParams({\n\t\t\t\t\tmailbox,\n\t\t\t\t\tlimit: String(this.pageSize),\n\t\t\t\t})\n\t\t\t\tif (append === true && this.nextCursor !== null) {\n\t\t\t\t\tparams.set('cursor', String(this.nextCursor))\n\t\t\t\t}\n\t\t\t\tconst response = await fetch(\n\t\t\t\t\tprefixUrl(`${this.apiBase}/integrations/email/accounts/${accountId}/messages?${params.toString()}`),\n\t\t\t\t\t{ headers: buildHeaders() },\n\t\t\t\t)\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tconst data = await response.json()\n\t\t\t\t\tconst items = Array.isArray(data.items) ? data.items : []\n\t\t\t\t\tthis.messages = append === true ? [...this.messages, ...items] : items\n\t\t\t\t\tthis.nextCursor = data.nextCursor !== undefined ? data.nextCursor : null\n\t\t\t\t} else {\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'Could not load messages.')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnEmailPicker] fetch messages failed', err)\n\t\t\t\tthis.error = t('nextcloud-vue', 'Could not load messages.')\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t\tthis.loadingMore = false\n\t\t\t}\n\t\t},\n\n\t\tpickAccount(account) {\n\t\t\tthis.selectedAccountId = account.id\n\t\t\tthis.selectedMailbox = null\n\t\t\tthis.selectedMessageId = null\n\t\t\tthis.selectedMessageUid = null\n\t\t\tthis.mailboxes = []\n\t\t\tthis.messages = []\n\t\t\tthis.step = 2\n\t\t\tthis.fetchMailboxes(account.id)\n\t\t},\n\n\t\tpickMailbox(mailbox) {\n\t\t\tthis.selectedMailbox = mailbox.name\n\t\t\tthis.selectedMessageId = null\n\t\t\tthis.selectedMessageUid = null\n\t\t\tthis.messages = []\n\t\t\tthis.filterText = ''\n\t\t\tthis.step = 3\n\t\t\tthis.fetchMessages(this.selectedAccountId, mailbox.name)\n\t\t},\n\n\t\tpickMessage(msg) {\n\t\t\tthis.selectedMessageId = msg.id\n\t\t\tthis.selectedMessageUid = msg.uid || ''\n\t\t},\n\n\t\tloadMoreMessages() {\n\t\t\tif (this.nextCursor === null || this.selectedAccountId === null || this.selectedMailbox === null) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.fetchMessages(this.selectedAccountId, this.selectedMailbox, true)\n\t\t},\n\n\t\tgoBack() {\n\t\t\tif (this.step === 3) {\n\t\t\t\tthis.selectedMessageId = null\n\t\t\t\tthis.selectedMessageUid = null\n\t\t\t\tthis.messages = []\n\t\t\t\tthis.nextCursor = null\n\t\t\t\tthis.filterText = ''\n\t\t\t\tthis.step = 2\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.step === 2) {\n\t\t\t\tthis.selectedMailbox = null\n\t\t\t\tthis.mailboxes = []\n\t\t\t\tthis.step = 1\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Dismiss the dialog.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonClose() {\n\t\t\t/**\n\t\t\t * @event close Emitted when the dialog should be closed (cancel or close button).\n\t\t\t */\n\t\t\tthis.$emit('close')\n\t\t},\n\n\t\tconfirm() {\n\t\t\tif (this.selectedMessageId === null || this.selectedAccountId === null) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event link Emitted when the user confirms the selection. Payload: `{ mailAccountId, messageId, messageUid }`.\n\t\t\t */\n\t\t\tthis.$emit('link', {\n\t\t\t\tmailAccountId: this.selectedAccountId,\n\t\t\t\tmessageId: String(this.selectedMessageId),\n\t\t\t\tmessageUid: String(this.selectedMessageUid || ''),\n\t\t\t})\n\t\t},\n\n\t\tformatDate(raw) {\n\t\t\tif (!raw) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst d = new Date(raw)\n\t\t\tif (Number.isNaN(d.getTime())) {\n\t\t\t\treturn String(raw)\n\t\t\t}\n\t\t\treturn d.toLocaleDateString(userLocale(), { year: 'numeric', month: 'short', day: 'numeric' })\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-email-picker {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n\tpadding: 4px 0;\n}\n\n.cn-email-picker__steps {\n\tdisplay: flex;\n\tgap: 16px;\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0 4px 8px;\n\tborder-bottom: 1px solid var(--color-border);\n}\n\n.cn-email-picker__step {\n\tfont-size: 0.85em;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-email-picker__step--active {\n\tcolor: var(--color-main-text);\n\tfont-weight: 600;\n}\n\n.cn-email-picker__step--done {\n\tcolor: var(--color-success, #46ba61);\n}\n\n.cn-email-picker__error {\n\tmargin: 4px 0;\n}\n\n.cn-email-picker__panel {\n\tmin-height: 180px;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n}\n\n.cn-email-picker__filter {\n\twidth: 100%;\n}\n\n.cn-email-picker__list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n\tmax-height: 360px;\n\toverflow-y: auto;\n}\n\n.cn-email-picker__list--messages {\n\tmax-height: 320px;\n}\n\n.cn-email-picker__row {\n\tborder-radius: var(--border-radius);\n}\n\n.cn-email-picker__row--selected {\n\tbackground: var(--color-primary-element-light);\n}\n\n.cn-email-picker__row-button {\n\tdisplay: flex;\n\twidth: 100%;\n\talign-items: center;\n\tgap: 10px;\n\tpadding: 8px 10px;\n\tbackground: transparent;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tcolor: var(--color-main-text);\n\tcursor: pointer;\n\ttext-align: left;\n}\n\n.cn-email-picker__row-button:hover {\n\tbackground: var(--color-background-hover);\n}\n\n.cn-email-picker__row--selected .cn-email-picker__row-button {\n\tborder-color: var(--color-primary-element);\n}\n\n.cn-email-picker__row-button--message {\n\talign-items: flex-start;\n}\n\n.cn-email-picker__row-label {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tflex: 1 1 auto;\n\toverflow: hidden;\n}\n\n.cn-email-picker__row-primary {\n\tfont-weight: 500;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n}\n\n.cn-email-picker__row-secondary {\n\tfont-size: 0.8em;\n\tcolor: var(--color-text-maxcontrast);\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n}\n\n.cn-email-picker__message {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tflex: 1 1 auto;\n\toverflow: hidden;\n}\n\n.cn-email-picker__message-subject {\n\tfont-weight: 500;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n}\n\n.cn-email-picker__message-meta {\n\tdisplay: flex;\n\tgap: 8px;\n\tjustify-content: space-between;\n\tfont-size: 0.8em;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-email-picker__message-sender {\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n}\n\n.cn-email-picker__message-date {\n\tflex-shrink: 0;\n}\n\n.cn-email-picker__load-more {\n\tmargin-top: 4px;\n}\n</style>\n"],"names":["_createBlock","_createElementVNode","_openBlock","_createElementBlock","_Fragment","_renderList","_createVNode","_toDisplayString"],"mappings":"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"}
@@ -2,7 +2,7 @@ import script from './CnFederationStatus.vue2.js';
2
2
  import { render } from './CnFederationStatus.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-94f90dd6";
5
+ script.__scopeId = "data-v-479dc9bd";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -1,3 +1,5 @@
1
+ import { userLocale } from '../../utils/userLocale.js';
2
+
1
3
  /**
2
4
  * CnFederationStatus — Federation-status widget for federated
3
5
  * directories. Renders an aggregate summary (`up` / `degraded` /
@@ -155,7 +157,7 @@ var script = {
155
157
  if (Number.isNaN(d.getTime())) {
156
158
  return iso
157
159
  }
158
- return d.toLocaleString()
160
+ return d.toLocaleString(userLocale())
159
161
  } catch {
160
162
  return iso
161
163
  }
@@ -1 +1 @@
1
- {"version":3,"file":"CnFederationStatus.vue2.js","sources":["../../../../src/components/CnFederationStatus/CnFederationStatus.vue"],"sourcesContent":["<template>\n\t<div class=\"cn-federation-status\" data-testid=\"cn-federation-status\">\n\t\t<header v-if=\"title || description\" class=\"cn-federation-status__header\">\n\t\t\t<h3 v-if=\"title\" class=\"cn-federation-status__title\">\n\t\t\t\t{{ title }}\n\t\t\t</h3>\n\t\t\t<p v-if=\"description\" class=\"cn-federation-status__description\">\n\t\t\t\t{{ description }}\n\t\t\t</p>\n\t\t</header>\n\n\t\t<!-- Aggregate counts — the KPI part of this component.\n\n\t\t Each count is a canonical INLINE KPI (src/css/kpi-card.css): the\n\t\t card's number weight, colour and label scale at the compact size,\n\t\t because a roll-up above a list is one line of context rather than\n\t\t four tiles. Splitting the count and the label into their own spans\n\t\t is what lets them take those two different treatments; before,\n\t\t both sat in one text node at one size and the number read as prose.\n\n\t\t The per-node list below is deliberately NOT a KPI surface — see the\n\t\t note in the stylesheet block. -->\n\t\t<div v-if=\"!hideSummary\" class=\"cn-federation-status__summary\" data-testid=\"cn-federation-status-summary\">\n\t\t\t<span class=\"cn-federation-status__summary-item cn-federation-status__summary-item--up cn-kpi-inline cn-kpi-inline--success\">\n\t\t\t\t<span class=\"cn-federation-status__dot cn-federation-status__dot--up\" />\n\t\t\t\t<span class=\"cn-kpi-inline__value\">{{ counts.up }}</span>\n\t\t\t\t<span class=\"cn-kpi-inline__label\">{{ upLabel }}</span>\n\t\t\t</span>\n\t\t\t<span class=\"cn-federation-status__summary-item cn-federation-status__summary-item--degraded cn-kpi-inline cn-kpi-inline--warning\">\n\t\t\t\t<span class=\"cn-federation-status__dot cn-federation-status__dot--degraded\" />\n\t\t\t\t<span class=\"cn-kpi-inline__value\">{{ counts.degraded }}</span>\n\t\t\t\t<span class=\"cn-kpi-inline__label\">{{ degradedLabel }}</span>\n\t\t\t</span>\n\t\t\t<span class=\"cn-federation-status__summary-item cn-federation-status__summary-item--down cn-kpi-inline cn-kpi-inline--error\">\n\t\t\t\t<span class=\"cn-federation-status__dot cn-federation-status__dot--down\" />\n\t\t\t\t<span class=\"cn-kpi-inline__value\">{{ counts.down }}</span>\n\t\t\t\t<span class=\"cn-kpi-inline__label\">{{ downLabel }}</span>\n\t\t\t</span>\n\t\t\t<span v-if=\"counts.unknown > 0\"\n\t\t\t\tclass=\"cn-federation-status__summary-item cn-federation-status__summary-item--unknown cn-kpi-inline cn-kpi-inline--muted\">\n\t\t\t\t<span class=\"cn-federation-status__dot cn-federation-status__dot--unknown\" />\n\t\t\t\t<span class=\"cn-kpi-inline__value\">{{ counts.unknown }}</span>\n\t\t\t\t<span class=\"cn-kpi-inline__label\">{{ unknownLabel }}</span>\n\t\t\t</span>\n\t\t</div>\n\n\t\t<!-- Empty state. -->\n\t\t<p v-if=\"nodes.length === 0\" class=\"cn-federation-status__empty\">\n\t\t\t{{ emptyLabel }}\n\t\t</p>\n\n\t\t<!-- Per-node list. -->\n\t\t<ul v-else class=\"cn-federation-status__list\">\n\t\t\t<li v-for=\"node in sortedNodes\"\n\t\t\t\t:key=\"node.id || node.name\"\n\t\t\t\tclass=\"cn-federation-status__node\"\n\t\t\t\t:class=\"'cn-federation-status__node--' + normaliseStatus(node.status)\"\n\t\t\t\t:data-status=\"normaliseStatus(node.status)\"\n\t\t\t\t@click=\"onNodeClick(node)\">\n\t\t\t\t<!-- `__dot` as well as `__node-dot`: the size, the shape and the\n\t\t\t\t accent-driven fill all live on `__dot`, and the row's dot\n\t\t\t\t carried only the `--status` modifier — which after this change\n\t\t\t\t sets a custom property and nothing else. Without the base class\n\t\t\t\t the row's dot was a zero-by-zero span the whole time: a\n\t\t\t\t coloured background on an empty inline element paints nothing.\n\t\t\t\t Same class, same dot, in the strip and in the row. -->\n\t\t\t\t<span class=\"cn-federation-status__node-dot cn-federation-status__dot\"\n\t\t\t\t\t:class=\"'cn-federation-status__dot--' + normaliseStatus(node.status)\" />\n\t\t\t\t<div class=\"cn-federation-status__node-body\">\n\t\t\t\t\t<span class=\"cn-federation-status__node-name\">{{ node.name || node.id }}</span>\n\t\t\t\t\t<small v-if=\"node.url\" class=\"cn-federation-status__node-url\">{{ node.url }}</small>\n\t\t\t\t\t<small v-if=\"node.message\" class=\"cn-federation-status__node-message\">{{ node.message }}</small>\n\t\t\t\t\t<small v-if=\"node.lastChecked\" class=\"cn-federation-status__node-checked\">\n\t\t\t\t\t\t{{ lastCheckedLabel }}: {{ formatTimestamp(node.lastChecked) }}\n\t\t\t\t\t</small>\n\t\t\t\t</div>\n\t\t\t\t<span class=\"cn-federation-status__node-label\">{{ statusLabel(node.status) }}</span>\n\t\t\t</li>\n\t\t</ul>\n\t</div>\n</template>\n\n<script>\n// The canonical KPI scale (`--cn-kpi-*`) lives in one stylesheet. Imported\n// here as well as from css/index.css so the tokens resolve even when the\n// consuming app pulls in components individually.\nimport '../../css/kpi-card.css'\n/**\n * CnFederationStatus — Federation-status widget for federated\n * directories. Renders an aggregate summary (`up` / `degraded` /\n * `down` / `unknown` counts) and a per-node list with status dot,\n * URL, last-checked timestamp, and click-emit for drilldown.\n *\n * Status values are normalised: `up` / `online` / `ok` map to `up`;\n * `degraded` / `partial` map to `degraded`; `down` / `offline` /\n * `error` map to `down`; anything else maps to `unknown`. Unknown\n * statuses are tolerated so consumers can ship without an\n * exhaustive enum.\n *\n * ```vue\n * <CnFederationStatus\n * title=\"Federation directory\"\n * :nodes=\"[\n * { id: 'a', name: 'Node A', url: 'https://a.example.org', status: 'up' },\n * { id: 'b', name: 'Node B', url: 'https://b.example.org', status: 'degraded', message: 'High latency' },\n * { id: 'c', name: 'Node C', url: 'https://c.example.org', status: 'down', lastChecked: '2026-05-20T11:00:00Z' },\n * ]\"\n * @node-click=\"openNodeDetail\" />\n * ```\n */\nexport default {\n\tname: 'CnFederationStatus',\n\tprops: {\n\t\t/**\n\t\t * Federation nodes to render. Each entry:\n\t\t * `{ id?, name?, url?, status, message?, lastChecked? }`.\n\t\t * Either `id` or `name` is required for the v-for key.\n\t\t *\n\t\t * @type {Array<{id?:string,name?:string,url?:string,status:string,message?:string,lastChecked?:string}>}\n\t\t */\n\t\tnodes: { type: Array, default: () => [] },\n\t\t/** Optional widget title rendered as `<h3>`. */\n\t\ttitle: { type: String, default: '' },\n\t\t/** Optional widget description rendered under the title. */\n\t\tdescription: { type: String, default: '' },\n\t\t/** Hide the aggregate summary row. */\n\t\thideSummary: { type: Boolean, default: false },\n\t\t/** Label for the `up` summary chip. */\n\t\tupLabel: { type: String, default: 'up' },\n\t\t/** Label for the `degraded` summary chip. */\n\t\tdegradedLabel: { type: String, default: 'degraded' },\n\t\t/** Label for the `down` summary chip. */\n\t\tdownLabel: { type: String, default: 'down' },\n\t\t/** Label for the `unknown` summary chip. */\n\t\tunknownLabel: { type: String, default: 'unknown' },\n\t\t/** Empty-state text when `nodes[]` is empty. */\n\t\temptyLabel: { type: String, default: 'No federation nodes registered.' },\n\t\t/** Label preceding the per-node last-checked timestamp. */\n\t\tlastCheckedLabel: { type: String, default: 'Last checked' },\n\t\t/**\n\t\t * Sort key. `'status'` puts `down` first, then `degraded`,\n\t\t * then `unknown`, then `up`. `'name'` sorts alphabetically.\n\t\t * `'none'` keeps the input order.\n\t\t *\n\t\t * @type {'status'|'name'|'none'}\n\t\t */\n\t\tsort: { type: String, default: 'status' },\n\t},\n\n\temits: ['node-click'],\n\tcomputed: {\n\t\t/**\n\t\t * Status counts across all nodes.\n\t\t *\n\t\t * @return {{up:number,degraded:number,down:number,unknown:number}}\n\t\t */\n\t\tcounts() {\n\t\t\tconst out = { up: 0, degraded: 0, down: 0, unknown: 0 }\n\t\t\tfor (const n of this.nodes) {\n\t\t\t\tconst s = this.normaliseStatus(n.status)\n\t\t\t\tout[s] = (out[s] || 0) + 1\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * Nodes sorted per the `sort` prop.\n\t\t *\n\t\t * @return {Array} The sorted nodes.\n\t\t */\n\t\tsortedNodes() {\n\t\t\tif (this.sort === 'none') {\n\t\t\t\treturn this.nodes\n\t\t\t}\n\t\t\tconst arr = [...this.nodes]\n\t\t\tif (this.sort === 'name') {\n\t\t\t\tarr.sort((a, b) => (a.name || a.id || '').localeCompare(b.name || b.id || ''))\n\t\t\t\treturn arr\n\t\t\t}\n\t\t\tconst order = { down: 0, degraded: 1, unknown: 2, up: 3 }\n\t\t\tarr.sort((a, b) => {\n\t\t\t\tconst sa = this.normaliseStatus(a.status)\n\t\t\t\tconst sb = this.normaliseStatus(b.status)\n\t\t\t\treturn (order[sa] - order[sb]) || (a.name || '').localeCompare(b.name || '')\n\t\t\t})\n\t\t\treturn arr\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Map a free-form status string to the normalised\n\t\t * `up | degraded | down | unknown` set.\n\t\t *\n\t\t * @param {string} status Raw status from the node.\n\t\t * @return {'up'|'degraded'|'down'|'unknown'} Normalised.\n\t\t */\n\t\tnormaliseStatus(status) {\n\t\t\tconst s = (status || '').toLowerCase()\n\t\t\tif (['up', 'online', 'ok', 'healthy', 'available'].includes(s)) {\n\t\t\t\treturn 'up'\n\t\t\t}\n\t\t\tif (['degraded', 'partial', 'slow', 'warning'].includes(s)) {\n\t\t\t\treturn 'degraded'\n\t\t\t}\n\t\t\tif (['down', 'offline', 'error', 'failed', 'unhealthy'].includes(s)) {\n\t\t\t\treturn 'down'\n\t\t\t}\n\t\t\treturn 'unknown'\n\t\t},\n\n\t\t/**\n\t\t * Render-friendly label for a status value.\n\t\t *\n\t\t * @param {string} status Raw status.\n\t\t * @return {string} The label to show in the per-node row.\n\t\t */\n\t\tstatusLabel(status) {\n\t\t\tconst norm = this.normaliseStatus(status)\n\t\t\tif (norm === 'up') {\n\t\t\t\treturn this.upLabel\n\t\t\t}\n\t\t\tif (norm === 'degraded') {\n\t\t\t\treturn this.degradedLabel\n\t\t\t}\n\t\t\tif (norm === 'down') {\n\t\t\t\treturn this.downLabel\n\t\t\t}\n\t\t\treturn this.unknownLabel\n\t\t},\n\n\t\t/**\n\t\t * Format an ISO timestamp into a short locale string.\n\t\t *\n\t\t * @param {string} iso ISO datetime string.\n\t\t * @return {string} Locale-formatted timestamp, or the\n\t\t * original string if parsing fails.\n\t\t */\n\t\tformatTimestamp(iso) {\n\t\t\ttry {\n\t\t\t\tconst d = new Date(iso)\n\t\t\t\tif (Number.isNaN(d.getTime())) {\n\t\t\t\t\treturn iso\n\t\t\t\t}\n\t\t\t\treturn d.toLocaleString()\n\t\t\t} catch {\n\t\t\t\treturn iso\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Forward a node click as a `@node-click` event.\n\t\t *\n\t\t * @param {object} node The clicked node.\n\t\t * @return {void}\n\t\t */\n\t\tonNodeClick(node) {\n\t\t\t/**\n\t\t\t * @event node-click Emitted when the user clicks a\n\t\t\t * node row. Payload is the original node object.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('node-click', node)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n/*\n * TWO densities on purpose, and only ONE of them is a KPI.\n *\n * The summary strip IS a KPI surface, so it uses the canonical inline KPI from\n * src/css/kpi-card.css — the card's number weight, its accent colours and its\n * label scale, one size down. Nothing about it is declared locally except the\n * dot.\n *\n * The per-node list is NOT, and forcing it into KPI cards would be a false\n * uniformity. A KPI tile answers \"how many?\" with one number a reader takes in\n * at a glance; a node row answers \"which one, where, and when was it last\n * checked?\" — a name, a URL, a message and a timestamp, four fields that only\n * make sense read line by line. Boxing each of those in a 2rem-number card\n * would show a directory of forty nodes as forty tiles with no number on them.\n * What the list DOES share is the vocabulary: the same status dot, in the same\n * accent colours, so the strip and the rows agree on what \"degraded\" looks\n * like.\n */\n\n.cn-federation-status {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: var(--cn-kpi-stack-gap, 12px);\n\tpadding: var(--cn-kpi-padding, 16px);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--cn-kpi-radius, 10px);\n\tbackground: var(--color-main-background);\n}\n\n.cn-federation-status__title {\n\tmargin: 0;\n\tfont-size: var(--cn-kpi-section-title-size, 14px);\n\tfont-weight: var(--cn-kpi-title-weight, 600);\n}\n\n.cn-federation-status__description {\n\tmargin: 4px 0 0;\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n}\n\n.cn-federation-status__summary {\n\tdisplay: flex;\n\tgap: var(--cn-kpi-grid-gap, 16px);\n\tflex-wrap: wrap;\n\talign-items: baseline;\n}\n\n/* Layout and typography come from `cn-kpi-inline`, which every summary item\n also carries. The only thing left to say here is that the dot sits on the\n text baseline rather than on the number's. */\n.cn-federation-status__summary-item {\n\talign-items: center;\n}\n\n/* One dot rule, four colours, no repetition: each status modifier re-points\n the SAME accent token the inline KPI's number reads, so a dot can never be\n amber next to a red number. */\n.cn-federation-status__dot {\n\twidth: 10px;\n\theight: 10px;\n\tborder-radius: 50%;\n\tdisplay: inline-block;\n\tflex-shrink: 0;\n\tbackground: var(--cn-kpi-accent, var(--color-text-maxcontrast));\n}\n\n.cn-federation-status__dot--up {\n\t--cn-kpi-accent: var(--color-success);\n}\n\n.cn-federation-status__dot--degraded {\n\t--cn-kpi-accent: var(--color-warning);\n}\n\n.cn-federation-status__dot--down {\n\t--cn-kpi-accent: var(--color-error);\n}\n\n.cn-federation-status__dot--unknown {\n\t--cn-kpi-accent: var(--color-text-maxcontrast);\n}\n\n.cn-federation-status__empty {\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n\tfont-style: italic;\n\ttext-align: center;\n\tmargin: 16px 0;\n}\n\n.cn-federation-status__list {\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n}\n\n.cn-federation-status__node {\n\tdisplay: flex;\n\talign-items: flex-start;\n\tgap: 12px;\n\tpadding: 8px 12px;\n\tborder-radius: var(--border-radius);\n\tcursor: pointer;\n\tbackground: var(--color-background-hover);\n}\n\n.cn-federation-status__node:hover {\n\tbackground: var(--color-background-darker, var(--color-background-hover));\n}\n\n.cn-federation-status__node-dot {\n\tmargin-top: 6px;\n\tflex-shrink: 0;\n}\n\n.cn-federation-status__node-body {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tflex: 1 1 auto;\n\tmin-width: 0;\n}\n\n.cn-federation-status__node-name {\n\tfont-size: var(--cn-kpi-title-size, 14px);\n\tfont-weight: var(--cn-kpi-title-weight, 600);\n}\n\n.cn-federation-status__node-url {\n\tfont-family: monospace;\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n}\n\n.cn-federation-status__node-message {\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n}\n\n.cn-federation-status__node-checked {\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n}\n\n.cn-federation-status__node-label {\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\ttext-transform: uppercase;\n\tletter-spacing: 0.05em;\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n\tmargin-top: 6px;\n}\n</style>\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"CnFederationStatus.vue2.js","sources":["../../../../src/components/CnFederationStatus/CnFederationStatus.vue"],"sourcesContent":["<template>\n\t<div class=\"cn-federation-status\" data-testid=\"cn-federation-status\">\n\t\t<header v-if=\"title || description\" class=\"cn-federation-status__header\">\n\t\t\t<h3 v-if=\"title\" class=\"cn-federation-status__title\">\n\t\t\t\t{{ title }}\n\t\t\t</h3>\n\t\t\t<p v-if=\"description\" class=\"cn-federation-status__description\">\n\t\t\t\t{{ description }}\n\t\t\t</p>\n\t\t</header>\n\n\t\t<!-- Aggregate counts — the KPI part of this component.\n\n\t\t Each count is a canonical INLINE KPI (src/css/kpi-card.css): the\n\t\t card's number weight, colour and label scale at the compact size,\n\t\t because a roll-up above a list is one line of context rather than\n\t\t four tiles. Splitting the count and the label into their own spans\n\t\t is what lets them take those two different treatments; before,\n\t\t both sat in one text node at one size and the number read as prose.\n\n\t\t The per-node list below is deliberately NOT a KPI surface — see the\n\t\t note in the stylesheet block. -->\n\t\t<div v-if=\"!hideSummary\" class=\"cn-federation-status__summary\" data-testid=\"cn-federation-status-summary\">\n\t\t\t<span class=\"cn-federation-status__summary-item cn-federation-status__summary-item--up cn-kpi-inline cn-kpi-inline--success\">\n\t\t\t\t<span class=\"cn-federation-status__dot cn-federation-status__dot--up\" />\n\t\t\t\t<span class=\"cn-kpi-inline__value\">{{ counts.up }}</span>\n\t\t\t\t<span class=\"cn-kpi-inline__label\">{{ upLabel }}</span>\n\t\t\t</span>\n\t\t\t<span class=\"cn-federation-status__summary-item cn-federation-status__summary-item--degraded cn-kpi-inline cn-kpi-inline--warning\">\n\t\t\t\t<span class=\"cn-federation-status__dot cn-federation-status__dot--degraded\" />\n\t\t\t\t<span class=\"cn-kpi-inline__value\">{{ counts.degraded }}</span>\n\t\t\t\t<span class=\"cn-kpi-inline__label\">{{ degradedLabel }}</span>\n\t\t\t</span>\n\t\t\t<span class=\"cn-federation-status__summary-item cn-federation-status__summary-item--down cn-kpi-inline cn-kpi-inline--error\">\n\t\t\t\t<span class=\"cn-federation-status__dot cn-federation-status__dot--down\" />\n\t\t\t\t<span class=\"cn-kpi-inline__value\">{{ counts.down }}</span>\n\t\t\t\t<span class=\"cn-kpi-inline__label\">{{ downLabel }}</span>\n\t\t\t</span>\n\t\t\t<span v-if=\"counts.unknown > 0\"\n\t\t\t\tclass=\"cn-federation-status__summary-item cn-federation-status__summary-item--unknown cn-kpi-inline cn-kpi-inline--muted\">\n\t\t\t\t<span class=\"cn-federation-status__dot cn-federation-status__dot--unknown\" />\n\t\t\t\t<span class=\"cn-kpi-inline__value\">{{ counts.unknown }}</span>\n\t\t\t\t<span class=\"cn-kpi-inline__label\">{{ unknownLabel }}</span>\n\t\t\t</span>\n\t\t</div>\n\n\t\t<!-- Empty state. -->\n\t\t<p v-if=\"nodes.length === 0\" class=\"cn-federation-status__empty\">\n\t\t\t{{ emptyLabel }}\n\t\t</p>\n\n\t\t<!-- Per-node list. -->\n\t\t<ul v-else class=\"cn-federation-status__list\">\n\t\t\t<li v-for=\"node in sortedNodes\"\n\t\t\t\t:key=\"node.id || node.name\"\n\t\t\t\tclass=\"cn-federation-status__node\"\n\t\t\t\t:class=\"'cn-federation-status__node--' + normaliseStatus(node.status)\"\n\t\t\t\t:data-status=\"normaliseStatus(node.status)\"\n\t\t\t\t@click=\"onNodeClick(node)\">\n\t\t\t\t<!-- `__dot` as well as `__node-dot`: the size, the shape and the\n\t\t\t\t accent-driven fill all live on `__dot`, and the row's dot\n\t\t\t\t carried only the `--status` modifier — which after this change\n\t\t\t\t sets a custom property and nothing else. Without the base class\n\t\t\t\t the row's dot was a zero-by-zero span the whole time: a\n\t\t\t\t coloured background on an empty inline element paints nothing.\n\t\t\t\t Same class, same dot, in the strip and in the row. -->\n\t\t\t\t<span class=\"cn-federation-status__node-dot cn-federation-status__dot\"\n\t\t\t\t\t:class=\"'cn-federation-status__dot--' + normaliseStatus(node.status)\" />\n\t\t\t\t<div class=\"cn-federation-status__node-body\">\n\t\t\t\t\t<span class=\"cn-federation-status__node-name\">{{ node.name || node.id }}</span>\n\t\t\t\t\t<small v-if=\"node.url\" class=\"cn-federation-status__node-url\">{{ node.url }}</small>\n\t\t\t\t\t<small v-if=\"node.message\" class=\"cn-federation-status__node-message\">{{ node.message }}</small>\n\t\t\t\t\t<small v-if=\"node.lastChecked\" class=\"cn-federation-status__node-checked\">\n\t\t\t\t\t\t{{ lastCheckedLabel }}: {{ formatTimestamp(node.lastChecked) }}\n\t\t\t\t\t</small>\n\t\t\t\t</div>\n\t\t\t\t<span class=\"cn-federation-status__node-label\">{{ statusLabel(node.status) }}</span>\n\t\t\t</li>\n\t\t</ul>\n\t</div>\n</template>\n\n<script>\nimport { userLocale } from '../../utils/userLocale.js'\n\n// The canonical KPI scale (`--cn-kpi-*`) lives in one stylesheet. Imported\n// here as well as from css/index.css so the tokens resolve even when the\n// consuming app pulls in components individually.\nimport '../../css/kpi-card.css'\n/**\n * CnFederationStatus — Federation-status widget for federated\n * directories. Renders an aggregate summary (`up` / `degraded` /\n * `down` / `unknown` counts) and a per-node list with status dot,\n * URL, last-checked timestamp, and click-emit for drilldown.\n *\n * Status values are normalised: `up` / `online` / `ok` map to `up`;\n * `degraded` / `partial` map to `degraded`; `down` / `offline` /\n * `error` map to `down`; anything else maps to `unknown`. Unknown\n * statuses are tolerated so consumers can ship without an\n * exhaustive enum.\n *\n * ```vue\n * <CnFederationStatus\n * title=\"Federation directory\"\n * :nodes=\"[\n * { id: 'a', name: 'Node A', url: 'https://a.example.org', status: 'up' },\n * { id: 'b', name: 'Node B', url: 'https://b.example.org', status: 'degraded', message: 'High latency' },\n * { id: 'c', name: 'Node C', url: 'https://c.example.org', status: 'down', lastChecked: '2026-05-20T11:00:00Z' },\n * ]\"\n * @node-click=\"openNodeDetail\" />\n * ```\n */\nexport default {\n\tname: 'CnFederationStatus',\n\tprops: {\n\t\t/**\n\t\t * Federation nodes to render. Each entry:\n\t\t * `{ id?, name?, url?, status, message?, lastChecked? }`.\n\t\t * Either `id` or `name` is required for the v-for key.\n\t\t *\n\t\t * @type {Array<{id?:string,name?:string,url?:string,status:string,message?:string,lastChecked?:string}>}\n\t\t */\n\t\tnodes: { type: Array, default: () => [] },\n\t\t/** Optional widget title rendered as `<h3>`. */\n\t\ttitle: { type: String, default: '' },\n\t\t/** Optional widget description rendered under the title. */\n\t\tdescription: { type: String, default: '' },\n\t\t/** Hide the aggregate summary row. */\n\t\thideSummary: { type: Boolean, default: false },\n\t\t/** Label for the `up` summary chip. */\n\t\tupLabel: { type: String, default: 'up' },\n\t\t/** Label for the `degraded` summary chip. */\n\t\tdegradedLabel: { type: String, default: 'degraded' },\n\t\t/** Label for the `down` summary chip. */\n\t\tdownLabel: { type: String, default: 'down' },\n\t\t/** Label for the `unknown` summary chip. */\n\t\tunknownLabel: { type: String, default: 'unknown' },\n\t\t/** Empty-state text when `nodes[]` is empty. */\n\t\temptyLabel: { type: String, default: 'No federation nodes registered.' },\n\t\t/** Label preceding the per-node last-checked timestamp. */\n\t\tlastCheckedLabel: { type: String, default: 'Last checked' },\n\t\t/**\n\t\t * Sort key. `'status'` puts `down` first, then `degraded`,\n\t\t * then `unknown`, then `up`. `'name'` sorts alphabetically.\n\t\t * `'none'` keeps the input order.\n\t\t *\n\t\t * @type {'status'|'name'|'none'}\n\t\t */\n\t\tsort: { type: String, default: 'status' },\n\t},\n\n\temits: ['node-click'],\n\tcomputed: {\n\t\t/**\n\t\t * Status counts across all nodes.\n\t\t *\n\t\t * @return {{up:number,degraded:number,down:number,unknown:number}}\n\t\t */\n\t\tcounts() {\n\t\t\tconst out = { up: 0, degraded: 0, down: 0, unknown: 0 }\n\t\t\tfor (const n of this.nodes) {\n\t\t\t\tconst s = this.normaliseStatus(n.status)\n\t\t\t\tout[s] = (out[s] || 0) + 1\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * Nodes sorted per the `sort` prop.\n\t\t *\n\t\t * @return {Array} The sorted nodes.\n\t\t */\n\t\tsortedNodes() {\n\t\t\tif (this.sort === 'none') {\n\t\t\t\treturn this.nodes\n\t\t\t}\n\t\t\tconst arr = [...this.nodes]\n\t\t\tif (this.sort === 'name') {\n\t\t\t\tarr.sort((a, b) => (a.name || a.id || '').localeCompare(b.name || b.id || ''))\n\t\t\t\treturn arr\n\t\t\t}\n\t\t\tconst order = { down: 0, degraded: 1, unknown: 2, up: 3 }\n\t\t\tarr.sort((a, b) => {\n\t\t\t\tconst sa = this.normaliseStatus(a.status)\n\t\t\t\tconst sb = this.normaliseStatus(b.status)\n\t\t\t\treturn (order[sa] - order[sb]) || (a.name || '').localeCompare(b.name || '')\n\t\t\t})\n\t\t\treturn arr\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Map a free-form status string to the normalised\n\t\t * `up | degraded | down | unknown` set.\n\t\t *\n\t\t * @param {string} status Raw status from the node.\n\t\t * @return {'up'|'degraded'|'down'|'unknown'} Normalised.\n\t\t */\n\t\tnormaliseStatus(status) {\n\t\t\tconst s = (status || '').toLowerCase()\n\t\t\tif (['up', 'online', 'ok', 'healthy', 'available'].includes(s)) {\n\t\t\t\treturn 'up'\n\t\t\t}\n\t\t\tif (['degraded', 'partial', 'slow', 'warning'].includes(s)) {\n\t\t\t\treturn 'degraded'\n\t\t\t}\n\t\t\tif (['down', 'offline', 'error', 'failed', 'unhealthy'].includes(s)) {\n\t\t\t\treturn 'down'\n\t\t\t}\n\t\t\treturn 'unknown'\n\t\t},\n\n\t\t/**\n\t\t * Render-friendly label for a status value.\n\t\t *\n\t\t * @param {string} status Raw status.\n\t\t * @return {string} The label to show in the per-node row.\n\t\t */\n\t\tstatusLabel(status) {\n\t\t\tconst norm = this.normaliseStatus(status)\n\t\t\tif (norm === 'up') {\n\t\t\t\treturn this.upLabel\n\t\t\t}\n\t\t\tif (norm === 'degraded') {\n\t\t\t\treturn this.degradedLabel\n\t\t\t}\n\t\t\tif (norm === 'down') {\n\t\t\t\treturn this.downLabel\n\t\t\t}\n\t\t\treturn this.unknownLabel\n\t\t},\n\n\t\t/**\n\t\t * Format an ISO timestamp into a short locale string.\n\t\t *\n\t\t * @param {string} iso ISO datetime string.\n\t\t * @return {string} Locale-formatted timestamp, or the\n\t\t * original string if parsing fails.\n\t\t */\n\t\tformatTimestamp(iso) {\n\t\t\ttry {\n\t\t\t\tconst d = new Date(iso)\n\t\t\t\tif (Number.isNaN(d.getTime())) {\n\t\t\t\t\treturn iso\n\t\t\t\t}\n\t\t\t\treturn d.toLocaleString(userLocale())\n\t\t\t} catch {\n\t\t\t\treturn iso\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Forward a node click as a `@node-click` event.\n\t\t *\n\t\t * @param {object} node The clicked node.\n\t\t * @return {void}\n\t\t */\n\t\tonNodeClick(node) {\n\t\t\t/**\n\t\t\t * @event node-click Emitted when the user clicks a\n\t\t\t * node row. Payload is the original node object.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('node-click', node)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n/*\n * TWO densities on purpose, and only ONE of them is a KPI.\n *\n * The summary strip IS a KPI surface, so it uses the canonical inline KPI from\n * src/css/kpi-card.css — the card's number weight, its accent colours and its\n * label scale, one size down. Nothing about it is declared locally except the\n * dot.\n *\n * The per-node list is NOT, and forcing it into KPI cards would be a false\n * uniformity. A KPI tile answers \"how many?\" with one number a reader takes in\n * at a glance; a node row answers \"which one, where, and when was it last\n * checked?\" — a name, a URL, a message and a timestamp, four fields that only\n * make sense read line by line. Boxing each of those in a 2rem-number card\n * would show a directory of forty nodes as forty tiles with no number on them.\n * What the list DOES share is the vocabulary: the same status dot, in the same\n * accent colours, so the strip and the rows agree on what \"degraded\" looks\n * like.\n */\n\n.cn-federation-status {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: var(--cn-kpi-stack-gap, 12px);\n\tpadding: var(--cn-kpi-padding, 16px);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--cn-kpi-radius, 10px);\n\tbackground: var(--color-main-background);\n}\n\n.cn-federation-status__title {\n\tmargin: 0;\n\tfont-size: var(--cn-kpi-section-title-size, 14px);\n\tfont-weight: var(--cn-kpi-title-weight, 600);\n}\n\n.cn-federation-status__description {\n\tmargin: 4px 0 0;\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n}\n\n.cn-federation-status__summary {\n\tdisplay: flex;\n\tgap: var(--cn-kpi-grid-gap, 16px);\n\tflex-wrap: wrap;\n\talign-items: baseline;\n}\n\n/* Layout and typography come from `cn-kpi-inline`, which every summary item\n also carries. The only thing left to say here is that the dot sits on the\n text baseline rather than on the number's. */\n.cn-federation-status__summary-item {\n\talign-items: center;\n}\n\n/* One dot rule, four colours, no repetition: each status modifier re-points\n the SAME accent token the inline KPI's number reads, so a dot can never be\n amber next to a red number. */\n.cn-federation-status__dot {\n\twidth: 10px;\n\theight: 10px;\n\tborder-radius: 50%;\n\tdisplay: inline-block;\n\tflex-shrink: 0;\n\tbackground: var(--cn-kpi-accent, var(--color-text-maxcontrast));\n}\n\n.cn-federation-status__dot--up {\n\t--cn-kpi-accent: var(--color-success);\n}\n\n.cn-federation-status__dot--degraded {\n\t--cn-kpi-accent: var(--color-warning);\n}\n\n.cn-federation-status__dot--down {\n\t--cn-kpi-accent: var(--color-error);\n}\n\n.cn-federation-status__dot--unknown {\n\t--cn-kpi-accent: var(--color-text-maxcontrast);\n}\n\n.cn-federation-status__empty {\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n\tfont-style: italic;\n\ttext-align: center;\n\tmargin: 16px 0;\n}\n\n.cn-federation-status__list {\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n}\n\n.cn-federation-status__node {\n\tdisplay: flex;\n\talign-items: flex-start;\n\tgap: 12px;\n\tpadding: 8px 12px;\n\tborder-radius: var(--border-radius);\n\tcursor: pointer;\n\tbackground: var(--color-background-hover);\n}\n\n.cn-federation-status__node:hover {\n\tbackground: var(--color-background-darker, var(--color-background-hover));\n}\n\n.cn-federation-status__node-dot {\n\tmargin-top: 6px;\n\tflex-shrink: 0;\n}\n\n.cn-federation-status__node-body {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tflex: 1 1 auto;\n\tmin-width: 0;\n}\n\n.cn-federation-status__node-name {\n\tfont-size: var(--cn-kpi-title-size, 14px);\n\tfont-weight: var(--cn-kpi-title-weight, 600);\n}\n\n.cn-federation-status__node-url {\n\tfont-family: monospace;\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n}\n\n.cn-federation-status__node-message {\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n}\n\n.cn-federation-status__node-checked {\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n}\n\n.cn-federation-status__node-label {\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\ttext-transform: uppercase;\n\tletter-spacing: 0.05em;\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n\tmargin-top: 6px;\n}\n</style>\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"CnFederationStatus.vue3.js","sources":["../../../../src/components/CnFederationStatus/CnFederationStatus.vue?vue&type=template&id=94f90dd6&lang.js"],"sourcesContent":["<template>\n\t<div class=\"cn-federation-status\" data-testid=\"cn-federation-status\">\n\t\t<header v-if=\"title || description\" class=\"cn-federation-status__header\">\n\t\t\t<h3 v-if=\"title\" class=\"cn-federation-status__title\">\n\t\t\t\t{{ title }}\n\t\t\t</h3>\n\t\t\t<p v-if=\"description\" class=\"cn-federation-status__description\">\n\t\t\t\t{{ description }}\n\t\t\t</p>\n\t\t</header>\n\n\t\t<!-- Aggregate counts — the KPI part of this component.\n\n\t\t Each count is a canonical INLINE KPI (src/css/kpi-card.css): the\n\t\t card's number weight, colour and label scale at the compact size,\n\t\t because a roll-up above a list is one line of context rather than\n\t\t four tiles. Splitting the count and the label into their own spans\n\t\t is what lets them take those two different treatments; before,\n\t\t both sat in one text node at one size and the number read as prose.\n\n\t\t The per-node list below is deliberately NOT a KPI surface — see the\n\t\t note in the stylesheet block. -->\n\t\t<div v-if=\"!hideSummary\" class=\"cn-federation-status__summary\" data-testid=\"cn-federation-status-summary\">\n\t\t\t<span class=\"cn-federation-status__summary-item cn-federation-status__summary-item--up cn-kpi-inline cn-kpi-inline--success\">\n\t\t\t\t<span class=\"cn-federation-status__dot cn-federation-status__dot--up\" />\n\t\t\t\t<span class=\"cn-kpi-inline__value\">{{ counts.up }}</span>\n\t\t\t\t<span class=\"cn-kpi-inline__label\">{{ upLabel }}</span>\n\t\t\t</span>\n\t\t\t<span class=\"cn-federation-status__summary-item cn-federation-status__summary-item--degraded cn-kpi-inline cn-kpi-inline--warning\">\n\t\t\t\t<span class=\"cn-federation-status__dot cn-federation-status__dot--degraded\" />\n\t\t\t\t<span class=\"cn-kpi-inline__value\">{{ counts.degraded }}</span>\n\t\t\t\t<span class=\"cn-kpi-inline__label\">{{ degradedLabel }}</span>\n\t\t\t</span>\n\t\t\t<span class=\"cn-federation-status__summary-item cn-federation-status__summary-item--down cn-kpi-inline cn-kpi-inline--error\">\n\t\t\t\t<span class=\"cn-federation-status__dot cn-federation-status__dot--down\" />\n\t\t\t\t<span class=\"cn-kpi-inline__value\">{{ counts.down }}</span>\n\t\t\t\t<span class=\"cn-kpi-inline__label\">{{ downLabel }}</span>\n\t\t\t</span>\n\t\t\t<span v-if=\"counts.unknown > 0\"\n\t\t\t\tclass=\"cn-federation-status__summary-item cn-federation-status__summary-item--unknown cn-kpi-inline cn-kpi-inline--muted\">\n\t\t\t\t<span class=\"cn-federation-status__dot cn-federation-status__dot--unknown\" />\n\t\t\t\t<span class=\"cn-kpi-inline__value\">{{ counts.unknown }}</span>\n\t\t\t\t<span class=\"cn-kpi-inline__label\">{{ unknownLabel }}</span>\n\t\t\t</span>\n\t\t</div>\n\n\t\t<!-- Empty state. -->\n\t\t<p v-if=\"nodes.length === 0\" class=\"cn-federation-status__empty\">\n\t\t\t{{ emptyLabel }}\n\t\t</p>\n\n\t\t<!-- Per-node list. -->\n\t\t<ul v-else class=\"cn-federation-status__list\">\n\t\t\t<li v-for=\"node in sortedNodes\"\n\t\t\t\t:key=\"node.id || node.name\"\n\t\t\t\tclass=\"cn-federation-status__node\"\n\t\t\t\t:class=\"'cn-federation-status__node--' + normaliseStatus(node.status)\"\n\t\t\t\t:data-status=\"normaliseStatus(node.status)\"\n\t\t\t\t@click=\"onNodeClick(node)\">\n\t\t\t\t<!-- `__dot` as well as `__node-dot`: the size, the shape and the\n\t\t\t\t accent-driven fill all live on `__dot`, and the row's dot\n\t\t\t\t carried only the `--status` modifier — which after this change\n\t\t\t\t sets a custom property and nothing else. Without the base class\n\t\t\t\t the row's dot was a zero-by-zero span the whole time: a\n\t\t\t\t coloured background on an empty inline element paints nothing.\n\t\t\t\t Same class, same dot, in the strip and in the row. -->\n\t\t\t\t<span class=\"cn-federation-status__node-dot cn-federation-status__dot\"\n\t\t\t\t\t:class=\"'cn-federation-status__dot--' + normaliseStatus(node.status)\" />\n\t\t\t\t<div class=\"cn-federation-status__node-body\">\n\t\t\t\t\t<span class=\"cn-federation-status__node-name\">{{ node.name || node.id }}</span>\n\t\t\t\t\t<small v-if=\"node.url\" class=\"cn-federation-status__node-url\">{{ node.url }}</small>\n\t\t\t\t\t<small v-if=\"node.message\" class=\"cn-federation-status__node-message\">{{ node.message }}</small>\n\t\t\t\t\t<small v-if=\"node.lastChecked\" class=\"cn-federation-status__node-checked\">\n\t\t\t\t\t\t{{ lastCheckedLabel }}: {{ formatTimestamp(node.lastChecked) }}\n\t\t\t\t\t</small>\n\t\t\t\t</div>\n\t\t\t\t<span class=\"cn-federation-status__node-label\">{{ statusLabel(node.status) }}</span>\n\t\t\t</li>\n\t\t</ul>\n\t</div>\n</template>\n\n<script>\n// The canonical KPI scale (`--cn-kpi-*`) lives in one stylesheet. Imported\n// here as well as from css/index.css so the tokens resolve even when the\n// consuming app pulls in components individually.\nimport '../../css/kpi-card.css'\n/**\n * CnFederationStatus — Federation-status widget for federated\n * directories. Renders an aggregate summary (`up` / `degraded` /\n * `down` / `unknown` counts) and a per-node list with status dot,\n * URL, last-checked timestamp, and click-emit for drilldown.\n *\n * Status values are normalised: `up` / `online` / `ok` map to `up`;\n * `degraded` / `partial` map to `degraded`; `down` / `offline` /\n * `error` map to `down`; anything else maps to `unknown`. Unknown\n * statuses are tolerated so consumers can ship without an\n * exhaustive enum.\n *\n * ```vue\n * <CnFederationStatus\n * title=\"Federation directory\"\n * :nodes=\"[\n * { id: 'a', name: 'Node A', url: 'https://a.example.org', status: 'up' },\n * { id: 'b', name: 'Node B', url: 'https://b.example.org', status: 'degraded', message: 'High latency' },\n * { id: 'c', name: 'Node C', url: 'https://c.example.org', status: 'down', lastChecked: '2026-05-20T11:00:00Z' },\n * ]\"\n * @node-click=\"openNodeDetail\" />\n * ```\n */\nexport default {\n\tname: 'CnFederationStatus',\n\tprops: {\n\t\t/**\n\t\t * Federation nodes to render. Each entry:\n\t\t * `{ id?, name?, url?, status, message?, lastChecked? }`.\n\t\t * Either `id` or `name` is required for the v-for key.\n\t\t *\n\t\t * @type {Array<{id?:string,name?:string,url?:string,status:string,message?:string,lastChecked?:string}>}\n\t\t */\n\t\tnodes: { type: Array, default: () => [] },\n\t\t/** Optional widget title rendered as `<h3>`. */\n\t\ttitle: { type: String, default: '' },\n\t\t/** Optional widget description rendered under the title. */\n\t\tdescription: { type: String, default: '' },\n\t\t/** Hide the aggregate summary row. */\n\t\thideSummary: { type: Boolean, default: false },\n\t\t/** Label for the `up` summary chip. */\n\t\tupLabel: { type: String, default: 'up' },\n\t\t/** Label for the `degraded` summary chip. */\n\t\tdegradedLabel: { type: String, default: 'degraded' },\n\t\t/** Label for the `down` summary chip. */\n\t\tdownLabel: { type: String, default: 'down' },\n\t\t/** Label for the `unknown` summary chip. */\n\t\tunknownLabel: { type: String, default: 'unknown' },\n\t\t/** Empty-state text when `nodes[]` is empty. */\n\t\temptyLabel: { type: String, default: 'No federation nodes registered.' },\n\t\t/** Label preceding the per-node last-checked timestamp. */\n\t\tlastCheckedLabel: { type: String, default: 'Last checked' },\n\t\t/**\n\t\t * Sort key. `'status'` puts `down` first, then `degraded`,\n\t\t * then `unknown`, then `up`. `'name'` sorts alphabetically.\n\t\t * `'none'` keeps the input order.\n\t\t *\n\t\t * @type {'status'|'name'|'none'}\n\t\t */\n\t\tsort: { type: String, default: 'status' },\n\t},\n\n\temits: ['node-click'],\n\tcomputed: {\n\t\t/**\n\t\t * Status counts across all nodes.\n\t\t *\n\t\t * @return {{up:number,degraded:number,down:number,unknown:number}}\n\t\t */\n\t\tcounts() {\n\t\t\tconst out = { up: 0, degraded: 0, down: 0, unknown: 0 }\n\t\t\tfor (const n of this.nodes) {\n\t\t\t\tconst s = this.normaliseStatus(n.status)\n\t\t\t\tout[s] = (out[s] || 0) + 1\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * Nodes sorted per the `sort` prop.\n\t\t *\n\t\t * @return {Array} The sorted nodes.\n\t\t */\n\t\tsortedNodes() {\n\t\t\tif (this.sort === 'none') {\n\t\t\t\treturn this.nodes\n\t\t\t}\n\t\t\tconst arr = [...this.nodes]\n\t\t\tif (this.sort === 'name') {\n\t\t\t\tarr.sort((a, b) => (a.name || a.id || '').localeCompare(b.name || b.id || ''))\n\t\t\t\treturn arr\n\t\t\t}\n\t\t\tconst order = { down: 0, degraded: 1, unknown: 2, up: 3 }\n\t\t\tarr.sort((a, b) => {\n\t\t\t\tconst sa = this.normaliseStatus(a.status)\n\t\t\t\tconst sb = this.normaliseStatus(b.status)\n\t\t\t\treturn (order[sa] - order[sb]) || (a.name || '').localeCompare(b.name || '')\n\t\t\t})\n\t\t\treturn arr\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Map a free-form status string to the normalised\n\t\t * `up | degraded | down | unknown` set.\n\t\t *\n\t\t * @param {string} status Raw status from the node.\n\t\t * @return {'up'|'degraded'|'down'|'unknown'} Normalised.\n\t\t */\n\t\tnormaliseStatus(status) {\n\t\t\tconst s = (status || '').toLowerCase()\n\t\t\tif (['up', 'online', 'ok', 'healthy', 'available'].includes(s)) {\n\t\t\t\treturn 'up'\n\t\t\t}\n\t\t\tif (['degraded', 'partial', 'slow', 'warning'].includes(s)) {\n\t\t\t\treturn 'degraded'\n\t\t\t}\n\t\t\tif (['down', 'offline', 'error', 'failed', 'unhealthy'].includes(s)) {\n\t\t\t\treturn 'down'\n\t\t\t}\n\t\t\treturn 'unknown'\n\t\t},\n\n\t\t/**\n\t\t * Render-friendly label for a status value.\n\t\t *\n\t\t * @param {string} status Raw status.\n\t\t * @return {string} The label to show in the per-node row.\n\t\t */\n\t\tstatusLabel(status) {\n\t\t\tconst norm = this.normaliseStatus(status)\n\t\t\tif (norm === 'up') {\n\t\t\t\treturn this.upLabel\n\t\t\t}\n\t\t\tif (norm === 'degraded') {\n\t\t\t\treturn this.degradedLabel\n\t\t\t}\n\t\t\tif (norm === 'down') {\n\t\t\t\treturn this.downLabel\n\t\t\t}\n\t\t\treturn this.unknownLabel\n\t\t},\n\n\t\t/**\n\t\t * Format an ISO timestamp into a short locale string.\n\t\t *\n\t\t * @param {string} iso ISO datetime string.\n\t\t * @return {string} Locale-formatted timestamp, or the\n\t\t * original string if parsing fails.\n\t\t */\n\t\tformatTimestamp(iso) {\n\t\t\ttry {\n\t\t\t\tconst d = new Date(iso)\n\t\t\t\tif (Number.isNaN(d.getTime())) {\n\t\t\t\t\treturn iso\n\t\t\t\t}\n\t\t\t\treturn d.toLocaleString()\n\t\t\t} catch {\n\t\t\t\treturn iso\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Forward a node click as a `@node-click` event.\n\t\t *\n\t\t * @param {object} node The clicked node.\n\t\t * @return {void}\n\t\t */\n\t\tonNodeClick(node) {\n\t\t\t/**\n\t\t\t * @event node-click Emitted when the user clicks a\n\t\t\t * node row. Payload is the original node object.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('node-click', node)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n/*\n * TWO densities on purpose, and only ONE of them is a KPI.\n *\n * The summary strip IS a KPI surface, so it uses the canonical inline KPI from\n * src/css/kpi-card.css — the card's number weight, its accent colours and its\n * label scale, one size down. Nothing about it is declared locally except the\n * dot.\n *\n * The per-node list is NOT, and forcing it into KPI cards would be a false\n * uniformity. A KPI tile answers \"how many?\" with one number a reader takes in\n * at a glance; a node row answers \"which one, where, and when was it last\n * checked?\" — a name, a URL, a message and a timestamp, four fields that only\n * make sense read line by line. Boxing each of those in a 2rem-number card\n * would show a directory of forty nodes as forty tiles with no number on them.\n * What the list DOES share is the vocabulary: the same status dot, in the same\n * accent colours, so the strip and the rows agree on what \"degraded\" looks\n * like.\n */\n\n.cn-federation-status {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: var(--cn-kpi-stack-gap, 12px);\n\tpadding: var(--cn-kpi-padding, 16px);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--cn-kpi-radius, 10px);\n\tbackground: var(--color-main-background);\n}\n\n.cn-federation-status__title {\n\tmargin: 0;\n\tfont-size: var(--cn-kpi-section-title-size, 14px);\n\tfont-weight: var(--cn-kpi-title-weight, 600);\n}\n\n.cn-federation-status__description {\n\tmargin: 4px 0 0;\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n}\n\n.cn-federation-status__summary {\n\tdisplay: flex;\n\tgap: var(--cn-kpi-grid-gap, 16px);\n\tflex-wrap: wrap;\n\talign-items: baseline;\n}\n\n/* Layout and typography come from `cn-kpi-inline`, which every summary item\n also carries. The only thing left to say here is that the dot sits on the\n text baseline rather than on the number's. */\n.cn-federation-status__summary-item {\n\talign-items: center;\n}\n\n/* One dot rule, four colours, no repetition: each status modifier re-points\n the SAME accent token the inline KPI's number reads, so a dot can never be\n amber next to a red number. */\n.cn-federation-status__dot {\n\twidth: 10px;\n\theight: 10px;\n\tborder-radius: 50%;\n\tdisplay: inline-block;\n\tflex-shrink: 0;\n\tbackground: var(--cn-kpi-accent, var(--color-text-maxcontrast));\n}\n\n.cn-federation-status__dot--up {\n\t--cn-kpi-accent: var(--color-success);\n}\n\n.cn-federation-status__dot--degraded {\n\t--cn-kpi-accent: var(--color-warning);\n}\n\n.cn-federation-status__dot--down {\n\t--cn-kpi-accent: var(--color-error);\n}\n\n.cn-federation-status__dot--unknown {\n\t--cn-kpi-accent: var(--color-text-maxcontrast);\n}\n\n.cn-federation-status__empty {\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n\tfont-style: italic;\n\ttext-align: center;\n\tmargin: 16px 0;\n}\n\n.cn-federation-status__list {\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n}\n\n.cn-federation-status__node {\n\tdisplay: flex;\n\talign-items: flex-start;\n\tgap: 12px;\n\tpadding: 8px 12px;\n\tborder-radius: var(--border-radius);\n\tcursor: pointer;\n\tbackground: var(--color-background-hover);\n}\n\n.cn-federation-status__node:hover {\n\tbackground: var(--color-background-darker, var(--color-background-hover));\n}\n\n.cn-federation-status__node-dot {\n\tmargin-top: 6px;\n\tflex-shrink: 0;\n}\n\n.cn-federation-status__node-body {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tflex: 1 1 auto;\n\tmin-width: 0;\n}\n\n.cn-federation-status__node-name {\n\tfont-size: var(--cn-kpi-title-size, 14px);\n\tfont-weight: var(--cn-kpi-title-weight, 600);\n}\n\n.cn-federation-status__node-url {\n\tfont-family: monospace;\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n}\n\n.cn-federation-status__node-message {\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n}\n\n.cn-federation-status__node-checked {\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n}\n\n.cn-federation-status__node-label {\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\ttext-transform: uppercase;\n\tletter-spacing: 0.05em;\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n\tmargin-top: 6px;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_toDisplayString","_createElementVNode","_Fragment","_renderList"],"mappings":"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"}
1
+ {"version":3,"file":"CnFederationStatus.vue3.js","sources":["../../../../src/components/CnFederationStatus/CnFederationStatus.vue?vue&type=template&id=479dc9bd&lang.js"],"sourcesContent":["<template>\n\t<div class=\"cn-federation-status\" data-testid=\"cn-federation-status\">\n\t\t<header v-if=\"title || description\" class=\"cn-federation-status__header\">\n\t\t\t<h3 v-if=\"title\" class=\"cn-federation-status__title\">\n\t\t\t\t{{ title }}\n\t\t\t</h3>\n\t\t\t<p v-if=\"description\" class=\"cn-federation-status__description\">\n\t\t\t\t{{ description }}\n\t\t\t</p>\n\t\t</header>\n\n\t\t<!-- Aggregate counts — the KPI part of this component.\n\n\t\t Each count is a canonical INLINE KPI (src/css/kpi-card.css): the\n\t\t card's number weight, colour and label scale at the compact size,\n\t\t because a roll-up above a list is one line of context rather than\n\t\t four tiles. Splitting the count and the label into their own spans\n\t\t is what lets them take those two different treatments; before,\n\t\t both sat in one text node at one size and the number read as prose.\n\n\t\t The per-node list below is deliberately NOT a KPI surface — see the\n\t\t note in the stylesheet block. -->\n\t\t<div v-if=\"!hideSummary\" class=\"cn-federation-status__summary\" data-testid=\"cn-federation-status-summary\">\n\t\t\t<span class=\"cn-federation-status__summary-item cn-federation-status__summary-item--up cn-kpi-inline cn-kpi-inline--success\">\n\t\t\t\t<span class=\"cn-federation-status__dot cn-federation-status__dot--up\" />\n\t\t\t\t<span class=\"cn-kpi-inline__value\">{{ counts.up }}</span>\n\t\t\t\t<span class=\"cn-kpi-inline__label\">{{ upLabel }}</span>\n\t\t\t</span>\n\t\t\t<span class=\"cn-federation-status__summary-item cn-federation-status__summary-item--degraded cn-kpi-inline cn-kpi-inline--warning\">\n\t\t\t\t<span class=\"cn-federation-status__dot cn-federation-status__dot--degraded\" />\n\t\t\t\t<span class=\"cn-kpi-inline__value\">{{ counts.degraded }}</span>\n\t\t\t\t<span class=\"cn-kpi-inline__label\">{{ degradedLabel }}</span>\n\t\t\t</span>\n\t\t\t<span class=\"cn-federation-status__summary-item cn-federation-status__summary-item--down cn-kpi-inline cn-kpi-inline--error\">\n\t\t\t\t<span class=\"cn-federation-status__dot cn-federation-status__dot--down\" />\n\t\t\t\t<span class=\"cn-kpi-inline__value\">{{ counts.down }}</span>\n\t\t\t\t<span class=\"cn-kpi-inline__label\">{{ downLabel }}</span>\n\t\t\t</span>\n\t\t\t<span v-if=\"counts.unknown > 0\"\n\t\t\t\tclass=\"cn-federation-status__summary-item cn-federation-status__summary-item--unknown cn-kpi-inline cn-kpi-inline--muted\">\n\t\t\t\t<span class=\"cn-federation-status__dot cn-federation-status__dot--unknown\" />\n\t\t\t\t<span class=\"cn-kpi-inline__value\">{{ counts.unknown }}</span>\n\t\t\t\t<span class=\"cn-kpi-inline__label\">{{ unknownLabel }}</span>\n\t\t\t</span>\n\t\t</div>\n\n\t\t<!-- Empty state. -->\n\t\t<p v-if=\"nodes.length === 0\" class=\"cn-federation-status__empty\">\n\t\t\t{{ emptyLabel }}\n\t\t</p>\n\n\t\t<!-- Per-node list. -->\n\t\t<ul v-else class=\"cn-federation-status__list\">\n\t\t\t<li v-for=\"node in sortedNodes\"\n\t\t\t\t:key=\"node.id || node.name\"\n\t\t\t\tclass=\"cn-federation-status__node\"\n\t\t\t\t:class=\"'cn-federation-status__node--' + normaliseStatus(node.status)\"\n\t\t\t\t:data-status=\"normaliseStatus(node.status)\"\n\t\t\t\t@click=\"onNodeClick(node)\">\n\t\t\t\t<!-- `__dot` as well as `__node-dot`: the size, the shape and the\n\t\t\t\t accent-driven fill all live on `__dot`, and the row's dot\n\t\t\t\t carried only the `--status` modifier — which after this change\n\t\t\t\t sets a custom property and nothing else. Without the base class\n\t\t\t\t the row's dot was a zero-by-zero span the whole time: a\n\t\t\t\t coloured background on an empty inline element paints nothing.\n\t\t\t\t Same class, same dot, in the strip and in the row. -->\n\t\t\t\t<span class=\"cn-federation-status__node-dot cn-federation-status__dot\"\n\t\t\t\t\t:class=\"'cn-federation-status__dot--' + normaliseStatus(node.status)\" />\n\t\t\t\t<div class=\"cn-federation-status__node-body\">\n\t\t\t\t\t<span class=\"cn-federation-status__node-name\">{{ node.name || node.id }}</span>\n\t\t\t\t\t<small v-if=\"node.url\" class=\"cn-federation-status__node-url\">{{ node.url }}</small>\n\t\t\t\t\t<small v-if=\"node.message\" class=\"cn-federation-status__node-message\">{{ node.message }}</small>\n\t\t\t\t\t<small v-if=\"node.lastChecked\" class=\"cn-federation-status__node-checked\">\n\t\t\t\t\t\t{{ lastCheckedLabel }}: {{ formatTimestamp(node.lastChecked) }}\n\t\t\t\t\t</small>\n\t\t\t\t</div>\n\t\t\t\t<span class=\"cn-federation-status__node-label\">{{ statusLabel(node.status) }}</span>\n\t\t\t</li>\n\t\t</ul>\n\t</div>\n</template>\n\n<script>\nimport { userLocale } from '../../utils/userLocale.js'\n\n// The canonical KPI scale (`--cn-kpi-*`) lives in one stylesheet. Imported\n// here as well as from css/index.css so the tokens resolve even when the\n// consuming app pulls in components individually.\nimport '../../css/kpi-card.css'\n/**\n * CnFederationStatus — Federation-status widget for federated\n * directories. Renders an aggregate summary (`up` / `degraded` /\n * `down` / `unknown` counts) and a per-node list with status dot,\n * URL, last-checked timestamp, and click-emit for drilldown.\n *\n * Status values are normalised: `up` / `online` / `ok` map to `up`;\n * `degraded` / `partial` map to `degraded`; `down` / `offline` /\n * `error` map to `down`; anything else maps to `unknown`. Unknown\n * statuses are tolerated so consumers can ship without an\n * exhaustive enum.\n *\n * ```vue\n * <CnFederationStatus\n * title=\"Federation directory\"\n * :nodes=\"[\n * { id: 'a', name: 'Node A', url: 'https://a.example.org', status: 'up' },\n * { id: 'b', name: 'Node B', url: 'https://b.example.org', status: 'degraded', message: 'High latency' },\n * { id: 'c', name: 'Node C', url: 'https://c.example.org', status: 'down', lastChecked: '2026-05-20T11:00:00Z' },\n * ]\"\n * @node-click=\"openNodeDetail\" />\n * ```\n */\nexport default {\n\tname: 'CnFederationStatus',\n\tprops: {\n\t\t/**\n\t\t * Federation nodes to render. Each entry:\n\t\t * `{ id?, name?, url?, status, message?, lastChecked? }`.\n\t\t * Either `id` or `name` is required for the v-for key.\n\t\t *\n\t\t * @type {Array<{id?:string,name?:string,url?:string,status:string,message?:string,lastChecked?:string}>}\n\t\t */\n\t\tnodes: { type: Array, default: () => [] },\n\t\t/** Optional widget title rendered as `<h3>`. */\n\t\ttitle: { type: String, default: '' },\n\t\t/** Optional widget description rendered under the title. */\n\t\tdescription: { type: String, default: '' },\n\t\t/** Hide the aggregate summary row. */\n\t\thideSummary: { type: Boolean, default: false },\n\t\t/** Label for the `up` summary chip. */\n\t\tupLabel: { type: String, default: 'up' },\n\t\t/** Label for the `degraded` summary chip. */\n\t\tdegradedLabel: { type: String, default: 'degraded' },\n\t\t/** Label for the `down` summary chip. */\n\t\tdownLabel: { type: String, default: 'down' },\n\t\t/** Label for the `unknown` summary chip. */\n\t\tunknownLabel: { type: String, default: 'unknown' },\n\t\t/** Empty-state text when `nodes[]` is empty. */\n\t\temptyLabel: { type: String, default: 'No federation nodes registered.' },\n\t\t/** Label preceding the per-node last-checked timestamp. */\n\t\tlastCheckedLabel: { type: String, default: 'Last checked' },\n\t\t/**\n\t\t * Sort key. `'status'` puts `down` first, then `degraded`,\n\t\t * then `unknown`, then `up`. `'name'` sorts alphabetically.\n\t\t * `'none'` keeps the input order.\n\t\t *\n\t\t * @type {'status'|'name'|'none'}\n\t\t */\n\t\tsort: { type: String, default: 'status' },\n\t},\n\n\temits: ['node-click'],\n\tcomputed: {\n\t\t/**\n\t\t * Status counts across all nodes.\n\t\t *\n\t\t * @return {{up:number,degraded:number,down:number,unknown:number}}\n\t\t */\n\t\tcounts() {\n\t\t\tconst out = { up: 0, degraded: 0, down: 0, unknown: 0 }\n\t\t\tfor (const n of this.nodes) {\n\t\t\t\tconst s = this.normaliseStatus(n.status)\n\t\t\t\tout[s] = (out[s] || 0) + 1\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * Nodes sorted per the `sort` prop.\n\t\t *\n\t\t * @return {Array} The sorted nodes.\n\t\t */\n\t\tsortedNodes() {\n\t\t\tif (this.sort === 'none') {\n\t\t\t\treturn this.nodes\n\t\t\t}\n\t\t\tconst arr = [...this.nodes]\n\t\t\tif (this.sort === 'name') {\n\t\t\t\tarr.sort((a, b) => (a.name || a.id || '').localeCompare(b.name || b.id || ''))\n\t\t\t\treturn arr\n\t\t\t}\n\t\t\tconst order = { down: 0, degraded: 1, unknown: 2, up: 3 }\n\t\t\tarr.sort((a, b) => {\n\t\t\t\tconst sa = this.normaliseStatus(a.status)\n\t\t\t\tconst sb = this.normaliseStatus(b.status)\n\t\t\t\treturn (order[sa] - order[sb]) || (a.name || '').localeCompare(b.name || '')\n\t\t\t})\n\t\t\treturn arr\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Map a free-form status string to the normalised\n\t\t * `up | degraded | down | unknown` set.\n\t\t *\n\t\t * @param {string} status Raw status from the node.\n\t\t * @return {'up'|'degraded'|'down'|'unknown'} Normalised.\n\t\t */\n\t\tnormaliseStatus(status) {\n\t\t\tconst s = (status || '').toLowerCase()\n\t\t\tif (['up', 'online', 'ok', 'healthy', 'available'].includes(s)) {\n\t\t\t\treturn 'up'\n\t\t\t}\n\t\t\tif (['degraded', 'partial', 'slow', 'warning'].includes(s)) {\n\t\t\t\treturn 'degraded'\n\t\t\t}\n\t\t\tif (['down', 'offline', 'error', 'failed', 'unhealthy'].includes(s)) {\n\t\t\t\treturn 'down'\n\t\t\t}\n\t\t\treturn 'unknown'\n\t\t},\n\n\t\t/**\n\t\t * Render-friendly label for a status value.\n\t\t *\n\t\t * @param {string} status Raw status.\n\t\t * @return {string} The label to show in the per-node row.\n\t\t */\n\t\tstatusLabel(status) {\n\t\t\tconst norm = this.normaliseStatus(status)\n\t\t\tif (norm === 'up') {\n\t\t\t\treturn this.upLabel\n\t\t\t}\n\t\t\tif (norm === 'degraded') {\n\t\t\t\treturn this.degradedLabel\n\t\t\t}\n\t\t\tif (norm === 'down') {\n\t\t\t\treturn this.downLabel\n\t\t\t}\n\t\t\treturn this.unknownLabel\n\t\t},\n\n\t\t/**\n\t\t * Format an ISO timestamp into a short locale string.\n\t\t *\n\t\t * @param {string} iso ISO datetime string.\n\t\t * @return {string} Locale-formatted timestamp, or the\n\t\t * original string if parsing fails.\n\t\t */\n\t\tformatTimestamp(iso) {\n\t\t\ttry {\n\t\t\t\tconst d = new Date(iso)\n\t\t\t\tif (Number.isNaN(d.getTime())) {\n\t\t\t\t\treturn iso\n\t\t\t\t}\n\t\t\t\treturn d.toLocaleString(userLocale())\n\t\t\t} catch {\n\t\t\t\treturn iso\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Forward a node click as a `@node-click` event.\n\t\t *\n\t\t * @param {object} node The clicked node.\n\t\t * @return {void}\n\t\t */\n\t\tonNodeClick(node) {\n\t\t\t/**\n\t\t\t * @event node-click Emitted when the user clicks a\n\t\t\t * node row. Payload is the original node object.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('node-click', node)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n/*\n * TWO densities on purpose, and only ONE of them is a KPI.\n *\n * The summary strip IS a KPI surface, so it uses the canonical inline KPI from\n * src/css/kpi-card.css — the card's number weight, its accent colours and its\n * label scale, one size down. Nothing about it is declared locally except the\n * dot.\n *\n * The per-node list is NOT, and forcing it into KPI cards would be a false\n * uniformity. A KPI tile answers \"how many?\" with one number a reader takes in\n * at a glance; a node row answers \"which one, where, and when was it last\n * checked?\" — a name, a URL, a message and a timestamp, four fields that only\n * make sense read line by line. Boxing each of those in a 2rem-number card\n * would show a directory of forty nodes as forty tiles with no number on them.\n * What the list DOES share is the vocabulary: the same status dot, in the same\n * accent colours, so the strip and the rows agree on what \"degraded\" looks\n * like.\n */\n\n.cn-federation-status {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: var(--cn-kpi-stack-gap, 12px);\n\tpadding: var(--cn-kpi-padding, 16px);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--cn-kpi-radius, 10px);\n\tbackground: var(--color-main-background);\n}\n\n.cn-federation-status__title {\n\tmargin: 0;\n\tfont-size: var(--cn-kpi-section-title-size, 14px);\n\tfont-weight: var(--cn-kpi-title-weight, 600);\n}\n\n.cn-federation-status__description {\n\tmargin: 4px 0 0;\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n}\n\n.cn-federation-status__summary {\n\tdisplay: flex;\n\tgap: var(--cn-kpi-grid-gap, 16px);\n\tflex-wrap: wrap;\n\talign-items: baseline;\n}\n\n/* Layout and typography come from `cn-kpi-inline`, which every summary item\n also carries. The only thing left to say here is that the dot sits on the\n text baseline rather than on the number's. */\n.cn-federation-status__summary-item {\n\talign-items: center;\n}\n\n/* One dot rule, four colours, no repetition: each status modifier re-points\n the SAME accent token the inline KPI's number reads, so a dot can never be\n amber next to a red number. */\n.cn-federation-status__dot {\n\twidth: 10px;\n\theight: 10px;\n\tborder-radius: 50%;\n\tdisplay: inline-block;\n\tflex-shrink: 0;\n\tbackground: var(--cn-kpi-accent, var(--color-text-maxcontrast));\n}\n\n.cn-federation-status__dot--up {\n\t--cn-kpi-accent: var(--color-success);\n}\n\n.cn-federation-status__dot--degraded {\n\t--cn-kpi-accent: var(--color-warning);\n}\n\n.cn-federation-status__dot--down {\n\t--cn-kpi-accent: var(--color-error);\n}\n\n.cn-federation-status__dot--unknown {\n\t--cn-kpi-accent: var(--color-text-maxcontrast);\n}\n\n.cn-federation-status__empty {\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n\tfont-style: italic;\n\ttext-align: center;\n\tmargin: 16px 0;\n}\n\n.cn-federation-status__list {\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n}\n\n.cn-federation-status__node {\n\tdisplay: flex;\n\talign-items: flex-start;\n\tgap: 12px;\n\tpadding: 8px 12px;\n\tborder-radius: var(--border-radius);\n\tcursor: pointer;\n\tbackground: var(--color-background-hover);\n}\n\n.cn-federation-status__node:hover {\n\tbackground: var(--color-background-darker, var(--color-background-hover));\n}\n\n.cn-federation-status__node-dot {\n\tmargin-top: 6px;\n\tflex-shrink: 0;\n}\n\n.cn-federation-status__node-body {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tflex: 1 1 auto;\n\tmin-width: 0;\n}\n\n.cn-federation-status__node-name {\n\tfont-size: var(--cn-kpi-title-size, 14px);\n\tfont-weight: var(--cn-kpi-title-weight, 600);\n}\n\n.cn-federation-status__node-url {\n\tfont-family: monospace;\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n}\n\n.cn-federation-status__node-message {\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n}\n\n.cn-federation-status__node-checked {\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n}\n\n.cn-federation-status__node-label {\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\ttext-transform: uppercase;\n\tletter-spacing: 0.05em;\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n\tmargin-top: 6px;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_toDisplayString","_createElementVNode","_Fragment","_renderList"],"mappings":"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"}
@@ -2,7 +2,7 @@ import script from './CnFileManager.vue2.js';
2
2
  import { render } from './CnFileManager.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-1b9de2b2";
5
+ script.__scopeId = "data-v-395592dc";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -1,3 +1,5 @@
1
+ import { userLocale } from '../../utils/userLocale.js';
2
+
1
3
  /**
2
4
  * CnFileManager — File list widget with drag-drop upload, per-file
3
5
  * actions, and a click-emit for preview / detail.
@@ -141,7 +143,7 @@ var script = {
141
143
  if (Number.isNaN(d.getTime())) {
142
144
  return iso
143
145
  }
144
- return d.toLocaleString()
146
+ return d.toLocaleString(userLocale())
145
147
  } catch {
146
148
  return iso
147
149
  }
@@ -1 +1 @@
1
- {"version":3,"file":"CnFileManager.vue2.js","sources":["../../../../src/components/CnFileManager/CnFileManager.vue"],"sourcesContent":["<template>\n\t<div class=\"cn-file-manager\" data-testid=\"cn-file-manager\">\n\t\t<header v-if=\"title || description || $slots.header\" class=\"cn-file-manager__header\">\n\t\t\t<!-- @slot header Overrides the default title/description header. Defaults to `<h3>{{ title }}</h3>` + `<p>{{ description }}</p>`. -->\n\t\t\t<slot name=\"header\">\n\t\t\t\t<h3 v-if=\"title\" class=\"cn-file-manager__title\">\n\t\t\t\t\t{{ title }}\n\t\t\t\t</h3>\n\t\t\t\t<p v-if=\"description\" class=\"cn-file-manager__description\">\n\t\t\t\t\t{{ description }}\n\t\t\t\t</p>\n\t\t\t</slot>\n\t\t</header>\n\n\t\t<!-- Drop zone — drag a file here to trigger @upload. -->\n\t\t<div v-if=\"!readOnly\"\n\t\t\tclass=\"cn-file-manager__dropzone\"\n\t\t\t:class=\"{ 'cn-file-manager__dropzone--active': isDragging }\"\n\t\t\tdata-testid=\"cn-file-manager-dropzone\"\n\t\t\t@dragover.prevent=\"onDragOver\"\n\t\t\t@dragleave.prevent=\"onDragLeave\"\n\t\t\t@drop.prevent=\"onDrop\"\n\t\t\t@click=\"openFilePicker\">\n\t\t\t<p>{{ dropzoneLabel }}</p>\n\t\t\t<small v-if=\"dropzoneHint\">{{ dropzoneHint }}</small>\n\t\t\t<input ref=\"fileInput\"\n\t\t\t\ttype=\"file\"\n\t\t\t\t:accept=\"accept\"\n\t\t\t\t:multiple=\"multiple\"\n\t\t\t\tclass=\"cn-file-manager__file-input\"\n\t\t\t\t@change=\"onFileInputChange\">\n\t\t</div>\n\n\t\t<!-- Empty state. -->\n\t\t<p v-if=\"files.length === 0\" class=\"cn-file-manager__empty\">\n\t\t\t{{ emptyLabel }}\n\t\t</p>\n\n\t\t<!-- File list. -->\n\t\t<ul v-else class=\"cn-file-manager__list\">\n\t\t\t<li v-for=\"file in files\"\n\t\t\t\t:key=\"file.id || file.name\"\n\t\t\t\tclass=\"cn-file-manager__item\"\n\t\t\t\t:data-file-id=\"file.id || file.name\"\n\t\t\t\t@click=\"onFileClick(file)\">\n\t\t\t\t<span class=\"cn-file-manager__item-icon\">{{ iconFor(file) }}</span>\n\t\t\t\t<div class=\"cn-file-manager__item-meta\">\n\t\t\t\t\t<span class=\"cn-file-manager__item-name\">{{ file.name }}</span>\n\t\t\t\t\t<small class=\"cn-file-manager__item-sub\">\n\t\t\t\t\t\t{{ humanSize(file.size) }}\n\t\t\t\t\t\t<template v-if=\"file.uploadedAt\"> · {{ formatTimestamp(file.uploadedAt) }}</template>\n\t\t\t\t\t\t<template v-if=\"file.uploadedBy\"> · {{ file.uploadedBy }}</template>\n\t\t\t\t\t</small>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"cn-file-manager__item-actions\" @click.stop>\n\t\t\t\t\t<!-- @slot item-actions Per-file actions. Scope:\n\t\t\t\t\t { file }. Default renders Download / Delete\n\t\t\t\t\t buttons emitting @download / @delete. -->\n\t\t\t\t\t<slot name=\"item-actions\" :file=\"file\">\n\t\t\t\t\t\t<button v-if=\"file.url || file.downloadHandler\"\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclass=\"cn-file-manager__action\"\n\t\t\t\t\t\t\t:title=\"downloadLabel\"\n\t\t\t\t\t\t\t@click.stop=\"onDownload(file)\">\n\t\t\t\t\t\t\t{{ downloadLabel }}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t\t<button v-if=\"!readOnly\"\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclass=\"cn-file-manager__action cn-file-manager__action--delete\"\n\t\t\t\t\t\t\t:title=\"deleteLabel\"\n\t\t\t\t\t\t\t:disabled=\"deletingIds[file.id || file.name]\"\n\t\t\t\t\t\t\t@click.stop=\"onDelete(file)\">\n\t\t\t\t\t\t\t{{ deleteLabel }}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</slot>\n\t\t\t\t</div>\n\t\t\t</li>\n\t\t</ul>\n\t</div>\n</template>\n\n<script>\n/**\n * CnFileManager — File list widget with drag-drop upload, per-file\n * actions, and a click-emit for preview / detail.\n *\n * The component does NOT own a network layer — consumers wire the\n * uploads and downloads via the `@upload`, `@download`, `@delete`\n * events. The widget owns the dropzone UI, the per-file row\n * rendering, the icon-by-extension mapping, and the human-readable\n * size + timestamp formatting.\n *\n * For OpenRegister-backed file attachments, the consuming app\n * typically converts the OR `_files` array into the `files[]` prop\n * shape and persists the deletes / uploads back via the usual\n * object-store calls.\n *\n * ```vue\n * <CnFileManager\n * title=\"Attachments\"\n * :files=\"attachments\"\n * accept=\".pdf,.png,.jpg\"\n * :multiple=\"true\"\n * :max-size-mb=\"20\"\n * @upload=\"onUpload\"\n * @download=\"onDownload\"\n * @delete=\"onDelete\"\n * @file-click=\"openPreview\" />\n * ```\n */\nexport default {\n\tname: 'CnFileManager',\n\tprops: {\n\t\t/**\n\t\t * Files to render. Each entry:\n\t\t * `{ id?, name, size?, type?, url?, uploadedAt?, uploadedBy? }`.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tfiles: { type: Array, default: () => [] },\n\t\t/** Optional widget title. */\n\t\ttitle: { type: String, default: '' },\n\t\t/** Optional description. */\n\t\tdescription: { type: String, default: '' },\n\t\t/** Hide the dropzone + delete actions. */\n\t\treadOnly: { type: Boolean, default: false },\n\t\t/** `accept` attribute for the file input. */\n\t\taccept: { type: String, default: '' },\n\t\t/** Allow multi-file selection. */\n\t\tmultiple: { type: Boolean, default: true },\n\t\t/** Per-file max size in megabytes. 0 disables. */\n\t\tmaxSizeMb: { type: Number, default: 0 },\n\t\t/** Dropzone primary label. */\n\t\tdropzoneLabel: { type: String, default: 'Drop files here or click to browse' },\n\t\t/** Dropzone secondary hint. */\n\t\tdropzoneHint: { type: String, default: '' },\n\t\t/** Empty-state text when files[] is empty. */\n\t\temptyLabel: { type: String, default: 'No files attached.' },\n\t\t/** Download button label / title. */\n\t\tdownloadLabel: { type: String, default: 'Download' },\n\t\t/** Delete button label / title. */\n\t\tdeleteLabel: { type: String, default: 'Delete' },\n\t},\n\n\temits: ['delete', 'download', 'file-click', 'upload', 'upload-rejected'],\n\tdata() {\n\t\treturn {\n\t\t\tisDragging: false,\n\t\t\tdeletingIds: {},\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Pick the emoji-icon for a file based on extension /\n\t\t * MIME type. Pure presentation; consumers can override\n\t\t * via the `#item-actions` slot if they want full icons.\n\t\t *\n\t\t * @param {object} file The file entry.\n\t\t * @return {string} The icon character.\n\t\t */\n\t\ticonFor(file) {\n\t\t\tconst ext = (file.name || '').split('.').pop().toLowerCase()\n\t\t\tconst type = (file.type || '').toLowerCase()\n\t\t\tif (type.startsWith('image/') || ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'].includes(ext)) {\n\t\t\t\treturn '🖼️'\n\t\t\t}\n\t\t\tif (type.startsWith('video/') || ['mp4', 'mov', 'avi', 'webm'].includes(ext)) {\n\t\t\t\treturn '🎬'\n\t\t\t}\n\t\t\tif (type.startsWith('audio/') || ['mp3', 'wav', 'ogg'].includes(ext)) {\n\t\t\t\treturn '🎵'\n\t\t\t}\n\t\t\tif (['pdf'].includes(ext)) {\n\t\t\t\treturn '📕'\n\t\t\t}\n\t\t\tif (['doc', 'docx', 'odt', 'txt', 'md'].includes(ext)) {\n\t\t\t\treturn '📄'\n\t\t\t}\n\t\t\tif (['xls', 'xlsx', 'ods', 'csv'].includes(ext)) {\n\t\t\t\treturn '📊'\n\t\t\t}\n\t\t\tif (['ppt', 'pptx', 'odp'].includes(ext)) {\n\t\t\t\treturn '📈'\n\t\t\t}\n\t\t\tif (['zip', 'rar', '7z', 'tar', 'gz'].includes(ext)) {\n\t\t\t\treturn '🗜️'\n\t\t\t}\n\t\t\tif (['xml', 'json', 'yml', 'yaml'].includes(ext)) {\n\t\t\t\treturn '🧾'\n\t\t\t}\n\t\t\treturn '📎'\n\t\t},\n\n\t\t/**\n\t\t * Format a byte count as a short human-readable string.\n\t\t *\n\t\t * @param {number} bytes Size in bytes.\n\t\t * @return {string} Like '1.2 MB'.\n\t\t */\n\t\thumanSize(bytes) {\n\t\t\tif (bytes === undefined || bytes === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (bytes < 1024) {\n\t\t\t\treturn `${bytes} B`\n\t\t\t}\n\t\t\tif (bytes < 1024 * 1024) {\n\t\t\t\treturn `${(bytes / 1024).toFixed(1)} KB`\n\t\t\t}\n\t\t\treturn `${(bytes / 1024 / 1024).toFixed(1)} MB`\n\t\t},\n\n\t\t/**\n\t\t * Format an ISO timestamp as a short locale string.\n\t\t *\n\t\t * @param {string} iso ISO datetime.\n\t\t * @return {string} Locale-formatted timestamp.\n\t\t */\n\t\tformatTimestamp(iso) {\n\t\t\ttry {\n\t\t\t\tconst d = new Date(iso)\n\t\t\t\tif (Number.isNaN(d.getTime())) {\n\t\t\t\t\treturn iso\n\t\t\t\t}\n\t\t\t\treturn d.toLocaleString()\n\t\t\t} catch {\n\t\t\t\treturn iso\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Programmatic open of the file picker. Useful from a\n\t\t * parent button.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\topenFilePicker() {\n\t\t\tif (this.readOnly) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.$refs.fileInput) {\n\t\t\t\tthis.$refs.fileInput.click()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Dropzone dragover handler.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonDragOver() {\n\t\t\tif (this.readOnly) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.isDragging = true\n\t\t},\n\n\t\t/**\n\t\t * Dropzone dragleave handler.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonDragLeave() {\n\t\t\tthis.isDragging = false\n\t\t},\n\n\t\t/**\n\t\t * Dropzone drop handler — extracts files + validates size +\n\t\t * emits `@upload`.\n\t\t *\n\t\t * @param {DragEvent} event The drop event.\n\t\t * @return {void}\n\t\t */\n\t\tonDrop(event) {\n\t\t\tthis.isDragging = false\n\t\t\tif (this.readOnly) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst list = event.dataTransfer ? Array.from(event.dataTransfer.files || []) : []\n\t\t\tthis.emitUpload(list)\n\t\t},\n\n\t\t/**\n\t\t * File-input change handler.\n\t\t *\n\t\t * @param {Event} event The change event.\n\t\t * @return {void}\n\t\t */\n\t\tonFileInputChange(event) {\n\t\t\tconst list = Array.from(event.target.files || [])\n\t\t\tthis.emitUpload(list)\n\t\t\tevent.target.value = ''\n\t\t},\n\n\t\t/**\n\t\t * Validate + emit `@upload` with the file batch.\n\t\t *\n\t\t * @param {File[]} list The selected files.\n\t\t * @return {void}\n\t\t */\n\t\temitUpload(list) {\n\t\t\tif (list.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.maxSizeMb > 0) {\n\t\t\t\tconst limit = this.maxSizeMb * 1024 * 1024\n\t\t\t\tconst oversized = list.find((f) => f.size > limit)\n\t\t\t\tif (oversized) {\n\t\t\t\t\t/**\n\t\t\t\t\t * @event upload-rejected Emitted when one or more\n\t\t\t\t\t * files in the batch exceed maxSizeMb.\n\t\t\t\t\t * @type {{ reason: 'size', file: File, limitMb: number }}\n\t\t\t\t\t */\n\t\t\t\t\tthis.$emit('upload-rejected', { reason: 'size', file: oversized, limitMb: this.maxSizeMb })\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event upload Emitted with the file batch from the\n\t\t\t * dropzone or file picker.\n\t\t\t * @type {File[]}\n\t\t\t */\n\t\t\tthis.$emit('upload', list)\n\t\t},\n\n\t\t/**\n\t\t * Forward a file-row click.\n\t\t *\n\t\t * @param {object} file The file entry.\n\t\t * @return {void}\n\t\t */\n\t\tonFileClick(file) {\n\t\t\t/**\n\t\t\t * @event file-click Emitted on row click (not on the\n\t\t\t * action buttons).\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('file-click', file)\n\t\t},\n\n\t\t/**\n\t\t * Forward a download action.\n\t\t *\n\t\t * @param {object} file The file entry.\n\t\t * @return {void}\n\t\t */\n\t\tonDownload(file) {\n\t\t\t/**\n\t\t\t * @event download Emitted when the Download button is\n\t\t\t * clicked.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('download', file)\n\t\t},\n\n\t\t/**\n\t\t * Forward a delete action; sets the in-flight flag so the\n\t\t * button disables until the parent removes the file or\n\t\t * clears it via `setDeleted`.\n\t\t *\n\t\t * @param {object} file The file entry.\n\t\t * @return {void}\n\t\t */\n\t\tonDelete(file) {\n\t\t\tconst id = file.id || file.name\n\t\t\tthis.deletingIds = { ...this.deletingIds, [id]: true }\n\t\t\t/**\n\t\t\t * @event delete Emitted when the Delete button is\n\t\t\t * clicked. Parent persists then removes from files[].\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('delete', file)\n\t\t},\n\n\t\t/**\n\t\t * Public method — clear the in-flight delete marker for a\n\t\t * file id. Use when the parent's persist fails so the\n\t\t * Delete button becomes clickable again.\n\t\t *\n\t\t * @param {string} id File id.\n\t\t * @return {void}\n\t\t */\n\t\tclearDeleting(id) {\n\t\t\tconst next = { ...this.deletingIds }\n\t\t\tdelete next[id]\n\t\t\tthis.deletingIds = next\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-file-manager {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n}\n\n.cn-file-manager__title {\n\tmargin: 0;\n\tfont-size: 1.1em;\n}\n\n.cn-file-manager__description {\n\tmargin: 4px 0 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-file-manager__dropzone {\n\tborder: 2px dashed var(--color-border);\n\tborder-radius: var(--border-radius);\n\tpadding: 24px;\n\ttext-align: center;\n\tbackground: var(--color-background-hover);\n\tcursor: pointer;\n\ttransition: background 0.15s ease, border-color 0.15s ease;\n}\n\n.cn-file-manager__dropzone:hover,\n.cn-file-manager__dropzone--active {\n\tbackground: var(--color-primary-element-light);\n\tborder-color: var(--color-primary-element);\n}\n\n.cn-file-manager__dropzone p {\n\tmargin: 0;\n}\n\n.cn-file-manager__dropzone small {\n\tdisplay: block;\n\tmargin-top: 4px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-file-manager__file-input {\n\tdisplay: none;\n}\n\n.cn-file-manager__empty {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-style: italic;\n\tmargin: 16px 0;\n\ttext-align: center;\n}\n\n.cn-file-manager__list {\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n}\n\n.cn-file-manager__item {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 12px;\n\tpadding: 8px 12px;\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-background-hover);\n\tcursor: pointer;\n}\n\n.cn-file-manager__item:hover {\n\tbackground: var(--color-background-darker, var(--color-background-hover));\n}\n\n.cn-file-manager__item-icon {\n\tfont-size: 1.4em;\n}\n\n.cn-file-manager__item-meta {\n\tflex: 1 1 auto;\n\tmin-width: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n}\n\n.cn-file-manager__item-name {\n\tfont-weight: 500;\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n\n.cn-file-manager__item-sub {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-file-manager__item-actions {\n\tdisplay: flex;\n\tgap: 4px;\n}\n\n.cn-file-manager__action {\n\tbackground: none;\n\tborder: 1px solid var(--color-border);\n\tpadding: 4px 8px;\n\tborder-radius: var(--border-radius);\n\tcursor: pointer;\n\tfont-size: 0.85em;\n}\n\n.cn-file-manager__action:hover {\n\tbackground: var(--color-main-background);\n}\n\n.cn-file-manager__action--delete {\n\tcolor: var(--color-error);\n}\n\n.cn-file-manager__action:disabled {\n\topacity: 0.4;\n\tcursor: not-allowed;\n}\n</style>\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"CnFileManager.vue2.js","sources":["../../../../src/components/CnFileManager/CnFileManager.vue"],"sourcesContent":["<template>\n\t<div class=\"cn-file-manager\" data-testid=\"cn-file-manager\">\n\t\t<header v-if=\"title || description || $slots.header\" class=\"cn-file-manager__header\">\n\t\t\t<!-- @slot header Overrides the default title/description header. Defaults to `<h3>{{ title }}</h3>` + `<p>{{ description }}</p>`. -->\n\t\t\t<slot name=\"header\">\n\t\t\t\t<h3 v-if=\"title\" class=\"cn-file-manager__title\">\n\t\t\t\t\t{{ title }}\n\t\t\t\t</h3>\n\t\t\t\t<p v-if=\"description\" class=\"cn-file-manager__description\">\n\t\t\t\t\t{{ description }}\n\t\t\t\t</p>\n\t\t\t</slot>\n\t\t</header>\n\n\t\t<!-- Drop zone — drag a file here to trigger @upload. -->\n\t\t<div v-if=\"!readOnly\"\n\t\t\tclass=\"cn-file-manager__dropzone\"\n\t\t\t:class=\"{ 'cn-file-manager__dropzone--active': isDragging }\"\n\t\t\tdata-testid=\"cn-file-manager-dropzone\"\n\t\t\t@dragover.prevent=\"onDragOver\"\n\t\t\t@dragleave.prevent=\"onDragLeave\"\n\t\t\t@drop.prevent=\"onDrop\"\n\t\t\t@click=\"openFilePicker\">\n\t\t\t<p>{{ dropzoneLabel }}</p>\n\t\t\t<small v-if=\"dropzoneHint\">{{ dropzoneHint }}</small>\n\t\t\t<input ref=\"fileInput\"\n\t\t\t\ttype=\"file\"\n\t\t\t\t:accept=\"accept\"\n\t\t\t\t:multiple=\"multiple\"\n\t\t\t\tclass=\"cn-file-manager__file-input\"\n\t\t\t\t@change=\"onFileInputChange\">\n\t\t</div>\n\n\t\t<!-- Empty state. -->\n\t\t<p v-if=\"files.length === 0\" class=\"cn-file-manager__empty\">\n\t\t\t{{ emptyLabel }}\n\t\t</p>\n\n\t\t<!-- File list. -->\n\t\t<ul v-else class=\"cn-file-manager__list\">\n\t\t\t<li v-for=\"file in files\"\n\t\t\t\t:key=\"file.id || file.name\"\n\t\t\t\tclass=\"cn-file-manager__item\"\n\t\t\t\t:data-file-id=\"file.id || file.name\"\n\t\t\t\t@click=\"onFileClick(file)\">\n\t\t\t\t<span class=\"cn-file-manager__item-icon\">{{ iconFor(file) }}</span>\n\t\t\t\t<div class=\"cn-file-manager__item-meta\">\n\t\t\t\t\t<span class=\"cn-file-manager__item-name\">{{ file.name }}</span>\n\t\t\t\t\t<small class=\"cn-file-manager__item-sub\">\n\t\t\t\t\t\t{{ humanSize(file.size) }}\n\t\t\t\t\t\t<template v-if=\"file.uploadedAt\"> · {{ formatTimestamp(file.uploadedAt) }}</template>\n\t\t\t\t\t\t<template v-if=\"file.uploadedBy\"> · {{ file.uploadedBy }}</template>\n\t\t\t\t\t</small>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"cn-file-manager__item-actions\" @click.stop>\n\t\t\t\t\t<!-- @slot item-actions Per-file actions. Scope:\n\t\t\t\t\t { file }. Default renders Download / Delete\n\t\t\t\t\t buttons emitting @download / @delete. -->\n\t\t\t\t\t<slot name=\"item-actions\" :file=\"file\">\n\t\t\t\t\t\t<button v-if=\"file.url || file.downloadHandler\"\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclass=\"cn-file-manager__action\"\n\t\t\t\t\t\t\t:title=\"downloadLabel\"\n\t\t\t\t\t\t\t@click.stop=\"onDownload(file)\">\n\t\t\t\t\t\t\t{{ downloadLabel }}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t\t<button v-if=\"!readOnly\"\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclass=\"cn-file-manager__action cn-file-manager__action--delete\"\n\t\t\t\t\t\t\t:title=\"deleteLabel\"\n\t\t\t\t\t\t\t:disabled=\"deletingIds[file.id || file.name]\"\n\t\t\t\t\t\t\t@click.stop=\"onDelete(file)\">\n\t\t\t\t\t\t\t{{ deleteLabel }}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</slot>\n\t\t\t\t</div>\n\t\t\t</li>\n\t\t</ul>\n\t</div>\n</template>\n\n<script>\nimport { userLocale } from '../../utils/userLocale.js'\n/**\n * CnFileManager — File list widget with drag-drop upload, per-file\n * actions, and a click-emit for preview / detail.\n *\n * The component does NOT own a network layer — consumers wire the\n * uploads and downloads via the `@upload`, `@download`, `@delete`\n * events. The widget owns the dropzone UI, the per-file row\n * rendering, the icon-by-extension mapping, and the human-readable\n * size + timestamp formatting.\n *\n * For OpenRegister-backed file attachments, the consuming app\n * typically converts the OR `_files` array into the `files[]` prop\n * shape and persists the deletes / uploads back via the usual\n * object-store calls.\n *\n * ```vue\n * <CnFileManager\n * title=\"Attachments\"\n * :files=\"attachments\"\n * accept=\".pdf,.png,.jpg\"\n * :multiple=\"true\"\n * :max-size-mb=\"20\"\n * @upload=\"onUpload\"\n * @download=\"onDownload\"\n * @delete=\"onDelete\"\n * @file-click=\"openPreview\" />\n * ```\n */\nexport default {\n\tname: 'CnFileManager',\n\tprops: {\n\t\t/**\n\t\t * Files to render. Each entry:\n\t\t * `{ id?, name, size?, type?, url?, uploadedAt?, uploadedBy? }`.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tfiles: { type: Array, default: () => [] },\n\t\t/** Optional widget title. */\n\t\ttitle: { type: String, default: '' },\n\t\t/** Optional description. */\n\t\tdescription: { type: String, default: '' },\n\t\t/** Hide the dropzone + delete actions. */\n\t\treadOnly: { type: Boolean, default: false },\n\t\t/** `accept` attribute for the file input. */\n\t\taccept: { type: String, default: '' },\n\t\t/** Allow multi-file selection. */\n\t\tmultiple: { type: Boolean, default: true },\n\t\t/** Per-file max size in megabytes. 0 disables. */\n\t\tmaxSizeMb: { type: Number, default: 0 },\n\t\t/** Dropzone primary label. */\n\t\tdropzoneLabel: { type: String, default: 'Drop files here or click to browse' },\n\t\t/** Dropzone secondary hint. */\n\t\tdropzoneHint: { type: String, default: '' },\n\t\t/** Empty-state text when files[] is empty. */\n\t\temptyLabel: { type: String, default: 'No files attached.' },\n\t\t/** Download button label / title. */\n\t\tdownloadLabel: { type: String, default: 'Download' },\n\t\t/** Delete button label / title. */\n\t\tdeleteLabel: { type: String, default: 'Delete' },\n\t},\n\n\temits: ['delete', 'download', 'file-click', 'upload', 'upload-rejected'],\n\tdata() {\n\t\treturn {\n\t\t\tisDragging: false,\n\t\t\tdeletingIds: {},\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Pick the emoji-icon for a file based on extension /\n\t\t * MIME type. Pure presentation; consumers can override\n\t\t * via the `#item-actions` slot if they want full icons.\n\t\t *\n\t\t * @param {object} file The file entry.\n\t\t * @return {string} The icon character.\n\t\t */\n\t\ticonFor(file) {\n\t\t\tconst ext = (file.name || '').split('.').pop().toLowerCase()\n\t\t\tconst type = (file.type || '').toLowerCase()\n\t\t\tif (type.startsWith('image/') || ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'].includes(ext)) {\n\t\t\t\treturn '🖼️'\n\t\t\t}\n\t\t\tif (type.startsWith('video/') || ['mp4', 'mov', 'avi', 'webm'].includes(ext)) {\n\t\t\t\treturn '🎬'\n\t\t\t}\n\t\t\tif (type.startsWith('audio/') || ['mp3', 'wav', 'ogg'].includes(ext)) {\n\t\t\t\treturn '🎵'\n\t\t\t}\n\t\t\tif (['pdf'].includes(ext)) {\n\t\t\t\treturn '📕'\n\t\t\t}\n\t\t\tif (['doc', 'docx', 'odt', 'txt', 'md'].includes(ext)) {\n\t\t\t\treturn '📄'\n\t\t\t}\n\t\t\tif (['xls', 'xlsx', 'ods', 'csv'].includes(ext)) {\n\t\t\t\treturn '📊'\n\t\t\t}\n\t\t\tif (['ppt', 'pptx', 'odp'].includes(ext)) {\n\t\t\t\treturn '📈'\n\t\t\t}\n\t\t\tif (['zip', 'rar', '7z', 'tar', 'gz'].includes(ext)) {\n\t\t\t\treturn '🗜️'\n\t\t\t}\n\t\t\tif (['xml', 'json', 'yml', 'yaml'].includes(ext)) {\n\t\t\t\treturn '🧾'\n\t\t\t}\n\t\t\treturn '📎'\n\t\t},\n\n\t\t/**\n\t\t * Format a byte count as a short human-readable string.\n\t\t *\n\t\t * @param {number} bytes Size in bytes.\n\t\t * @return {string} Like '1.2 MB'.\n\t\t */\n\t\thumanSize(bytes) {\n\t\t\tif (bytes === undefined || bytes === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (bytes < 1024) {\n\t\t\t\treturn `${bytes} B`\n\t\t\t}\n\t\t\tif (bytes < 1024 * 1024) {\n\t\t\t\treturn `${(bytes / 1024).toFixed(1)} KB`\n\t\t\t}\n\t\t\treturn `${(bytes / 1024 / 1024).toFixed(1)} MB`\n\t\t},\n\n\t\t/**\n\t\t * Format an ISO timestamp as a short locale string.\n\t\t *\n\t\t * @param {string} iso ISO datetime.\n\t\t * @return {string} Locale-formatted timestamp.\n\t\t */\n\t\tformatTimestamp(iso) {\n\t\t\ttry {\n\t\t\t\tconst d = new Date(iso)\n\t\t\t\tif (Number.isNaN(d.getTime())) {\n\t\t\t\t\treturn iso\n\t\t\t\t}\n\t\t\t\treturn d.toLocaleString(userLocale())\n\t\t\t} catch {\n\t\t\t\treturn iso\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Programmatic open of the file picker. Useful from a\n\t\t * parent button.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\topenFilePicker() {\n\t\t\tif (this.readOnly) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.$refs.fileInput) {\n\t\t\t\tthis.$refs.fileInput.click()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Dropzone dragover handler.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonDragOver() {\n\t\t\tif (this.readOnly) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.isDragging = true\n\t\t},\n\n\t\t/**\n\t\t * Dropzone dragleave handler.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonDragLeave() {\n\t\t\tthis.isDragging = false\n\t\t},\n\n\t\t/**\n\t\t * Dropzone drop handler — extracts files + validates size +\n\t\t * emits `@upload`.\n\t\t *\n\t\t * @param {DragEvent} event The drop event.\n\t\t * @return {void}\n\t\t */\n\t\tonDrop(event) {\n\t\t\tthis.isDragging = false\n\t\t\tif (this.readOnly) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst list = event.dataTransfer ? Array.from(event.dataTransfer.files || []) : []\n\t\t\tthis.emitUpload(list)\n\t\t},\n\n\t\t/**\n\t\t * File-input change handler.\n\t\t *\n\t\t * @param {Event} event The change event.\n\t\t * @return {void}\n\t\t */\n\t\tonFileInputChange(event) {\n\t\t\tconst list = Array.from(event.target.files || [])\n\t\t\tthis.emitUpload(list)\n\t\t\tevent.target.value = ''\n\t\t},\n\n\t\t/**\n\t\t * Validate + emit `@upload` with the file batch.\n\t\t *\n\t\t * @param {File[]} list The selected files.\n\t\t * @return {void}\n\t\t */\n\t\temitUpload(list) {\n\t\t\tif (list.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.maxSizeMb > 0) {\n\t\t\t\tconst limit = this.maxSizeMb * 1024 * 1024\n\t\t\t\tconst oversized = list.find((f) => f.size > limit)\n\t\t\t\tif (oversized) {\n\t\t\t\t\t/**\n\t\t\t\t\t * @event upload-rejected Emitted when one or more\n\t\t\t\t\t * files in the batch exceed maxSizeMb.\n\t\t\t\t\t * @type {{ reason: 'size', file: File, limitMb: number }}\n\t\t\t\t\t */\n\t\t\t\t\tthis.$emit('upload-rejected', { reason: 'size', file: oversized, limitMb: this.maxSizeMb })\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event upload Emitted with the file batch from the\n\t\t\t * dropzone or file picker.\n\t\t\t * @type {File[]}\n\t\t\t */\n\t\t\tthis.$emit('upload', list)\n\t\t},\n\n\t\t/**\n\t\t * Forward a file-row click.\n\t\t *\n\t\t * @param {object} file The file entry.\n\t\t * @return {void}\n\t\t */\n\t\tonFileClick(file) {\n\t\t\t/**\n\t\t\t * @event file-click Emitted on row click (not on the\n\t\t\t * action buttons).\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('file-click', file)\n\t\t},\n\n\t\t/**\n\t\t * Forward a download action.\n\t\t *\n\t\t * @param {object} file The file entry.\n\t\t * @return {void}\n\t\t */\n\t\tonDownload(file) {\n\t\t\t/**\n\t\t\t * @event download Emitted when the Download button is\n\t\t\t * clicked.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('download', file)\n\t\t},\n\n\t\t/**\n\t\t * Forward a delete action; sets the in-flight flag so the\n\t\t * button disables until the parent removes the file or\n\t\t * clears it via `setDeleted`.\n\t\t *\n\t\t * @param {object} file The file entry.\n\t\t * @return {void}\n\t\t */\n\t\tonDelete(file) {\n\t\t\tconst id = file.id || file.name\n\t\t\tthis.deletingIds = { ...this.deletingIds, [id]: true }\n\t\t\t/**\n\t\t\t * @event delete Emitted when the Delete button is\n\t\t\t * clicked. Parent persists then removes from files[].\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('delete', file)\n\t\t},\n\n\t\t/**\n\t\t * Public method — clear the in-flight delete marker for a\n\t\t * file id. Use when the parent's persist fails so the\n\t\t * Delete button becomes clickable again.\n\t\t *\n\t\t * @param {string} id File id.\n\t\t * @return {void}\n\t\t */\n\t\tclearDeleting(id) {\n\t\t\tconst next = { ...this.deletingIds }\n\t\t\tdelete next[id]\n\t\t\tthis.deletingIds = next\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-file-manager {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n}\n\n.cn-file-manager__title {\n\tmargin: 0;\n\tfont-size: 1.1em;\n}\n\n.cn-file-manager__description {\n\tmargin: 4px 0 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-file-manager__dropzone {\n\tborder: 2px dashed var(--color-border);\n\tborder-radius: var(--border-radius);\n\tpadding: 24px;\n\ttext-align: center;\n\tbackground: var(--color-background-hover);\n\tcursor: pointer;\n\ttransition: background 0.15s ease, border-color 0.15s ease;\n}\n\n.cn-file-manager__dropzone:hover,\n.cn-file-manager__dropzone--active {\n\tbackground: var(--color-primary-element-light);\n\tborder-color: var(--color-primary-element);\n}\n\n.cn-file-manager__dropzone p {\n\tmargin: 0;\n}\n\n.cn-file-manager__dropzone small {\n\tdisplay: block;\n\tmargin-top: 4px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-file-manager__file-input {\n\tdisplay: none;\n}\n\n.cn-file-manager__empty {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-style: italic;\n\tmargin: 16px 0;\n\ttext-align: center;\n}\n\n.cn-file-manager__list {\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n}\n\n.cn-file-manager__item {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 12px;\n\tpadding: 8px 12px;\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-background-hover);\n\tcursor: pointer;\n}\n\n.cn-file-manager__item:hover {\n\tbackground: var(--color-background-darker, var(--color-background-hover));\n}\n\n.cn-file-manager__item-icon {\n\tfont-size: 1.4em;\n}\n\n.cn-file-manager__item-meta {\n\tflex: 1 1 auto;\n\tmin-width: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n}\n\n.cn-file-manager__item-name {\n\tfont-weight: 500;\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n\n.cn-file-manager__item-sub {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-file-manager__item-actions {\n\tdisplay: flex;\n\tgap: 4px;\n}\n\n.cn-file-manager__action {\n\tbackground: none;\n\tborder: 1px solid var(--color-border);\n\tpadding: 4px 8px;\n\tborder-radius: var(--border-radius);\n\tcursor: pointer;\n\tfont-size: 0.85em;\n}\n\n.cn-file-manager__action:hover {\n\tbackground: var(--color-main-background);\n}\n\n.cn-file-manager__action--delete {\n\tcolor: var(--color-error);\n}\n\n.cn-file-manager__action:disabled {\n\topacity: 0.4;\n\tcursor: not-allowed;\n}\n</style>\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"CnFileManager.vue3.js","sources":["../../../../src/components/CnFileManager/CnFileManager.vue?vue&type=template&id=1b9de2b2&lang.js"],"sourcesContent":["<template>\n\t<div class=\"cn-file-manager\" data-testid=\"cn-file-manager\">\n\t\t<header v-if=\"title || description || $slots.header\" class=\"cn-file-manager__header\">\n\t\t\t<!-- @slot header Overrides the default title/description header. Defaults to `<h3>{{ title }}</h3>` + `<p>{{ description }}</p>`. -->\n\t\t\t<slot name=\"header\">\n\t\t\t\t<h3 v-if=\"title\" class=\"cn-file-manager__title\">\n\t\t\t\t\t{{ title }}\n\t\t\t\t</h3>\n\t\t\t\t<p v-if=\"description\" class=\"cn-file-manager__description\">\n\t\t\t\t\t{{ description }}\n\t\t\t\t</p>\n\t\t\t</slot>\n\t\t</header>\n\n\t\t<!-- Drop zone — drag a file here to trigger @upload. -->\n\t\t<div v-if=\"!readOnly\"\n\t\t\tclass=\"cn-file-manager__dropzone\"\n\t\t\t:class=\"{ 'cn-file-manager__dropzone--active': isDragging }\"\n\t\t\tdata-testid=\"cn-file-manager-dropzone\"\n\t\t\t@dragover.prevent=\"onDragOver\"\n\t\t\t@dragleave.prevent=\"onDragLeave\"\n\t\t\t@drop.prevent=\"onDrop\"\n\t\t\t@click=\"openFilePicker\">\n\t\t\t<p>{{ dropzoneLabel }}</p>\n\t\t\t<small v-if=\"dropzoneHint\">{{ dropzoneHint }}</small>\n\t\t\t<input ref=\"fileInput\"\n\t\t\t\ttype=\"file\"\n\t\t\t\t:accept=\"accept\"\n\t\t\t\t:multiple=\"multiple\"\n\t\t\t\tclass=\"cn-file-manager__file-input\"\n\t\t\t\t@change=\"onFileInputChange\">\n\t\t</div>\n\n\t\t<!-- Empty state. -->\n\t\t<p v-if=\"files.length === 0\" class=\"cn-file-manager__empty\">\n\t\t\t{{ emptyLabel }}\n\t\t</p>\n\n\t\t<!-- File list. -->\n\t\t<ul v-else class=\"cn-file-manager__list\">\n\t\t\t<li v-for=\"file in files\"\n\t\t\t\t:key=\"file.id || file.name\"\n\t\t\t\tclass=\"cn-file-manager__item\"\n\t\t\t\t:data-file-id=\"file.id || file.name\"\n\t\t\t\t@click=\"onFileClick(file)\">\n\t\t\t\t<span class=\"cn-file-manager__item-icon\">{{ iconFor(file) }}</span>\n\t\t\t\t<div class=\"cn-file-manager__item-meta\">\n\t\t\t\t\t<span class=\"cn-file-manager__item-name\">{{ file.name }}</span>\n\t\t\t\t\t<small class=\"cn-file-manager__item-sub\">\n\t\t\t\t\t\t{{ humanSize(file.size) }}\n\t\t\t\t\t\t<template v-if=\"file.uploadedAt\"> · {{ formatTimestamp(file.uploadedAt) }}</template>\n\t\t\t\t\t\t<template v-if=\"file.uploadedBy\"> · {{ file.uploadedBy }}</template>\n\t\t\t\t\t</small>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"cn-file-manager__item-actions\" @click.stop>\n\t\t\t\t\t<!-- @slot item-actions Per-file actions. Scope:\n\t\t\t\t\t { file }. Default renders Download / Delete\n\t\t\t\t\t buttons emitting @download / @delete. -->\n\t\t\t\t\t<slot name=\"item-actions\" :file=\"file\">\n\t\t\t\t\t\t<button v-if=\"file.url || file.downloadHandler\"\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclass=\"cn-file-manager__action\"\n\t\t\t\t\t\t\t:title=\"downloadLabel\"\n\t\t\t\t\t\t\t@click.stop=\"onDownload(file)\">\n\t\t\t\t\t\t\t{{ downloadLabel }}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t\t<button v-if=\"!readOnly\"\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclass=\"cn-file-manager__action cn-file-manager__action--delete\"\n\t\t\t\t\t\t\t:title=\"deleteLabel\"\n\t\t\t\t\t\t\t:disabled=\"deletingIds[file.id || file.name]\"\n\t\t\t\t\t\t\t@click.stop=\"onDelete(file)\">\n\t\t\t\t\t\t\t{{ deleteLabel }}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</slot>\n\t\t\t\t</div>\n\t\t\t</li>\n\t\t</ul>\n\t</div>\n</template>\n\n<script>\n/**\n * CnFileManager — File list widget with drag-drop upload, per-file\n * actions, and a click-emit for preview / detail.\n *\n * The component does NOT own a network layer — consumers wire the\n * uploads and downloads via the `@upload`, `@download`, `@delete`\n * events. The widget owns the dropzone UI, the per-file row\n * rendering, the icon-by-extension mapping, and the human-readable\n * size + timestamp formatting.\n *\n * For OpenRegister-backed file attachments, the consuming app\n * typically converts the OR `_files` array into the `files[]` prop\n * shape and persists the deletes / uploads back via the usual\n * object-store calls.\n *\n * ```vue\n * <CnFileManager\n * title=\"Attachments\"\n * :files=\"attachments\"\n * accept=\".pdf,.png,.jpg\"\n * :multiple=\"true\"\n * :max-size-mb=\"20\"\n * @upload=\"onUpload\"\n * @download=\"onDownload\"\n * @delete=\"onDelete\"\n * @file-click=\"openPreview\" />\n * ```\n */\nexport default {\n\tname: 'CnFileManager',\n\tprops: {\n\t\t/**\n\t\t * Files to render. Each entry:\n\t\t * `{ id?, name, size?, type?, url?, uploadedAt?, uploadedBy? }`.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tfiles: { type: Array, default: () => [] },\n\t\t/** Optional widget title. */\n\t\ttitle: { type: String, default: '' },\n\t\t/** Optional description. */\n\t\tdescription: { type: String, default: '' },\n\t\t/** Hide the dropzone + delete actions. */\n\t\treadOnly: { type: Boolean, default: false },\n\t\t/** `accept` attribute for the file input. */\n\t\taccept: { type: String, default: '' },\n\t\t/** Allow multi-file selection. */\n\t\tmultiple: { type: Boolean, default: true },\n\t\t/** Per-file max size in megabytes. 0 disables. */\n\t\tmaxSizeMb: { type: Number, default: 0 },\n\t\t/** Dropzone primary label. */\n\t\tdropzoneLabel: { type: String, default: 'Drop files here or click to browse' },\n\t\t/** Dropzone secondary hint. */\n\t\tdropzoneHint: { type: String, default: '' },\n\t\t/** Empty-state text when files[] is empty. */\n\t\temptyLabel: { type: String, default: 'No files attached.' },\n\t\t/** Download button label / title. */\n\t\tdownloadLabel: { type: String, default: 'Download' },\n\t\t/** Delete button label / title. */\n\t\tdeleteLabel: { type: String, default: 'Delete' },\n\t},\n\n\temits: ['delete', 'download', 'file-click', 'upload', 'upload-rejected'],\n\tdata() {\n\t\treturn {\n\t\t\tisDragging: false,\n\t\t\tdeletingIds: {},\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Pick the emoji-icon for a file based on extension /\n\t\t * MIME type. Pure presentation; consumers can override\n\t\t * via the `#item-actions` slot if they want full icons.\n\t\t *\n\t\t * @param {object} file The file entry.\n\t\t * @return {string} The icon character.\n\t\t */\n\t\ticonFor(file) {\n\t\t\tconst ext = (file.name || '').split('.').pop().toLowerCase()\n\t\t\tconst type = (file.type || '').toLowerCase()\n\t\t\tif (type.startsWith('image/') || ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'].includes(ext)) {\n\t\t\t\treturn '🖼️'\n\t\t\t}\n\t\t\tif (type.startsWith('video/') || ['mp4', 'mov', 'avi', 'webm'].includes(ext)) {\n\t\t\t\treturn '🎬'\n\t\t\t}\n\t\t\tif (type.startsWith('audio/') || ['mp3', 'wav', 'ogg'].includes(ext)) {\n\t\t\t\treturn '🎵'\n\t\t\t}\n\t\t\tif (['pdf'].includes(ext)) {\n\t\t\t\treturn '📕'\n\t\t\t}\n\t\t\tif (['doc', 'docx', 'odt', 'txt', 'md'].includes(ext)) {\n\t\t\t\treturn '📄'\n\t\t\t}\n\t\t\tif (['xls', 'xlsx', 'ods', 'csv'].includes(ext)) {\n\t\t\t\treturn '📊'\n\t\t\t}\n\t\t\tif (['ppt', 'pptx', 'odp'].includes(ext)) {\n\t\t\t\treturn '📈'\n\t\t\t}\n\t\t\tif (['zip', 'rar', '7z', 'tar', 'gz'].includes(ext)) {\n\t\t\t\treturn '🗜️'\n\t\t\t}\n\t\t\tif (['xml', 'json', 'yml', 'yaml'].includes(ext)) {\n\t\t\t\treturn '🧾'\n\t\t\t}\n\t\t\treturn '📎'\n\t\t},\n\n\t\t/**\n\t\t * Format a byte count as a short human-readable string.\n\t\t *\n\t\t * @param {number} bytes Size in bytes.\n\t\t * @return {string} Like '1.2 MB'.\n\t\t */\n\t\thumanSize(bytes) {\n\t\t\tif (bytes === undefined || bytes === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (bytes < 1024) {\n\t\t\t\treturn `${bytes} B`\n\t\t\t}\n\t\t\tif (bytes < 1024 * 1024) {\n\t\t\t\treturn `${(bytes / 1024).toFixed(1)} KB`\n\t\t\t}\n\t\t\treturn `${(bytes / 1024 / 1024).toFixed(1)} MB`\n\t\t},\n\n\t\t/**\n\t\t * Format an ISO timestamp as a short locale string.\n\t\t *\n\t\t * @param {string} iso ISO datetime.\n\t\t * @return {string} Locale-formatted timestamp.\n\t\t */\n\t\tformatTimestamp(iso) {\n\t\t\ttry {\n\t\t\t\tconst d = new Date(iso)\n\t\t\t\tif (Number.isNaN(d.getTime())) {\n\t\t\t\t\treturn iso\n\t\t\t\t}\n\t\t\t\treturn d.toLocaleString()\n\t\t\t} catch {\n\t\t\t\treturn iso\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Programmatic open of the file picker. Useful from a\n\t\t * parent button.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\topenFilePicker() {\n\t\t\tif (this.readOnly) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.$refs.fileInput) {\n\t\t\t\tthis.$refs.fileInput.click()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Dropzone dragover handler.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonDragOver() {\n\t\t\tif (this.readOnly) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.isDragging = true\n\t\t},\n\n\t\t/**\n\t\t * Dropzone dragleave handler.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonDragLeave() {\n\t\t\tthis.isDragging = false\n\t\t},\n\n\t\t/**\n\t\t * Dropzone drop handler — extracts files + validates size +\n\t\t * emits `@upload`.\n\t\t *\n\t\t * @param {DragEvent} event The drop event.\n\t\t * @return {void}\n\t\t */\n\t\tonDrop(event) {\n\t\t\tthis.isDragging = false\n\t\t\tif (this.readOnly) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst list = event.dataTransfer ? Array.from(event.dataTransfer.files || []) : []\n\t\t\tthis.emitUpload(list)\n\t\t},\n\n\t\t/**\n\t\t * File-input change handler.\n\t\t *\n\t\t * @param {Event} event The change event.\n\t\t * @return {void}\n\t\t */\n\t\tonFileInputChange(event) {\n\t\t\tconst list = Array.from(event.target.files || [])\n\t\t\tthis.emitUpload(list)\n\t\t\tevent.target.value = ''\n\t\t},\n\n\t\t/**\n\t\t * Validate + emit `@upload` with the file batch.\n\t\t *\n\t\t * @param {File[]} list The selected files.\n\t\t * @return {void}\n\t\t */\n\t\temitUpload(list) {\n\t\t\tif (list.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.maxSizeMb > 0) {\n\t\t\t\tconst limit = this.maxSizeMb * 1024 * 1024\n\t\t\t\tconst oversized = list.find((f) => f.size > limit)\n\t\t\t\tif (oversized) {\n\t\t\t\t\t/**\n\t\t\t\t\t * @event upload-rejected Emitted when one or more\n\t\t\t\t\t * files in the batch exceed maxSizeMb.\n\t\t\t\t\t * @type {{ reason: 'size', file: File, limitMb: number }}\n\t\t\t\t\t */\n\t\t\t\t\tthis.$emit('upload-rejected', { reason: 'size', file: oversized, limitMb: this.maxSizeMb })\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event upload Emitted with the file batch from the\n\t\t\t * dropzone or file picker.\n\t\t\t * @type {File[]}\n\t\t\t */\n\t\t\tthis.$emit('upload', list)\n\t\t},\n\n\t\t/**\n\t\t * Forward a file-row click.\n\t\t *\n\t\t * @param {object} file The file entry.\n\t\t * @return {void}\n\t\t */\n\t\tonFileClick(file) {\n\t\t\t/**\n\t\t\t * @event file-click Emitted on row click (not on the\n\t\t\t * action buttons).\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('file-click', file)\n\t\t},\n\n\t\t/**\n\t\t * Forward a download action.\n\t\t *\n\t\t * @param {object} file The file entry.\n\t\t * @return {void}\n\t\t */\n\t\tonDownload(file) {\n\t\t\t/**\n\t\t\t * @event download Emitted when the Download button is\n\t\t\t * clicked.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('download', file)\n\t\t},\n\n\t\t/**\n\t\t * Forward a delete action; sets the in-flight flag so the\n\t\t * button disables until the parent removes the file or\n\t\t * clears it via `setDeleted`.\n\t\t *\n\t\t * @param {object} file The file entry.\n\t\t * @return {void}\n\t\t */\n\t\tonDelete(file) {\n\t\t\tconst id = file.id || file.name\n\t\t\tthis.deletingIds = { ...this.deletingIds, [id]: true }\n\t\t\t/**\n\t\t\t * @event delete Emitted when the Delete button is\n\t\t\t * clicked. Parent persists then removes from files[].\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('delete', file)\n\t\t},\n\n\t\t/**\n\t\t * Public method — clear the in-flight delete marker for a\n\t\t * file id. Use when the parent's persist fails so the\n\t\t * Delete button becomes clickable again.\n\t\t *\n\t\t * @param {string} id File id.\n\t\t * @return {void}\n\t\t */\n\t\tclearDeleting(id) {\n\t\t\tconst next = { ...this.deletingIds }\n\t\t\tdelete next[id]\n\t\t\tthis.deletingIds = next\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-file-manager {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n}\n\n.cn-file-manager__title {\n\tmargin: 0;\n\tfont-size: 1.1em;\n}\n\n.cn-file-manager__description {\n\tmargin: 4px 0 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-file-manager__dropzone {\n\tborder: 2px dashed var(--color-border);\n\tborder-radius: var(--border-radius);\n\tpadding: 24px;\n\ttext-align: center;\n\tbackground: var(--color-background-hover);\n\tcursor: pointer;\n\ttransition: background 0.15s ease, border-color 0.15s ease;\n}\n\n.cn-file-manager__dropzone:hover,\n.cn-file-manager__dropzone--active {\n\tbackground: var(--color-primary-element-light);\n\tborder-color: var(--color-primary-element);\n}\n\n.cn-file-manager__dropzone p {\n\tmargin: 0;\n}\n\n.cn-file-manager__dropzone small {\n\tdisplay: block;\n\tmargin-top: 4px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-file-manager__file-input {\n\tdisplay: none;\n}\n\n.cn-file-manager__empty {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-style: italic;\n\tmargin: 16px 0;\n\ttext-align: center;\n}\n\n.cn-file-manager__list {\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n}\n\n.cn-file-manager__item {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 12px;\n\tpadding: 8px 12px;\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-background-hover);\n\tcursor: pointer;\n}\n\n.cn-file-manager__item:hover {\n\tbackground: var(--color-background-darker, var(--color-background-hover));\n}\n\n.cn-file-manager__item-icon {\n\tfont-size: 1.4em;\n}\n\n.cn-file-manager__item-meta {\n\tflex: 1 1 auto;\n\tmin-width: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n}\n\n.cn-file-manager__item-name {\n\tfont-weight: 500;\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n\n.cn-file-manager__item-sub {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-file-manager__item-actions {\n\tdisplay: flex;\n\tgap: 4px;\n}\n\n.cn-file-manager__action {\n\tbackground: none;\n\tborder: 1px solid var(--color-border);\n\tpadding: 4px 8px;\n\tborder-radius: var(--border-radius);\n\tcursor: pointer;\n\tfont-size: 0.85em;\n}\n\n.cn-file-manager__action:hover {\n\tbackground: var(--color-main-background);\n}\n\n.cn-file-manager__action--delete {\n\tcolor: var(--color-error);\n}\n\n.cn-file-manager__action:disabled {\n\topacity: 0.4;\n\tcursor: not-allowed;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_renderSlot","_toDisplayString","_normalizeClass","_createElementVNode","_Fragment","_renderList","_createTextVNode","_withModifiers"],"mappings":"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"}
1
+ {"version":3,"file":"CnFileManager.vue3.js","sources":["../../../../src/components/CnFileManager/CnFileManager.vue?vue&type=template&id=395592dc&lang.js"],"sourcesContent":["<template>\n\t<div class=\"cn-file-manager\" data-testid=\"cn-file-manager\">\n\t\t<header v-if=\"title || description || $slots.header\" class=\"cn-file-manager__header\">\n\t\t\t<!-- @slot header Overrides the default title/description header. Defaults to `<h3>{{ title }}</h3>` + `<p>{{ description }}</p>`. -->\n\t\t\t<slot name=\"header\">\n\t\t\t\t<h3 v-if=\"title\" class=\"cn-file-manager__title\">\n\t\t\t\t\t{{ title }}\n\t\t\t\t</h3>\n\t\t\t\t<p v-if=\"description\" class=\"cn-file-manager__description\">\n\t\t\t\t\t{{ description }}\n\t\t\t\t</p>\n\t\t\t</slot>\n\t\t</header>\n\n\t\t<!-- Drop zone — drag a file here to trigger @upload. -->\n\t\t<div v-if=\"!readOnly\"\n\t\t\tclass=\"cn-file-manager__dropzone\"\n\t\t\t:class=\"{ 'cn-file-manager__dropzone--active': isDragging }\"\n\t\t\tdata-testid=\"cn-file-manager-dropzone\"\n\t\t\t@dragover.prevent=\"onDragOver\"\n\t\t\t@dragleave.prevent=\"onDragLeave\"\n\t\t\t@drop.prevent=\"onDrop\"\n\t\t\t@click=\"openFilePicker\">\n\t\t\t<p>{{ dropzoneLabel }}</p>\n\t\t\t<small v-if=\"dropzoneHint\">{{ dropzoneHint }}</small>\n\t\t\t<input ref=\"fileInput\"\n\t\t\t\ttype=\"file\"\n\t\t\t\t:accept=\"accept\"\n\t\t\t\t:multiple=\"multiple\"\n\t\t\t\tclass=\"cn-file-manager__file-input\"\n\t\t\t\t@change=\"onFileInputChange\">\n\t\t</div>\n\n\t\t<!-- Empty state. -->\n\t\t<p v-if=\"files.length === 0\" class=\"cn-file-manager__empty\">\n\t\t\t{{ emptyLabel }}\n\t\t</p>\n\n\t\t<!-- File list. -->\n\t\t<ul v-else class=\"cn-file-manager__list\">\n\t\t\t<li v-for=\"file in files\"\n\t\t\t\t:key=\"file.id || file.name\"\n\t\t\t\tclass=\"cn-file-manager__item\"\n\t\t\t\t:data-file-id=\"file.id || file.name\"\n\t\t\t\t@click=\"onFileClick(file)\">\n\t\t\t\t<span class=\"cn-file-manager__item-icon\">{{ iconFor(file) }}</span>\n\t\t\t\t<div class=\"cn-file-manager__item-meta\">\n\t\t\t\t\t<span class=\"cn-file-manager__item-name\">{{ file.name }}</span>\n\t\t\t\t\t<small class=\"cn-file-manager__item-sub\">\n\t\t\t\t\t\t{{ humanSize(file.size) }}\n\t\t\t\t\t\t<template v-if=\"file.uploadedAt\"> · {{ formatTimestamp(file.uploadedAt) }}</template>\n\t\t\t\t\t\t<template v-if=\"file.uploadedBy\"> · {{ file.uploadedBy }}</template>\n\t\t\t\t\t</small>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"cn-file-manager__item-actions\" @click.stop>\n\t\t\t\t\t<!-- @slot item-actions Per-file actions. Scope:\n\t\t\t\t\t { file }. Default renders Download / Delete\n\t\t\t\t\t buttons emitting @download / @delete. -->\n\t\t\t\t\t<slot name=\"item-actions\" :file=\"file\">\n\t\t\t\t\t\t<button v-if=\"file.url || file.downloadHandler\"\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclass=\"cn-file-manager__action\"\n\t\t\t\t\t\t\t:title=\"downloadLabel\"\n\t\t\t\t\t\t\t@click.stop=\"onDownload(file)\">\n\t\t\t\t\t\t\t{{ downloadLabel }}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t\t<button v-if=\"!readOnly\"\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclass=\"cn-file-manager__action cn-file-manager__action--delete\"\n\t\t\t\t\t\t\t:title=\"deleteLabel\"\n\t\t\t\t\t\t\t:disabled=\"deletingIds[file.id || file.name]\"\n\t\t\t\t\t\t\t@click.stop=\"onDelete(file)\">\n\t\t\t\t\t\t\t{{ deleteLabel }}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</slot>\n\t\t\t\t</div>\n\t\t\t</li>\n\t\t</ul>\n\t</div>\n</template>\n\n<script>\nimport { userLocale } from '../../utils/userLocale.js'\n/**\n * CnFileManager — File list widget with drag-drop upload, per-file\n * actions, and a click-emit for preview / detail.\n *\n * The component does NOT own a network layer — consumers wire the\n * uploads and downloads via the `@upload`, `@download`, `@delete`\n * events. The widget owns the dropzone UI, the per-file row\n * rendering, the icon-by-extension mapping, and the human-readable\n * size + timestamp formatting.\n *\n * For OpenRegister-backed file attachments, the consuming app\n * typically converts the OR `_files` array into the `files[]` prop\n * shape and persists the deletes / uploads back via the usual\n * object-store calls.\n *\n * ```vue\n * <CnFileManager\n * title=\"Attachments\"\n * :files=\"attachments\"\n * accept=\".pdf,.png,.jpg\"\n * :multiple=\"true\"\n * :max-size-mb=\"20\"\n * @upload=\"onUpload\"\n * @download=\"onDownload\"\n * @delete=\"onDelete\"\n * @file-click=\"openPreview\" />\n * ```\n */\nexport default {\n\tname: 'CnFileManager',\n\tprops: {\n\t\t/**\n\t\t * Files to render. Each entry:\n\t\t * `{ id?, name, size?, type?, url?, uploadedAt?, uploadedBy? }`.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tfiles: { type: Array, default: () => [] },\n\t\t/** Optional widget title. */\n\t\ttitle: { type: String, default: '' },\n\t\t/** Optional description. */\n\t\tdescription: { type: String, default: '' },\n\t\t/** Hide the dropzone + delete actions. */\n\t\treadOnly: { type: Boolean, default: false },\n\t\t/** `accept` attribute for the file input. */\n\t\taccept: { type: String, default: '' },\n\t\t/** Allow multi-file selection. */\n\t\tmultiple: { type: Boolean, default: true },\n\t\t/** Per-file max size in megabytes. 0 disables. */\n\t\tmaxSizeMb: { type: Number, default: 0 },\n\t\t/** Dropzone primary label. */\n\t\tdropzoneLabel: { type: String, default: 'Drop files here or click to browse' },\n\t\t/** Dropzone secondary hint. */\n\t\tdropzoneHint: { type: String, default: '' },\n\t\t/** Empty-state text when files[] is empty. */\n\t\temptyLabel: { type: String, default: 'No files attached.' },\n\t\t/** Download button label / title. */\n\t\tdownloadLabel: { type: String, default: 'Download' },\n\t\t/** Delete button label / title. */\n\t\tdeleteLabel: { type: String, default: 'Delete' },\n\t},\n\n\temits: ['delete', 'download', 'file-click', 'upload', 'upload-rejected'],\n\tdata() {\n\t\treturn {\n\t\t\tisDragging: false,\n\t\t\tdeletingIds: {},\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Pick the emoji-icon for a file based on extension /\n\t\t * MIME type. Pure presentation; consumers can override\n\t\t * via the `#item-actions` slot if they want full icons.\n\t\t *\n\t\t * @param {object} file The file entry.\n\t\t * @return {string} The icon character.\n\t\t */\n\t\ticonFor(file) {\n\t\t\tconst ext = (file.name || '').split('.').pop().toLowerCase()\n\t\t\tconst type = (file.type || '').toLowerCase()\n\t\t\tif (type.startsWith('image/') || ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'].includes(ext)) {\n\t\t\t\treturn '🖼️'\n\t\t\t}\n\t\t\tif (type.startsWith('video/') || ['mp4', 'mov', 'avi', 'webm'].includes(ext)) {\n\t\t\t\treturn '🎬'\n\t\t\t}\n\t\t\tif (type.startsWith('audio/') || ['mp3', 'wav', 'ogg'].includes(ext)) {\n\t\t\t\treturn '🎵'\n\t\t\t}\n\t\t\tif (['pdf'].includes(ext)) {\n\t\t\t\treturn '📕'\n\t\t\t}\n\t\t\tif (['doc', 'docx', 'odt', 'txt', 'md'].includes(ext)) {\n\t\t\t\treturn '📄'\n\t\t\t}\n\t\t\tif (['xls', 'xlsx', 'ods', 'csv'].includes(ext)) {\n\t\t\t\treturn '📊'\n\t\t\t}\n\t\t\tif (['ppt', 'pptx', 'odp'].includes(ext)) {\n\t\t\t\treturn '📈'\n\t\t\t}\n\t\t\tif (['zip', 'rar', '7z', 'tar', 'gz'].includes(ext)) {\n\t\t\t\treturn '🗜️'\n\t\t\t}\n\t\t\tif (['xml', 'json', 'yml', 'yaml'].includes(ext)) {\n\t\t\t\treturn '🧾'\n\t\t\t}\n\t\t\treturn '📎'\n\t\t},\n\n\t\t/**\n\t\t * Format a byte count as a short human-readable string.\n\t\t *\n\t\t * @param {number} bytes Size in bytes.\n\t\t * @return {string} Like '1.2 MB'.\n\t\t */\n\t\thumanSize(bytes) {\n\t\t\tif (bytes === undefined || bytes === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (bytes < 1024) {\n\t\t\t\treturn `${bytes} B`\n\t\t\t}\n\t\t\tif (bytes < 1024 * 1024) {\n\t\t\t\treturn `${(bytes / 1024).toFixed(1)} KB`\n\t\t\t}\n\t\t\treturn `${(bytes / 1024 / 1024).toFixed(1)} MB`\n\t\t},\n\n\t\t/**\n\t\t * Format an ISO timestamp as a short locale string.\n\t\t *\n\t\t * @param {string} iso ISO datetime.\n\t\t * @return {string} Locale-formatted timestamp.\n\t\t */\n\t\tformatTimestamp(iso) {\n\t\t\ttry {\n\t\t\t\tconst d = new Date(iso)\n\t\t\t\tif (Number.isNaN(d.getTime())) {\n\t\t\t\t\treturn iso\n\t\t\t\t}\n\t\t\t\treturn d.toLocaleString(userLocale())\n\t\t\t} catch {\n\t\t\t\treturn iso\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Programmatic open of the file picker. Useful from a\n\t\t * parent button.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\topenFilePicker() {\n\t\t\tif (this.readOnly) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.$refs.fileInput) {\n\t\t\t\tthis.$refs.fileInput.click()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Dropzone dragover handler.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonDragOver() {\n\t\t\tif (this.readOnly) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.isDragging = true\n\t\t},\n\n\t\t/**\n\t\t * Dropzone dragleave handler.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonDragLeave() {\n\t\t\tthis.isDragging = false\n\t\t},\n\n\t\t/**\n\t\t * Dropzone drop handler — extracts files + validates size +\n\t\t * emits `@upload`.\n\t\t *\n\t\t * @param {DragEvent} event The drop event.\n\t\t * @return {void}\n\t\t */\n\t\tonDrop(event) {\n\t\t\tthis.isDragging = false\n\t\t\tif (this.readOnly) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst list = event.dataTransfer ? Array.from(event.dataTransfer.files || []) : []\n\t\t\tthis.emitUpload(list)\n\t\t},\n\n\t\t/**\n\t\t * File-input change handler.\n\t\t *\n\t\t * @param {Event} event The change event.\n\t\t * @return {void}\n\t\t */\n\t\tonFileInputChange(event) {\n\t\t\tconst list = Array.from(event.target.files || [])\n\t\t\tthis.emitUpload(list)\n\t\t\tevent.target.value = ''\n\t\t},\n\n\t\t/**\n\t\t * Validate + emit `@upload` with the file batch.\n\t\t *\n\t\t * @param {File[]} list The selected files.\n\t\t * @return {void}\n\t\t */\n\t\temitUpload(list) {\n\t\t\tif (list.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.maxSizeMb > 0) {\n\t\t\t\tconst limit = this.maxSizeMb * 1024 * 1024\n\t\t\t\tconst oversized = list.find((f) => f.size > limit)\n\t\t\t\tif (oversized) {\n\t\t\t\t\t/**\n\t\t\t\t\t * @event upload-rejected Emitted when one or more\n\t\t\t\t\t * files in the batch exceed maxSizeMb.\n\t\t\t\t\t * @type {{ reason: 'size', file: File, limitMb: number }}\n\t\t\t\t\t */\n\t\t\t\t\tthis.$emit('upload-rejected', { reason: 'size', file: oversized, limitMb: this.maxSizeMb })\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event upload Emitted with the file batch from the\n\t\t\t * dropzone or file picker.\n\t\t\t * @type {File[]}\n\t\t\t */\n\t\t\tthis.$emit('upload', list)\n\t\t},\n\n\t\t/**\n\t\t * Forward a file-row click.\n\t\t *\n\t\t * @param {object} file The file entry.\n\t\t * @return {void}\n\t\t */\n\t\tonFileClick(file) {\n\t\t\t/**\n\t\t\t * @event file-click Emitted on row click (not on the\n\t\t\t * action buttons).\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('file-click', file)\n\t\t},\n\n\t\t/**\n\t\t * Forward a download action.\n\t\t *\n\t\t * @param {object} file The file entry.\n\t\t * @return {void}\n\t\t */\n\t\tonDownload(file) {\n\t\t\t/**\n\t\t\t * @event download Emitted when the Download button is\n\t\t\t * clicked.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('download', file)\n\t\t},\n\n\t\t/**\n\t\t * Forward a delete action; sets the in-flight flag so the\n\t\t * button disables until the parent removes the file or\n\t\t * clears it via `setDeleted`.\n\t\t *\n\t\t * @param {object} file The file entry.\n\t\t * @return {void}\n\t\t */\n\t\tonDelete(file) {\n\t\t\tconst id = file.id || file.name\n\t\t\tthis.deletingIds = { ...this.deletingIds, [id]: true }\n\t\t\t/**\n\t\t\t * @event delete Emitted when the Delete button is\n\t\t\t * clicked. Parent persists then removes from files[].\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('delete', file)\n\t\t},\n\n\t\t/**\n\t\t * Public method — clear the in-flight delete marker for a\n\t\t * file id. Use when the parent's persist fails so the\n\t\t * Delete button becomes clickable again.\n\t\t *\n\t\t * @param {string} id File id.\n\t\t * @return {void}\n\t\t */\n\t\tclearDeleting(id) {\n\t\t\tconst next = { ...this.deletingIds }\n\t\t\tdelete next[id]\n\t\t\tthis.deletingIds = next\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-file-manager {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n}\n\n.cn-file-manager__title {\n\tmargin: 0;\n\tfont-size: 1.1em;\n}\n\n.cn-file-manager__description {\n\tmargin: 4px 0 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-file-manager__dropzone {\n\tborder: 2px dashed var(--color-border);\n\tborder-radius: var(--border-radius);\n\tpadding: 24px;\n\ttext-align: center;\n\tbackground: var(--color-background-hover);\n\tcursor: pointer;\n\ttransition: background 0.15s ease, border-color 0.15s ease;\n}\n\n.cn-file-manager__dropzone:hover,\n.cn-file-manager__dropzone--active {\n\tbackground: var(--color-primary-element-light);\n\tborder-color: var(--color-primary-element);\n}\n\n.cn-file-manager__dropzone p {\n\tmargin: 0;\n}\n\n.cn-file-manager__dropzone small {\n\tdisplay: block;\n\tmargin-top: 4px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-file-manager__file-input {\n\tdisplay: none;\n}\n\n.cn-file-manager__empty {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-style: italic;\n\tmargin: 16px 0;\n\ttext-align: center;\n}\n\n.cn-file-manager__list {\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n}\n\n.cn-file-manager__item {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 12px;\n\tpadding: 8px 12px;\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-background-hover);\n\tcursor: pointer;\n}\n\n.cn-file-manager__item:hover {\n\tbackground: var(--color-background-darker, var(--color-background-hover));\n}\n\n.cn-file-manager__item-icon {\n\tfont-size: 1.4em;\n}\n\n.cn-file-manager__item-meta {\n\tflex: 1 1 auto;\n\tmin-width: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n}\n\n.cn-file-manager__item-name {\n\tfont-weight: 500;\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n\n.cn-file-manager__item-sub {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-file-manager__item-actions {\n\tdisplay: flex;\n\tgap: 4px;\n}\n\n.cn-file-manager__action {\n\tbackground: none;\n\tborder: 1px solid var(--color-border);\n\tpadding: 4px 8px;\n\tborder-radius: var(--border-radius);\n\tcursor: pointer;\n\tfont-size: 0.85em;\n}\n\n.cn-file-manager__action:hover {\n\tbackground: var(--color-main-background);\n}\n\n.cn-file-manager__action--delete {\n\tcolor: var(--color-error);\n}\n\n.cn-file-manager__action:disabled {\n\topacity: 0.4;\n\tcursor: not-allowed;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_renderSlot","_toDisplayString","_normalizeClass","_createElementVNode","_Fragment","_renderList","_createTextVNode","_withModifiers"],"mappings":"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"}