@leavepulse/ui 0.78.3 → 0.80.1

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 (234) hide show
  1. package/dist/assets/{LpCommandPalette-DDJKICKh.css → LpCommandPalette-C1LRFzbN.css} +19 -19
  2. package/dist/chunks/{ContextMenuItems-DmaKSJVv.js → ContextMenuItems-DLK4q-mm.js} +1 -1
  3. package/dist/chunks/{DropzoneLinkField-CyE7TJ5f.js → DropzoneLinkField-CcJu8De3.js} +1 -1
  4. package/dist/chunks/{FieldLeading-mr81aEAP.js → FieldLeading-CldGLINn.js} +1 -1
  5. package/dist/chunks/{FieldWrapper-DnuMTrqW.js → FieldWrapper-Cj8ZgoKp.js} +1 -1
  6. package/dist/chunks/{IconCloseButton-COZEsa9u.js → IconCloseButton-CV4iHRpy.js} +1 -1
  7. package/dist/chunks/{LayoutCanvas-CZKOltfA.js → LayoutCanvas-BQbq7Ekw.js} +1 -1
  8. package/dist/chunks/{LayoutNode-D2f7v683.js → LayoutNode-BMedRk3g.js} +1 -1
  9. package/dist/chunks/{LpActionBar-BGMk5KoG.js → LpActionBar-C7g52Cyo.js} +1 -1
  10. package/dist/chunks/{LpAddressInput-ft8fQOGx.js → LpAddressInput-1uakhQk5.js} +7 -7
  11. package/dist/chunks/{LpAlert-CjvTZIKF.js → LpAlert-DWFeR82L.js} +1 -1
  12. package/dist/chunks/{LpAppShell-DRHoQtr-.js → LpAppShell-InIK01Nn.js} +4 -4
  13. package/dist/chunks/{LpAutocomplete-CrZbd0uv.js → LpAutocomplete-DY3LvpB9.js} +4 -4
  14. package/dist/chunks/{LpAvatar-cQVsNk6F.js → LpAvatar-CEZmMcGh.js} +1 -1
  15. package/dist/chunks/{LpBadge-D5QQdDQ_.js → LpBadge-C_VqLHtg.js} +1 -1
  16. package/dist/chunks/{LpBottomNav-zxLGGbjm.js → LpBottomNav-4I89sPDZ.js} +1 -1
  17. package/dist/chunks/{LpBreadcrumbs-CIIBsygI.js → LpBreadcrumbs-1BBzrfxG.js} +1 -1
  18. package/dist/chunks/{LpCalendar-D4-LVjfF.js → LpCalendar-D2dcXjUK.js} +1 -1
  19. package/dist/chunks/{LpCheckbox-CCWCwD0y.js → LpCheckbox-CN-MEnbX.js} +2 -2
  20. package/dist/chunks/{LpCodeBlock-DBqy4RLd.js → LpCodeBlock-Cj7zk4QE.js} +2 -2
  21. package/dist/chunks/{LpCodeDisplay-D59uPwRb.js → LpCodeDisplay-COzosm77.js} +4 -4
  22. package/dist/chunks/{LpColorPicker-BONURBFN.js → LpColorPicker-BezW6DX1.js} +3 -3
  23. package/dist/chunks/{LpConfirmDialog-CnrmzkuN.js → LpConfirmDialog-D9GkDMYr.js} +2 -2
  24. package/dist/chunks/{LpConfirmHost-CwbObVGH.js → LpConfirmHost-BH7kf0dm.js} +1 -1
  25. package/dist/chunks/{LpContextMenu-Bx_IhQDS.js → LpContextMenu-bn5bc9t7.js} +2 -2
  26. package/dist/chunks/{LpContextSwitcher-1C1tExdl.js → LpContextSwitcher-CGr6ZAuq.js} +2 -2
  27. package/dist/chunks/{LpCopyButton-sAMvWeLP.js → LpCopyButton-BSr6YaFR.js} +1 -1
  28. package/dist/chunks/{LpDataList-CaVfsozH.js → LpDataList-Cv7ZUlyy.js} +2 -2
  29. package/dist/chunks/{LpDatePicker-C2AOtxgF.js → LpDatePicker-CR7vsb5M.js} +5 -5
  30. package/dist/chunks/{LpDateRangePicker-CiXdIAyh.js → LpDateRangePicker-B9fRO3G6.js} +3 -3
  31. package/dist/chunks/{LpDisclosure-DG7JCo-d.js → LpDisclosure-CE4mneaE.js} +1 -1
  32. package/dist/chunks/{LpDrawer-DXNQFpAA.js → LpDrawer-DU_5MqBz.js} +1 -1
  33. package/dist/chunks/{LpDropdownMenu-CLzxIOgM.js → LpDropdownMenu-_u6GPWhp.js} +2 -2
  34. package/dist/chunks/{LpEditable-DVT0Rh8F.js → LpEditable-DYoMayjt.js} +2 -2
  35. package/dist/chunks/{LpEmptyState-DurPF001.js → LpEmptyState-DpdHeSfz.js} +1 -1
  36. package/dist/chunks/{LpFieldNotice-DJ10cYIo.js → LpFieldNotice-DONFCWV7.js} +1 -1
  37. package/dist/chunks/{LpFileTree-CpNurwZp.js → LpFileTree-f5ktGqsN.js} +4 -4
  38. package/dist/chunks/{LpFileTreeNode-DdF8Cq-s.js → LpFileTreeNode-nbp-JwV9.js} +4 -4
  39. package/dist/chunks/{LpFormField-BKYn9dyP.js → LpFormField-Bz50_LZW.js} +1 -1
  40. package/dist/chunks/{LpHintLayer-BeKRsRon.js → LpHintLayer-B6bA5Cb_.js} +1 -1
  41. package/dist/chunks/{LpHints-qf3xYsza.js → LpHints-DyJ5hVcr.js} +3 -3
  42. package/dist/chunks/{LpIcon-C6JpQ5td.js → LpIcon-BCQLuHSP.js} +1 -1
  43. package/dist/chunks/{LpInfraNode-Bb5nbuFY.js → LpInfraNode-BS1RXYAs.js} +2 -2
  44. package/dist/chunks/{LpInput-CbI7z5fQ.js → LpInput-BjbXdjcW.js} +3 -3
  45. package/dist/chunks/{LpInspector-DO3fTCQ1.js → LpInspector-BIyJyM6r.js} +4 -4
  46. package/dist/chunks/{LpLightbox-CtDTQV01.js → LpLightbox-CSAJuUaJ.js} +2 -2
  47. package/dist/chunks/{LpLogInspector-CcSJSGan.js → LpLogInspector-OJEz1vod.js} +2 -2
  48. package/dist/chunks/{LpLogViewer-CWu49Dko.js → LpLogViewer-CRLwQpk0.js} +6 -6
  49. package/dist/chunks/{LpMapPicker-D5_YLLBC.js → LpMapPicker-DW2EnrCJ.js} +1 -1
  50. package/dist/chunks/{LpMediaGallery-BYEwySoF.js → LpMediaGallery-BNEYctDP.js} +3 -3
  51. package/dist/chunks/{LpModal-DDvrSAnf.js → LpModal-DdTqdf4x.js} +1 -1
  52. package/dist/chunks/{LpModalNav-DI3BW5ff.js → LpModalNav-BL7Dp0om.js} +3 -3
  53. package/dist/chunks/LpNotificationBell-d0F6MWxD.js +163 -0
  54. package/dist/chunks/LpNotificationFeed-BIe5lapb.js +470 -0
  55. package/dist/chunks/{LpNumberField-C8LfUz_R.js → LpNumberField-D5Dx-hzD.js} +3 -3
  56. package/dist/chunks/{LpOtpInput-B1X9l9no.js → LpOtpInput-BVl74Lfl.js} +1 -1
  57. package/dist/chunks/{LpPagination-snQ020xQ.js → LpPagination-D0wb-Uru.js} +1 -1
  58. package/dist/chunks/{LpPaletteTheme-B-xuZiKK.js → LpPaletteTheme-DdilCBXA.js} +2 -2
  59. package/dist/chunks/{LpPasswordInput-Duc_3wPv.js → LpPasswordInput-DUtqWMkA.js} +2 -2
  60. package/dist/chunks/{LpPhoneInput-CctcRK9K.js → LpPhoneInput-CalRjkqc.js} +3 -3
  61. package/dist/chunks/{LpPie-oVNHjsKD.js → LpPie-BVAEmVBT.js} +2 -2
  62. package/dist/chunks/{LpPipeline-BSXT6oUJ.js → LpPipeline-Dk7_sRP1.js} +2 -2
  63. package/dist/chunks/{LpQuota-CgWPgdVt.js → LpQuota-DKx0IZYA.js} +1 -1
  64. package/dist/chunks/{LpRadioGroup-DTM0nd9G.js → LpRadioGroup-E3SP6j9h.js} +2 -2
  65. package/dist/chunks/{LpRangeCalendar-BrqlGZzE.js → LpRangeCalendar-BFJFaen4.js} +1 -1
  66. package/dist/chunks/{LpRangeSlider-DJsEZXad.js → LpRangeSlider-b6ipywRB.js} +1 -1
  67. package/dist/chunks/{LpRepeater-Czib3rmv.js → LpRepeater-BLrw62Bh.js} +2 -2
  68. package/dist/chunks/{LpSecretField-BtlCk3IH.js → LpSecretField-BV8BNjKc.js} +5 -5
  69. package/dist/chunks/{LpSection-BfteipxU.js → LpSection-CghNNxIv.js} +1 -1
  70. package/dist/chunks/{LpSegmented-sskrJKJJ.js → LpSegmented-DjvrHL_G.js} +2 -2
  71. package/dist/chunks/{LpSelect-JFZehOkd.js → LpSelect-DYPimzAZ.js} +4 -4
  72. package/dist/chunks/{LpSidebar-CvRvVBTt.js → LpSidebar-D3S8rYFf.js} +4 -4
  73. package/dist/chunks/{LpSidebarNav-CE0jQS8i.js → LpSidebarNav-Dlse8vxT.js} +3 -3
  74. package/dist/chunks/{LpSidebarRow-DOTg1txp.js → LpSidebarRow-Cqd_6tGG.js} +3 -3
  75. package/dist/chunks/{LpSlider-C4ilCtZX.js → LpSlider-B9XIC75O.js} +1 -1
  76. package/dist/chunks/{LpStat-DPjc6_fX.js → LpStat-kyLRmX-m.js} +2 -2
  77. package/dist/chunks/{LpStatus-DGJFwk5F.js → LpStatus-szB2dWzM.js} +1 -1
  78. package/dist/chunks/{LpStepMark-Dw2MRTrt.js → LpStepMark-DP3CDSm9.js} +1 -1
  79. package/dist/chunks/{LpStepper-DNnh7lCu.js → LpStepper-BS3iaJIz.js} +1 -1
  80. package/dist/chunks/{LpSwitch-DyAYTchz.js → LpSwitch-DRFy8RNr.js} +1 -1
  81. package/dist/chunks/{LpTabFace-DQoV8mQG.js → LpTabFace-C9-OperL.js} +2 -2
  82. package/dist/chunks/{LpTableFilter-CJg5eIcV.js → LpTableFilter-CEIwLupk.js} +2 -2
  83. package/dist/chunks/{LpTagsInput-CkNxpNYa.js → LpTagsInput-ClDeF_aU.js} +5 -5
  84. package/dist/chunks/{LpTextarea-CFK_5QB8.js → LpTextarea-DEY6pUBy.js} +1 -1
  85. package/dist/chunks/{LpThemeStudio-R-wohbCY.js → LpThemeStudio-WY72Z1Db.js} +5 -5
  86. package/dist/chunks/{LpThemeSwitcher-Bzy15MCP.js → LpThemeSwitcher-9uCH9c-K.js} +3 -3
  87. package/dist/chunks/{LpTimeField-B5n0m8m0.js → LpTimeField-BmgxCzQO.js} +2 -2
  88. package/dist/chunks/{LpToaster-Ct_ZOhCh.js → LpToaster-DLKO-efn.js} +2 -2
  89. package/dist/chunks/{LpTopologyCanvas-BNgw9RyZ.js → LpTopologyCanvas-B-PhPa7k.js} +4 -4
  90. package/dist/chunks/{LpTour-ClKYhx4h.js → LpTour-ODlBcat2.js} +1 -1
  91. package/dist/chunks/{LpTruncate-ellq9RYM.js → LpTruncate--nhT0iUo.js} +1 -1
  92. package/dist/chunks/{LpWindow-B8YSRVTH.js → LpWindow-DJxE8Zl-.js} +2 -2
  93. package/dist/chunks/{LpWorkspaceSwitcher-ugv-fyig.js → LpWorkspaceSwitcher-Bpf41Bw5.js} +3 -3
  94. package/dist/chunks/{MenuTitle-Bh5dAdV1.js → MenuTitle-CMjvLsvC.js} +1 -1
  95. package/dist/chunks/{OptionContent-CGb8QlHA.js → OptionContent-DY8NaG8s.js} +1 -1
  96. package/dist/chunks/{PickerField-BM1dyafh.js → PickerField-CgfkaBgg.js} +1 -1
  97. package/dist/chunks/{RevealToggle-D_iyrB8d.js → RevealToggle-CPMXxaKv.js} +1 -1
  98. package/dist/chunks/{TourCard-DAzTYfav.js → TourCard-D4b3lMHi.js} +1 -1
  99. package/dist/chunks/icons-C1vppR4f.js +59 -0
  100. package/dist/component-names.d.ts +1 -1
  101. package/dist/components/ContextMenuItems.vue.js +1 -1
  102. package/dist/components/DropzoneLinkField.vue.js +1 -1
  103. package/dist/components/FieldLeading.vue.js +1 -1
  104. package/dist/components/FieldWrapper.vue.js +1 -1
  105. package/dist/components/IconCloseButton.vue.js +1 -1
  106. package/dist/components/LayoutCanvas.vue.js +1 -1
  107. package/dist/components/LayoutNode.vue.js +1 -1
  108. package/dist/components/LpActionBar.vue.js +1 -1
  109. package/dist/components/LpAddressInput.vue.js +1 -1
  110. package/dist/components/LpAlert.vue.js +1 -1
  111. package/dist/components/LpAppShell.vue.js +1 -1
  112. package/dist/components/LpAutocomplete.vue.js +1 -1
  113. package/dist/components/LpAvatar.vue.js +1 -1
  114. package/dist/components/LpBadge.vue.js +1 -1
  115. package/dist/components/LpBottomNav.vue.js +1 -1
  116. package/dist/components/LpBreadcrumbs.vue.js +1 -1
  117. package/dist/components/LpCalendar.vue.js +1 -1
  118. package/dist/components/LpChart.vue.js +2 -2
  119. package/dist/components/LpCheckbox.vue.js +1 -1
  120. package/dist/components/LpCodeBlock.vue.js +1 -1
  121. package/dist/components/LpCodeDisplay.vue.js +1 -1
  122. package/dist/components/LpColorPicker.vue.js +1 -1
  123. package/dist/components/LpCommandPalette.vue.d.ts +22 -1
  124. package/dist/components/LpCommandPalette.vue.js +36 -16
  125. package/dist/components/LpConfirmDialog.vue.js +1 -1
  126. package/dist/components/LpConfirmHost.vue.js +1 -1
  127. package/dist/components/LpContextMenu.vue.js +1 -1
  128. package/dist/components/LpContextSwitcher.vue.js +1 -1
  129. package/dist/components/LpCopyButton.vue.js +1 -1
  130. package/dist/components/LpDataList.vue.js +1 -1
  131. package/dist/components/LpDatePicker.vue.js +1 -1
  132. package/dist/components/LpDateRangePicker.vue.js +1 -1
  133. package/dist/components/LpDisclosure.vue.js +1 -1
  134. package/dist/components/LpDock.vue.js +1 -1
  135. package/dist/components/LpDrawer.vue.js +1 -1
  136. package/dist/components/LpDropdownMenu.vue.js +1 -1
  137. package/dist/components/LpDropzone.vue.js +2 -2
  138. package/dist/components/LpEditable.vue.js +1 -1
  139. package/dist/components/LpEmptyState.vue.js +1 -1
  140. package/dist/components/LpFieldNotice.vue.js +1 -1
  141. package/dist/components/LpFileTree.vue.js +1 -1
  142. package/dist/components/LpFileTreeNode.vue.js +1 -1
  143. package/dist/components/LpFormField.vue.js +1 -1
  144. package/dist/components/LpHintLayer.vue.js +1 -1
  145. package/dist/components/LpHints.vue.js +1 -1
  146. package/dist/components/LpIcon.vue.js +1 -1
  147. package/dist/components/LpImageEditor.vue.js +1 -1
  148. package/dist/components/LpInfraNode.vue.js +1 -1
  149. package/dist/components/LpInput.vue.js +1 -1
  150. package/dist/components/LpInspector.vue.js +1 -1
  151. package/dist/components/LpLightbox.vue.js +1 -1
  152. package/dist/components/LpLogInspector.vue.js +1 -1
  153. package/dist/components/LpLogViewer.vue.js +1 -1
  154. package/dist/components/LpMapPicker.vue.js +1 -1
  155. package/dist/components/LpMediaGallery.vue.js +1 -1
  156. package/dist/components/LpModal.vue.js +1 -1
  157. package/dist/components/LpModalNav.vue.js +1 -1
  158. package/dist/components/LpNotificationBell.vue.d.ts +28 -64
  159. package/dist/components/LpNotificationBell.vue.js +1 -1
  160. package/dist/components/LpNotificationFeed.vue.d.ts +78 -0
  161. package/dist/components/LpNotificationFeed.vue.js +4 -0
  162. package/dist/components/LpNumberField.vue.js +1 -1
  163. package/dist/components/LpOtpInput.vue.js +1 -1
  164. package/dist/components/LpPagination.vue.js +1 -1
  165. package/dist/components/LpPaletteFacts.vue.js +1 -1
  166. package/dist/components/LpPaletteHeader.vue.js +2 -2
  167. package/dist/components/LpPaletteTheme.vue.js +1 -1
  168. package/dist/components/LpPasswordInput.vue.js +1 -1
  169. package/dist/components/LpPhoneInput.vue.js +1 -1
  170. package/dist/components/LpPie.vue.js +1 -1
  171. package/dist/components/LpPipeline.vue.js +1 -1
  172. package/dist/components/LpQuota.vue.js +1 -1
  173. package/dist/components/LpRadioGroup.vue.js +1 -1
  174. package/dist/components/LpRangeCalendar.vue.js +1 -1
  175. package/dist/components/LpRangeSlider.vue.js +1 -1
  176. package/dist/components/LpRepeater.vue.js +1 -1
  177. package/dist/components/LpSecretField.vue.js +1 -1
  178. package/dist/components/LpSection.vue.js +1 -1
  179. package/dist/components/LpSegmented.vue.js +1 -1
  180. package/dist/components/LpSelect.vue.js +1 -1
  181. package/dist/components/LpShortcutsDialog.vue.js +1 -1
  182. package/dist/components/LpSidebar.vue.js +1 -1
  183. package/dist/components/LpSidebarNav.vue.js +1 -1
  184. package/dist/components/LpSidebarRow.vue.js +1 -1
  185. package/dist/components/LpSlider.vue.js +1 -1
  186. package/dist/components/LpStat.vue.js +1 -1
  187. package/dist/components/LpStatus.vue.js +1 -1
  188. package/dist/components/LpStepMark.vue.js +1 -1
  189. package/dist/components/LpStepper.vue.js +1 -1
  190. package/dist/components/LpSwitch.vue.js +1 -1
  191. package/dist/components/LpTabFace.vue.js +1 -1
  192. package/dist/components/LpTable.vue.js +9 -9
  193. package/dist/components/LpTableFilter.vue.js +1 -1
  194. package/dist/components/LpTabs.vue.js +2 -2
  195. package/dist/components/LpTagsInput.vue.js +1 -1
  196. package/dist/components/LpTextarea.vue.js +1 -1
  197. package/dist/components/LpThemeStudio.vue.js +1 -1
  198. package/dist/components/LpThemeSwitcher.vue.js +1 -1
  199. package/dist/components/LpTimeField.vue.js +1 -1
  200. package/dist/components/LpToaster.vue.js +1 -1
  201. package/dist/components/LpTopologyCanvas.vue.js +1 -1
  202. package/dist/components/LpTour.vue.js +1 -1
  203. package/dist/components/LpTruncate.vue.js +1 -1
  204. package/dist/components/LpWindow.vue.js +1 -1
  205. package/dist/components/LpWorkspaceSwitcher.vue.js +1 -1
  206. package/dist/components/MenuTitle.vue.js +1 -1
  207. package/dist/components/OptionContent.vue.js +1 -1
  208. package/dist/components/PickerField.vue.js +1 -1
  209. package/dist/components/RevealToggle.vue.js +1 -1
  210. package/dist/components/TourCard.vue.js +1 -1
  211. package/dist/components/commandPalette.d.ts +14 -0
  212. package/dist/components/commandPalette.js +42 -0
  213. package/dist/components/menuWhen.js +1 -1
  214. package/dist/components/notifications.d.ts +156 -0
  215. package/dist/components/notifications.js +59 -0
  216. package/dist/components/relativeTime.js +6 -1
  217. package/dist/composables/useAsyncTask.d.ts +6 -1
  218. package/dist/composables/useAsyncTask.js +4 -1
  219. package/dist/icons-all.js +1 -1
  220. package/dist/index.d.ts +2 -1
  221. package/dist/index.js +208 -206
  222. package/package.json +1 -1
  223. package/src/component-names.ts +1 -0
  224. package/src/components/LpCommandPalette.vue +64 -14
  225. package/src/components/LpNotificationBell.vue +105 -279
  226. package/src/components/LpNotificationFeed.vue +404 -0
  227. package/src/components/commandPalette.ts +62 -0
  228. package/src/components/notifications.ts +197 -0
  229. package/src/components/relativeTime.ts +13 -1
  230. package/src/composables/useAsyncTask.ts +10 -2
  231. package/src/index.ts +6 -1
  232. package/src/kit-icons.ts +1 -1
  233. package/dist/chunks/LpNotificationBell-DUbVyK6R.js +0 -311
  234. package/dist/chunks/icons-CYfcpSDS.js +0 -59
@@ -87,6 +87,7 @@ import {
87
87
  toRecentItem,
88
88
  type RecentEntry,
89
89
  type RecentItem,
90
+ settleFetched,
90
91
  } from "./commandPalette"
91
92
  import { useOutsideGuard, usePortalTarget, useView, viewWindow } from "../composables/useView"
92
93
 
@@ -279,12 +280,31 @@ export interface CommandPrompt {
279
280
  * after the palette closed, is discarded and never reaches the list, so a
280
281
  * fetcher does not need to check the signal itself. Checking it before an
281
282
  * expensive step is only a saving.
283
+ *
284
+ * A fetcher that gathers from sources of different speeds (a local index, a
285
+ * database, a slow remote) can return an async iterable instead — an async
286
+ * generator is the easy way — and yield as it goes, so the quick sources show
287
+ * without waiting for the slow one. Each batch is the whole list so far and
288
+ * replaces the one before; the last is the answer. The search keeps its
289
+ * loading bar until the iterable ends, the highlight stays on the row it was
290
+ * on as rows are added, and when the query moves on the signal aborts and the
291
+ * iterable is closed (`return()`) — a generator parked in an `await` only
292
+ * closes at its next `yield`, running its `finally` there, so pass the signal
293
+ * on to what it awaits. A throw after some batches keeps them on screen, with
294
+ * a line saying the rest could not be fetched:
295
+ *
296
+ * async function* search(q, signal) {
297
+ * const local = index.find(q)
298
+ * yield local
299
+ * const remote = await api.search(q, { signal })
300
+ * yield [...local, ...remote]
301
+ * }
282
302
  */
283
303
  export type CommandFetcher = (
284
304
  query: string,
285
305
  signal: AbortSignal,
286
306
  parsed: PaletteQuery,
287
- ) => Command[] | Promise<Command[]>
307
+ ) => Command[] | Promise<Command[]> | AsyncIterable<Command[]>
288
308
 
289
309
  /** Footer hints and the mode chip, spelled out so an app can translate them. */
290
310
  export type PaletteLabels = Partial<Record<PaletteMode, string>> & {
@@ -305,6 +325,8 @@ export type PaletteLabels = Partial<Record<PaletteMode, string>> & {
305
325
  searching: string
306
326
  /** Shown when `search` (or a fetching mode) threw. */
307
327
  searchFailed: string
328
+ /** Shown under streamed results when the fetcher threw after some of them; defaults to "Some results could not be loaded." */
329
+ searchIncomplete?: string
308
330
  retry: string
309
331
  /** The row that unfolds a group cut by `groupLimit`; `{count}` is how many are hidden. */
310
332
  showMore: string
@@ -508,6 +530,7 @@ const DEFAULT_LABELS: Required<PaletteLabels> = {
508
530
  back: "back",
509
531
  searching: "Searching…",
510
532
  searchFailed: "Search is unavailable right now.",
533
+ searchIncomplete: "Some results could not be loaded.",
511
534
  retry: "Retry",
512
535
  showMore: "Show {count} more",
513
536
  actions: "actions",
@@ -762,11 +785,14 @@ function cancel() {
762
785
  * drops a superseded request before `fail` runs.
763
786
  */
764
787
  const failed = ref(false)
788
+ /** The failure came after some batches had shown: those stay, the line says the rest is missing. */
789
+ const incomplete = ref(false)
765
790
 
766
791
  function refresh(keepRows = false) {
767
792
  cancel()
768
793
  if (!keepRows) found.value = []
769
794
  failed.value = false
795
+ incomplete.value = false
770
796
  const run = fetcher()
771
797
  if (!run) return
772
798
  const parsed = parsePaletteQuery(query.value.trim())
@@ -778,14 +804,25 @@ function refresh(keepRows = false) {
778
804
  if (!inMode && q.length < Math.max(1, props.minChars)) return
779
805
  // A failing provider: the static commands still stand, with a line saying
780
806
  // that the rest could not be fetched.
781
- task.run((signal) => run(q, signal, parsed), {
782
- delay: props.debounce,
783
- apply: (hits) => (found.value = hits),
784
- fail: () => {
785
- found.value = []
786
- failed.value = true
807
+ // Streamed batches go through `emit`, which drops them once this request
808
+ // is superseded; `streamed` tells a failure after them from one before.
809
+ let streamed = false
810
+ task.run(
811
+ (signal, emit) =>
812
+ settleFetched(run(q, signal, parsed), signal, (rows) => {
813
+ streamed = true
814
+ emit(rows)
815
+ }),
816
+ {
817
+ delay: props.debounce,
818
+ apply: (hits) => (found.value = hits),
819
+ fail: () => {
820
+ if (!streamed) found.value = []
821
+ incomplete.value = streamed
822
+ failed.value = true
823
+ },
787
824
  },
788
- })
825
+ )
789
826
  }
790
827
 
791
828
  // ── filtering + scoring ──────────────────────────────────────
@@ -1483,6 +1520,7 @@ watch(
1483
1520
  modeItems.value = []
1484
1521
  found.value = []
1485
1522
  failed.value = false
1523
+ incomplete.value = false
1486
1524
  if (o) {
1487
1525
  await nextTick()
1488
1526
  // Closed again meanwhile: the box belongs to a dialog on its way out.
@@ -1547,22 +1585,34 @@ const guardOutside = useOutsideGuard()
1547
1585
  <!-- Filter. The trail sits inside the box as chips, the way a scoped
1548
1586
  search reads everywhere else: what you type is filtered by what is
1549
1587
  to its left. -->
1550
- <div class="flex shrink-0 items-center gap-2.5 border-b border-line px-4">
1588
+ <div class="relative flex shrink-0 items-center gap-2.5 border-b border-line px-4">
1551
1589
  <LpIcon
1552
- v-if="!searching && !executing && !validating"
1590
+ v-if="!executing && !validating"
1553
1591
  name="lucide:search"
1554
1592
  :size="18"
1555
1593
  class="shrink-0 text-muted"
1556
1594
  />
1557
- <!-- At once, since it takes the search icon's place. It is the only
1558
- thing that tells a screen reader a search went out, so it speaks. -->
1595
+ <!-- At once, since it takes the search icon's place. -->
1559
1596
  <LpLoader
1560
1597
  v-else
1561
1598
  size="inline"
1562
1599
  :delay="0"
1563
- :labels="{ loading: executing || validating ? text.running : text.searching }"
1600
+ :labels="{ loading: text.running }"
1564
1601
  class="shrink-0 text-[18px] text-muted"
1565
1602
  />
1603
+ <!-- A search under way runs a bar along the box's lower edge, over
1604
+ the border, for as long as it lasts: results that stream in
1605
+ batches are already on screen by then, and the bar is what says
1606
+ more may come. It is also what tells a screen reader a search
1607
+ went out, so it speaks. -->
1608
+ <LpLoader
1609
+ v-if="searching"
1610
+ variant="bar"
1611
+ size="sm"
1612
+ tone="brand"
1613
+ :labels="{ loading: text.searching }"
1614
+ class="pointer-events-none absolute inset-x-0 -bottom-px"
1615
+ />
1566
1616
 
1567
1617
  <button
1568
1618
  v-for="(step, i) in trail"
@@ -1733,7 +1783,7 @@ const guardOutside = useOutsideGuard()
1733
1783
  :class="flat.length ? 'pb-1 pt-3' : 'py-10'"
1734
1784
  >
1735
1785
  <LpIcon name="lucide:circle-alert" :size="15" class="shrink-0" />
1736
- <span>{{ text.searchFailed }}</span>
1786
+ <span>{{ incomplete ? text.searchIncomplete : text.searchFailed }}</span>
1737
1787
  <button
1738
1788
  type="button"
1739
1789
  class="rounded-inner px-1.5 py-0.5 font-medium text-brand outline-none transition-colors hover:bg-brand-soft focus-visible:ring-2 focus-visible:ring-ring"
@@ -1,84 +1,41 @@
1
1
  <script setup lang="ts">
2
2
  /*
3
- * Notification bell: an icon button with an unread badge that opens a popover
4
- * feed on left-click and a quick-action menu on right-click. Each feed row has
5
- * its own right-click menu too. Presentational and data-driven — the consumer
6
- * fetches via the SDK, binds `items` / `unread-count` / `v-model:open`, and
7
- * reacts to `mark-read` / `mark-unread` / `mark-all-read` / `dismiss` /
8
- * `select`. Mirrors the kit convention (cf. LpSidebar): data in, events out,
9
- * no data fetching inside.
3
+ * Notification bell: an icon button with an unread badge that opens the
4
+ * notification feed (LpNotificationFeed) in a popover on left-click and a
5
+ * quick-action menu on right-click. Data in, events out, no fetching inside:
6
+ * the consumer binds `items` / `unread-count` / `v-model:open` and reacts to
7
+ * the feed's events, which the bell passes on.
8
+ *
9
+ * Everything the feed takes, the bell takes too: rows with their own buttons
10
+ * (`item.actions`), senders (`item.actor`), tabs (`categories`, `v-model:filter`),
11
+ * Load more, a link to the whole feed, a settings button.
10
12
  */
11
13
  import { computed, getCurrentInstance } from "vue"
12
14
  import { useLabels } from "../composables/useLpConfig"
13
- import { useNow } from "../composables/useNow"
14
15
  import LpBadge from "./LpBadge.vue"
15
16
  import LpButton from "./LpButton.vue"
16
17
  import LpContextMenu, { type ContextMenuItemDef } from "./LpContextMenu.vue"
17
- import LpEmptyState from "./LpEmptyState.vue"
18
18
  import LpIcon from "./LpIcon.vue"
19
+ import LpNotificationFeed from "./LpNotificationFeed.vue"
19
20
  import LpPopover from "./LpPopover.vue"
20
- import LpRelativeTime from "./LpRelativeTime.vue"
21
21
  import LpRollingText from "./LpRollingText.vue"
22
- import LpScrollArea from "./LpScrollArea.vue"
23
- import LpSkeleton from "./LpSkeleton.vue"
24
- import LpShift from "./LpShift.vue"
25
- import { toneLook, type ToneName } from "./tone"
26
-
27
- export interface NotificationItem {
28
- id: string
29
- title: string
30
- body?: string
31
- /** Optional icon name (e.g. "lucide:check-circle"). */
32
- icon?: string
33
- /**
34
- * Colours the row's icon by what KIND of notification it is — a billing
35
- * notice and a security one should not look alike at a glance.
36
- *
37
- * A named tone rather than a class, so the categories stay themeable:
38
- * consumers were mapping their own types onto raw palette classes, which is
39
- * the one place a theme cannot follow them.
40
- */
41
- tone?: ToneName
42
- /** Optional deep link; the row becomes clickable when set. */
43
- link?: string
44
- /** ISO-8601 creation time; rendered as a relative label. */
45
- createdAt?: string
46
- /** Whether the notification has been read. */
47
- read?: boolean
48
- }
49
-
50
- /**
51
- * Every user-facing string the bell renders on its own. Apps with i18n pass
52
- * their translations in; the English defaults keep the component usable bare.
53
- * Without this the header/row menus would be hardcoded English in every locale.
54
- */
55
- export interface NotificationLabels {
56
- title?: string
57
- markAllRead?: string
58
- clear?: string
59
- markAsRead?: string
60
- markAsUnread?: string
61
- open?: string
62
- dismiss?: string
63
- empty?: string
64
- ariaLabel?: string
65
- /** Appended to the button's name while there is unread news; `{count}` is filled in. */
66
- unread?: string
67
- /** Screen-reader marker on an unread row. */
68
- unreadItem?: string
69
- /**
70
- * No longer read: times are worded by Intl in the app's locale. Still
71
- * accepted so existing label objects keep type-checking.
72
- * @deprecated
73
- */
74
- justNow?: string
75
- /** @deprecated See `justNow`. */
76
- minutes?: string
77
- /** @deprecated See `justNow`. */
78
- hours?: string
79
- /** @deprecated See `justNow`. */
80
- days?: string
81
- }
22
+ import {
23
+ NOTIFICATION_LABELS,
24
+ type NotificationAction,
25
+ type NotificationCategory,
26
+ type NotificationItem,
27
+ type NotificationLabels,
28
+ type NotificationMenu,
29
+ } from "./notifications"
30
+
31
+ export type {
32
+ NotificationAction,
33
+ NotificationActor,
34
+ NotificationCategory,
35
+ NotificationItem,
36
+ NotificationLabels,
37
+ NotificationMenu,
38
+ } from "./notifications"
82
39
 
83
40
  const props = withDefaults(
84
41
  defineProps<{
@@ -90,166 +47,101 @@ const props = withDefaults(
90
47
  title?: string
91
48
  /** Cap the badge display (e.g. "9+"). */
92
49
  maxBadge?: number
93
- /** Right-click menu items; overrides the built-in quick actions. */
50
+ /** Right-click menu on the bell; overrides the built-in quick actions. */
94
51
  menuItems?: ContextMenuItemDef[]
95
- /** Translations for the strings the bell renders itself. */
52
+ /** Extra right-click entries for a feed row. */
53
+ itemMenu?: NotificationMenu
54
+ /** Tabs after All and Unread, matched against `item.category`. */
55
+ categories?: NotificationCategory[]
56
+ /** Head runs of rows with Today / Yesterday / Earlier. Default true. */
57
+ groupByDay?: boolean
58
+ /** More is waiting: the feed offers Load more (`@load-more`). */
59
+ hasMore?: boolean
60
+ loadingMore?: boolean
61
+ /** The feed's footer link to the whole list ("View all notifications"). */
62
+ allLink?: string
63
+ /** Translations for the strings the bell and its feed render themselves. */
96
64
  labels?: NotificationLabels
97
- /**
98
- * Formats `item.createdAt` for display. Defaults to a narrow relative time
99
- * in the app's locale ("now", "5m ago", "3h ago"); pass your own to word
100
- * it differently.
101
- */
65
+ /** Formats `item.createdAt`; defaults to a narrow relative time in the app's locale. */
102
66
  formatTime?: (iso: string) => string
103
67
  }>(),
104
- { items: () => [], maxBadge: 9 },
68
+ { items: () => [], maxBadge: 9, groupByDay: true },
105
69
  )
106
70
 
107
- // Single lookup for every label, so the template never repeats the fallbacks.
108
- // `title` stays a top-level prop for compatibility and wins over labels.title.
109
- const text = useLabels(
110
- "notifications",
111
- {
112
- title: "Notifications",
113
- markAllRead: "Mark all read",
114
- clear: "Clear",
115
- markAsRead: "Mark as read",
116
- markAsUnread: "Mark as unread",
117
- open: "Open",
118
- dismiss: "Dismiss",
119
- empty: "No notifications",
120
- ariaLabel: "",
121
- unread: "{count} unread",
122
- unreadItem: "Unread",
123
- },
124
- () => props.labels,
125
- )
126
- const l = computed(() => ({
127
- ...text.value,
128
- title: props.title ?? text.value.title,
129
- empty: props.emptyLabel ?? text.value.empty,
130
- ariaLabel: text.value.ariaLabel || text.value.title,
131
- }))
132
-
133
- // Popover open state. A defineModel (not a plain `open` prop) gives a local
134
- // fallback, so the bell still opens when the consumer only listens to
135
- // @update:open without binding :open — otherwise the popover is stuck in
136
- // controlled-but-uncontrolled limbo and never opens.
71
+ // A defineModel (not a plain prop) gives a local fallback, so the bell still
72
+ // opens when the consumer only listens to @update:open without binding :open.
137
73
  const open = defineModel<boolean>("open", { default: false })
74
+ const filter = defineModel<string>("filter", { default: "all" })
138
75
 
139
76
  const emit = defineEmits<{
140
77
  (e: "markRead", id: string): void
141
78
  (e: "markUnread", id: string): void
142
79
  (e: "markAllRead"): void
143
- /** Empty the whole feed. The header button only appears when this is bound. */
80
+ /** Empty the whole feed. Only offered when this is bound. */
144
81
  (e: "clear"): void
145
82
  (e: "dismiss", id: string): void
146
83
  (e: "select", item: NotificationItem): void
84
+ (e: "action", item: NotificationItem, action: NotificationAction): void
85
+ /** The feed's gear. Only drawn when this is bound. */
86
+ (e: "settings"): void
87
+ (e: "loadMore"): void
147
88
  }>()
148
89
 
149
- const unread = computed(() =>
150
- props.unreadCount ?? props.items.filter((n) => !n.read).length,
151
- )
152
- const badgeLabel = computed(() =>
153
- unread.value > props.maxBadge ? `${props.maxBadge}+` : String(unread.value),
154
- )
90
+ const text = useLabels("notifications", NOTIFICATION_LABELS, () => props.labels as Partial<typeof NOTIFICATION_LABELS>)
91
+ const l = computed(() => {
92
+ const title = props.title ?? text.value.title
93
+ return { ...text.value, title, ariaLabel: text.value.ariaLabel || title }
94
+ })
95
+
96
+ const unread = computed(() => props.unreadCount ?? props.items.filter((n) => !n.read).length)
155
97
  const hasUnread = computed(() => unread.value > 0)
98
+ const badgeLabel = computed(() => (unread.value > props.maxBadge ? `${props.maxBadge}+` : String(unread.value)))
156
99
 
157
100
  // The badge is drawn, not read: its count goes into the button's own name.
158
101
  const buttonLabel = computed(() =>
159
102
  hasUnread.value ? `${l.value.ariaLabel}, ${l.value.unread.replace("{count}", String(unread.value))}` : l.value.ariaLabel,
160
103
  )
161
104
 
162
-
163
105
  /*
164
- * Which optional actions the consumer actually handles. A button for an event
165
- * nobody listens to is a button that does nothing when pressed.
166
- *
167
- * NOT `useAttrs()`: a listener for a DECLARED emit is removed from `$attrs`, so
168
- * an `@clear` handler never appears there. The raw vnode props are where it
169
- * survives.
106
+ * The optional events the consumer handles. NOT `useAttrs()`: a listener for a
107
+ * DECLARED emit is removed from `$attrs`, so it is read off the raw vnode
108
+ * props. Only those are passed on, so the feed draws no button nobody answers.
170
109
  */
171
110
  const instance = getCurrentInstance()
172
111
  const handlesClear = computed(() => Boolean(instance?.vnode.props?.onClear))
112
+ const optional = computed(() => ({
113
+ ...(handlesClear.value ? { clear: () => emit("clear") } : {}),
114
+ ...(instance?.vnode.props?.onSettings
115
+ ? {
116
+ settings: () => {
117
+ open.value = false
118
+ emit("settings")
119
+ },
120
+ }
121
+ : {}),
122
+ }))
173
123
 
174
- // Right-click quick actions on the bell. "Open" is intentionally omitted — a
175
- // left-click already opens the feed, so the menu only carries actions you
176
- // can't get from the click itself. Because `open` is a defineModel we open on
177
- // left-click ourselves (the popover is anchor-only), which frees the trigger
178
- // button to also host LpContextMenu without the two fighting over the trigger.
124
+ // Right-click quick actions on the bell. "Open" is left out: a left-click
125
+ // already opens the feed. The popover's trigger and the context menu's both
126
+ // pass their handlers down to the one button (they forward attributes), so
127
+ // the button toggles nothing itself: a click of its own toggled twice.
179
128
  const contextMenu = computed<ContextMenuItemDef[]>(() => {
180
129
  if (props.menuItems) return props.menuItems
181
130
  const items: ContextMenuItemDef[] = [
182
- {
183
- label: l.value.markAllRead,
184
- icon: "lucide:check-check",
185
- disabled: !hasUnread.value,
186
- onSelect: () => emit("markAllRead"),
187
- },
131
+ { label: l.value.markAllRead, icon: "lucide:check-check", disabled: !hasUnread.value, onSelect: () => emit("markAllRead") },
188
132
  ]
189
- // Same rule as the header button: offered only when the caller can act on it.
190
133
  if (handlesClear.value) {
191
- items.push({
192
- label: l.value.clear,
193
- icon: "lucide:trash-2",
194
- disabled: !props.items.length,
195
- onSelect: () => emit("clear"),
196
- })
134
+ items.push({ label: l.value.clear, icon: "lucide:trash-2", disabled: !props.items.length, onSelect: () => emit("clear") })
197
135
  }
198
136
  return items
199
137
  })
200
138
 
201
- // Picking an item goes somewhere (a page, a dialog, a tour step), so the feed
202
- // steps out of the way: left open it would cover what the item just opened.
139
+ // Picking an item goes somewhere (a page, a dialog), so the feed steps out of
140
+ // the way: left open it would cover what the item just opened.
203
141
  function onSelect(item: NotificationItem) {
204
- if (!item.read) emit("markRead", item.id)
205
142
  open.value = false
206
143
  emit("select", item)
207
144
  }
208
-
209
- // Per-row right-click actions: toggle read state, open a deep link, dismiss.
210
- // Producers wire the matching @mark-read / @mark-unread / @dismiss handlers; an
211
- // action only appears when it makes sense for that row.
212
- function rowMenu(item: NotificationItem): ContextMenuItemDef[] {
213
- const actions: ContextMenuItemDef[] = []
214
- if (item.read) {
215
- actions.push({
216
- label: l.value.markAsUnread,
217
- icon: "lucide:dot",
218
- onSelect: () => emit("markUnread", item.id),
219
- })
220
- } else {
221
- actions.push({
222
- label: l.value.markAsRead,
223
- icon: "lucide:check",
224
- onSelect: () => emit("markRead", item.id),
225
- })
226
- }
227
- if (item.link) {
228
- actions.push({
229
- label: l.value.open,
230
- icon: "lucide:external-link",
231
- onSelect: () => onSelect(item),
232
- })
233
- }
234
- actions.push({
235
- label: l.value.dismiss,
236
- icon: "lucide:x",
237
- danger: true,
238
- separatorBefore: true,
239
- onSelect: () => emit("dismiss", item.id),
240
- })
241
- return actions
242
- }
243
-
244
- // A `formatTime` prop takes over the row's time entirely; it is re-asked twice
245
- // a minute so its "5m" keeps moving. Without one, the row renders
246
- // LpRelativeTime, which words the time through Intl in the app's locale and
247
- // wakes only when its own text would change.
248
- const customNow = useNow(() => !!props.formatTime, { nextIn: () => 30_000 })
249
- function customTime(iso: string): string {
250
- void customNow.value
251
- return props.formatTime ? props.formatTime(iso) : ""
252
- }
253
145
  </script>
254
146
 
255
147
  <template>
@@ -258,12 +150,8 @@ function customTime(iso: string): string {
258
150
  side="bottom"
259
151
  align="end"
260
152
  :side-offset="8"
261
- panel-class="w-80 p-0"
153
+ panel-class="w-[min(24rem,calc(100vw-1rem))] p-0"
262
154
  >
263
- <!-- `open` is controlled via defineModel, so the popover trigger is just an
264
- anchor: we toggle open on click ourselves. That frees the button to
265
- also be wrapped in LpContextMenu for the right-click quick-actions menu
266
- (left-click → feed, right-click → menu) without two triggers clashing. -->
267
155
  <template #trigger>
268
156
  <LpContextMenu :items="contextMenu">
269
157
  <LpButton
@@ -271,7 +159,6 @@ function customTime(iso: string): string {
271
159
  size="sm"
272
160
  class="relative"
273
161
  :aria-label="buttonLabel"
274
- @click="open = !open"
275
162
  >
276
163
  <LpIcon name="lucide:bell" :size="18" />
277
164
  <!-- The badge pops in and out, and its count rolls rather than
@@ -293,90 +180,29 @@ function customTime(iso: string): string {
293
180
  </LpContextMenu>
294
181
  </template>
295
182
 
296
- <header class="flex items-center justify-between gap-2 px-3 py-2.5">
297
- <span class="text-sm font-semibold">{{ l.title }}</span>
298
- <span class="flex items-center gap-0.5">
299
- <!-- Emptying the feed is a different act from reading it, and it is
300
- destructive, so it is muted and sits away from the primary action.
301
- Only offered when a listener is bound: a caller with no way to
302
- clear its feed must not be given a button that does nothing. -->
303
- <button
304
- v-if="items.length && handlesClear"
305
- type="button"
306
- class="rounded-inner px-1.5 py-0.5 text-xs font-medium text-muted outline-none transition-colors hover:bg-surface-soft hover:text-ink focus-visible:bg-surface-soft"
307
- @click="emit('clear')"
308
- >
309
- {{ l.clear }}
310
- </button>
311
- <button
312
- v-if="hasUnread"
313
- type="button"
314
- class="rounded-inner px-1.5 py-0.5 text-xs font-medium text-brand outline-none transition-colors hover:bg-brand-soft focus-visible:bg-brand-soft"
315
- @click="emit('markAllRead')"
316
- >
317
- {{ l.markAllRead }}
318
- </button>
319
- </span>
320
- </header>
321
-
322
- <!-- Skeleton → empty → feed are three different heights, so the panel
323
- jumped as the list loaded and again whenever it emptied. Easing the
324
- swap keeps the popover from snapping under the bell. -->
325
- <LpShift axis="height">
326
- <div v-if="loading" class="space-y-1.5 px-2 pb-2">
327
- <LpSkeleton v-for="i in 4" :key="i" class="h-12" />
328
- </div>
329
-
330
- <LpEmptyState
331
- v-else-if="!items.length"
332
- icon="lucide:bell-off"
333
- :title="l.empty"
334
- class="px-3 py-10"
335
- />
336
-
337
- <!-- The feed sits in a thin gutter; rows are self-contained tiles (rounded,
338
- hover-lit) instead of full-width strips with hard dividers — lighter and
339
- easier to scan. Each row carries its own right-click action menu. -->
340
- <LpScrollArea v-else class="max-h-96" content-class="flex flex-col gap-0.5 px-2 pb-2">
341
- <LpContextMenu v-for="item in items" :key="item.id" :items="rowMenu(item)">
342
- <component
343
- :is="item.link ? 'a' : 'button'"
344
- :href="item.link || undefined"
345
- :type="item.link ? undefined : 'button'"
346
- class="group/notif flex w-full items-start gap-2.5 rounded-control px-2 py-2 text-left outline-none transition-colors hover:bg-surface-soft focus-visible:bg-surface-soft"
347
- :class="item.read ? '' : 'bg-brand-soft/35'"
348
- @click="onSelect(item)"
349
- >
350
- <!-- A read notification is always muted: the tone says what kind of
351
- thing it is, and that only needs to compete for attention while
352
- it is still unread. -->
353
- <span
354
- class="grid size-7 shrink-0 place-items-center rounded-control"
355
- :class="item.read ? 'bg-surface-soft text-muted' : toneLook(item.tone, 'brand').chip"
356
- >
357
- <LpIcon :name="item.icon || 'lucide:bell'" :size="15" />
358
- </span>
359
- <span class="min-w-0 flex-1">
360
- <span class="flex items-center gap-1.5">
361
- <span class="truncate text-sm font-medium" :class="item.read ? 'text-muted-strong' : 'text-ink'">
362
- {{ item.title }}
363
- </span>
364
- <span v-if="!item.read" class="size-1.5 shrink-0 rounded-full bg-brand" />
365
- <span v-if="!item.read" class="sr-only">{{ l.unreadItem }}</span>
366
- </span>
367
- <span v-if="item.body" class="mt-0.5 line-clamp-2 block text-xs text-muted">
368
- {{ item.body }}
369
- </span>
370
- <span v-if="item.createdAt" class="mt-0.5 block text-[11px] text-muted/70">
371
- <template v-if="formatTime">{{ customTime(item.createdAt) }}</template>
372
- <!-- The row is itself a button or link, so the exact date goes
373
- to the native title rather than a focusable tooltip. -->
374
- <LpRelativeTime v-else :date="item.createdAt" format="narrow" :tooltip="false" />
375
- </span>
376
- </span>
377
- </component>
378
- </LpContextMenu>
379
- </LpScrollArea>
380
- </LpShift>
183
+ <LpNotificationFeed
184
+ v-model:filter="filter"
185
+ :items="items"
186
+ :unread-count="unreadCount"
187
+ :loading="loading"
188
+ :title="title"
189
+ :empty-label="emptyLabel"
190
+ :categories="categories"
191
+ :group-by-day="groupByDay"
192
+ :has-more="hasMore"
193
+ :loading-more="loadingMore"
194
+ :all-link="allLink"
195
+ :item-menu="itemMenu"
196
+ :labels="labels"
197
+ :format-time="formatTime"
198
+ v-on="optional"
199
+ @mark-read="emit('markRead', $event)"
200
+ @mark-unread="emit('markUnread', $event)"
201
+ @mark-all-read="emit('markAllRead')"
202
+ @dismiss="emit('dismiss', $event)"
203
+ @select="onSelect"
204
+ @action="(item, action) => emit('action', item, action)"
205
+ @load-more="emit('loadMore')"
206
+ />
381
207
  </LpPopover>
382
208
  </template>