@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.
- package/dist/assets/{LpCommandPalette-DDJKICKh.css → LpCommandPalette-C1LRFzbN.css} +19 -19
- package/dist/chunks/{ContextMenuItems-DmaKSJVv.js → ContextMenuItems-DLK4q-mm.js} +1 -1
- package/dist/chunks/{DropzoneLinkField-CyE7TJ5f.js → DropzoneLinkField-CcJu8De3.js} +1 -1
- package/dist/chunks/{FieldLeading-mr81aEAP.js → FieldLeading-CldGLINn.js} +1 -1
- package/dist/chunks/{FieldWrapper-DnuMTrqW.js → FieldWrapper-Cj8ZgoKp.js} +1 -1
- package/dist/chunks/{IconCloseButton-COZEsa9u.js → IconCloseButton-CV4iHRpy.js} +1 -1
- package/dist/chunks/{LayoutCanvas-CZKOltfA.js → LayoutCanvas-BQbq7Ekw.js} +1 -1
- package/dist/chunks/{LayoutNode-D2f7v683.js → LayoutNode-BMedRk3g.js} +1 -1
- package/dist/chunks/{LpActionBar-BGMk5KoG.js → LpActionBar-C7g52Cyo.js} +1 -1
- package/dist/chunks/{LpAddressInput-ft8fQOGx.js → LpAddressInput-1uakhQk5.js} +7 -7
- package/dist/chunks/{LpAlert-CjvTZIKF.js → LpAlert-DWFeR82L.js} +1 -1
- package/dist/chunks/{LpAppShell-DRHoQtr-.js → LpAppShell-InIK01Nn.js} +4 -4
- package/dist/chunks/{LpAutocomplete-CrZbd0uv.js → LpAutocomplete-DY3LvpB9.js} +4 -4
- package/dist/chunks/{LpAvatar-cQVsNk6F.js → LpAvatar-CEZmMcGh.js} +1 -1
- package/dist/chunks/{LpBadge-D5QQdDQ_.js → LpBadge-C_VqLHtg.js} +1 -1
- package/dist/chunks/{LpBottomNav-zxLGGbjm.js → LpBottomNav-4I89sPDZ.js} +1 -1
- package/dist/chunks/{LpBreadcrumbs-CIIBsygI.js → LpBreadcrumbs-1BBzrfxG.js} +1 -1
- package/dist/chunks/{LpCalendar-D4-LVjfF.js → LpCalendar-D2dcXjUK.js} +1 -1
- package/dist/chunks/{LpCheckbox-CCWCwD0y.js → LpCheckbox-CN-MEnbX.js} +2 -2
- package/dist/chunks/{LpCodeBlock-DBqy4RLd.js → LpCodeBlock-Cj7zk4QE.js} +2 -2
- package/dist/chunks/{LpCodeDisplay-D59uPwRb.js → LpCodeDisplay-COzosm77.js} +4 -4
- package/dist/chunks/{LpColorPicker-BONURBFN.js → LpColorPicker-BezW6DX1.js} +3 -3
- package/dist/chunks/{LpConfirmDialog-CnrmzkuN.js → LpConfirmDialog-D9GkDMYr.js} +2 -2
- package/dist/chunks/{LpConfirmHost-CwbObVGH.js → LpConfirmHost-BH7kf0dm.js} +1 -1
- package/dist/chunks/{LpContextMenu-Bx_IhQDS.js → LpContextMenu-bn5bc9t7.js} +2 -2
- package/dist/chunks/{LpContextSwitcher-1C1tExdl.js → LpContextSwitcher-CGr6ZAuq.js} +2 -2
- package/dist/chunks/{LpCopyButton-sAMvWeLP.js → LpCopyButton-BSr6YaFR.js} +1 -1
- package/dist/chunks/{LpDataList-CaVfsozH.js → LpDataList-Cv7ZUlyy.js} +2 -2
- package/dist/chunks/{LpDatePicker-C2AOtxgF.js → LpDatePicker-CR7vsb5M.js} +5 -5
- package/dist/chunks/{LpDateRangePicker-CiXdIAyh.js → LpDateRangePicker-B9fRO3G6.js} +3 -3
- package/dist/chunks/{LpDisclosure-DG7JCo-d.js → LpDisclosure-CE4mneaE.js} +1 -1
- package/dist/chunks/{LpDrawer-DXNQFpAA.js → LpDrawer-DU_5MqBz.js} +1 -1
- package/dist/chunks/{LpDropdownMenu-CLzxIOgM.js → LpDropdownMenu-_u6GPWhp.js} +2 -2
- package/dist/chunks/{LpEditable-DVT0Rh8F.js → LpEditable-DYoMayjt.js} +2 -2
- package/dist/chunks/{LpEmptyState-DurPF001.js → LpEmptyState-DpdHeSfz.js} +1 -1
- package/dist/chunks/{LpFieldNotice-DJ10cYIo.js → LpFieldNotice-DONFCWV7.js} +1 -1
- package/dist/chunks/{LpFileTree-CpNurwZp.js → LpFileTree-f5ktGqsN.js} +4 -4
- package/dist/chunks/{LpFileTreeNode-DdF8Cq-s.js → LpFileTreeNode-nbp-JwV9.js} +4 -4
- package/dist/chunks/{LpFormField-BKYn9dyP.js → LpFormField-Bz50_LZW.js} +1 -1
- package/dist/chunks/{LpHintLayer-BeKRsRon.js → LpHintLayer-B6bA5Cb_.js} +1 -1
- package/dist/chunks/{LpHints-qf3xYsza.js → LpHints-DyJ5hVcr.js} +3 -3
- package/dist/chunks/{LpIcon-C6JpQ5td.js → LpIcon-BCQLuHSP.js} +1 -1
- package/dist/chunks/{LpInfraNode-Bb5nbuFY.js → LpInfraNode-BS1RXYAs.js} +2 -2
- package/dist/chunks/{LpInput-CbI7z5fQ.js → LpInput-BjbXdjcW.js} +3 -3
- package/dist/chunks/{LpInspector-DO3fTCQ1.js → LpInspector-BIyJyM6r.js} +4 -4
- package/dist/chunks/{LpLightbox-CtDTQV01.js → LpLightbox-CSAJuUaJ.js} +2 -2
- package/dist/chunks/{LpLogInspector-CcSJSGan.js → LpLogInspector-OJEz1vod.js} +2 -2
- package/dist/chunks/{LpLogViewer-CWu49Dko.js → LpLogViewer-CRLwQpk0.js} +6 -6
- package/dist/chunks/{LpMapPicker-D5_YLLBC.js → LpMapPicker-DW2EnrCJ.js} +1 -1
- package/dist/chunks/{LpMediaGallery-BYEwySoF.js → LpMediaGallery-BNEYctDP.js} +3 -3
- package/dist/chunks/{LpModal-DDvrSAnf.js → LpModal-DdTqdf4x.js} +1 -1
- package/dist/chunks/{LpModalNav-DI3BW5ff.js → LpModalNav-BL7Dp0om.js} +3 -3
- package/dist/chunks/LpNotificationBell-d0F6MWxD.js +163 -0
- package/dist/chunks/LpNotificationFeed-BIe5lapb.js +470 -0
- package/dist/chunks/{LpNumberField-C8LfUz_R.js → LpNumberField-D5Dx-hzD.js} +3 -3
- package/dist/chunks/{LpOtpInput-B1X9l9no.js → LpOtpInput-BVl74Lfl.js} +1 -1
- package/dist/chunks/{LpPagination-snQ020xQ.js → LpPagination-D0wb-Uru.js} +1 -1
- package/dist/chunks/{LpPaletteTheme-B-xuZiKK.js → LpPaletteTheme-DdilCBXA.js} +2 -2
- package/dist/chunks/{LpPasswordInput-Duc_3wPv.js → LpPasswordInput-DUtqWMkA.js} +2 -2
- package/dist/chunks/{LpPhoneInput-CctcRK9K.js → LpPhoneInput-CalRjkqc.js} +3 -3
- package/dist/chunks/{LpPie-oVNHjsKD.js → LpPie-BVAEmVBT.js} +2 -2
- package/dist/chunks/{LpPipeline-BSXT6oUJ.js → LpPipeline-Dk7_sRP1.js} +2 -2
- package/dist/chunks/{LpQuota-CgWPgdVt.js → LpQuota-DKx0IZYA.js} +1 -1
- package/dist/chunks/{LpRadioGroup-DTM0nd9G.js → LpRadioGroup-E3SP6j9h.js} +2 -2
- package/dist/chunks/{LpRangeCalendar-BrqlGZzE.js → LpRangeCalendar-BFJFaen4.js} +1 -1
- package/dist/chunks/{LpRangeSlider-DJsEZXad.js → LpRangeSlider-b6ipywRB.js} +1 -1
- package/dist/chunks/{LpRepeater-Czib3rmv.js → LpRepeater-BLrw62Bh.js} +2 -2
- package/dist/chunks/{LpSecretField-BtlCk3IH.js → LpSecretField-BV8BNjKc.js} +5 -5
- package/dist/chunks/{LpSection-BfteipxU.js → LpSection-CghNNxIv.js} +1 -1
- package/dist/chunks/{LpSegmented-sskrJKJJ.js → LpSegmented-DjvrHL_G.js} +2 -2
- package/dist/chunks/{LpSelect-JFZehOkd.js → LpSelect-DYPimzAZ.js} +4 -4
- package/dist/chunks/{LpSidebar-CvRvVBTt.js → LpSidebar-D3S8rYFf.js} +4 -4
- package/dist/chunks/{LpSidebarNav-CE0jQS8i.js → LpSidebarNav-Dlse8vxT.js} +3 -3
- package/dist/chunks/{LpSidebarRow-DOTg1txp.js → LpSidebarRow-Cqd_6tGG.js} +3 -3
- package/dist/chunks/{LpSlider-C4ilCtZX.js → LpSlider-B9XIC75O.js} +1 -1
- package/dist/chunks/{LpStat-DPjc6_fX.js → LpStat-kyLRmX-m.js} +2 -2
- package/dist/chunks/{LpStatus-DGJFwk5F.js → LpStatus-szB2dWzM.js} +1 -1
- package/dist/chunks/{LpStepMark-Dw2MRTrt.js → LpStepMark-DP3CDSm9.js} +1 -1
- package/dist/chunks/{LpStepper-DNnh7lCu.js → LpStepper-BS3iaJIz.js} +1 -1
- package/dist/chunks/{LpSwitch-DyAYTchz.js → LpSwitch-DRFy8RNr.js} +1 -1
- package/dist/chunks/{LpTabFace-DQoV8mQG.js → LpTabFace-C9-OperL.js} +2 -2
- package/dist/chunks/{LpTableFilter-CJg5eIcV.js → LpTableFilter-CEIwLupk.js} +2 -2
- package/dist/chunks/{LpTagsInput-CkNxpNYa.js → LpTagsInput-ClDeF_aU.js} +5 -5
- package/dist/chunks/{LpTextarea-CFK_5QB8.js → LpTextarea-DEY6pUBy.js} +1 -1
- package/dist/chunks/{LpThemeStudio-R-wohbCY.js → LpThemeStudio-WY72Z1Db.js} +5 -5
- package/dist/chunks/{LpThemeSwitcher-Bzy15MCP.js → LpThemeSwitcher-9uCH9c-K.js} +3 -3
- package/dist/chunks/{LpTimeField-B5n0m8m0.js → LpTimeField-BmgxCzQO.js} +2 -2
- package/dist/chunks/{LpToaster-Ct_ZOhCh.js → LpToaster-DLKO-efn.js} +2 -2
- package/dist/chunks/{LpTopologyCanvas-BNgw9RyZ.js → LpTopologyCanvas-B-PhPa7k.js} +4 -4
- package/dist/chunks/{LpTour-ClKYhx4h.js → LpTour-ODlBcat2.js} +1 -1
- package/dist/chunks/{LpTruncate-ellq9RYM.js → LpTruncate--nhT0iUo.js} +1 -1
- package/dist/chunks/{LpWindow-B8YSRVTH.js → LpWindow-DJxE8Zl-.js} +2 -2
- package/dist/chunks/{LpWorkspaceSwitcher-ugv-fyig.js → LpWorkspaceSwitcher-Bpf41Bw5.js} +3 -3
- package/dist/chunks/{MenuTitle-Bh5dAdV1.js → MenuTitle-CMjvLsvC.js} +1 -1
- package/dist/chunks/{OptionContent-CGb8QlHA.js → OptionContent-DY8NaG8s.js} +1 -1
- package/dist/chunks/{PickerField-BM1dyafh.js → PickerField-CgfkaBgg.js} +1 -1
- package/dist/chunks/{RevealToggle-D_iyrB8d.js → RevealToggle-CPMXxaKv.js} +1 -1
- package/dist/chunks/{TourCard-DAzTYfav.js → TourCard-D4b3lMHi.js} +1 -1
- package/dist/chunks/icons-C1vppR4f.js +59 -0
- package/dist/component-names.d.ts +1 -1
- package/dist/components/ContextMenuItems.vue.js +1 -1
- package/dist/components/DropzoneLinkField.vue.js +1 -1
- package/dist/components/FieldLeading.vue.js +1 -1
- package/dist/components/FieldWrapper.vue.js +1 -1
- package/dist/components/IconCloseButton.vue.js +1 -1
- package/dist/components/LayoutCanvas.vue.js +1 -1
- package/dist/components/LayoutNode.vue.js +1 -1
- package/dist/components/LpActionBar.vue.js +1 -1
- package/dist/components/LpAddressInput.vue.js +1 -1
- package/dist/components/LpAlert.vue.js +1 -1
- package/dist/components/LpAppShell.vue.js +1 -1
- package/dist/components/LpAutocomplete.vue.js +1 -1
- package/dist/components/LpAvatar.vue.js +1 -1
- package/dist/components/LpBadge.vue.js +1 -1
- package/dist/components/LpBottomNav.vue.js +1 -1
- package/dist/components/LpBreadcrumbs.vue.js +1 -1
- package/dist/components/LpCalendar.vue.js +1 -1
- package/dist/components/LpChart.vue.js +2 -2
- package/dist/components/LpCheckbox.vue.js +1 -1
- package/dist/components/LpCodeBlock.vue.js +1 -1
- package/dist/components/LpCodeDisplay.vue.js +1 -1
- package/dist/components/LpColorPicker.vue.js +1 -1
- package/dist/components/LpCommandPalette.vue.d.ts +22 -1
- package/dist/components/LpCommandPalette.vue.js +36 -16
- package/dist/components/LpConfirmDialog.vue.js +1 -1
- package/dist/components/LpConfirmHost.vue.js +1 -1
- package/dist/components/LpContextMenu.vue.js +1 -1
- package/dist/components/LpContextSwitcher.vue.js +1 -1
- package/dist/components/LpCopyButton.vue.js +1 -1
- package/dist/components/LpDataList.vue.js +1 -1
- package/dist/components/LpDatePicker.vue.js +1 -1
- package/dist/components/LpDateRangePicker.vue.js +1 -1
- package/dist/components/LpDisclosure.vue.js +1 -1
- package/dist/components/LpDock.vue.js +1 -1
- package/dist/components/LpDrawer.vue.js +1 -1
- package/dist/components/LpDropdownMenu.vue.js +1 -1
- package/dist/components/LpDropzone.vue.js +2 -2
- package/dist/components/LpEditable.vue.js +1 -1
- package/dist/components/LpEmptyState.vue.js +1 -1
- package/dist/components/LpFieldNotice.vue.js +1 -1
- package/dist/components/LpFileTree.vue.js +1 -1
- package/dist/components/LpFileTreeNode.vue.js +1 -1
- package/dist/components/LpFormField.vue.js +1 -1
- package/dist/components/LpHintLayer.vue.js +1 -1
- package/dist/components/LpHints.vue.js +1 -1
- package/dist/components/LpIcon.vue.js +1 -1
- package/dist/components/LpImageEditor.vue.js +1 -1
- package/dist/components/LpInfraNode.vue.js +1 -1
- package/dist/components/LpInput.vue.js +1 -1
- package/dist/components/LpInspector.vue.js +1 -1
- package/dist/components/LpLightbox.vue.js +1 -1
- package/dist/components/LpLogInspector.vue.js +1 -1
- package/dist/components/LpLogViewer.vue.js +1 -1
- package/dist/components/LpMapPicker.vue.js +1 -1
- package/dist/components/LpMediaGallery.vue.js +1 -1
- package/dist/components/LpModal.vue.js +1 -1
- package/dist/components/LpModalNav.vue.js +1 -1
- package/dist/components/LpNotificationBell.vue.d.ts +28 -64
- package/dist/components/LpNotificationBell.vue.js +1 -1
- package/dist/components/LpNotificationFeed.vue.d.ts +78 -0
- package/dist/components/LpNotificationFeed.vue.js +4 -0
- package/dist/components/LpNumberField.vue.js +1 -1
- package/dist/components/LpOtpInput.vue.js +1 -1
- package/dist/components/LpPagination.vue.js +1 -1
- package/dist/components/LpPaletteFacts.vue.js +1 -1
- package/dist/components/LpPaletteHeader.vue.js +2 -2
- package/dist/components/LpPaletteTheme.vue.js +1 -1
- package/dist/components/LpPasswordInput.vue.js +1 -1
- package/dist/components/LpPhoneInput.vue.js +1 -1
- package/dist/components/LpPie.vue.js +1 -1
- package/dist/components/LpPipeline.vue.js +1 -1
- package/dist/components/LpQuota.vue.js +1 -1
- package/dist/components/LpRadioGroup.vue.js +1 -1
- package/dist/components/LpRangeCalendar.vue.js +1 -1
- package/dist/components/LpRangeSlider.vue.js +1 -1
- package/dist/components/LpRepeater.vue.js +1 -1
- package/dist/components/LpSecretField.vue.js +1 -1
- package/dist/components/LpSection.vue.js +1 -1
- package/dist/components/LpSegmented.vue.js +1 -1
- package/dist/components/LpSelect.vue.js +1 -1
- package/dist/components/LpShortcutsDialog.vue.js +1 -1
- package/dist/components/LpSidebar.vue.js +1 -1
- package/dist/components/LpSidebarNav.vue.js +1 -1
- package/dist/components/LpSidebarRow.vue.js +1 -1
- package/dist/components/LpSlider.vue.js +1 -1
- package/dist/components/LpStat.vue.js +1 -1
- package/dist/components/LpStatus.vue.js +1 -1
- package/dist/components/LpStepMark.vue.js +1 -1
- package/dist/components/LpStepper.vue.js +1 -1
- package/dist/components/LpSwitch.vue.js +1 -1
- package/dist/components/LpTabFace.vue.js +1 -1
- package/dist/components/LpTable.vue.js +9 -9
- package/dist/components/LpTableFilter.vue.js +1 -1
- package/dist/components/LpTabs.vue.js +2 -2
- package/dist/components/LpTagsInput.vue.js +1 -1
- package/dist/components/LpTextarea.vue.js +1 -1
- package/dist/components/LpThemeStudio.vue.js +1 -1
- package/dist/components/LpThemeSwitcher.vue.js +1 -1
- package/dist/components/LpTimeField.vue.js +1 -1
- package/dist/components/LpToaster.vue.js +1 -1
- package/dist/components/LpTopologyCanvas.vue.js +1 -1
- package/dist/components/LpTour.vue.js +1 -1
- package/dist/components/LpTruncate.vue.js +1 -1
- package/dist/components/LpWindow.vue.js +1 -1
- package/dist/components/LpWorkspaceSwitcher.vue.js +1 -1
- package/dist/components/MenuTitle.vue.js +1 -1
- package/dist/components/OptionContent.vue.js +1 -1
- package/dist/components/PickerField.vue.js +1 -1
- package/dist/components/RevealToggle.vue.js +1 -1
- package/dist/components/TourCard.vue.js +1 -1
- package/dist/components/commandPalette.d.ts +14 -0
- package/dist/components/commandPalette.js +42 -0
- package/dist/components/menuWhen.js +1 -1
- package/dist/components/notifications.d.ts +156 -0
- package/dist/components/notifications.js +59 -0
- package/dist/components/relativeTime.js +6 -1
- package/dist/composables/useAsyncTask.d.ts +6 -1
- package/dist/composables/useAsyncTask.js +4 -1
- package/dist/icons-all.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +208 -206
- package/package.json +1 -1
- package/src/component-names.ts +1 -0
- package/src/components/LpCommandPalette.vue +64 -14
- package/src/components/LpNotificationBell.vue +105 -279
- package/src/components/LpNotificationFeed.vue +404 -0
- package/src/components/commandPalette.ts +62 -0
- package/src/components/notifications.ts +197 -0
- package/src/components/relativeTime.ts +13 -1
- package/src/composables/useAsyncTask.ts +10 -2
- package/src/index.ts +6 -1
- package/src/kit-icons.ts +1 -1
- package/dist/chunks/LpNotificationBell-DUbVyK6R.js +0 -311
- 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
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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="!
|
|
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.
|
|
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:
|
|
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
|
|
4
|
-
* feed
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
50
|
+
/** Right-click menu on the bell; overrides the built-in quick actions. */
|
|
94
51
|
menuItems?: ContextMenuItemDef[]
|
|
95
|
-
/**
|
|
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
|
-
//
|
|
108
|
-
//
|
|
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.
|
|
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
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
*
|
|
165
|
-
*
|
|
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
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
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
|
|
202
|
-
//
|
|
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-
|
|
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
|
-
<
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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>
|