@memberjunction/ng-dashboards 6.2.0-edge.3 → 6.2.0-edge.5

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 (247) hide show
  1. package/dist/AI/components/analytics/realtime/realtime-transcripts-data.d.ts +4 -3
  2. package/dist/AI/components/analytics/realtime/realtime-transcripts-data.d.ts.map +1 -1
  3. package/dist/AI/components/analytics/realtime/realtime-transcripts-data.js +8 -5
  4. package/dist/AI/components/analytics/realtime/realtime-transcripts-data.js.map +1 -1
  5. package/dist/AI/components/duplicates/compare-value.component.d.ts +67 -0
  6. package/dist/AI/components/duplicates/compare-value.component.d.ts.map +1 -0
  7. package/dist/AI/components/duplicates/compare-value.component.js +168 -0
  8. package/dist/AI/components/duplicates/compare-value.component.js.map +1 -0
  9. package/dist/AI/components/duplicates/duplicate-detection-resource.component.d.ts +49 -3
  10. package/dist/AI/components/duplicates/duplicate-detection-resource.component.d.ts.map +1 -1
  11. package/dist/AI/components/duplicates/duplicate-detection-resource.component.js +287 -169
  12. package/dist/AI/components/duplicates/duplicate-detection-resource.component.js.map +1 -1
  13. package/dist/AI/components/duplicates/duplicate-detection-run-scope.d.ts +77 -0
  14. package/dist/AI/components/duplicates/duplicate-detection-run-scope.d.ts.map +1 -0
  15. package/dist/AI/components/duplicates/duplicate-detection-run-scope.js +116 -0
  16. package/dist/AI/components/duplicates/duplicate-detection-run-scope.js.map +1 -0
  17. package/dist/AI/components/prompts/model-prompt-priority-matrix.component.d.ts +1 -0
  18. package/dist/AI/components/prompts/model-prompt-priority-matrix.component.d.ts.map +1 -1
  19. package/dist/AI/components/prompts/model-prompt-priority-matrix.component.js +16 -4
  20. package/dist/AI/components/prompts/model-prompt-priority-matrix.component.js.map +1 -1
  21. package/dist/AI/components/tags/tags-resource.component.d.ts +2 -0
  22. package/dist/AI/components/tags/tags-resource.component.d.ts.map +1 -1
  23. package/dist/AI/components/tags/tags-resource.component.js +12 -6
  24. package/dist/AI/components/tags/tags-resource.component.js.map +1 -1
  25. package/dist/AI/components/vectors/sensitive-fields.d.ts +53 -0
  26. package/dist/AI/components/vectors/sensitive-fields.d.ts.map +1 -0
  27. package/dist/AI/components/vectors/sensitive-fields.js +226 -0
  28. package/dist/AI/components/vectors/sensitive-fields.js.map +1 -0
  29. package/dist/AI/components/vectors/vector-document-rules.d.ts +126 -0
  30. package/dist/AI/components/vectors/vector-document-rules.d.ts.map +1 -0
  31. package/dist/AI/components/vectors/vector-document-rules.js +168 -0
  32. package/dist/AI/components/vectors/vector-document-rules.js.map +1 -0
  33. package/dist/AI/components/vectors/vector-index-auto.d.ts +29 -0
  34. package/dist/AI/components/vectors/vector-index-auto.d.ts.map +1 -0
  35. package/dist/AI/components/vectors/vector-index-auto.js +59 -0
  36. package/dist/AI/components/vectors/vector-index-auto.js.map +1 -0
  37. package/dist/AI/components/vectors/vector-management-resource.component.d.ts +58 -0
  38. package/dist/AI/components/vectors/vector-management-resource.component.d.ts.map +1 -1
  39. package/dist/AI/components/vectors/vector-management-resource.component.js +493 -250
  40. package/dist/AI/components/vectors/vector-management-resource.component.js.map +1 -1
  41. package/dist/Actions/components/actions-overview.component.d.ts.map +1 -1
  42. package/dist/Actions/components/actions-overview.component.js +7 -3
  43. package/dist/Actions/components/actions-overview.component.js.map +1 -1
  44. package/dist/Actions/components/categories-list-view.component.d.ts.map +1 -1
  45. package/dist/Actions/components/categories-list-view.component.js +3 -2
  46. package/dist/Actions/components/categories-list-view.component.js.map +1 -1
  47. package/dist/Communication/communication-agent-context.d.ts +11 -1
  48. package/dist/Communication/communication-agent-context.d.ts.map +1 -1
  49. package/dist/Communication/communication-agent-context.js +15 -0
  50. package/dist/Communication/communication-agent-context.js.map +1 -1
  51. package/dist/Communication/communication-dashboard.component.d.ts.map +1 -1
  52. package/dist/Communication/communication-dashboard.component.js +5 -0
  53. package/dist/Communication/communication-dashboard.component.js.map +1 -1
  54. package/dist/Communication/communication-providers-resource.component.d.ts +27 -1
  55. package/dist/Communication/communication-providers-resource.component.d.ts.map +1 -1
  56. package/dist/Communication/communication-providers-resource.component.js +722 -78
  57. package/dist/Communication/communication-providers-resource.component.js.map +1 -1
  58. package/dist/ComponentStudio/component-studio-dashboard.component.d.ts.map +1 -1
  59. package/dist/ComponentStudio/component-studio-dashboard.component.js +1 -0
  60. package/dist/ComponentStudio/component-studio-dashboard.component.js.map +1 -1
  61. package/dist/ComponentStudio/components/ai-assistant/ai-assistant-panel.component.d.ts +0 -3
  62. package/dist/ComponentStudio/components/ai-assistant/ai-assistant-panel.component.d.ts.map +1 -1
  63. package/dist/ComponentStudio/components/ai-assistant/ai-assistant-panel.component.js +5 -14
  64. package/dist/ComponentStudio/components/ai-assistant/ai-assistant-panel.component.js.map +1 -1
  65. package/dist/ComponentStudio/components/artifact-load-dialog.component.d.ts.map +1 -1
  66. package/dist/ComponentStudio/components/artifact-load-dialog.component.js +1 -0
  67. package/dist/ComponentStudio/components/artifact-load-dialog.component.js.map +1 -1
  68. package/dist/ComponentStudio/components/artifact-selection-dialog.component.d.ts.map +1 -1
  69. package/dist/ComponentStudio/components/artifact-selection-dialog.component.js +1 -0
  70. package/dist/ComponentStudio/components/artifact-selection-dialog.component.js.map +1 -1
  71. package/dist/Credentials/components/credentials-list-resource.component.d.ts +2 -0
  72. package/dist/Credentials/components/credentials-list-resource.component.d.ts.map +1 -1
  73. package/dist/Credentials/components/credentials-list-resource.component.js +12 -10
  74. package/dist/Credentials/components/credentials-list-resource.component.js.map +1 -1
  75. package/dist/DashboardBrowser/dashboard-browser-agent-context.d.ts +14 -43
  76. package/dist/DashboardBrowser/dashboard-browser-agent-context.d.ts.map +1 -1
  77. package/dist/DashboardBrowser/dashboard-browser-agent-context.js +6 -29
  78. package/dist/DashboardBrowser/dashboard-browser-agent-context.js.map +1 -1
  79. package/dist/DashboardBrowser/dashboard-browser-resource.component.d.ts +174 -321
  80. package/dist/DashboardBrowser/dashboard-browser-resource.component.d.ts.map +1 -1
  81. package/dist/DashboardBrowser/dashboard-browser-resource.component.js +522 -1195
  82. package/dist/DashboardBrowser/dashboard-browser-resource.component.js.map +1 -1
  83. package/dist/DashboardBrowser/dashboard-library-filter.d.ts +116 -0
  84. package/dist/DashboardBrowser/dashboard-library-filter.d.ts.map +1 -0
  85. package/dist/DashboardBrowser/dashboard-library-filter.js +277 -0
  86. package/dist/DashboardBrowser/dashboard-library-filter.js.map +1 -0
  87. package/dist/DashboardBrowser/dashboard-share-dialog.component.d.ts +1 -1
  88. package/dist/DashboardBrowser/dashboard-share-dialog.component.js.map +1 -1
  89. package/dist/DashboardBrowser/dashboards-agent-tools.d.ts +26 -0
  90. package/dist/DashboardBrowser/dashboards-agent-tools.d.ts.map +1 -0
  91. package/dist/DashboardBrowser/dashboards-agent-tools.js +32 -0
  92. package/dist/DashboardBrowser/dashboards-agent-tools.js.map +1 -0
  93. package/dist/DashboardBrowser/dashboards-categories-resource.component.d.ts +58 -0
  94. package/dist/DashboardBrowser/dashboards-categories-resource.component.d.ts.map +1 -0
  95. package/dist/DashboardBrowser/dashboards-categories-resource.component.js +391 -0
  96. package/dist/DashboardBrowser/dashboards-categories-resource.component.js.map +1 -0
  97. package/dist/DashboardBrowser/dashboards-categories.helpers.d.ts +37 -0
  98. package/dist/DashboardBrowser/dashboards-categories.helpers.d.ts.map +1 -0
  99. package/dist/DashboardBrowser/dashboards-categories.helpers.js +68 -0
  100. package/dist/DashboardBrowser/dashboards-categories.helpers.js.map +1 -0
  101. package/dist/DashboardEditor/dashboard-editor.component.d.ts +295 -0
  102. package/dist/DashboardEditor/dashboard-editor.component.d.ts.map +1 -0
  103. package/dist/DashboardEditor/dashboard-editor.component.js +1022 -0
  104. package/dist/DashboardEditor/dashboard-editor.component.js.map +1 -0
  105. package/dist/DashboardEditor/dashboard-editor.types.d.ts +23 -0
  106. package/dist/DashboardEditor/dashboard-editor.types.d.ts.map +1 -0
  107. package/dist/DashboardEditor/dashboard-editor.types.js +3 -0
  108. package/dist/DashboardEditor/dashboard-editor.types.js.map +1 -0
  109. package/dist/DashboardEditor/dashboard-viewer-factory.d.ts +11 -0
  110. package/dist/DashboardEditor/dashboard-viewer-factory.d.ts.map +1 -0
  111. package/dist/DashboardEditor/dashboard-viewer-factory.js +17 -0
  112. package/dist/DashboardEditor/dashboard-viewer-factory.js.map +1 -0
  113. package/dist/DataExplorer/data-explorer-dashboard.component.d.ts +51 -0
  114. package/dist/DataExplorer/data-explorer-dashboard.component.d.ts.map +1 -1
  115. package/dist/DataExplorer/data-explorer-dashboard.component.js +375 -223
  116. package/dist/DataExplorer/data-explorer-dashboard.component.js.map +1 -1
  117. package/dist/DataExplorer/services/explorer-state.service.d.ts +1 -1
  118. package/dist/DataExplorer/services/explorer-state.service.js +2 -5
  119. package/dist/DataExplorer/services/explorer-state.service.js.map +1 -1
  120. package/dist/DevTools/app-state-inspector.component.d.ts.map +1 -1
  121. package/dist/DevTools/app-state-inspector.component.js +1 -0
  122. package/dist/DevTools/app-state-inspector.component.js.map +1 -1
  123. package/dist/DevTools/class-registry.component.d.ts.map +1 -1
  124. package/dist/DevTools/class-registry.component.js +1 -0
  125. package/dist/DevTools/class-registry.component.js.map +1 -1
  126. package/dist/DevTools/event-monitor.component.d.ts.map +1 -1
  127. package/dist/DevTools/event-monitor.component.js +2 -0
  128. package/dist/DevTools/event-monitor.component.js.map +1 -1
  129. package/dist/DevTools/graphql-console.component.d.ts.map +1 -1
  130. package/dist/DevTools/graphql-console.component.js.map +1 -1
  131. package/dist/DevTools/grid-width-lab.component.d.ts.map +1 -1
  132. package/dist/DevTools/grid-width-lab.component.js +1 -0
  133. package/dist/DevTools/grid-width-lab.component.js.map +1 -1
  134. package/dist/DevTools/layout-inspector.component.d.ts.map +1 -1
  135. package/dist/DevTools/layout-inspector.component.js +1 -0
  136. package/dist/DevTools/layout-inspector.component.js.map +1 -1
  137. package/dist/DevTools/lazy-module-status.component.d.ts.map +1 -1
  138. package/dist/DevTools/lazy-module-status.component.js +1 -0
  139. package/dist/DevTools/lazy-module-status.component.js.map +1 -1
  140. package/dist/DevTools/settings-explorer.component.d.ts.map +1 -1
  141. package/dist/DevTools/settings-explorer.component.js +1 -0
  142. package/dist/DevTools/settings-explorer.component.js.map +1 -1
  143. package/dist/DevTools/tab-strip-lab.component.d.ts.map +1 -1
  144. package/dist/DevTools/tab-strip-lab.component.js +1 -0
  145. package/dist/DevTools/tab-strip-lab.component.js.map +1 -1
  146. package/dist/EntityAdmin/entity-admin-dashboard.component.d.ts +5 -0
  147. package/dist/EntityAdmin/entity-admin-dashboard.component.d.ts.map +1 -1
  148. package/dist/EntityAdmin/entity-admin-dashboard.component.js +8 -5
  149. package/dist/EntityAdmin/entity-admin-dashboard.component.js.map +1 -1
  150. package/dist/FormBuilder/form-builder-resource.component.d.ts +0 -14
  151. package/dist/FormBuilder/form-builder-resource.component.d.ts.map +1 -1
  152. package/dist/FormBuilder/form-builder-resource.component.js +7 -29
  153. package/dist/FormBuilder/form-builder-resource.component.js.map +1 -1
  154. package/dist/Home/home-agent-context.d.ts +29 -10
  155. package/dist/Home/home-agent-context.d.ts.map +1 -1
  156. package/dist/Home/home-agent-context.js +50 -24
  157. package/dist/Home/home-agent-context.js.map +1 -1
  158. package/dist/Home/home-dashboard-switcher.component.d.ts +95 -0
  159. package/dist/Home/home-dashboard-switcher.component.d.ts.map +1 -0
  160. package/dist/Home/home-dashboard-switcher.component.js +442 -0
  161. package/dist/Home/home-dashboard-switcher.component.js.map +1 -0
  162. package/dist/Home/home-dashboard.component.d.ts +330 -17
  163. package/dist/Home/home-dashboard.component.d.ts.map +1 -1
  164. package/dist/Home/home-dashboard.component.js +1702 -1014
  165. package/dist/Home/home-dashboard.component.js.map +1 -1
  166. package/dist/Home/home-menus.d.ts +38 -0
  167. package/dist/Home/home-menus.d.ts.map +1 -0
  168. package/dist/Home/home-menus.js +53 -0
  169. package/dist/Home/home-menus.js.map +1 -0
  170. package/dist/Home/home-pinned-dashboards.d.ts +33 -0
  171. package/dist/Home/home-pinned-dashboards.d.ts.map +1 -0
  172. package/dist/Home/home-pinned-dashboards.js +76 -0
  173. package/dist/Home/home-pinned-dashboards.js.map +1 -0
  174. package/dist/Integration/components/overview/overview.component.js +2 -2
  175. package/dist/KnowledgeHub/components/config/knowledge-config-resource.component.js +2 -2
  176. package/dist/KnowledgeHub/components/config/knowledge-config-resource.component.js.map +1 -1
  177. package/dist/KnowledgeHub/components/feature-pipelines/feature-pipeline.engine.d.ts +11 -1
  178. package/dist/KnowledgeHub/components/feature-pipelines/feature-pipeline.engine.d.ts.map +1 -1
  179. package/dist/KnowledgeHub/components/feature-pipelines/feature-pipeline.engine.js +24 -2
  180. package/dist/KnowledgeHub/components/feature-pipelines/feature-pipeline.engine.js.map +1 -1
  181. package/dist/KnowledgeHub/components/feature-pipelines/feature-pipelines-resource.component.d.ts +16 -2
  182. package/dist/KnowledgeHub/components/feature-pipelines/feature-pipelines-resource.component.d.ts.map +1 -1
  183. package/dist/KnowledgeHub/components/feature-pipelines/feature-pipelines-resource.component.js +87 -33
  184. package/dist/KnowledgeHub/components/feature-pipelines/feature-pipelines-resource.component.js.map +1 -1
  185. package/dist/LoginStudio/login-studio-inspector.component.d.ts +130 -0
  186. package/dist/LoginStudio/login-studio-inspector.component.d.ts.map +1 -0
  187. package/dist/LoginStudio/login-studio-inspector.component.js +781 -0
  188. package/dist/LoginStudio/login-studio-inspector.component.js.map +1 -0
  189. package/dist/LoginStudio/login-studio-resource.component.d.ts +153 -0
  190. package/dist/LoginStudio/login-studio-resource.component.d.ts.map +1 -0
  191. package/dist/LoginStudio/login-studio-resource.component.js +857 -0
  192. package/dist/LoginStudio/login-studio-resource.component.js.map +1 -0
  193. package/dist/PredictiveStudio/components/ps-production.component.d.ts +5 -1
  194. package/dist/PredictiveStudio/components/ps-production.component.d.ts.map +1 -1
  195. package/dist/PredictiveStudio/components/ps-production.component.js +64 -41
  196. package/dist/PredictiveStudio/components/ps-production.component.js.map +1 -1
  197. package/dist/QueryBrowser/query-browser-resource.component.d.ts.map +1 -1
  198. package/dist/QueryBrowser/query-browser-resource.component.js +3 -2
  199. package/dist/QueryBrowser/query-browser-resource.component.js.map +1 -1
  200. package/dist/RealtimeRecordings/realtime-recordings-dashboard.component.js +2 -2
  201. package/dist/RealtimeRecordings/realtime-recordings-dashboard.component.js.map +1 -1
  202. package/dist/TelephonyOperations/telephony-operations-dashboard.component.d.ts +1 -0
  203. package/dist/TelephonyOperations/telephony-operations-dashboard.component.d.ts.map +1 -1
  204. package/dist/TelephonyOperations/telephony-operations-dashboard.component.js +7 -0
  205. package/dist/TelephonyOperations/telephony-operations-dashboard.component.js.map +1 -1
  206. package/dist/Testing/components/testing-explorer.component.d.ts.map +1 -1
  207. package/dist/Testing/components/testing-explorer.component.js +7 -7
  208. package/dist/Testing/components/testing-explorer.component.js.map +1 -1
  209. package/dist/Workflows/components/workflow-runs-resource.component.js +4 -4
  210. package/dist/Workflows/components/workflow-runs-resource.component.js.map +1 -1
  211. package/dist/ai-dashboards.module.d.ts +21 -20
  212. package/dist/ai-dashboards.module.d.ts.map +1 -1
  213. package/dist/ai-dashboards.module.js +4 -1
  214. package/dist/ai-dashboards.module.js.map +1 -1
  215. package/dist/core-dashboards.module.d.ts +70 -62
  216. package/dist/core-dashboards.module.d.ts.map +1 -1
  217. package/dist/core-dashboards.module.js +33 -5
  218. package/dist/core-dashboards.module.js.map +1 -1
  219. package/dist/data-explorer-dashboards.module.d.ts +1 -1
  220. package/dist/data-explorer-dashboards.module.d.ts.map +1 -1
  221. package/dist/data-explorer-dashboards.module.js +3 -1
  222. package/dist/data-explorer-dashboards.module.js.map +1 -1
  223. package/dist/public-api.d.ts +3 -0
  224. package/dist/public-api.d.ts.map +1 -1
  225. package/dist/public-api.js +4 -0
  226. package/dist/public-api.js.map +1 -1
  227. package/dist/shared/dashboard-library-changes.d.ts +23 -0
  228. package/dist/shared/dashboard-library-changes.d.ts.map +1 -0
  229. package/dist/shared/dashboard-library-changes.js +26 -0
  230. package/dist/shared/dashboard-library-changes.js.map +1 -0
  231. package/dist/shared/dashboard-owner-names.d.ts +17 -0
  232. package/dist/shared/dashboard-owner-names.d.ts.map +1 -0
  233. package/dist/shared/dashboard-owner-names.js +65 -0
  234. package/dist/shared/dashboard-owner-names.js.map +1 -0
  235. package/dist/shared/dashboard-recents.d.ts +34 -0
  236. package/dist/shared/dashboard-recents.d.ts.map +1 -0
  237. package/dist/shared/dashboard-recents.js +87 -0
  238. package/dist/shared/dashboard-recents.js.map +1 -0
  239. package/dist/shared/dashboards-app-install.d.ts +59 -0
  240. package/dist/shared/dashboards-app-install.d.ts.map +1 -0
  241. package/dist/shared/dashboards-app-install.js +97 -0
  242. package/dist/shared/dashboards-app-install.js.map +1 -0
  243. package/dist/shared/dashboards-app.helpers.d.ts +42 -0
  244. package/dist/shared/dashboards-app.helpers.d.ts.map +1 -0
  245. package/dist/shared/dashboards-app.helpers.js +143 -0
  246. package/dist/shared/dashboards-app.helpers.js.map +1 -0
  247. package/package.json +73 -69
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @fileoverview Placement and keyboard helpers for Home's fixed-position menus: the pin options menu and the
3
+ * Dashboards switcher. Pure, so they are tested without a browser.
4
+ */
5
+ /** The trigger's box in viewport pixels. */
6
+ export interface MenuAnchor {
7
+ Left: number;
8
+ Right: number;
9
+ Top: number;
10
+ Bottom: number;
11
+ }
12
+ /** The viewport size in pixels. */
13
+ export interface MenuViewport {
14
+ Width: number;
15
+ Height: number;
16
+ }
17
+ /** Where a fixed-position menu goes, in viewport pixels. */
18
+ export interface MenuPlacement {
19
+ Left: number;
20
+ Top: number;
21
+ /** The tallest the menu can be: the room on its side of the trigger, and never less than 120px. */
22
+ MaxHeight: number;
23
+ }
24
+ /** The box of an element, as a menu anchor. */
25
+ export declare function MenuAnchorOf(element: Element): MenuAnchor;
26
+ /**
27
+ * Places a menu under its trigger. 'right' lines up the right edges; when that passes the viewport's left margin, the
28
+ * menu lines up with the trigger's left edge instead. The menu stays inside the side margins. With a known height (the
29
+ * menu's full height), a menu that does not fit below opens above the trigger when it fits there. `MaxHeight` is the
30
+ * room on the side the menu opens to: down to the bottom margin, or up to the top margin.
31
+ */
32
+ export declare function PlaceMenu(anchor: MenuAnchor, width: number, height: number, align: 'left' | 'right', viewport: MenuViewport): MenuPlacement;
33
+ /**
34
+ * The menu row to focus for ArrowDown, ArrowUp, Home or End. `current` is the focused row, or -1 when focus is outside
35
+ * the rows (the filter box). Home and End only act on a row, so they still move the caret in the filter box.
36
+ */
37
+ export declare function NextMenuFocusIndex(key: string, current: number, count: number): number | null;
38
+ //# sourceMappingURL=home-menus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"home-menus.d.ts","sourceRoot":"","sources":["../../src/Home/home-menus.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,4CAA4C;AAC5C,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,mCAAmC;AACnC,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,4DAA4D;AAC5D,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,mGAAmG;IACnG,SAAS,EAAE,MAAM,CAAC;CACnB;AAOD,+CAA+C;AAC/C,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,UAAU,CAGzD;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,EAAE,QAAQ,EAAE,YAAY,GAAG,aAAa,CAW3I;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAgB7F"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @fileoverview Placement and keyboard helpers for Home's fixed-position menus: the pin options menu and the
3
+ * Dashboards switcher. Pure, so they are tested without a browser.
4
+ */
5
+ const MENU_GAP = 4;
6
+ const VIEWPORT_MARGIN = 8;
7
+ /** The smallest max height a menu gets, so a menu with little room stays usable. */
8
+ const MENU_MIN_MAX_HEIGHT = 120;
9
+ /** The box of an element, as a menu anchor. */
10
+ export function MenuAnchorOf(element) {
11
+ const rect = element.getBoundingClientRect();
12
+ return { Left: rect.left, Right: rect.right, Top: rect.top, Bottom: rect.bottom };
13
+ }
14
+ /**
15
+ * Places a menu under its trigger. 'right' lines up the right edges; when that passes the viewport's left margin, the
16
+ * menu lines up with the trigger's left edge instead. The menu stays inside the side margins. With a known height (the
17
+ * menu's full height), a menu that does not fit below opens above the trigger when it fits there. `MaxHeight` is the
18
+ * room on the side the menu opens to: down to the bottom margin, or up to the top margin.
19
+ */
20
+ export function PlaceMenu(anchor, width, height, align, viewport) {
21
+ let left = align === 'right' ? anchor.Right - width : anchor.Left;
22
+ if (align === 'right' && left < VIEWPORT_MARGIN) {
23
+ left = anchor.Left;
24
+ }
25
+ left = Math.max(VIEWPORT_MARGIN, Math.min(left, viewport.Width - width - VIEWPORT_MARGIN));
26
+ const below = anchor.Bottom + MENU_GAP;
27
+ const above = anchor.Top - MENU_GAP - height;
28
+ const opensAbove = height > 0 && below + height > viewport.Height - VIEWPORT_MARGIN && above >= VIEWPORT_MARGIN;
29
+ const room = opensAbove ? anchor.Top - MENU_GAP - VIEWPORT_MARGIN : viewport.Height - VIEWPORT_MARGIN - below;
30
+ return { Left: left, Top: opensAbove ? above : below, MaxHeight: Math.max(MENU_MIN_MAX_HEIGHT, room) };
31
+ }
32
+ /**
33
+ * The menu row to focus for ArrowDown, ArrowUp, Home or End. `current` is the focused row, or -1 when focus is outside
34
+ * the rows (the filter box). Home and End only act on a row, so they still move the caret in the filter box.
35
+ */
36
+ export function NextMenuFocusIndex(key, current, count) {
37
+ if (count === 0) {
38
+ return null;
39
+ }
40
+ switch (key) {
41
+ case 'ArrowDown':
42
+ return current < 0 ? 0 : (current + 1) % count;
43
+ case 'ArrowUp':
44
+ return current < 0 ? count - 1 : (current - 1 + count) % count;
45
+ case 'Home':
46
+ return current < 0 ? null : 0;
47
+ case 'End':
48
+ return current < 0 ? null : count - 1;
49
+ default:
50
+ return null;
51
+ }
52
+ }
53
+ //# sourceMappingURL=home-menus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"home-menus.js","sourceRoot":"","sources":["../../src/Home/home-menus.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAwBH,MAAM,QAAQ,GAAG,CAAC,CAAC;AACnB,MAAM,eAAe,GAAG,CAAC,CAAC;AAC1B,oFAAoF;AACpF,MAAM,mBAAmB,GAAG,GAAG,CAAC;AAEhC,+CAA+C;AAC/C,MAAM,UAAU,YAAY,CAAC,OAAgB;IAC3C,MAAM,IAAI,GAAG,OAAO,CAAC,qBAAqB,EAAE,CAAC;IAC7C,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;AACpF,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CAAC,MAAkB,EAAE,KAAa,EAAE,MAAc,EAAE,KAAuB,EAAE,QAAsB;IAC1H,IAAI,IAAI,GAAG,KAAK,KAAK,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC;IAClE,IAAI,KAAK,KAAK,OAAO,IAAI,IAAI,GAAG,eAAe,EAAE,CAAC;QAChD,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;IACrB,CAAC;IACD,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,eAAe,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,QAAQ,CAAC,KAAK,GAAG,KAAK,GAAG,eAAe,CAAC,CAAC,CAAC;IAC3F,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,GAAG,QAAQ,CAAC;IACvC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,GAAG,QAAQ,GAAG,MAAM,CAAC;IAC7C,MAAM,UAAU,GAAG,MAAM,GAAG,CAAC,IAAI,KAAK,GAAG,MAAM,GAAG,QAAQ,CAAC,MAAM,GAAG,eAAe,IAAI,KAAK,IAAI,eAAe,CAAC;IAChH,MAAM,IAAI,GAAG,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,GAAG,QAAQ,GAAG,eAAe,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,GAAG,eAAe,GAAG,KAAK,CAAC;IAC9G,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,CAAC,mBAAmB,EAAE,IAAI,CAAC,EAAE,CAAC;AACzG,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAW,EAAE,OAAe,EAAE,KAAa;IAC5E,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;QAChB,OAAO,IAAI,CAAC;IACd,CAAC;IACD,QAAQ,GAAG,EAAE,CAAC;QACZ,KAAK,WAAW;YACd,OAAO,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC;QACjD,KAAK,SAAS;YACZ,OAAO,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,KAAK,CAAC;QACjE,KAAK,MAAM;YACT,OAAO,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QAChC,KAAK,KAAK;YACR,OAAO,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC;QACxC;YACE,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC","sourcesContent":["/**\n * @fileoverview Placement and keyboard helpers for Home's fixed-position menus: the pin options menu and the\n * Dashboards switcher. Pure, so they are tested without a browser.\n */\n\n/** The trigger's box in viewport pixels. */\nexport interface MenuAnchor {\n Left: number;\n Right: number;\n Top: number;\n Bottom: number;\n}\n\n/** The viewport size in pixels. */\nexport interface MenuViewport {\n Width: number;\n Height: number;\n}\n\n/** Where a fixed-position menu goes, in viewport pixels. */\nexport interface MenuPlacement {\n Left: number;\n Top: number;\n /** The tallest the menu can be: the room on its side of the trigger, and never less than 120px. */\n MaxHeight: number;\n}\n\nconst MENU_GAP = 4;\nconst VIEWPORT_MARGIN = 8;\n/** The smallest max height a menu gets, so a menu with little room stays usable. */\nconst MENU_MIN_MAX_HEIGHT = 120;\n\n/** The box of an element, as a menu anchor. */\nexport function MenuAnchorOf(element: Element): MenuAnchor {\n const rect = element.getBoundingClientRect();\n return { Left: rect.left, Right: rect.right, Top: rect.top, Bottom: rect.bottom };\n}\n\n/**\n * Places a menu under its trigger. 'right' lines up the right edges; when that passes the viewport's left margin, the\n * menu lines up with the trigger's left edge instead. The menu stays inside the side margins. With a known height (the\n * menu's full height), a menu that does not fit below opens above the trigger when it fits there. `MaxHeight` is the\n * room on the side the menu opens to: down to the bottom margin, or up to the top margin.\n */\nexport function PlaceMenu(anchor: MenuAnchor, width: number, height: number, align: 'left' | 'right', viewport: MenuViewport): MenuPlacement {\n let left = align === 'right' ? anchor.Right - width : anchor.Left;\n if (align === 'right' && left < VIEWPORT_MARGIN) {\n left = anchor.Left;\n }\n left = Math.max(VIEWPORT_MARGIN, Math.min(left, viewport.Width - width - VIEWPORT_MARGIN));\n const below = anchor.Bottom + MENU_GAP;\n const above = anchor.Top - MENU_GAP - height;\n const opensAbove = height > 0 && below + height > viewport.Height - VIEWPORT_MARGIN && above >= VIEWPORT_MARGIN;\n const room = opensAbove ? anchor.Top - MENU_GAP - VIEWPORT_MARGIN : viewport.Height - VIEWPORT_MARGIN - below;\n return { Left: left, Top: opensAbove ? above : below, MaxHeight: Math.max(MENU_MIN_MAX_HEIGHT, room) };\n}\n\n/**\n * The menu row to focus for ArrowDown, ArrowUp, Home or End. `current` is the focused row, or -1 when focus is outside\n * the rows (the filter box). Home and End only act on a row, so they still move the caret in the filter box.\n */\nexport function NextMenuFocusIndex(key: string, current: number, count: number): number | null {\n if (count === 0) {\n return null;\n }\n switch (key) {\n case 'ArrowDown':\n return current < 0 ? 0 : (current + 1) % count;\n case 'ArrowUp':\n return current < 0 ? count - 1 : (current - 1 + count) % count;\n case 'Home':\n return current < 0 ? null : 0;\n case 'End':\n return current < 0 ? null : count - 1;\n default:\n return null;\n }\n}\n"]}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @fileoverview Pure helpers for the dashboards pinned to Home: the type of a pin, the dashboard a pin opens, the
3
+ * names dashboard pins show, and the list, filter and lookup of the Dashboards switcher.
4
+ */
5
+ import type { MJDashboardEntity } from '@memberjunction/core-entities';
6
+ import type { HomeAppPinnedItem } from '@memberjunction/ng-shared';
7
+ /** A dashboard the Dashboards switcher lists: a pinned Config dashboard the user can open. */
8
+ export interface HomeSwitcherDashboard {
9
+ ID: string;
10
+ Name: string;
11
+ }
12
+ /**
13
+ * A pin's resource type: its stored ResourceType when that is a known type, else its configuration's resourceType,
14
+ * else a type read from its configuration keys (legacy pins). Unknown pins keep what they stored.
15
+ */
16
+ export declare function ResolvePinResourceType(pin: HomeAppPinnedItem): string;
17
+ /** The dashboard a Dashboards pin opens: its dashboardId, else its recordId (a pin of an app's default dashboard tab). Null for other pins. */
18
+ export declare function DashboardIdOfPin(pin: HomeAppPinnedItem): string | null;
19
+ /** The dashboard with this id (any letter case), or null. */
20
+ export declare function FindDashboard<T extends {
21
+ ID: string;
22
+ }>(dashboards: readonly T[], dashboardId: string | null): T | null;
23
+ /** True for a dashboard Home shows in its dashboard view: a Config dashboard. Code dashboards open in a tab. */
24
+ export declare function CanShowInHome(dashboard: Pick<MJDashboardEntity, 'Type'>): boolean;
25
+ /** The names dashboard pins show, by pin id. A pin whose dashboard is not in `dashboards` is left out and shows its stored name. */
26
+ export declare function BuildPinDashboardNames(pins: readonly HomeAppPinnedItem[], dashboards: readonly MJDashboardEntity[]): Map<string, string>;
27
+ /** The pinned Config dashboards in pin order, each once. Pins whose dashboard is not in `dashboards` are left out. */
28
+ export declare function BuildPinnedDashboards(pins: readonly HomeAppPinnedItem[], dashboards: readonly MJDashboardEntity[]): HomeSwitcherDashboard[];
29
+ /** The dashboards whose name contains `query` (case-insensitive). An empty query keeps them all. */
30
+ export declare function FilterSwitcherDashboards(dashboards: readonly HomeSwitcherDashboard[], query: string): HomeSwitcherDashboard[];
31
+ /** A pinned dashboard by id (any letter case), else by name (exact, then partial). Null on a miss. */
32
+ export declare function ResolvePinnedDashboardReference(reference: string, dashboards: readonly HomeSwitcherDashboard[]): HomeSwitcherDashboard | null;
33
+ //# sourceMappingURL=home-pinned-dashboards.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"home-pinned-dashboards.d.ts","sourceRoot":"","sources":["../../src/Home/home-pinned-dashboards.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAEvE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAGnE,8FAA8F;AAC9F,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAID;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,iBAAiB,GAAG,MAAM,CAarE;AAED,+IAA+I;AAC/I,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,iBAAiB,GAAG,MAAM,GAAG,IAAI,CAItE;AAED,6DAA6D;AAC7D,wBAAgB,aAAa,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EAAE,UAAU,EAAE,SAAS,CAAC,EAAE,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,IAAI,CAEtH;AAED,gHAAgH;AAChH,wBAAgB,aAAa,CAAC,SAAS,EAAE,IAAI,CAAC,iBAAiB,EAAE,MAAM,CAAC,GAAG,OAAO,CAEjF;AAED,oIAAoI;AACpI,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,SAAS,iBAAiB,EAAE,EAAE,UAAU,EAAE,SAAS,iBAAiB,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAOxI;AAED,sHAAsH;AACtH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,SAAS,iBAAiB,EAAE,EAAE,UAAU,EAAE,SAAS,iBAAiB,EAAE,GAAG,qBAAqB,EAAE,CAS3I;AAED,oGAAoG;AACpG,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,SAAS,qBAAqB,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,qBAAqB,EAAE,CAG7H;AAED,sGAAsG;AACtG,wBAAgB,+BAA+B,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,qBAAqB,EAAE,GAAG,qBAAqB,GAAG,IAAI,CAE7I"}
@@ -0,0 +1,76 @@
1
+ import { UUIDsEqual } from '@memberjunction/global';
2
+ import { ResolveNamedRecord } from './home-agent-context';
3
+ const KNOWN_PIN_TYPES = ['Dashboards', 'User Views', 'Queries', 'Reports', 'Records', 'Custom', 'Actions'];
4
+ /**
5
+ * A pin's resource type: its stored ResourceType when that is a known type, else its configuration's resourceType,
6
+ * else a type read from its configuration keys (legacy pins). Unknown pins keep what they stored.
7
+ */
8
+ export function ResolvePinResourceType(pin) {
9
+ const config = pin.Configuration;
10
+ if (KNOWN_PIN_TYPES.includes(pin.ResourceType))
11
+ return pin.ResourceType;
12
+ const configType = config['resourceType'];
13
+ if (typeof configType === 'string' && KNOWN_PIN_TYPES.includes(configType))
14
+ return configType;
15
+ if (config['dashboardId'])
16
+ return 'Dashboards';
17
+ if (config['viewId'])
18
+ return 'User Views';
19
+ if (config['queryId'])
20
+ return 'Queries';
21
+ if (config['reportId'])
22
+ return 'Reports';
23
+ if ((config['Entity'] || config['entity']) && config['recordId'])
24
+ return 'Records';
25
+ if (config['actionId'])
26
+ return 'Actions';
27
+ if (config['navItemName'])
28
+ return 'Custom';
29
+ return pin.ResourceType;
30
+ }
31
+ /** The dashboard a Dashboards pin opens: its dashboardId, else its recordId (a pin of an app's default dashboard tab). Null for other pins. */
32
+ export function DashboardIdOfPin(pin) {
33
+ if (ResolvePinResourceType(pin) !== 'Dashboards')
34
+ return null;
35
+ const id = pin.Configuration['dashboardId'] ?? pin.Configuration['recordId'];
36
+ return typeof id === 'string' && id.trim() ? id.trim() : null;
37
+ }
38
+ /** The dashboard with this id (any letter case), or null. */
39
+ export function FindDashboard(dashboards, dashboardId) {
40
+ return dashboardId ? dashboards.find(d => UUIDsEqual(d.ID, dashboardId)) ?? null : null;
41
+ }
42
+ /** True for a dashboard Home shows in its dashboard view: a Config dashboard. Code dashboards open in a tab. */
43
+ export function CanShowInHome(dashboard) {
44
+ return dashboard.Type === 'Config';
45
+ }
46
+ /** The names dashboard pins show, by pin id. A pin whose dashboard is not in `dashboards` is left out and shows its stored name. */
47
+ export function BuildPinDashboardNames(pins, dashboards) {
48
+ const names = new Map();
49
+ for (const pin of pins) {
50
+ const dashboard = FindDashboard(dashboards, DashboardIdOfPin(pin));
51
+ if (dashboard)
52
+ names.set(pin.Id, dashboard.Name);
53
+ }
54
+ return names;
55
+ }
56
+ /** The pinned Config dashboards in pin order, each once. Pins whose dashboard is not in `dashboards` are left out. */
57
+ export function BuildPinnedDashboards(pins, dashboards) {
58
+ const result = [];
59
+ for (const pin of pins) {
60
+ const dashboard = FindDashboard(dashboards, DashboardIdOfPin(pin));
61
+ if (dashboard && CanShowInHome(dashboard) && !FindDashboard(result, dashboard.ID)) {
62
+ result.push({ ID: dashboard.ID, Name: dashboard.Name });
63
+ }
64
+ }
65
+ return result;
66
+ }
67
+ /** The dashboards whose name contains `query` (case-insensitive). An empty query keeps them all. */
68
+ export function FilterSwitcherDashboards(dashboards, query) {
69
+ const needle = query.trim().toLowerCase();
70
+ return needle ? dashboards.filter(d => d.Name.toLowerCase().includes(needle)) : [...dashboards];
71
+ }
72
+ /** A pinned dashboard by id (any letter case), else by name (exact, then partial). Null on a miss. */
73
+ export function ResolvePinnedDashboardReference(reference, dashboards) {
74
+ return FindDashboard(dashboards, reference.trim()) ?? ResolveNamedRecord(reference, dashboards);
75
+ }
76
+ //# sourceMappingURL=home-pinned-dashboards.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"home-pinned-dashboards.js","sourceRoot":"","sources":["../../src/Home/home-pinned-dashboards.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAQ1D,MAAM,eAAe,GAAG,CAAC,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;AAE3G;;;GAGG;AACH,MAAM,UAAU,sBAAsB,CAAC,GAAsB;IAC3D,MAAM,MAAM,GAAG,GAAG,CAAC,aAAa,CAAC;IACjC,IAAI,eAAe,CAAC,QAAQ,CAAC,GAAG,CAAC,YAAY,CAAC;QAAE,OAAO,GAAG,CAAC,YAAY,CAAC;IACxE,MAAM,UAAU,GAAG,MAAM,CAAC,cAAc,CAAC,CAAC;IAC1C,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,eAAe,CAAC,QAAQ,CAAC,UAAU,CAAC;QAAE,OAAO,UAAU,CAAC;IAC9F,IAAI,MAAM,CAAC,aAAa,CAAC;QAAE,OAAO,YAAY,CAAC;IAC/C,IAAI,MAAM,CAAC,QAAQ,CAAC;QAAE,OAAO,YAAY,CAAC;IAC1C,IAAI,MAAM,CAAC,SAAS,CAAC;QAAE,OAAO,SAAS,CAAC;IACxC,IAAI,MAAM,CAAC,UAAU,CAAC;QAAE,OAAO,SAAS,CAAC;IACzC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,MAAM,CAAC,UAAU,CAAC;QAAE,OAAO,SAAS,CAAC;IACnF,IAAI,MAAM,CAAC,UAAU,CAAC;QAAE,OAAO,SAAS,CAAC;IACzC,IAAI,MAAM,CAAC,aAAa,CAAC;QAAE,OAAO,QAAQ,CAAC;IAC3C,OAAO,GAAG,CAAC,YAAY,CAAC;AAC1B,CAAC;AAED,+IAA+I;AAC/I,MAAM,UAAU,gBAAgB,CAAC,GAAsB;IACrD,IAAI,sBAAsB,CAAC,GAAG,CAAC,KAAK,YAAY;QAAE,OAAO,IAAI,CAAC;IAC9D,MAAM,EAAE,GAAG,GAAG,CAAC,aAAa,CAAC,aAAa,CAAC,IAAI,GAAG,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC;IAC7E,OAAO,OAAO,EAAE,KAAK,QAAQ,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAChE,CAAC;AAED,6DAA6D;AAC7D,MAAM,UAAU,aAAa,CAA2B,UAAwB,EAAE,WAA0B;IAC1G,OAAO,WAAW,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,EAAE,WAAW,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;AAC1F,CAAC;AAED,gHAAgH;AAChH,MAAM,UAAU,aAAa,CAAC,SAA0C;IACtE,OAAO,SAAS,CAAC,IAAI,KAAK,QAAQ,CAAC;AACrC,CAAC;AAED,oIAAoI;AACpI,MAAM,UAAU,sBAAsB,CAAC,IAAkC,EAAE,UAAwC;IACjH,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;IACxC,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,SAAS,GAAG,aAAa,CAAC,UAAU,EAAE,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC;QACnE,IAAI,SAAS;YAAE,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;IACnD,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,sHAAsH;AACtH,MAAM,UAAU,qBAAqB,CAAC,IAAkC,EAAE,UAAwC;IAChH,MAAM,MAAM,GAA4B,EAAE,CAAC;IAC3C,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,SAAS,GAAG,aAAa,CAAC,UAAU,EAAE,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC;QACnE,IAAI,SAAS,IAAI,aAAa,CAAC,SAAS,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,EAAE,SAAS,CAAC,EAAE,CAAC,EAAE,CAAC;YAClF,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,SAAS,CAAC,EAAE,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC;QAC1D,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,oGAAoG;AACpG,MAAM,UAAU,wBAAwB,CAAC,UAA4C,EAAE,KAAa;IAClG,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC1C,OAAO,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC;AAClG,CAAC;AAED,sGAAsG;AACtG,MAAM,UAAU,+BAA+B,CAAC,SAAiB,EAAE,UAA4C;IAC7G,OAAO,aAAa,CAAC,UAAU,EAAE,SAAS,CAAC,IAAI,EAAE,CAAC,IAAI,kBAAkB,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;AAClG,CAAC","sourcesContent":["/**\n * @fileoverview Pure helpers for the dashboards pinned to Home: the type of a pin, the dashboard a pin opens, the\n * names dashboard pins show, and the list, filter and lookup of the Dashboards switcher.\n */\nimport type { MJDashboardEntity } from '@memberjunction/core-entities';\nimport { UUIDsEqual } from '@memberjunction/global';\nimport type { HomeAppPinnedItem } from '@memberjunction/ng-shared';\nimport { ResolveNamedRecord } from './home-agent-context';\n\n/** A dashboard the Dashboards switcher lists: a pinned Config dashboard the user can open. */\nexport interface HomeSwitcherDashboard {\n ID: string;\n Name: string;\n}\n\nconst KNOWN_PIN_TYPES = ['Dashboards', 'User Views', 'Queries', 'Reports', 'Records', 'Custom', 'Actions'];\n\n/**\n * A pin's resource type: its stored ResourceType when that is a known type, else its configuration's resourceType,\n * else a type read from its configuration keys (legacy pins). Unknown pins keep what they stored.\n */\nexport function ResolvePinResourceType(pin: HomeAppPinnedItem): string {\n const config = pin.Configuration;\n if (KNOWN_PIN_TYPES.includes(pin.ResourceType)) return pin.ResourceType;\n const configType = config['resourceType'];\n if (typeof configType === 'string' && KNOWN_PIN_TYPES.includes(configType)) return configType;\n if (config['dashboardId']) return 'Dashboards';\n if (config['viewId']) return 'User Views';\n if (config['queryId']) return 'Queries';\n if (config['reportId']) return 'Reports';\n if ((config['Entity'] || config['entity']) && config['recordId']) return 'Records';\n if (config['actionId']) return 'Actions';\n if (config['navItemName']) return 'Custom';\n return pin.ResourceType;\n}\n\n/** The dashboard a Dashboards pin opens: its dashboardId, else its recordId (a pin of an app's default dashboard tab). Null for other pins. */\nexport function DashboardIdOfPin(pin: HomeAppPinnedItem): string | null {\n if (ResolvePinResourceType(pin) !== 'Dashboards') return null;\n const id = pin.Configuration['dashboardId'] ?? pin.Configuration['recordId'];\n return typeof id === 'string' && id.trim() ? id.trim() : null;\n}\n\n/** The dashboard with this id (any letter case), or null. */\nexport function FindDashboard<T extends { ID: string }>(dashboards: readonly T[], dashboardId: string | null): T | null {\n return dashboardId ? dashboards.find(d => UUIDsEqual(d.ID, dashboardId)) ?? null : null;\n}\n\n/** True for a dashboard Home shows in its dashboard view: a Config dashboard. Code dashboards open in a tab. */\nexport function CanShowInHome(dashboard: Pick<MJDashboardEntity, 'Type'>): boolean {\n return dashboard.Type === 'Config';\n}\n\n/** The names dashboard pins show, by pin id. A pin whose dashboard is not in `dashboards` is left out and shows its stored name. */\nexport function BuildPinDashboardNames(pins: readonly HomeAppPinnedItem[], dashboards: readonly MJDashboardEntity[]): Map<string, string> {\n const names = new Map<string, string>();\n for (const pin of pins) {\n const dashboard = FindDashboard(dashboards, DashboardIdOfPin(pin));\n if (dashboard) names.set(pin.Id, dashboard.Name);\n }\n return names;\n}\n\n/** The pinned Config dashboards in pin order, each once. Pins whose dashboard is not in `dashboards` are left out. */\nexport function BuildPinnedDashboards(pins: readonly HomeAppPinnedItem[], dashboards: readonly MJDashboardEntity[]): HomeSwitcherDashboard[] {\n const result: HomeSwitcherDashboard[] = [];\n for (const pin of pins) {\n const dashboard = FindDashboard(dashboards, DashboardIdOfPin(pin));\n if (dashboard && CanShowInHome(dashboard) && !FindDashboard(result, dashboard.ID)) {\n result.push({ ID: dashboard.ID, Name: dashboard.Name });\n }\n }\n return result;\n}\n\n/** The dashboards whose name contains `query` (case-insensitive). An empty query keeps them all. */\nexport function FilterSwitcherDashboards(dashboards: readonly HomeSwitcherDashboard[], query: string): HomeSwitcherDashboard[] {\n const needle = query.trim().toLowerCase();\n return needle ? dashboards.filter(d => d.Name.toLowerCase().includes(needle)) : [...dashboards];\n}\n\n/** A pinned dashboard by id (any letter case), else by name (exact, then partial). Null on a miss. */\nexport function ResolvePinnedDashboardReference(reference: string, dashboards: readonly HomeSwitcherDashboard[]): HomeSwitcherDashboard | null {\n return FindDashboard(dashboards, reference.trim()) ?? ResolveNamedRecord(reference, dashboards);\n}\n"]}
@@ -739,7 +739,7 @@ let OverviewComponent = class OverviewComponent extends BaseResourceComponent {
739
739
  i0.ɵɵconditional(ctx.Notification ? 7 : -1);
740
740
  i0.ɵɵadvance();
741
741
  i0.ɵɵconditional(ctx.IsLoading ? 8 : ctx.Summaries.length === 0 ? 9 : 10);
742
- } }, dependencies: [i1.MJPageLayoutComponent, i1.MJPageHeaderComponent, i1.MJPageBodyComponent, i1.MJStatBadgeComponent, i1.MJRefreshButtonComponent, i1.MJEmptyStateComponent, i2.LoadingComponent, i3.DecimalPipe], styles: ["\n\n\n.overview[_ngcontent-%COMP%] {\n \n\n max-width: 1440px;\n margin: 0 auto;\n}\n\n\n\n\n\n\n.notification[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 12px 16px;\n border-radius: 10px;\n margin-bottom: 20px;\n font-size: 13px;\n font-weight: 500;\n animation: _ngcontent-%COMP%_slideDown 0.25s ease-out;\n}\n.notification[_ngcontent-%COMP%] i[_ngcontent-%COMP%]:first-child { font-size: 16px; }\n.notification-message[_ngcontent-%COMP%] { flex: 1; }\n.notification-success[_ngcontent-%COMP%] {\n background: var(--mj-status-success-bg);\n color: var(--mj-color-success-800);\n border: 1px solid var(--mj-color-success-200);\n}\n.notification-error[_ngcontent-%COMP%] {\n background: var(--mj-status-error-bg);\n color: var(--mj-status-error);\n border: 1px solid var(--mj-color-error-200);\n}\n.notification-dismiss[_ngcontent-%COMP%] {\n background: none;\n border: none;\n cursor: pointer;\n color: inherit;\n opacity: 0.5;\n font-size: 14px;\n padding: 4px;\n line-height: 1;\n}\n.notification-dismiss[_ngcontent-%COMP%]:hover { opacity: 1; }\n\n@keyframes _ngcontent-%COMP%_slideDown {\n from { opacity: 0; transform: translateY(-8px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n\n\n\n\n\n\n\n.metric-strip[_ngcontent-%COMP%] {\n display: grid;\n grid-template-columns: repeat(5, 1fr);\n gap: 16px;\n margin-bottom: 32px;\n}\n\n.metric-card[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 14px;\n padding: 18px 16px;\n border-radius: 12px;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);\n transition: box-shadow 0.2s, transform 0.2s;\n}\n.metric-card[_ngcontent-%COMP%]:hover {\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);\n transform: translateY(-1px);\n}\n\n.metric-icon[_ngcontent-%COMP%] {\n position: relative;\n width: 44px;\n height: 44px;\n border-radius: 10px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 18px;\n flex-shrink: 0;\n}\n.metric-icon-blue[_ngcontent-%COMP%] { background: var(--mj-status-info-bg); color: var(--mj-brand-primary); }\n.metric-icon-green[_ngcontent-%COMP%] { background: var(--mj-status-success-bg); color: var(--mj-color-success-600); }\n.metric-icon-emerald[_ngcontent-%COMP%] { background: var(--mj-status-success-bg); color: var(--mj-color-success-600); }\n.metric-icon-orange[_ngcontent-%COMP%] { background: var(--mj-color-warning-50); color: var(--mj-color-warning-600); }\n\n.metric-body[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n min-width: 0;\n}\n\n.metric-value[_ngcontent-%COMP%] {\n font-size: 22px;\n font-weight: 700;\n line-height: 1.2;\n color: var(--mj-text-primary);\n}\n.metric-value-indigo[_ngcontent-%COMP%] { color: var(--mj-color-indigo-500); }\n\n.metric-label[_ngcontent-%COMP%] {\n font-size: 11px;\n color: var(--mj-text-disabled);\n text-transform: uppercase;\n letter-spacing: 0.4px;\n font-weight: 500;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n\n.metric-ring[_ngcontent-%COMP%] {\n width: 44px;\n height: 44px;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n.metric-ring-inner[_ngcontent-%COMP%] {\n width: 32px;\n height: 32px;\n border-radius: 50%;\n background: var(--mj-bg-surface);\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.metric-ring-value[_ngcontent-%COMP%] {\n font-size: 8px;\n font-weight: 700;\n color: var(--mj-color-indigo-500);\n}\n\n\n.pulse-dot[_ngcontent-%COMP%] {\n position: absolute;\n top: 2px;\n right: 2px;\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: var(--mj-color-success-600);\n animation: _ngcontent-%COMP%_pulse 1.5s ease-in-out infinite;\n}\n\n@keyframes _ngcontent-%COMP%_pulse {\n 0%, 100% { opacity: 1; transform: scale(1); }\n 50% { opacity: 0.5; transform: scale(1.4); }\n}\n\n\n\n\n.section-heading[_ngcontent-%COMP%] {\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n margin-bottom: 16px;\n}\n.section-heading[_ngcontent-%COMP%] h3[_ngcontent-%COMP%] {\n margin: 0;\n font-size: 16px;\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n.section-heading[_ngcontent-%COMP%] h3[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n margin-right: 8px;\n color: var(--mj-text-muted);\n}\n\n.refresh-btn[_ngcontent-%COMP%] {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n \n flex-shrink: 0;\n box-sizing: border-box;\n padding: 6px 14px;\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n background: var(--mj-bg-surface);\n color: var(--mj-brand-primary);\n font-size: 12px;\n font-weight: 500;\n cursor: pointer;\n transition: background 0.15s, border-color 0.15s;\n}\n.refresh-btn[_ngcontent-%COMP%]:hover {\n background: var(--mj-status-info-bg);\n border-color: var(--mj-color-info-200);\n}\n\n.pipeline-grid[_ngcontent-%COMP%] {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 20px;\n margin-bottom: 32px;\n}\n\n.pipeline-card[_ngcontent-%COMP%] {\n position: relative;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 12px;\n \n\n\n overflow: hidden;\n padding: 20px;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);\n transition: box-shadow 0.2s, transform 0.2s;\n display: flex;\n flex-direction: column;\n gap: 16px;\n}\n.pipeline-card[_ngcontent-%COMP%]:hover {\n box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1);\n transform: translateY(-2px);\n}\n\n.pipeline-card-error[_ngcontent-%COMP%] {\n border-left: 3px solid var(--mj-status-error);\n}\n\n\n.card-header[_ngcontent-%COMP%] {\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n}\n.card-header-left[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 12px;\n min-width: 0;\n}\n.card-icon-wrap[_ngcontent-%COMP%] {\n width: 40px;\n height: 40px;\n border-radius: 10px;\n background: var(--mj-status-info-bg);\n color: var(--mj-brand-primary);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 16px;\n flex-shrink: 0;\n}\n.card-title-wrap[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n min-width: 0;\n}\n.card-title[_ngcontent-%COMP%] {\n font-size: 14px;\n font-weight: 600;\n color: var(--mj-text-primary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.card-subtitle[_ngcontent-%COMP%] {\n font-size: 11px;\n color: var(--mj-text-disabled);\n}\n\n\n.status-dot[_ngcontent-%COMP%] {\n width: 10px;\n height: 10px;\n border-radius: 50%;\n flex-shrink: 0;\n margin-top: 4px;\n position: relative;\n}\n.status-dot-green[_ngcontent-%COMP%] { background: var(--mj-color-success-600); }\n.status-dot-amber[_ngcontent-%COMP%] { background: var(--mj-status-warning); }\n.status-dot-red[_ngcontent-%COMP%] { background: var(--mj-status-error); }\n.status-dot-gray[_ngcontent-%COMP%] { background: var(--mj-color-neutral-300); }\n\n.dot-pulse[_ngcontent-%COMP%] {\n position: absolute;\n inset: -3px;\n border-radius: 50%;\n border: 2px solid var(--mj-status-warning);\n animation: _ngcontent-%COMP%_dotPulse 1.5s ease-in-out infinite;\n}\n@keyframes _ngcontent-%COMP%_dotPulse {\n 0%, 100% { opacity: 0.3; transform: scale(1); }\n 50% { opacity: 0; transform: scale(1.6); }\n}\n\n\n.pipeline-flow[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 0;\n padding: 8px 0;\n}\n.flow-node[_ngcontent-%COMP%] {\n width: 36px;\n height: 36px;\n border-radius: 8px;\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-muted);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 14px;\n flex-shrink: 0;\n}\n.flow-node-mj[_ngcontent-%COMP%] {\n background: var(--mj-status-info-bg);\n color: var(--mj-brand-primary);\n}\n.flow-arrow[_ngcontent-%COMP%] {\n flex: 1;\n display: flex;\n align-items: center;\n gap: 0;\n padding: 0 4px;\n}\n.flow-line[_ngcontent-%COMP%] {\n flex: 1;\n height: 2px;\n background: var(--mj-border-default);\n}\n.flow-count[_ngcontent-%COMP%] {\n font-size: 10px;\n color: var(--mj-text-muted);\n white-space: nowrap;\n padding: 2px 8px;\n background: var(--mj-bg-page);\n border: 1px solid var(--mj-border-default);\n border-radius: 10px;\n font-weight: 500;\n}\n\n\n.card-stats[_ngcontent-%COMP%] {\n display: grid;\n grid-template-columns: repeat(4, 1fr);\n gap: 8px;\n}\n.stat-item[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n align-items: center;\n text-align: center;\n}\n.stat-label[_ngcontent-%COMP%] {\n font-size: 10px;\n color: var(--mj-text-disabled);\n text-transform: uppercase;\n letter-spacing: 0.3px;\n margin-bottom: 2px;\n}\n.stat-value[_ngcontent-%COMP%] {\n font-size: 13px;\n font-weight: 600;\n color: var(--mj-text-secondary);\n}\n.stat-error[_ngcontent-%COMP%] {\n color: var(--mj-status-error);\n}\n\n\n.sparkline-row[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n.sparkline-label[_ngcontent-%COMP%] {\n font-size: 10px;\n color: var(--mj-text-disabled);\n text-transform: uppercase;\n letter-spacing: 0.3px;\n white-space: nowrap;\n}\n.sparkline-dots[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 5px;\n}\n.spark-dot[_ngcontent-%COMP%] {\n width: 7px;\n height: 7px;\n border-radius: 50%;\n display: inline-block;\n}\n.spark-green[_ngcontent-%COMP%] { background: var(--mj-color-success-600); }\n.spark-red[_ngcontent-%COMP%] { background: var(--mj-status-error); }\n.spark-amber[_ngcontent-%COMP%] { background: var(--mj-status-warning); }\n.spark-gray[_ngcontent-%COMP%] { background: var(--mj-color-neutral-300); }\n.sparkline-none[_ngcontent-%COMP%] {\n font-size: 10px;\n color: var(--mj-color-neutral-300);\n font-style: italic;\n}\n\n\n.card-footer[_ngcontent-%COMP%] {\n display: flex;\n justify-content: flex-end;\n \n\n min-width: 0;\n padding-top: 4px;\n border-top: 1px solid var(--mj-border-subtle);\n}\n\n.btn-sync[_ngcontent-%COMP%] {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n \n max-width: 100%;\n box-sizing: border-box;\n padding: 5px 12px;\n border: 1px solid var(--mj-border-default);\n border-radius: 6px;\n background: var(--mj-bg-surface);\n color: var(--mj-brand-primary);\n font-size: 12px;\n font-weight: 500;\n cursor: pointer;\n transition: background 0.15s, color 0.15s, border-color 0.15s;\n}\n.btn-sync[_ngcontent-%COMP%]:hover:not(:disabled) {\n background: var(--mj-status-info-bg);\n border-color: var(--mj-color-info-200);\n}\n.btn-sync[_ngcontent-%COMP%]:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n.btn-sync-disabled[_ngcontent-%COMP%] {\n color: var(--mj-text-disabled);\n border-color: var(--mj-border-default);\n}\n\n\n\n\n.bottom-split[_ngcontent-%COMP%] {\n display: flex;\n gap: 20px;\n}\n\n.panel-title[_ngcontent-%COMP%] {\n margin: 0 0 16px 0;\n font-size: 15px;\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n.panel-title[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n margin-right: 8px;\n color: var(--mj-text-muted);\n}\n\n\n.chart-panel[_ngcontent-%COMP%] {\n flex: 6;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 12px;\n padding: 20px;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);\n}\n\n.bar-chart[_ngcontent-%COMP%] {\n display: flex;\n align-items: flex-end;\n justify-content: space-between;\n gap: 12px;\n height: 200px;\n padding-top: 24px;\n}\n.bar-column[_ngcontent-%COMP%] {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: flex-end;\n height: 100%;\n position: relative;\n}\n.bar-count[_ngcontent-%COMP%] {\n font-size: 10px;\n font-weight: 600;\n color: var(--mj-text-muted);\n margin-bottom: 4px;\n}\n.bar-fill[_ngcontent-%COMP%] {\n width: 100%;\n max-width: 40px;\n background: linear-gradient(180deg, var(--mj-brand-primary) 0%, var(--mj-color-accent-400) 100%);\n border-radius: 4px 4px 0 0;\n min-height: 2px;\n transition: height 0.3s ease;\n}\n.bar-day[_ngcontent-%COMP%] {\n font-size: 11px;\n color: var(--mj-text-disabled);\n margin-top: 6px;\n text-align: center;\n}\n\n\n.feed-panel[_ngcontent-%COMP%] {\n flex: 4;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 12px;\n padding: 20px;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);\n max-height: 340px;\n overflow-y: auto;\n}\n\n.feed-list[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n.feed-item[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 10px;\n border-radius: 6px;\n transition: background 0.15s;\n}\n.feed-item[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-page);\n}\n\n.feed-icon-green[_ngcontent-%COMP%] { color: var(--mj-color-success-600); }\n.feed-icon-red[_ngcontent-%COMP%] { color: var(--mj-color-error-600); }\n.feed-icon-amber[_ngcontent-%COMP%] { color: var(--mj-color-warning-600); }\n\n.feed-details[_ngcontent-%COMP%] {\n flex: 1;\n display: flex;\n flex-direction: column;\n min-width: 0;\n}\n.feed-name[_ngcontent-%COMP%] {\n font-size: 13px;\n font-weight: 600;\n color: var(--mj-text-primary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.feed-meta[_ngcontent-%COMP%] {\n font-size: 11px;\n color: var(--mj-text-disabled);\n}\n\n.feed-badge[_ngcontent-%COMP%] {\n font-size: 10px;\n font-weight: 600;\n padding: 2px 8px;\n border-radius: 10px;\n text-transform: uppercase;\n white-space: nowrap;\n flex-shrink: 0;\n}\n.feed-badge-green[_ngcontent-%COMP%] { background: var(--mj-status-success-bg); color: var(--mj-color-success-800); }\n.feed-badge-amber[_ngcontent-%COMP%] { background: var(--mj-color-warning-50); color: var(--mj-color-warning-800); }\n.feed-badge-red[_ngcontent-%COMP%] { background: var(--mj-status-error-bg); color: var(--mj-status-error); }\n\n.empty-hint[_ngcontent-%COMP%] {\n color: var(--mj-text-disabled);\n font-size: 13px;\n font-style: italic;\n}\n\n\n\n\n@media (max-width: 1200px) {\n .pipeline-grid[_ngcontent-%COMP%] {\n grid-template-columns: repeat(2, 1fr);\n }\n .metric-strip[_ngcontent-%COMP%] {\n grid-template-columns: repeat(3, 1fr);\n }\n}\n\n@media (max-width: 900px) {\n .pipeline-grid[_ngcontent-%COMP%] {\n grid-template-columns: 1fr;\n }\n .metric-strip[_ngcontent-%COMP%] {\n grid-template-columns: repeat(2, 1fr);\n }\n .bottom-split[_ngcontent-%COMP%] {\n flex-direction: column;\n }\n .card-stats[_ngcontent-%COMP%] {\n grid-template-columns: repeat(2, 1fr);\n }\n}\n\n@media (max-width: 600px) {\n .overview[_ngcontent-%COMP%] {\n padding: 16px;\n }\n .metric-strip[_ngcontent-%COMP%] {\n grid-template-columns: 1fr;\n }\n}"] });
742
+ } }, dependencies: [i1.MJPageLayoutComponent, i1.MJPageHeaderComponent, i1.MJPageBodyComponent, i1.MJStatBadgeComponent, i1.MJRefreshButtonComponent, i1.MJEmptyStateComponent, i2.LoadingComponent, i3.DecimalPipe], styles: ["\n\n\n.overview[_ngcontent-%COMP%] {\n \n\n max-width: 1440px;\n margin: 0 auto;\n}\n\n\n\n\n\n\n.notification[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 12px 16px;\n border-radius: 10px;\n margin-bottom: 20px;\n font-size: 13px;\n font-weight: 500;\n animation: _ngcontent-%COMP%_slideDown 0.25s ease-out;\n}\n.notification[_ngcontent-%COMP%] i[_ngcontent-%COMP%]:first-child { font-size: 16px; }\n.notification-message[_ngcontent-%COMP%] { flex: 1; }\n.notification-success[_ngcontent-%COMP%] {\n background: var(--mj-status-success-bg);\n color: var(--mj-color-success-800);\n border: 1px solid var(--mj-color-success-200);\n}\n.notification-error[_ngcontent-%COMP%] {\n background: var(--mj-status-error-bg);\n color: var(--mj-status-error);\n border: 1px solid var(--mj-color-error-200);\n}\n.notification-dismiss[_ngcontent-%COMP%] {\n background: none;\n border: none;\n cursor: pointer;\n color: inherit;\n opacity: 0.5;\n font-size: 14px;\n padding: 4px;\n line-height: 1;\n}\n.notification-dismiss[_ngcontent-%COMP%]:hover { opacity: 1; }\n\n@keyframes _ngcontent-%COMP%_slideDown {\n from { opacity: 0; transform: translateY(-8px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n\n\n\n\n\n\n\n.metric-strip[_ngcontent-%COMP%] {\n display: grid;\n grid-template-columns: repeat(5, 1fr);\n gap: 16px;\n margin-bottom: 32px;\n}\n\n.metric-card[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 14px;\n padding: 18px 16px;\n border-radius: 12px;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);\n transition: box-shadow 0.2s, transform 0.2s;\n}\n.metric-card[_ngcontent-%COMP%]:hover {\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);\n transform: translateY(-1px);\n}\n\n.metric-icon[_ngcontent-%COMP%] {\n position: relative;\n width: 44px;\n height: 44px;\n border-radius: 10px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 18px;\n flex-shrink: 0;\n}\n.metric-icon-blue[_ngcontent-%COMP%] { background: var(--mj-status-info-bg); color: var(--mj-brand-primary); }\n.metric-icon-green[_ngcontent-%COMP%] { background: var(--mj-status-success-bg); color: var(--mj-color-success-600); }\n.metric-icon-emerald[_ngcontent-%COMP%] { background: var(--mj-status-success-bg); color: var(--mj-color-success-600); }\n.metric-icon-orange[_ngcontent-%COMP%] { background: var(--mj-color-warning-50); color: var(--mj-color-warning-600); }\n\n.metric-body[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n min-width: 0;\n}\n\n.metric-value[_ngcontent-%COMP%] {\n font-size: 22px;\n font-weight: 700;\n line-height: 1.2;\n color: var(--mj-text-primary);\n}\n.metric-value-indigo[_ngcontent-%COMP%] { color: var(--mj-color-indigo-500); }\n\n.metric-label[_ngcontent-%COMP%] {\n font-size: 11px;\n color: var(--mj-text-disabled);\n text-transform: uppercase;\n letter-spacing: 0.4px;\n font-weight: 500;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n\n.metric-ring[_ngcontent-%COMP%] {\n width: 44px;\n height: 44px;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n.metric-ring-inner[_ngcontent-%COMP%] {\n width: 32px;\n height: 32px;\n border-radius: 50%;\n background: var(--mj-bg-surface);\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.metric-ring-value[_ngcontent-%COMP%] {\n font-size: 8px;\n font-weight: 700;\n color: var(--mj-color-indigo-500);\n}\n\n\n.pulse-dot[_ngcontent-%COMP%] {\n position: absolute;\n top: 2px;\n right: 2px;\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: var(--mj-color-success-600);\n animation: _ngcontent-%COMP%_pulse 1.5s ease-in-out infinite;\n}\n\n@keyframes _ngcontent-%COMP%_pulse {\n 0%, 100% { opacity: 1; transform: scale(1); }\n 50% { opacity: 0.5; transform: scale(1.4); }\n}\n\n\n\n\n.section-heading[_ngcontent-%COMP%] {\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n margin-bottom: 16px;\n}\n.section-heading[_ngcontent-%COMP%] h3[_ngcontent-%COMP%] {\n margin: 0;\n font-size: 16px;\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n.section-heading[_ngcontent-%COMP%] h3[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n margin-right: 8px;\n color: var(--mj-text-muted);\n}\n\n.refresh-btn[_ngcontent-%COMP%] {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n \n flex-shrink: 0;\n box-sizing: border-box;\n padding: 6px 14px;\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n background: var(--mj-bg-surface);\n color: var(--mj-brand-primary);\n font-size: 12px;\n font-weight: 500;\n cursor: pointer;\n transition: background 0.15s, border-color 0.15s;\n}\n.refresh-btn[_ngcontent-%COMP%]:hover {\n background: var(--mj-status-info-bg);\n border-color: var(--mj-status-info-border);\n}\n\n.pipeline-grid[_ngcontent-%COMP%] {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 20px;\n margin-bottom: 32px;\n}\n\n.pipeline-card[_ngcontent-%COMP%] {\n position: relative;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 12px;\n \n\n\n overflow: hidden;\n padding: 20px;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);\n transition: box-shadow 0.2s, transform 0.2s;\n display: flex;\n flex-direction: column;\n gap: 16px;\n}\n.pipeline-card[_ngcontent-%COMP%]:hover {\n box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1);\n transform: translateY(-2px);\n}\n\n.pipeline-card-error[_ngcontent-%COMP%] {\n border-left: 3px solid var(--mj-status-error);\n}\n\n\n.card-header[_ngcontent-%COMP%] {\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n}\n.card-header-left[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 12px;\n min-width: 0;\n}\n.card-icon-wrap[_ngcontent-%COMP%] {\n width: 40px;\n height: 40px;\n border-radius: 10px;\n background: var(--mj-status-info-bg);\n color: var(--mj-brand-primary);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 16px;\n flex-shrink: 0;\n}\n.card-title-wrap[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n min-width: 0;\n}\n.card-title[_ngcontent-%COMP%] {\n font-size: 14px;\n font-weight: 600;\n color: var(--mj-text-primary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.card-subtitle[_ngcontent-%COMP%] {\n font-size: 11px;\n color: var(--mj-text-disabled);\n}\n\n\n.status-dot[_ngcontent-%COMP%] {\n width: 10px;\n height: 10px;\n border-radius: 50%;\n flex-shrink: 0;\n margin-top: 4px;\n position: relative;\n}\n.status-dot-green[_ngcontent-%COMP%] { background: var(--mj-color-success-600); }\n.status-dot-amber[_ngcontent-%COMP%] { background: var(--mj-status-warning); }\n.status-dot-red[_ngcontent-%COMP%] { background: var(--mj-status-error); }\n.status-dot-gray[_ngcontent-%COMP%] { background: var(--mj-color-neutral-300); }\n\n.dot-pulse[_ngcontent-%COMP%] {\n position: absolute;\n inset: -3px;\n border-radius: 50%;\n border: 2px solid var(--mj-status-warning);\n animation: _ngcontent-%COMP%_dotPulse 1.5s ease-in-out infinite;\n}\n@keyframes _ngcontent-%COMP%_dotPulse {\n 0%, 100% { opacity: 0.3; transform: scale(1); }\n 50% { opacity: 0; transform: scale(1.6); }\n}\n\n\n.pipeline-flow[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 0;\n padding: 8px 0;\n}\n.flow-node[_ngcontent-%COMP%] {\n width: 36px;\n height: 36px;\n border-radius: 8px;\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-muted);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 14px;\n flex-shrink: 0;\n}\n.flow-node-mj[_ngcontent-%COMP%] {\n background: var(--mj-status-info-bg);\n color: var(--mj-brand-primary);\n}\n.flow-arrow[_ngcontent-%COMP%] {\n flex: 1;\n display: flex;\n align-items: center;\n gap: 0;\n padding: 0 4px;\n}\n.flow-line[_ngcontent-%COMP%] {\n flex: 1;\n height: 2px;\n background: var(--mj-border-default);\n}\n.flow-count[_ngcontent-%COMP%] {\n font-size: 10px;\n color: var(--mj-text-muted);\n white-space: nowrap;\n padding: 2px 8px;\n background: var(--mj-bg-page);\n border: 1px solid var(--mj-border-default);\n border-radius: 10px;\n font-weight: 500;\n}\n\n\n.card-stats[_ngcontent-%COMP%] {\n display: grid;\n grid-template-columns: repeat(4, 1fr);\n gap: 8px;\n}\n.stat-item[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n align-items: center;\n text-align: center;\n}\n.stat-label[_ngcontent-%COMP%] {\n font-size: 10px;\n color: var(--mj-text-disabled);\n text-transform: uppercase;\n letter-spacing: 0.3px;\n margin-bottom: 2px;\n}\n.stat-value[_ngcontent-%COMP%] {\n font-size: 13px;\n font-weight: 600;\n color: var(--mj-text-secondary);\n}\n.stat-error[_ngcontent-%COMP%] {\n color: var(--mj-status-error);\n}\n\n\n.sparkline-row[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n.sparkline-label[_ngcontent-%COMP%] {\n font-size: 10px;\n color: var(--mj-text-disabled);\n text-transform: uppercase;\n letter-spacing: 0.3px;\n white-space: nowrap;\n}\n.sparkline-dots[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 5px;\n}\n.spark-dot[_ngcontent-%COMP%] {\n width: 7px;\n height: 7px;\n border-radius: 50%;\n display: inline-block;\n}\n.spark-green[_ngcontent-%COMP%] { background: var(--mj-color-success-600); }\n.spark-red[_ngcontent-%COMP%] { background: var(--mj-status-error); }\n.spark-amber[_ngcontent-%COMP%] { background: var(--mj-status-warning); }\n.spark-gray[_ngcontent-%COMP%] { background: var(--mj-color-neutral-300); }\n.sparkline-none[_ngcontent-%COMP%] {\n font-size: 10px;\n color: var(--mj-color-neutral-300);\n font-style: italic;\n}\n\n\n.card-footer[_ngcontent-%COMP%] {\n display: flex;\n justify-content: flex-end;\n \n\n min-width: 0;\n padding-top: 4px;\n border-top: 1px solid var(--mj-border-subtle);\n}\n\n.btn-sync[_ngcontent-%COMP%] {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n \n max-width: 100%;\n box-sizing: border-box;\n padding: 5px 12px;\n border: 1px solid var(--mj-border-default);\n border-radius: 6px;\n background: var(--mj-bg-surface);\n color: var(--mj-brand-primary);\n font-size: 12px;\n font-weight: 500;\n cursor: pointer;\n transition: background 0.15s, color 0.15s, border-color 0.15s;\n}\n.btn-sync[_ngcontent-%COMP%]:hover:not(:disabled) {\n background: var(--mj-status-info-bg);\n border-color: var(--mj-status-info-border);\n}\n.btn-sync[_ngcontent-%COMP%]:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n.btn-sync-disabled[_ngcontent-%COMP%] {\n color: var(--mj-text-disabled);\n border-color: var(--mj-border-default);\n}\n\n\n\n\n.bottom-split[_ngcontent-%COMP%] {\n display: flex;\n gap: 20px;\n}\n\n.panel-title[_ngcontent-%COMP%] {\n margin: 0 0 16px 0;\n font-size: 15px;\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n.panel-title[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n margin-right: 8px;\n color: var(--mj-text-muted);\n}\n\n\n.chart-panel[_ngcontent-%COMP%] {\n flex: 6;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 12px;\n padding: 20px;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);\n}\n\n.bar-chart[_ngcontent-%COMP%] {\n display: flex;\n align-items: flex-end;\n justify-content: space-between;\n gap: 12px;\n height: 200px;\n padding-top: 24px;\n}\n.bar-column[_ngcontent-%COMP%] {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: flex-end;\n height: 100%;\n position: relative;\n}\n.bar-count[_ngcontent-%COMP%] {\n font-size: 10px;\n font-weight: 600;\n color: var(--mj-text-muted);\n margin-bottom: 4px;\n}\n.bar-fill[_ngcontent-%COMP%] {\n width: 100%;\n max-width: 40px;\n background: linear-gradient(180deg, var(--mj-brand-primary) 0%, var(--mj-color-accent-400) 100%);\n border-radius: 4px 4px 0 0;\n min-height: 2px;\n transition: height 0.3s ease;\n}\n.bar-day[_ngcontent-%COMP%] {\n font-size: 11px;\n color: var(--mj-text-disabled);\n margin-top: 6px;\n text-align: center;\n}\n\n\n.feed-panel[_ngcontent-%COMP%] {\n flex: 4;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 12px;\n padding: 20px;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);\n max-height: 340px;\n overflow-y: auto;\n}\n\n.feed-list[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n.feed-item[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 10px;\n border-radius: 6px;\n transition: background 0.15s;\n}\n.feed-item[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-page);\n}\n\n.feed-icon-green[_ngcontent-%COMP%] { color: var(--mj-color-success-600); }\n.feed-icon-red[_ngcontent-%COMP%] { color: var(--mj-color-error-600); }\n.feed-icon-amber[_ngcontent-%COMP%] { color: var(--mj-color-warning-600); }\n\n.feed-details[_ngcontent-%COMP%] {\n flex: 1;\n display: flex;\n flex-direction: column;\n min-width: 0;\n}\n.feed-name[_ngcontent-%COMP%] {\n font-size: 13px;\n font-weight: 600;\n color: var(--mj-text-primary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.feed-meta[_ngcontent-%COMP%] {\n font-size: 11px;\n color: var(--mj-text-disabled);\n}\n\n.feed-badge[_ngcontent-%COMP%] {\n font-size: 10px;\n font-weight: 600;\n padding: 2px 8px;\n border-radius: 10px;\n text-transform: uppercase;\n white-space: nowrap;\n flex-shrink: 0;\n}\n.feed-badge-green[_ngcontent-%COMP%] { background: var(--mj-status-success-bg); color: var(--mj-color-success-800); }\n.feed-badge-amber[_ngcontent-%COMP%] { background: var(--mj-color-warning-50); color: var(--mj-color-warning-800); }\n.feed-badge-red[_ngcontent-%COMP%] { background: var(--mj-status-error-bg); color: var(--mj-status-error); }\n\n.empty-hint[_ngcontent-%COMP%] {\n color: var(--mj-text-disabled);\n font-size: 13px;\n font-style: italic;\n}\n\n\n\n\n@media (max-width: 1200px) {\n .pipeline-grid[_ngcontent-%COMP%] {\n grid-template-columns: repeat(2, 1fr);\n }\n .metric-strip[_ngcontent-%COMP%] {\n grid-template-columns: repeat(3, 1fr);\n }\n}\n\n@media (max-width: 900px) {\n .pipeline-grid[_ngcontent-%COMP%] {\n grid-template-columns: 1fr;\n }\n .metric-strip[_ngcontent-%COMP%] {\n grid-template-columns: repeat(2, 1fr);\n }\n .bottom-split[_ngcontent-%COMP%] {\n flex-direction: column;\n }\n .card-stats[_ngcontent-%COMP%] {\n grid-template-columns: repeat(2, 1fr);\n }\n}\n\n@media (max-width: 600px) {\n .overview[_ngcontent-%COMP%] {\n padding: 16px;\n }\n .metric-strip[_ngcontent-%COMP%] {\n grid-template-columns: 1fr;\n }\n}"] });
743
743
  };
744
744
  OverviewComponent = __decorate([
745
745
  RegisterClass(BaseResourceComponent, 'IntegrationOverview')
@@ -747,7 +747,7 @@ OverviewComponent = __decorate([
747
747
  export { OverviewComponent };
748
748
  (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(OverviewComponent, [{
749
749
  type: Component,
750
- args: [{ standalone: false, selector: 'app-integration-overview', template: "<mj-page-layout>\n <mj-page-header\n Title=\"Integration Overview\"\n Icon=\"fa-solid fa-gauge-high\"\n Subtitle=\"Health, throughput, and activity across all integrations\">\n @if (Summaries.length > 0) {\n <div meta>\n <mj-stat-badge [Count]=\"KPIs.TotalIntegrations\" Label=\"integrations\"></mj-stat-badge>\n </div>\n }\n <div actions>\n <mj-refresh-button [Loading]=\"IsLoading\" (Clicked)=\"Refresh()\"></mj-refresh-button>\n </div>\n </mj-page-header>\n\n <mj-page-body>\n<div class=\"overview\">\n <!-- Notification Banner -->\n @if (Notification) {\n <div class=\"notification\" [class.notification-success]=\"Notification.Type === 'success'\"\n [class.notification-error]=\"Notification.Type === 'error'\">\n @if (Notification.Type === 'success') {\n <i class=\"fa-solid fa-circle-check\"></i>\n } @else {\n <i class=\"fa-solid fa-circle-xmark\"></i>\n }\n <span class=\"notification-message\">{{ Notification.Message }}</span>\n <button class=\"notification-dismiss\" (click)=\"DismissNotification()\">\n <i class=\"fa-solid fa-times\"></i>\n </button>\n </div>\n }\n\n @if (IsLoading) {\n <mj-loading text=\"Loading integrations...\" size=\"medium\"></mj-loading>\n } @else if (Summaries.length === 0) {\n <mj-empty-state Icon=\"fa-solid fa-plug-circle-xmark\"\n Title=\"No Integrations Configured\"\n Message=\"Set up your first integration using the Connection Studio to start syncing data.\" />\n } @else {\n <!-- ====================================================================\n SECTION 1: Metric Strip\n ==================================================================== -->\n <div class=\"metric-strip\">\n <!-- Total Pipelines -->\n <div class=\"metric-card\">\n <div class=\"metric-icon metric-icon-blue\">\n <i class=\"fa-solid fa-diagram-project\"></i>\n </div>\n <div class=\"metric-body\">\n <span class=\"metric-value\">{{ KPIs.TotalIntegrations }}</span>\n <span class=\"metric-label\">Total Pipelines</span>\n </div>\n </div>\n\n <!-- Records Today -->\n <div class=\"metric-card\">\n <div class=\"metric-icon metric-icon-green\">\n <i class=\"fa-solid fa-database\"></i>\n </div>\n <div class=\"metric-body\">\n <span class=\"metric-value\">{{ FormatNumber(KPIs.RecordsSyncedToday) }}</span>\n <span class=\"metric-label\">Records Today</span>\n </div>\n </div>\n\n <!-- Success Rate (with conic gradient ring) -->\n <div class=\"metric-card\">\n <div class=\"metric-ring\" [style.background]=\"SuccessRateGradient\">\n <div class=\"metric-ring-inner\">\n <span class=\"metric-ring-value\">{{ SuccessRate }}%</span>\n </div>\n </div>\n <div class=\"metric-body\">\n <span class=\"metric-value metric-value-indigo\">{{ SuccessRate }}%</span>\n <span class=\"metric-label\">Success Rate</span>\n </div>\n </div>\n\n <!-- Active Now -->\n <div class=\"metric-card\">\n <div class=\"metric-icon metric-icon-emerald\">\n <i class=\"fa-solid fa-bolt\"></i>\n @if (ActiveCount > 0) {\n <span class=\"pulse-dot\"></span>\n }\n </div>\n <div class=\"metric-body\">\n <span class=\"metric-value\">{{ ActiveCount }}</span>\n <span class=\"metric-label\">Active Now</span>\n </div>\n </div>\n\n <!-- Avg Duration -->\n <div class=\"metric-card\">\n <div class=\"metric-icon metric-icon-orange\">\n <i class=\"fa-solid fa-clock\"></i>\n </div>\n <div class=\"metric-body\">\n <span class=\"metric-value\">{{ FormatDuration(KPIs.AverageSyncDurationMs) }}</span>\n <span class=\"metric-label\">Avg Duration</span>\n </div>\n </div>\n </div>\n\n <!-- ====================================================================\n SECTION 2: Pipeline Health Grid\n ==================================================================== -->\n <div class=\"section-heading\">\n <h3><i class=\"fa-solid fa-heart-pulse\"></i> Pipeline Health</h3>\n <button class=\"refresh-btn\" (click)=\"Refresh()\">\n <i class=\"fa-solid fa-arrows-rotate\"></i> Refresh\n </button>\n </div>\n\n <div class=\"pipeline-grid\">\n @for (summary of Summaries; track summary.Integration.ID) {\n <div [class]=\"GetCardBorderClass(summary.StatusColor)\">\n <!-- Card Header -->\n <div class=\"card-header\">\n <div class=\"card-header-left\">\n <div class=\"card-icon-wrap\">\n <i [class]=\"GetIntegrationIcon(summary)\"></i>\n </div>\n <div class=\"card-title-wrap\">\n <span class=\"card-title\">{{ summary.Integration.Name }}</span>\n <span class=\"card-subtitle\">{{ summary.SourceType?.Name ?? 'Custom' }}</span>\n </div>\n </div>\n <div [class]=\"GetStatusDotClass(summary.StatusColor)\"\n [title]=\"GetStatusLabel(summary.StatusColor)\">\n @if (summary.LatestRun?.Status === 'In Progress') {\n <span class=\"dot-pulse\"></span>\n }\n </div>\n </div>\n\n <!-- Pipeline Flow Visual -->\n <div class=\"pipeline-flow\">\n <div class=\"flow-node\">\n <i [class]=\"GetIntegrationIcon(summary)\"></i>\n </div>\n <div class=\"flow-arrow\">\n <div class=\"flow-line\"></div>\n <span class=\"flow-count\">{{ GetEntityMapCount(summary.Integration.ID) }} maps</span>\n <div class=\"flow-line\"></div>\n </div>\n <div class=\"flow-node flow-node-mj\">\n <i class=\"fa-solid fa-layer-group\"></i>\n </div>\n </div>\n\n <!-- Stats Row -->\n <div class=\"card-stats\">\n <div class=\"stat-item\">\n <span class=\"stat-label\">Last sync</span>\n <span class=\"stat-value\">{{ summary.RelativeTime }}</span>\n </div>\n <div class=\"stat-item\">\n <span class=\"stat-label\">Records</span>\n <span class=\"stat-value\">{{ FormatNumber(summary.LatestRun?.TotalRecords ?? 0) }}</span>\n </div>\n <div class=\"stat-item\">\n <span class=\"stat-label\">Duration</span>\n <span class=\"stat-value\">{{ FormatDuration(summary.DurationMs) }}</span>\n </div>\n <div class=\"stat-item\">\n <span class=\"stat-label\">Errors</span>\n <span class=\"stat-value\" [class.stat-error]=\"summary.TotalErrors > 0\">\n {{ summary.TotalErrors }}\n </span>\n </div>\n </div>\n\n <!-- Sparkline (last 5 runs) -->\n <div class=\"sparkline-row\">\n <span class=\"sparkline-label\">Recent runs</span>\n <div class=\"sparkline-dots\">\n @for (run of summary.RecentRuns; track run.ID) {\n <span [class]=\"GetSparklineDotClass(run)\"\n [title]=\"GetSparklineTooltip(run)\"></span>\n }\n @if (summary.RecentRuns.length === 0) {\n <span class=\"sparkline-none\">No runs</span>\n }\n </div>\n </div>\n\n <!-- Card Footer -->\n <div class=\"card-footer\">\n @if (IsRunning(summary.Integration.ID)) {\n <button class=\"btn-sync btn-sync-disabled\" disabled>\n <i class=\"fa-solid fa-spinner fa-spin\"></i> Running...\n </button>\n } @else {\n <button class=\"btn-sync\"\n (click)=\"RunSync(summary.Integration.ID)\"\n [disabled]=\"!summary.Integration.IsActive || RunningIntegrationID !== null\">\n <i class=\"fa-solid fa-play\"></i> Sync Now\n </button>\n }\n </div>\n </div>\n }\n </div>\n\n <!-- ====================================================================\n SECTION 3: Bottom - Bar Chart & Activity Feed\n ==================================================================== -->\n <div class=\"bottom-split\">\n <!-- Bar Chart (left, 60%) -->\n <div class=\"chart-panel\">\n <h3 class=\"panel-title\"><i class=\"fa-solid fa-chart-bar\"></i> Records Synced (7 days)</h3>\n @if (DailyCounts.length === 0) {\n <p class=\"empty-hint\">No data available.</p>\n } @else {\n <div class=\"bar-chart\">\n @for (day of DailyCounts; track day.Date) {\n <div class=\"bar-column\">\n <div class=\"bar-count\">{{ FormatNumber(day.Records) }}</div>\n <div class=\"bar-fill\" [style.height.%]=\"BarHeight(day.Records)\"></div>\n <div class=\"bar-day\">{{ day.Label }}</div>\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Activity Feed (right, 40%) -->\n <div class=\"feed-panel\">\n <h3 class=\"panel-title\"><i class=\"fa-solid fa-stream\"></i> Recent Activity</h3>\n @if (ActivityFeed.length === 0) {\n <p class=\"empty-hint\">No recent activity.</p>\n } @else {\n <div class=\"feed-list\">\n @for (item of ActivityFeed; track item.RunID) {\n <div class=\"feed-item\">\n <i [class]=\"ActivityStatusIcon(item.Status)\"\n [class.feed-icon-green]=\"item.StatusColor === 'green'\"\n [class.feed-icon-red]=\"item.StatusColor === 'red'\"\n [class.feed-icon-amber]=\"item.StatusColor === 'amber'\">\n </i>\n <div class=\"feed-details\">\n <span class=\"feed-name\">{{ item.IntegrationName }}</span>\n <span class=\"feed-meta\">{{ item.TotalRecords | number }} records &middot; {{ item.RelativeTime }}</span>\n </div>\n <span class=\"feed-badge\" [class]=\"'feed-badge-' + item.StatusColor\">\n {{ item.Status }}\n </span>\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n</div>\n </mj-page-body>\n</mj-page-layout>\n", styles: ["/* ==========================================================================\n Integration Overview - Root\n ========================================================================== */\n.overview {\n /* padding + flex/scroll now owned by <mj-page-body>; keep max-width for\n comfortable reading on ultra-wide displays. */\n max-width: 1440px;\n margin: 0 auto;\n}\n\n/* Slot passthrough now in <mj-page-header>; stat badges now via <mj-stat-badge>. */\n\n/* ==========================================================================\n Notification Banner\n ========================================================================== */\n.notification {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 12px 16px;\n border-radius: 10px;\n margin-bottom: 20px;\n font-size: 13px;\n font-weight: 500;\n animation: slideDown 0.25s ease-out;\n}\n.notification i:first-child { font-size: 16px; }\n.notification-message { flex: 1; }\n.notification-success {\n background: var(--mj-status-success-bg);\n color: var(--mj-color-success-800);\n border: 1px solid var(--mj-color-success-200);\n}\n.notification-error {\n background: var(--mj-status-error-bg);\n color: var(--mj-status-error);\n border: 1px solid var(--mj-color-error-200);\n}\n.notification-dismiss {\n background: none;\n border: none;\n cursor: pointer;\n color: inherit;\n opacity: 0.5;\n font-size: 14px;\n padding: 4px;\n line-height: 1;\n}\n.notification-dismiss:hover { opacity: 1; }\n\n@keyframes slideDown {\n from { opacity: 0; transform: translateY(-8px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n/* ==========================================================================\n Empty State\n ========================================================================== */\n\n/* ==========================================================================\n SECTION 1: Metric Strip\n ========================================================================== */\n.metric-strip {\n display: grid;\n grid-template-columns: repeat(5, 1fr);\n gap: 16px;\n margin-bottom: 32px;\n}\n\n.metric-card {\n display: flex;\n align-items: center;\n gap: 14px;\n padding: 18px 16px;\n border-radius: 12px;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);\n transition: box-shadow 0.2s, transform 0.2s;\n}\n.metric-card:hover {\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);\n transform: translateY(-1px);\n}\n\n.metric-icon {\n position: relative;\n width: 44px;\n height: 44px;\n border-radius: 10px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 18px;\n flex-shrink: 0;\n}\n.metric-icon-blue { background: var(--mj-status-info-bg); color: var(--mj-brand-primary); }\n.metric-icon-green { background: var(--mj-status-success-bg); color: var(--mj-color-success-600); }\n.metric-icon-emerald { background: var(--mj-status-success-bg); color: var(--mj-color-success-600); }\n.metric-icon-orange { background: var(--mj-color-warning-50); color: var(--mj-color-warning-600); }\n\n.metric-body {\n display: flex;\n flex-direction: column;\n min-width: 0;\n}\n\n.metric-value {\n font-size: 22px;\n font-weight: 700;\n line-height: 1.2;\n color: var(--mj-text-primary);\n}\n.metric-value-indigo { color: var(--mj-color-indigo-500); }\n\n.metric-label {\n font-size: 11px;\n color: var(--mj-text-disabled);\n text-transform: uppercase;\n letter-spacing: 0.4px;\n font-weight: 500;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* Conic gradient ring for success rate */\n.metric-ring {\n width: 44px;\n height: 44px;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n.metric-ring-inner {\n width: 32px;\n height: 32px;\n border-radius: 50%;\n background: var(--mj-bg-surface);\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.metric-ring-value {\n font-size: 8px;\n font-weight: 700;\n color: var(--mj-color-indigo-500);\n}\n\n/* Pulse dot for active syncs */\n.pulse-dot {\n position: absolute;\n top: 2px;\n right: 2px;\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: var(--mj-color-success-600);\n animation: pulse 1.5s ease-in-out infinite;\n}\n\n@keyframes pulse {\n 0%, 100% { opacity: 1; transform: scale(1); }\n 50% { opacity: 0.5; transform: scale(1.4); }\n}\n\n/* ==========================================================================\n SECTION 2: Pipeline Health Grid\n ========================================================================== */\n.section-heading {\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n margin-bottom: 16px;\n}\n.section-heading h3 {\n margin: 0;\n font-size: 16px;\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n.section-heading h3 i {\n margin-right: 8px;\n color: var(--mj-text-muted);\n}\n\n.refresh-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n /* Don't let the button shrink/overflow the heading row on narrow widths. */\n flex-shrink: 0;\n box-sizing: border-box;\n padding: 6px 14px;\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n background: var(--mj-bg-surface);\n color: var(--mj-brand-primary);\n font-size: 12px;\n font-weight: 500;\n cursor: pointer;\n transition: background 0.15s, border-color 0.15s;\n}\n.refresh-btn:hover {\n background: var(--mj-status-info-bg);\n border-color: var(--mj-color-info-200);\n}\n\n.pipeline-grid {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 20px;\n margin-bottom: 32px;\n}\n\n.pipeline-card {\n position: relative;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 12px;\n /* Clip children to the card's rounded border so footer buttons (.btn-sync)\n and any flush-edge content can't poke past the rounded corners. box-shadow\n is drawn outside the border-box, so the hover shadow below is unaffected. */\n overflow: hidden;\n padding: 20px;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);\n transition: box-shadow 0.2s, transform 0.2s;\n display: flex;\n flex-direction: column;\n gap: 16px;\n}\n.pipeline-card:hover {\n box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1);\n transform: translateY(-2px);\n}\n\n.pipeline-card-error {\n border-left: 3px solid var(--mj-status-error);\n}\n\n/* Card Header */\n.card-header {\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n}\n.card-header-left {\n display: flex;\n align-items: center;\n gap: 12px;\n min-width: 0;\n}\n.card-icon-wrap {\n width: 40px;\n height: 40px;\n border-radius: 10px;\n background: var(--mj-status-info-bg);\n color: var(--mj-brand-primary);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 16px;\n flex-shrink: 0;\n}\n.card-title-wrap {\n display: flex;\n flex-direction: column;\n min-width: 0;\n}\n.card-title {\n font-size: 14px;\n font-weight: 600;\n color: var(--mj-text-primary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.card-subtitle {\n font-size: 11px;\n color: var(--mj-text-disabled);\n}\n\n/* Status Dot */\n.status-dot {\n width: 10px;\n height: 10px;\n border-radius: 50%;\n flex-shrink: 0;\n margin-top: 4px;\n position: relative;\n}\n.status-dot-green { background: var(--mj-color-success-600); }\n.status-dot-amber { background: var(--mj-status-warning); }\n.status-dot-red { background: var(--mj-status-error); }\n.status-dot-gray { background: var(--mj-color-neutral-300); }\n\n.dot-pulse {\n position: absolute;\n inset: -3px;\n border-radius: 50%;\n border: 2px solid var(--mj-status-warning);\n animation: dotPulse 1.5s ease-in-out infinite;\n}\n@keyframes dotPulse {\n 0%, 100% { opacity: 0.3; transform: scale(1); }\n 50% { opacity: 0; transform: scale(1.6); }\n}\n\n/* Pipeline Flow Visual */\n.pipeline-flow {\n display: flex;\n align-items: center;\n gap: 0;\n padding: 8px 0;\n}\n.flow-node {\n width: 36px;\n height: 36px;\n border-radius: 8px;\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-muted);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 14px;\n flex-shrink: 0;\n}\n.flow-node-mj {\n background: var(--mj-status-info-bg);\n color: var(--mj-brand-primary);\n}\n.flow-arrow {\n flex: 1;\n display: flex;\n align-items: center;\n gap: 0;\n padding: 0 4px;\n}\n.flow-line {\n flex: 1;\n height: 2px;\n background: var(--mj-border-default);\n}\n.flow-count {\n font-size: 10px;\n color: var(--mj-text-muted);\n white-space: nowrap;\n padding: 2px 8px;\n background: var(--mj-bg-page);\n border: 1px solid var(--mj-border-default);\n border-radius: 10px;\n font-weight: 500;\n}\n\n/* Card Stats */\n.card-stats {\n display: grid;\n grid-template-columns: repeat(4, 1fr);\n gap: 8px;\n}\n.stat-item {\n display: flex;\n flex-direction: column;\n align-items: center;\n text-align: center;\n}\n.stat-label {\n font-size: 10px;\n color: var(--mj-text-disabled);\n text-transform: uppercase;\n letter-spacing: 0.3px;\n margin-bottom: 2px;\n}\n.stat-value {\n font-size: 13px;\n font-weight: 600;\n color: var(--mj-text-secondary);\n}\n.stat-error {\n color: var(--mj-status-error);\n}\n\n/* Sparkline Row */\n.sparkline-row {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n.sparkline-label {\n font-size: 10px;\n color: var(--mj-text-disabled);\n text-transform: uppercase;\n letter-spacing: 0.3px;\n white-space: nowrap;\n}\n.sparkline-dots {\n display: flex;\n align-items: center;\n gap: 5px;\n}\n.spark-dot {\n width: 7px;\n height: 7px;\n border-radius: 50%;\n display: inline-block;\n}\n.spark-green { background: var(--mj-color-success-600); }\n.spark-red { background: var(--mj-status-error); }\n.spark-amber { background: var(--mj-status-warning); }\n.spark-gray { background: var(--mj-color-neutral-300); }\n.sparkline-none {\n font-size: 10px;\n color: var(--mj-color-neutral-300);\n font-style: italic;\n}\n\n/* Card Footer */\n.card-footer {\n display: flex;\n justify-content: flex-end;\n /* min-width:0 lets the footer shrink so its button is bounded by the card's\n content box and can't be pushed past the right/bottom padding edge. */\n min-width: 0;\n padding-top: 4px;\n border-top: 1px solid var(--mj-border-subtle);\n}\n\n.btn-sync {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n /* Keep the button bounded by its container so it stays inside the card. */\n max-width: 100%;\n box-sizing: border-box;\n padding: 5px 12px;\n border: 1px solid var(--mj-border-default);\n border-radius: 6px;\n background: var(--mj-bg-surface);\n color: var(--mj-brand-primary);\n font-size: 12px;\n font-weight: 500;\n cursor: pointer;\n transition: background 0.15s, color 0.15s, border-color 0.15s;\n}\n.btn-sync:hover:not(:disabled) {\n background: var(--mj-status-info-bg);\n border-color: var(--mj-color-info-200);\n}\n.btn-sync:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n.btn-sync-disabled {\n color: var(--mj-text-disabled);\n border-color: var(--mj-border-default);\n}\n\n/* ==========================================================================\n SECTION 3: Bottom Split - Chart + Feed\n ========================================================================== */\n.bottom-split {\n display: flex;\n gap: 20px;\n}\n\n.panel-title {\n margin: 0 0 16px 0;\n font-size: 15px;\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n.panel-title i {\n margin-right: 8px;\n color: var(--mj-text-muted);\n}\n\n/* Bar Chart Panel (left, 60%) */\n.chart-panel {\n flex: 6;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 12px;\n padding: 20px;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);\n}\n\n.bar-chart {\n display: flex;\n align-items: flex-end;\n justify-content: space-between;\n gap: 12px;\n height: 200px;\n padding-top: 24px;\n}\n.bar-column {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: flex-end;\n height: 100%;\n position: relative;\n}\n.bar-count {\n font-size: 10px;\n font-weight: 600;\n color: var(--mj-text-muted);\n margin-bottom: 4px;\n}\n.bar-fill {\n width: 100%;\n max-width: 40px;\n background: linear-gradient(180deg, var(--mj-brand-primary) 0%, var(--mj-color-accent-400) 100%);\n border-radius: 4px 4px 0 0;\n min-height: 2px;\n transition: height 0.3s ease;\n}\n.bar-day {\n font-size: 11px;\n color: var(--mj-text-disabled);\n margin-top: 6px;\n text-align: center;\n}\n\n/* Activity Feed Panel (right, 40%) */\n.feed-panel {\n flex: 4;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 12px;\n padding: 20px;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);\n max-height: 340px;\n overflow-y: auto;\n}\n\n.feed-list {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n.feed-item {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 10px;\n border-radius: 6px;\n transition: background 0.15s;\n}\n.feed-item:hover {\n background: var(--mj-bg-page);\n}\n\n.feed-icon-green { color: var(--mj-color-success-600); }\n.feed-icon-red { color: var(--mj-color-error-600); }\n.feed-icon-amber { color: var(--mj-color-warning-600); }\n\n.feed-details {\n flex: 1;\n display: flex;\n flex-direction: column;\n min-width: 0;\n}\n.feed-name {\n font-size: 13px;\n font-weight: 600;\n color: var(--mj-text-primary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.feed-meta {\n font-size: 11px;\n color: var(--mj-text-disabled);\n}\n\n.feed-badge {\n font-size: 10px;\n font-weight: 600;\n padding: 2px 8px;\n border-radius: 10px;\n text-transform: uppercase;\n white-space: nowrap;\n flex-shrink: 0;\n}\n.feed-badge-green { background: var(--mj-status-success-bg); color: var(--mj-color-success-800); }\n.feed-badge-amber { background: var(--mj-color-warning-50); color: var(--mj-color-warning-800); }\n.feed-badge-red { background: var(--mj-status-error-bg); color: var(--mj-status-error); }\n\n.empty-hint {\n color: var(--mj-text-disabled);\n font-size: 13px;\n font-style: italic;\n}\n\n/* ==========================================================================\n Responsive\n ========================================================================== */\n@media (max-width: 1200px) {\n .pipeline-grid {\n grid-template-columns: repeat(2, 1fr);\n }\n .metric-strip {\n grid-template-columns: repeat(3, 1fr);\n }\n}\n\n@media (max-width: 900px) {\n .pipeline-grid {\n grid-template-columns: 1fr;\n }\n .metric-strip {\n grid-template-columns: repeat(2, 1fr);\n }\n .bottom-split {\n flex-direction: column;\n }\n .card-stats {\n grid-template-columns: repeat(2, 1fr);\n }\n}\n\n@media (max-width: 600px) {\n .overview {\n padding: 16px;\n }\n .metric-strip {\n grid-template-columns: 1fr;\n }\n}\n"] }]
750
+ args: [{ standalone: false, selector: 'app-integration-overview', template: "<mj-page-layout>\n <mj-page-header\n Title=\"Integration Overview\"\n Icon=\"fa-solid fa-gauge-high\"\n Subtitle=\"Health, throughput, and activity across all integrations\">\n @if (Summaries.length > 0) {\n <div meta>\n <mj-stat-badge [Count]=\"KPIs.TotalIntegrations\" Label=\"integrations\"></mj-stat-badge>\n </div>\n }\n <div actions>\n <mj-refresh-button [Loading]=\"IsLoading\" (Clicked)=\"Refresh()\"></mj-refresh-button>\n </div>\n </mj-page-header>\n\n <mj-page-body>\n<div class=\"overview\">\n <!-- Notification Banner -->\n @if (Notification) {\n <div class=\"notification\" [class.notification-success]=\"Notification.Type === 'success'\"\n [class.notification-error]=\"Notification.Type === 'error'\">\n @if (Notification.Type === 'success') {\n <i class=\"fa-solid fa-circle-check\"></i>\n } @else {\n <i class=\"fa-solid fa-circle-xmark\"></i>\n }\n <span class=\"notification-message\">{{ Notification.Message }}</span>\n <button class=\"notification-dismiss\" (click)=\"DismissNotification()\">\n <i class=\"fa-solid fa-times\"></i>\n </button>\n </div>\n }\n\n @if (IsLoading) {\n <mj-loading text=\"Loading integrations...\" size=\"medium\"></mj-loading>\n } @else if (Summaries.length === 0) {\n <mj-empty-state Icon=\"fa-solid fa-plug-circle-xmark\"\n Title=\"No Integrations Configured\"\n Message=\"Set up your first integration using the Connection Studio to start syncing data.\" />\n } @else {\n <!-- ====================================================================\n SECTION 1: Metric Strip\n ==================================================================== -->\n <div class=\"metric-strip\">\n <!-- Total Pipelines -->\n <div class=\"metric-card\">\n <div class=\"metric-icon metric-icon-blue\">\n <i class=\"fa-solid fa-diagram-project\"></i>\n </div>\n <div class=\"metric-body\">\n <span class=\"metric-value\">{{ KPIs.TotalIntegrations }}</span>\n <span class=\"metric-label\">Total Pipelines</span>\n </div>\n </div>\n\n <!-- Records Today -->\n <div class=\"metric-card\">\n <div class=\"metric-icon metric-icon-green\">\n <i class=\"fa-solid fa-database\"></i>\n </div>\n <div class=\"metric-body\">\n <span class=\"metric-value\">{{ FormatNumber(KPIs.RecordsSyncedToday) }}</span>\n <span class=\"metric-label\">Records Today</span>\n </div>\n </div>\n\n <!-- Success Rate (with conic gradient ring) -->\n <div class=\"metric-card\">\n <div class=\"metric-ring\" [style.background]=\"SuccessRateGradient\">\n <div class=\"metric-ring-inner\">\n <span class=\"metric-ring-value\">{{ SuccessRate }}%</span>\n </div>\n </div>\n <div class=\"metric-body\">\n <span class=\"metric-value metric-value-indigo\">{{ SuccessRate }}%</span>\n <span class=\"metric-label\">Success Rate</span>\n </div>\n </div>\n\n <!-- Active Now -->\n <div class=\"metric-card\">\n <div class=\"metric-icon metric-icon-emerald\">\n <i class=\"fa-solid fa-bolt\"></i>\n @if (ActiveCount > 0) {\n <span class=\"pulse-dot\"></span>\n }\n </div>\n <div class=\"metric-body\">\n <span class=\"metric-value\">{{ ActiveCount }}</span>\n <span class=\"metric-label\">Active Now</span>\n </div>\n </div>\n\n <!-- Avg Duration -->\n <div class=\"metric-card\">\n <div class=\"metric-icon metric-icon-orange\">\n <i class=\"fa-solid fa-clock\"></i>\n </div>\n <div class=\"metric-body\">\n <span class=\"metric-value\">{{ FormatDuration(KPIs.AverageSyncDurationMs) }}</span>\n <span class=\"metric-label\">Avg Duration</span>\n </div>\n </div>\n </div>\n\n <!-- ====================================================================\n SECTION 2: Pipeline Health Grid\n ==================================================================== -->\n <div class=\"section-heading\">\n <h3><i class=\"fa-solid fa-heart-pulse\"></i> Pipeline Health</h3>\n <button class=\"refresh-btn\" (click)=\"Refresh()\">\n <i class=\"fa-solid fa-arrows-rotate\"></i> Refresh\n </button>\n </div>\n\n <div class=\"pipeline-grid\">\n @for (summary of Summaries; track summary.Integration.ID) {\n <div [class]=\"GetCardBorderClass(summary.StatusColor)\">\n <!-- Card Header -->\n <div class=\"card-header\">\n <div class=\"card-header-left\">\n <div class=\"card-icon-wrap\">\n <i [class]=\"GetIntegrationIcon(summary)\"></i>\n </div>\n <div class=\"card-title-wrap\">\n <span class=\"card-title\">{{ summary.Integration.Name }}</span>\n <span class=\"card-subtitle\">{{ summary.SourceType?.Name ?? 'Custom' }}</span>\n </div>\n </div>\n <div [class]=\"GetStatusDotClass(summary.StatusColor)\"\n [title]=\"GetStatusLabel(summary.StatusColor)\">\n @if (summary.LatestRun?.Status === 'In Progress') {\n <span class=\"dot-pulse\"></span>\n }\n </div>\n </div>\n\n <!-- Pipeline Flow Visual -->\n <div class=\"pipeline-flow\">\n <div class=\"flow-node\">\n <i [class]=\"GetIntegrationIcon(summary)\"></i>\n </div>\n <div class=\"flow-arrow\">\n <div class=\"flow-line\"></div>\n <span class=\"flow-count\">{{ GetEntityMapCount(summary.Integration.ID) }} maps</span>\n <div class=\"flow-line\"></div>\n </div>\n <div class=\"flow-node flow-node-mj\">\n <i class=\"fa-solid fa-layer-group\"></i>\n </div>\n </div>\n\n <!-- Stats Row -->\n <div class=\"card-stats\">\n <div class=\"stat-item\">\n <span class=\"stat-label\">Last sync</span>\n <span class=\"stat-value\">{{ summary.RelativeTime }}</span>\n </div>\n <div class=\"stat-item\">\n <span class=\"stat-label\">Records</span>\n <span class=\"stat-value\">{{ FormatNumber(summary.LatestRun?.TotalRecords ?? 0) }}</span>\n </div>\n <div class=\"stat-item\">\n <span class=\"stat-label\">Duration</span>\n <span class=\"stat-value\">{{ FormatDuration(summary.DurationMs) }}</span>\n </div>\n <div class=\"stat-item\">\n <span class=\"stat-label\">Errors</span>\n <span class=\"stat-value\" [class.stat-error]=\"summary.TotalErrors > 0\">\n {{ summary.TotalErrors }}\n </span>\n </div>\n </div>\n\n <!-- Sparkline (last 5 runs) -->\n <div class=\"sparkline-row\">\n <span class=\"sparkline-label\">Recent runs</span>\n <div class=\"sparkline-dots\">\n @for (run of summary.RecentRuns; track run.ID) {\n <span [class]=\"GetSparklineDotClass(run)\"\n [title]=\"GetSparklineTooltip(run)\"></span>\n }\n @if (summary.RecentRuns.length === 0) {\n <span class=\"sparkline-none\">No runs</span>\n }\n </div>\n </div>\n\n <!-- Card Footer -->\n <div class=\"card-footer\">\n @if (IsRunning(summary.Integration.ID)) {\n <button class=\"btn-sync btn-sync-disabled\" disabled>\n <i class=\"fa-solid fa-spinner fa-spin\"></i> Running...\n </button>\n } @else {\n <button class=\"btn-sync\"\n (click)=\"RunSync(summary.Integration.ID)\"\n [disabled]=\"!summary.Integration.IsActive || RunningIntegrationID !== null\">\n <i class=\"fa-solid fa-play\"></i> Sync Now\n </button>\n }\n </div>\n </div>\n }\n </div>\n\n <!-- ====================================================================\n SECTION 3: Bottom - Bar Chart & Activity Feed\n ==================================================================== -->\n <div class=\"bottom-split\">\n <!-- Bar Chart (left, 60%) -->\n <div class=\"chart-panel\">\n <h3 class=\"panel-title\"><i class=\"fa-solid fa-chart-bar\"></i> Records Synced (7 days)</h3>\n @if (DailyCounts.length === 0) {\n <p class=\"empty-hint\">No data available.</p>\n } @else {\n <div class=\"bar-chart\">\n @for (day of DailyCounts; track day.Date) {\n <div class=\"bar-column\">\n <div class=\"bar-count\">{{ FormatNumber(day.Records) }}</div>\n <div class=\"bar-fill\" [style.height.%]=\"BarHeight(day.Records)\"></div>\n <div class=\"bar-day\">{{ day.Label }}</div>\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Activity Feed (right, 40%) -->\n <div class=\"feed-panel\">\n <h3 class=\"panel-title\"><i class=\"fa-solid fa-stream\"></i> Recent Activity</h3>\n @if (ActivityFeed.length === 0) {\n <p class=\"empty-hint\">No recent activity.</p>\n } @else {\n <div class=\"feed-list\">\n @for (item of ActivityFeed; track item.RunID) {\n <div class=\"feed-item\">\n <i [class]=\"ActivityStatusIcon(item.Status)\"\n [class.feed-icon-green]=\"item.StatusColor === 'green'\"\n [class.feed-icon-red]=\"item.StatusColor === 'red'\"\n [class.feed-icon-amber]=\"item.StatusColor === 'amber'\">\n </i>\n <div class=\"feed-details\">\n <span class=\"feed-name\">{{ item.IntegrationName }}</span>\n <span class=\"feed-meta\">{{ item.TotalRecords | number }} records &middot; {{ item.RelativeTime }}</span>\n </div>\n <span class=\"feed-badge\" [class]=\"'feed-badge-' + item.StatusColor\">\n {{ item.Status }}\n </span>\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n</div>\n </mj-page-body>\n</mj-page-layout>\n", styles: ["/* ==========================================================================\n Integration Overview - Root\n ========================================================================== */\n.overview {\n /* padding + flex/scroll now owned by <mj-page-body>; keep max-width for\n comfortable reading on ultra-wide displays. */\n max-width: 1440px;\n margin: 0 auto;\n}\n\n/* Slot passthrough now in <mj-page-header>; stat badges now via <mj-stat-badge>. */\n\n/* ==========================================================================\n Notification Banner\n ========================================================================== */\n.notification {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 12px 16px;\n border-radius: 10px;\n margin-bottom: 20px;\n font-size: 13px;\n font-weight: 500;\n animation: slideDown 0.25s ease-out;\n}\n.notification i:first-child { font-size: 16px; }\n.notification-message { flex: 1; }\n.notification-success {\n background: var(--mj-status-success-bg);\n color: var(--mj-color-success-800);\n border: 1px solid var(--mj-color-success-200);\n}\n.notification-error {\n background: var(--mj-status-error-bg);\n color: var(--mj-status-error);\n border: 1px solid var(--mj-color-error-200);\n}\n.notification-dismiss {\n background: none;\n border: none;\n cursor: pointer;\n color: inherit;\n opacity: 0.5;\n font-size: 14px;\n padding: 4px;\n line-height: 1;\n}\n.notification-dismiss:hover { opacity: 1; }\n\n@keyframes slideDown {\n from { opacity: 0; transform: translateY(-8px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n/* ==========================================================================\n Empty State\n ========================================================================== */\n\n/* ==========================================================================\n SECTION 1: Metric Strip\n ========================================================================== */\n.metric-strip {\n display: grid;\n grid-template-columns: repeat(5, 1fr);\n gap: 16px;\n margin-bottom: 32px;\n}\n\n.metric-card {\n display: flex;\n align-items: center;\n gap: 14px;\n padding: 18px 16px;\n border-radius: 12px;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);\n transition: box-shadow 0.2s, transform 0.2s;\n}\n.metric-card:hover {\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);\n transform: translateY(-1px);\n}\n\n.metric-icon {\n position: relative;\n width: 44px;\n height: 44px;\n border-radius: 10px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 18px;\n flex-shrink: 0;\n}\n.metric-icon-blue { background: var(--mj-status-info-bg); color: var(--mj-brand-primary); }\n.metric-icon-green { background: var(--mj-status-success-bg); color: var(--mj-color-success-600); }\n.metric-icon-emerald { background: var(--mj-status-success-bg); color: var(--mj-color-success-600); }\n.metric-icon-orange { background: var(--mj-color-warning-50); color: var(--mj-color-warning-600); }\n\n.metric-body {\n display: flex;\n flex-direction: column;\n min-width: 0;\n}\n\n.metric-value {\n font-size: 22px;\n font-weight: 700;\n line-height: 1.2;\n color: var(--mj-text-primary);\n}\n.metric-value-indigo { color: var(--mj-color-indigo-500); }\n\n.metric-label {\n font-size: 11px;\n color: var(--mj-text-disabled);\n text-transform: uppercase;\n letter-spacing: 0.4px;\n font-weight: 500;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* Conic gradient ring for success rate */\n.metric-ring {\n width: 44px;\n height: 44px;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n.metric-ring-inner {\n width: 32px;\n height: 32px;\n border-radius: 50%;\n background: var(--mj-bg-surface);\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.metric-ring-value {\n font-size: 8px;\n font-weight: 700;\n color: var(--mj-color-indigo-500);\n}\n\n/* Pulse dot for active syncs */\n.pulse-dot {\n position: absolute;\n top: 2px;\n right: 2px;\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: var(--mj-color-success-600);\n animation: pulse 1.5s ease-in-out infinite;\n}\n\n@keyframes pulse {\n 0%, 100% { opacity: 1; transform: scale(1); }\n 50% { opacity: 0.5; transform: scale(1.4); }\n}\n\n/* ==========================================================================\n SECTION 2: Pipeline Health Grid\n ========================================================================== */\n.section-heading {\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n margin-bottom: 16px;\n}\n.section-heading h3 {\n margin: 0;\n font-size: 16px;\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n.section-heading h3 i {\n margin-right: 8px;\n color: var(--mj-text-muted);\n}\n\n.refresh-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n /* Don't let the button shrink/overflow the heading row on narrow widths. */\n flex-shrink: 0;\n box-sizing: border-box;\n padding: 6px 14px;\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n background: var(--mj-bg-surface);\n color: var(--mj-brand-primary);\n font-size: 12px;\n font-weight: 500;\n cursor: pointer;\n transition: background 0.15s, border-color 0.15s;\n}\n.refresh-btn:hover {\n background: var(--mj-status-info-bg);\n border-color: var(--mj-status-info-border);\n}\n\n.pipeline-grid {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 20px;\n margin-bottom: 32px;\n}\n\n.pipeline-card {\n position: relative;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 12px;\n /* Clip children to the card's rounded border so footer buttons (.btn-sync)\n and any flush-edge content can't poke past the rounded corners. box-shadow\n is drawn outside the border-box, so the hover shadow below is unaffected. */\n overflow: hidden;\n padding: 20px;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);\n transition: box-shadow 0.2s, transform 0.2s;\n display: flex;\n flex-direction: column;\n gap: 16px;\n}\n.pipeline-card:hover {\n box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1);\n transform: translateY(-2px);\n}\n\n.pipeline-card-error {\n border-left: 3px solid var(--mj-status-error);\n}\n\n/* Card Header */\n.card-header {\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n}\n.card-header-left {\n display: flex;\n align-items: center;\n gap: 12px;\n min-width: 0;\n}\n.card-icon-wrap {\n width: 40px;\n height: 40px;\n border-radius: 10px;\n background: var(--mj-status-info-bg);\n color: var(--mj-brand-primary);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 16px;\n flex-shrink: 0;\n}\n.card-title-wrap {\n display: flex;\n flex-direction: column;\n min-width: 0;\n}\n.card-title {\n font-size: 14px;\n font-weight: 600;\n color: var(--mj-text-primary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.card-subtitle {\n font-size: 11px;\n color: var(--mj-text-disabled);\n}\n\n/* Status Dot */\n.status-dot {\n width: 10px;\n height: 10px;\n border-radius: 50%;\n flex-shrink: 0;\n margin-top: 4px;\n position: relative;\n}\n.status-dot-green { background: var(--mj-color-success-600); }\n.status-dot-amber { background: var(--mj-status-warning); }\n.status-dot-red { background: var(--mj-status-error); }\n.status-dot-gray { background: var(--mj-color-neutral-300); }\n\n.dot-pulse {\n position: absolute;\n inset: -3px;\n border-radius: 50%;\n border: 2px solid var(--mj-status-warning);\n animation: dotPulse 1.5s ease-in-out infinite;\n}\n@keyframes dotPulse {\n 0%, 100% { opacity: 0.3; transform: scale(1); }\n 50% { opacity: 0; transform: scale(1.6); }\n}\n\n/* Pipeline Flow Visual */\n.pipeline-flow {\n display: flex;\n align-items: center;\n gap: 0;\n padding: 8px 0;\n}\n.flow-node {\n width: 36px;\n height: 36px;\n border-radius: 8px;\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-muted);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 14px;\n flex-shrink: 0;\n}\n.flow-node-mj {\n background: var(--mj-status-info-bg);\n color: var(--mj-brand-primary);\n}\n.flow-arrow {\n flex: 1;\n display: flex;\n align-items: center;\n gap: 0;\n padding: 0 4px;\n}\n.flow-line {\n flex: 1;\n height: 2px;\n background: var(--mj-border-default);\n}\n.flow-count {\n font-size: 10px;\n color: var(--mj-text-muted);\n white-space: nowrap;\n padding: 2px 8px;\n background: var(--mj-bg-page);\n border: 1px solid var(--mj-border-default);\n border-radius: 10px;\n font-weight: 500;\n}\n\n/* Card Stats */\n.card-stats {\n display: grid;\n grid-template-columns: repeat(4, 1fr);\n gap: 8px;\n}\n.stat-item {\n display: flex;\n flex-direction: column;\n align-items: center;\n text-align: center;\n}\n.stat-label {\n font-size: 10px;\n color: var(--mj-text-disabled);\n text-transform: uppercase;\n letter-spacing: 0.3px;\n margin-bottom: 2px;\n}\n.stat-value {\n font-size: 13px;\n font-weight: 600;\n color: var(--mj-text-secondary);\n}\n.stat-error {\n color: var(--mj-status-error);\n}\n\n/* Sparkline Row */\n.sparkline-row {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n.sparkline-label {\n font-size: 10px;\n color: var(--mj-text-disabled);\n text-transform: uppercase;\n letter-spacing: 0.3px;\n white-space: nowrap;\n}\n.sparkline-dots {\n display: flex;\n align-items: center;\n gap: 5px;\n}\n.spark-dot {\n width: 7px;\n height: 7px;\n border-radius: 50%;\n display: inline-block;\n}\n.spark-green { background: var(--mj-color-success-600); }\n.spark-red { background: var(--mj-status-error); }\n.spark-amber { background: var(--mj-status-warning); }\n.spark-gray { background: var(--mj-color-neutral-300); }\n.sparkline-none {\n font-size: 10px;\n color: var(--mj-color-neutral-300);\n font-style: italic;\n}\n\n/* Card Footer */\n.card-footer {\n display: flex;\n justify-content: flex-end;\n /* min-width:0 lets the footer shrink so its button is bounded by the card's\n content box and can't be pushed past the right/bottom padding edge. */\n min-width: 0;\n padding-top: 4px;\n border-top: 1px solid var(--mj-border-subtle);\n}\n\n.btn-sync {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n /* Keep the button bounded by its container so it stays inside the card. */\n max-width: 100%;\n box-sizing: border-box;\n padding: 5px 12px;\n border: 1px solid var(--mj-border-default);\n border-radius: 6px;\n background: var(--mj-bg-surface);\n color: var(--mj-brand-primary);\n font-size: 12px;\n font-weight: 500;\n cursor: pointer;\n transition: background 0.15s, color 0.15s, border-color 0.15s;\n}\n.btn-sync:hover:not(:disabled) {\n background: var(--mj-status-info-bg);\n border-color: var(--mj-status-info-border);\n}\n.btn-sync:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n.btn-sync-disabled {\n color: var(--mj-text-disabled);\n border-color: var(--mj-border-default);\n}\n\n/* ==========================================================================\n SECTION 3: Bottom Split - Chart + Feed\n ========================================================================== */\n.bottom-split {\n display: flex;\n gap: 20px;\n}\n\n.panel-title {\n margin: 0 0 16px 0;\n font-size: 15px;\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n.panel-title i {\n margin-right: 8px;\n color: var(--mj-text-muted);\n}\n\n/* Bar Chart Panel (left, 60%) */\n.chart-panel {\n flex: 6;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 12px;\n padding: 20px;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);\n}\n\n.bar-chart {\n display: flex;\n align-items: flex-end;\n justify-content: space-between;\n gap: 12px;\n height: 200px;\n padding-top: 24px;\n}\n.bar-column {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: flex-end;\n height: 100%;\n position: relative;\n}\n.bar-count {\n font-size: 10px;\n font-weight: 600;\n color: var(--mj-text-muted);\n margin-bottom: 4px;\n}\n.bar-fill {\n width: 100%;\n max-width: 40px;\n background: linear-gradient(180deg, var(--mj-brand-primary) 0%, var(--mj-color-accent-400) 100%);\n border-radius: 4px 4px 0 0;\n min-height: 2px;\n transition: height 0.3s ease;\n}\n.bar-day {\n font-size: 11px;\n color: var(--mj-text-disabled);\n margin-top: 6px;\n text-align: center;\n}\n\n/* Activity Feed Panel (right, 40%) */\n.feed-panel {\n flex: 4;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 12px;\n padding: 20px;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);\n max-height: 340px;\n overflow-y: auto;\n}\n\n.feed-list {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n.feed-item {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 10px;\n border-radius: 6px;\n transition: background 0.15s;\n}\n.feed-item:hover {\n background: var(--mj-bg-page);\n}\n\n.feed-icon-green { color: var(--mj-color-success-600); }\n.feed-icon-red { color: var(--mj-color-error-600); }\n.feed-icon-amber { color: var(--mj-color-warning-600); }\n\n.feed-details {\n flex: 1;\n display: flex;\n flex-direction: column;\n min-width: 0;\n}\n.feed-name {\n font-size: 13px;\n font-weight: 600;\n color: var(--mj-text-primary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.feed-meta {\n font-size: 11px;\n color: var(--mj-text-disabled);\n}\n\n.feed-badge {\n font-size: 10px;\n font-weight: 600;\n padding: 2px 8px;\n border-radius: 10px;\n text-transform: uppercase;\n white-space: nowrap;\n flex-shrink: 0;\n}\n.feed-badge-green { background: var(--mj-status-success-bg); color: var(--mj-color-success-800); }\n.feed-badge-amber { background: var(--mj-color-warning-50); color: var(--mj-color-warning-800); }\n.feed-badge-red { background: var(--mj-status-error-bg); color: var(--mj-status-error); }\n\n.empty-hint {\n color: var(--mj-text-disabled);\n font-size: 13px;\n font-style: italic;\n}\n\n/* ==========================================================================\n Responsive\n ========================================================================== */\n@media (max-width: 1200px) {\n .pipeline-grid {\n grid-template-columns: repeat(2, 1fr);\n }\n .metric-strip {\n grid-template-columns: repeat(3, 1fr);\n }\n}\n\n@media (max-width: 900px) {\n .pipeline-grid {\n grid-template-columns: 1fr;\n }\n .metric-strip {\n grid-template-columns: repeat(2, 1fr);\n }\n .bottom-split {\n flex-direction: column;\n }\n .card-stats {\n grid-template-columns: repeat(2, 1fr);\n }\n}\n\n@media (max-width: 600px) {\n .overview {\n padding: 16px;\n }\n .metric-strip {\n grid-template-columns: 1fr;\n }\n}\n"] }]
751
751
  }], null, null); })();
752
752
  (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(OverviewComponent, { className: "OverviewComponent", filePath: "src/Integration/components/overview/overview.component.ts", lineNumber: 41 }); })();
753
753
  export function LoadOverviewComponent() {