@leavepulse/ui 0.78.2 → 0.79.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-DkvU6IBB.js → LpAppShell-InIK01Nn.js} +37 -20
- 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.d.ts +18 -0
- 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.js +2 -2
- 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/menuWhen.js +1 -1
- package/dist/components/notifications.d.ts +156 -0
- package/dist/components/notifications.js +59 -0
- 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/LpAppShell.vue +56 -18
- package/src/components/LpNotificationBell.vue +105 -279
- package/src/components/LpNotificationFeed.vue +404 -0
- package/src/components/notifications.ts +197 -0
- 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
|
@@ -47,8 +47,9 @@ export default { inheritAttrs: false }
|
|
|
47
47
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, useId, useSlots, watch } from "vue"
|
|
48
48
|
import { easingToken, durationToken, prefersReducedMotion } from "../composables/easing"
|
|
49
49
|
import { useLabels } from "../composables/useLpConfig"
|
|
50
|
+
import { useResizeObserver } from "../composables/useResizeObserver"
|
|
50
51
|
import { useScrollMemory } from "../composables/scrollMemory"
|
|
51
|
-
import { nodeWindow, useView, viewWindow } from "../composables/useView"
|
|
52
|
+
import { isHTMLElement, nodeWindow, useView, viewWindow } from "../composables/useView"
|
|
52
53
|
import AppShellContextNav from "./AppShellContextNav.vue"
|
|
53
54
|
import {
|
|
54
55
|
contextNavItems,
|
|
@@ -129,6 +130,15 @@ const props = withDefaults(
|
|
|
129
130
|
* `min-h-[calc(100dvh-4rem)]` or `fullBleed`. `"region"`: the shell is
|
|
130
131
|
* the viewport and the page scrolls inside main. Read once: changing it
|
|
131
132
|
* remounts the page.
|
|
133
|
+
*
|
|
134
|
+
* A page's own sticky parts keep clear of the shell's with two variables
|
|
135
|
+
* set inside it: `--lp-shell-top`, how much of the window's top the shell
|
|
136
|
+
* covers (the header, 0 while `headerCollapse` has it away, plus the
|
|
137
|
+
* sub-page row), and `--lp-shell-bottom`, how much of its foot (the bottom
|
|
138
|
+
* bar). Both change as those do, and are 0px in `"region"` and on a
|
|
139
|
+
* `fullBleed` page, where nothing of the shell's lies over the page:
|
|
140
|
+
* `position: sticky; top: var(--lp-shell-top)`. Keep the page's sticky
|
|
141
|
+
* layers under `--z-sticky`, the shell's.
|
|
132
142
|
*/
|
|
133
143
|
scroll?: "document" | "region"
|
|
134
144
|
/**
|
|
@@ -329,8 +339,8 @@ useScrollMemory(
|
|
|
329
339
|
() => props.path,
|
|
330
340
|
() => props.hash,
|
|
331
341
|
"",
|
|
332
|
-
// The sticky header
|
|
333
|
-
() => (documentScroll && !bleeding.value ? (headerEl.value?.offsetHeight ?? 0) : 0),
|
|
342
|
+
// The sticky header and sub-page row cover the top of the window; a region starts below them.
|
|
343
|
+
() => (documentScroll && !bleeding.value ? (headerEl.value?.offsetHeight ?? 0) + contextRowHeight.value : 0),
|
|
334
344
|
)
|
|
335
345
|
// The side region keeps its own place on the same address.
|
|
336
346
|
useScrollMemory(
|
|
@@ -389,17 +399,19 @@ watch(bleeding, (bleed) => {
|
|
|
389
399
|
void nextTick(() => view.scrollTo({ top, behavior: "instant" }))
|
|
390
400
|
}
|
|
391
401
|
})
|
|
392
|
-
|
|
402
|
+
/*
|
|
403
|
+
* What the shell's sticky bars cover of the window, for the page: the sub-page
|
|
404
|
+
* row's height (it sits under the header) and the bottom bar's (0 where hidden).
|
|
405
|
+
*/
|
|
393
406
|
const bottomBarEl = ref<{ $el: HTMLElement } | null>(null)
|
|
394
407
|
const bottomBarHeight = ref(0)
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
onBeforeUnmount(() => bottomBarSize?.disconnect())
|
|
408
|
+
useResizeObserver(
|
|
409
|
+
() => (bottomBarEl.value?.$el?.nodeType === 1 ? bottomBarEl.value.$el : null),
|
|
410
|
+
(_, el) => (bottomBarHeight.value = (el as HTMLElement).offsetHeight),
|
|
411
|
+
{ immediate: true },
|
|
412
|
+
)
|
|
413
|
+
const contextRowEl = ref<HTMLElement | null>(null)
|
|
414
|
+
const contextRowHeight = ref(0)
|
|
403
415
|
|
|
404
416
|
let releaseDocument: (() => void) | undefined
|
|
405
417
|
onMounted(() => {
|
|
@@ -480,7 +492,7 @@ const asideClasses = computed(() =>
|
|
|
480
492
|
const columnClass = computed(() => (hasAside.value ? "grid min-w-0 flex-1 grid-cols-[minmax(0,1fr)_auto]" : "flex min-w-0 flex-1 flex-col"))
|
|
481
493
|
const columnStyle = computed(() => ({
|
|
482
494
|
...(hasAside.value ? { gridTemplateRows: `auto ${contextVisible.value ? "auto " : ""}minmax(0,1fr) auto` } : {}),
|
|
483
|
-
...
|
|
495
|
+
...shellVars.value,
|
|
484
496
|
}))
|
|
485
497
|
const spanClass = computed(() => (hasAside.value ? "col-span-2" : undefined))
|
|
486
498
|
|
|
@@ -520,6 +532,17 @@ const contextItems = computed(() => (props.adaptiveNav?.contextNav ? contextNavI
|
|
|
520
532
|
const contextVisible = computed(
|
|
521
533
|
() => (navMode.value === "bottom" || navMode.value === "drawer") && (contextItems.value.length > 0 || (!!slots["context-nav"] && !!props.adaptiveNav?.contextNav)),
|
|
522
534
|
)
|
|
535
|
+
// The row is the app's own slot or the shell's, so it is found where it renders: right after the header.
|
|
536
|
+
watch(
|
|
537
|
+
[contextVisible, mounted],
|
|
538
|
+
() => {
|
|
539
|
+
const next = headerEl.value?.nextElementSibling
|
|
540
|
+
contextRowEl.value = contextVisible.value && isHTMLElement(next) && next !== mainEl.value ? next : null
|
|
541
|
+
if (!contextRowEl.value) contextRowHeight.value = 0
|
|
542
|
+
},
|
|
543
|
+
{ flush: "post" },
|
|
544
|
+
)
|
|
545
|
+
useResizeObserver(contextRowEl, (_, el) => (contextRowHeight.value = (el as HTMLElement).offsetHeight), { immediate: true })
|
|
523
546
|
|
|
524
547
|
function onSelect(item: SidebarItem) {
|
|
525
548
|
emit("select", item)
|
|
@@ -564,12 +587,27 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
|
|
|
564
587
|
|
|
565
588
|
/*
|
|
566
589
|
* What stays in view while the window scrolls the page. The header sits on
|
|
567
|
-
* top; the row of sub-pages
|
|
568
|
-
*
|
|
590
|
+
* top; the row of sub-pages sits under it and follows it out when it
|
|
591
|
+
* collapses (--lp-shell-header is how much of the header is showing).
|
|
592
|
+
*
|
|
593
|
+
* Two variables are the page's to use (documented on `scroll`), and set in
|
|
594
|
+
* every mode, so `var(--lp-shell-top)` never needs a fallback:
|
|
595
|
+
* --lp-shell-top is how much of the window's top the shell covers (header
|
|
596
|
+
* plus sub-page row), --lp-shell-bottom how much of its foot (the bottom
|
|
597
|
+
* bar). Both are 0px where the page scrolls in its own region or is full bleed.
|
|
569
598
|
*/
|
|
570
599
|
const STICKY = "sticky z-(--z-sticky)"
|
|
571
600
|
const railClass = computed(() => (windowScrolls.value ? "sticky top-0 flex h-dvh shrink-0 self-start" : "contents"))
|
|
572
|
-
const
|
|
601
|
+
const shellHeader = computed(() => (headerHidden.value ? "0px" : headerHeight.value))
|
|
602
|
+
const shellVars = computed(() =>
|
|
603
|
+
windowScrolls.value
|
|
604
|
+
? {
|
|
605
|
+
"--lp-shell-header": shellHeader.value,
|
|
606
|
+
"--lp-shell-top": `calc(${shellHeader.value} + ${contextRowHeight.value}px)`,
|
|
607
|
+
"--lp-shell-bottom": `${bottomBarHeight.value}px`,
|
|
608
|
+
}
|
|
609
|
+
: { "--lp-shell-header": "0px", "--lp-shell-top": "0px", "--lp-shell-bottom": "0px" },
|
|
610
|
+
)
|
|
573
611
|
const BACK_TO_TOP_ABOVE_BAR: Record<NavBreakpoint, string> = {
|
|
574
612
|
sm: "bottom-[calc(4.5rem+env(safe-area-inset-bottom))] sm:bottom-[calc(1rem+env(safe-area-inset-bottom))]",
|
|
575
613
|
md: "bottom-[calc(4.5rem+env(safe-area-inset-bottom))] md:bottom-[calc(1rem+env(safe-area-inset-bottom))]",
|
|
@@ -725,7 +763,7 @@ const backToTopClass = computed(() =>
|
|
|
725
763
|
<template v-if="contextVisible">
|
|
726
764
|
<slot name="context-nav" :items="contextItems" :mode="navMode">
|
|
727
765
|
<AppShellContextNav
|
|
728
|
-
:class="[spanClass, windowScrolls && `${STICKY} top-(--lp-shell-
|
|
766
|
+
:class="[spanClass, windowScrolls && `${STICKY} top-(--lp-shell-header) transition-[top] duration-[var(--duration-medium)] ease-[var(--ease-settle)]`]"
|
|
729
767
|
:items="contextItems"
|
|
730
768
|
:is-active="activeTest"
|
|
731
769
|
:label="text.contextNav"
|
|
@@ -769,7 +807,7 @@ const backToTopClass = computed(() =>
|
|
|
769
807
|
<slot name="aside" />
|
|
770
808
|
</LpScrollArea>
|
|
771
809
|
</aside>
|
|
772
|
-
<LpPageScrollbar v-if="windowScrolls && pageScrollbar" top="var(--lp-shell-top)"
|
|
810
|
+
<LpPageScrollbar v-if="windowScrolls && pageScrollbar" top="var(--lp-shell-top)" bottom="var(--lp-shell-bottom)" />
|
|
773
811
|
<LpBottomNav
|
|
774
812
|
v-if="bottomNav"
|
|
775
813
|
ref="bottomBarEl"
|
|
@@ -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>
|