@leavepulse/ui 0.58.1 → 0.60.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/assets/{LpCommandPalette-DmQUsJqA.css → LpCommandPalette-DA5pmuPi.css} +21 -21
- package/dist/assets/{LpTable-BNzavdlv.css → LpTable-BZZf_rrt.css} +19 -19
- package/dist/chunks/{ContextMenuItems-BY5sTVQF.js → ContextMenuItems-qXReFtQW.js} +2 -2
- package/dist/chunks/{LayoutCanvas-BcGLJVBq.js → LayoutCanvas-t7LWFCEn.js} +1 -1
- package/dist/chunks/{LayoutNode-BkBn6dvM.js → LayoutNode-DHsE6GwM.js} +1 -1
- package/dist/chunks/{LpActionBar-DCN-_pZP.js → LpActionBar-BsSp66S_.js} +2 -2
- package/dist/chunks/{LpAddressInput-Dio6Sqed.js → LpAddressInput-DD4zTN4P.js} +7 -7
- package/dist/chunks/{LpAlert-CXMn1GYE.js → LpAlert--KLL4NLk.js} +1 -1
- package/dist/chunks/{LpAppShell-Be6T96td.js → LpAppShell-Cv-Y9qNA.js} +3 -3
- package/dist/chunks/{LpAutocomplete-hJ_6CHlh.js → LpAutocomplete-PnL2ntKl.js} +1 -1
- package/dist/chunks/{LpAvatar-BxNs23Xu.js → LpAvatar-CwGs1JCQ.js} +2 -2
- package/dist/chunks/{LpBreadcrumbs-BLzOEgAF.js → LpBreadcrumbs-CndzPYiG.js} +1 -1
- package/dist/chunks/{LpButton-vhlwmpBy.js → LpButton-CJSD7N0Z.js} +1 -1
- package/dist/chunks/{LpCalendar-BLoE8Qm7.js → LpCalendar-CXVDbb2M.js} +1 -1
- package/dist/chunks/{LpCard-DhR7u0d8.js → LpCard-B2D0GIUU.js} +1 -1
- package/dist/chunks/{LpCheckbox-B_C8sOoT.js → LpCheckbox-DV9W6T3H.js} +1 -1
- package/dist/chunks/{LpCodeBlock-CXsqJwDy.js → LpCodeBlock-t6MLA3fb.js} +5 -5
- package/dist/chunks/{LpColorPicker-DQ88niXq.js → LpColorPicker-CvBF8TeL.js} +6 -6
- package/dist/chunks/{LpConfirmDialog-DxlSdjmk.js → LpConfirmDialog-tkgyY3eu.js} +3 -3
- package/dist/chunks/{LpConfirmHost-BcrmTNu_.js → LpConfirmHost-CoYIcUdl.js} +1 -1
- package/dist/chunks/{LpContextMenu-B1NGfpp1.js → LpContextMenu-BnN1k7ga.js} +17 -5
- package/dist/chunks/{LpDatePicker-B-VMHMgy.js → LpDatePicker-DxdrVNCG.js} +3 -3
- package/dist/chunks/{LpDateRangePicker-B68KPKPl.js → LpDateRangePicker-DjeaqikC.js} +2 -2
- package/dist/chunks/{LpDisclosure-Ct-VBHxt.js → LpDisclosure-s8cQ7KQq.js} +1 -1
- package/dist/chunks/{LpDrawer-DSknigxH.js → LpDrawer-C97yoWmh.js} +2 -2
- package/dist/chunks/{LpDropdownMenu-C7oHVlKY.js → LpDropdownMenu-CbyYu4Rw.js} +12 -5
- package/dist/chunks/{LpEmptyState-CBwwkkdA.js → LpEmptyState-CX8fdGSU.js} +1 -1
- package/dist/chunks/{LpFieldNotice-9hH4ITZa.js → LpFieldNotice-B--RzKfj.js} +1 -1
- package/dist/chunks/{LpFileTree-CMA0_rsB.js → LpFileTree-DEqh2ySW.js} +2 -2
- package/dist/chunks/{LpFileTreeNode-B4T258hF.js → LpFileTreeNode-DCZA7sQ-.js} +3 -3
- package/dist/chunks/{LpFormField-BpQ6WJPe.js → LpFormField-DMPPL1jF.js} +1 -1
- package/dist/chunks/{LpHintLayer-B5-6FyEf.js → LpHintLayer-DsclGYcD.js} +1 -1
- package/dist/chunks/{LpHints-BqoZXHZk.js → LpHints-BTpvisgP.js} +10 -4
- package/dist/chunks/{LpIcon-B5guGKrW.js → LpIcon-CTuDVTYv.js} +1 -1
- package/dist/chunks/{LpInput-WvyP02ID.js → LpInput-C7FHxzj2.js} +2 -2
- package/dist/chunks/{LpLightbox-Dq-e36uT.js → LpLightbox-DiksFa4L.js} +1 -1
- package/dist/chunks/{LpLogViewer-DwiYE32z.js → LpLogViewer-BuPe23sZ.js} +2 -2
- package/dist/chunks/{LpMapPicker-BH5csaBt.js → LpMapPicker-DybIlWnF.js} +1 -1
- package/dist/chunks/{LpMediaGallery-B1kDypJ-.js → LpMediaGallery-Cic9UHsa.js} +2 -2
- package/dist/chunks/{LpModal-BxYssnXg.js → LpModal-QFfctZsj.js} +2 -2
- package/dist/chunks/{LpModalNav-Bn2ws64b.js → LpModalNav-Cw7m-eks.js} +4 -4
- package/dist/chunks/{LpNotificationBell-DJfs4JjQ.js → LpNotificationBell-xH_JphpS.js} +6 -6
- package/dist/chunks/{LpNumberField-CJPjmoDz.js → LpNumberField-BFSRVGkJ.js} +1 -1
- package/dist/chunks/{LpPagination-DehAaJLY.js → LpPagination-BFl0I7tL.js} +1 -1
- package/dist/chunks/{LpPasswordInput-Bg5uTZbx.js → LpPasswordInput-C1G4k_r0.js} +3 -3
- package/dist/chunks/{LpPhoneInput-DStwcbnQ.js → LpPhoneInput-CZrpVxla.js} +3 -3
- package/dist/chunks/{LpPie-DeTkbbOK.js → LpPie-BD5Lsh-2.js} +3 -3
- package/dist/chunks/{LpRangeCalendar-D5sDID9W.js → LpRangeCalendar-COeOYF52.js} +1 -1
- package/dist/chunks/{LpSection-B19VPYmr.js → LpSection-BAkbf0OK.js} +1 -1
- package/dist/chunks/{LpSegmented-D3Eam8sa.js → LpSegmented-DslFku0N.js} +3 -3
- package/dist/chunks/{LpSelect-DynjWK1A.js → LpSelect-C4OXN_q3.js} +1 -1
- package/dist/chunks/{LpSidebar-CshW0fqC.js → LpSidebar-B3E_iViL.js} +3 -3
- package/dist/chunks/{LpSidebarNav-C9b3i9G2.js → LpSidebarNav-32sAXNM-.js} +4 -4
- package/dist/chunks/{LpStat-BS2TEXYK.js → LpStat-oYk6V1LZ.js} +1 -1
- package/dist/chunks/{LpStepper-BSglWF45.js → LpStepper-YgHGzgfD.js} +1 -1
- package/dist/chunks/{LpTableFilter-CwHXVxJt.js → LpTableFilter-Ba-rS4H1.js} +2 -2
- package/dist/chunks/{LpTabs-BBGRHY38.js → LpTabs-00v3VQx4.js} +7 -6
- package/dist/chunks/{LpTagsInput-CHbQoAAA.js → LpTagsInput-Sc56iy_j.js} +3 -3
- package/dist/chunks/{LpTextarea-Bz2IWhTH.js → LpTextarea-CQZccQI3.js} +1 -1
- package/dist/chunks/{LpThemeSwitcher-B_qhSl7q.js → LpThemeSwitcher-CeBG0e_D.js} +2 -2
- package/dist/chunks/{LpTimeField-DvGXgdES.js → LpTimeField-BTEt0kvL.js} +1 -1
- package/dist/chunks/{LpToaster-Bo9Q4QBP.js → LpToaster-BcKs7vX7.js} +3 -3
- package/dist/chunks/{LpTour-D7m9ZxNk.js → LpTour-C29-1wha.js} +4 -1
- package/dist/chunks/{MenuTitle-DQP6hc2N.js → MenuTitle-FeC6I24N.js} +3 -3
- package/dist/chunks/{TourCard-DCHOjr6e.js → TourCard-AjN0uhs1.js} +2 -2
- package/dist/chunks/{icons-QqBscG5I.js → icons-CuO3W46H.js} +1 -1
- package/dist/components/ContextMenuItems.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/LpBreadcrumbs.vue.js +1 -1
- package/dist/components/LpButton.vue.js +1 -1
- package/dist/components/LpCalendar.vue.js +1 -1
- package/dist/components/LpCard.vue.js +1 -1
- package/dist/components/LpChart.vue.js +4 -4
- package/dist/components/LpCheckbox.vue.js +1 -1
- package/dist/components/LpCodeBlock.vue.js +1 -1
- package/dist/components/LpColorPicker.vue.js +1 -1
- package/dist/components/LpCommandPalette.vue.js +38 -27
- package/dist/components/LpConfirmDialog.vue.js +1 -1
- package/dist/components/LpConfirmHost.vue.js +1 -1
- package/dist/components/LpContextMenu.vue.d.ts +7 -2
- package/dist/components/LpContextMenu.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/LpDrawer.vue.js +1 -1
- package/dist/components/LpDropdownMenu.vue.d.ts +7 -2
- package/dist/components/LpDropdownMenu.vue.js +1 -1
- package/dist/components/LpDropzone.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 +4 -2
- package/dist/components/LpInput.vue.js +1 -1
- package/dist/components/LpLightbox.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.js +1 -1
- package/dist/components/LpNumberField.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/LpPasswordInput.vue.js +1 -1
- package/dist/components/LpPhoneInput.vue.js +1 -1
- package/dist/components/LpPie.vue.js +1 -1
- package/dist/components/LpRangeCalendar.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/LpStat.vue.js +1 -1
- package/dist/components/LpStepper.vue.js +1 -1
- package/dist/components/LpTable.vue.js +13 -12
- package/dist/components/LpTableFilter.vue.js +1 -1
- package/dist/components/LpTabs.vue.d.ts +7 -0
- package/dist/components/LpTabs.vue.js +1 -1
- package/dist/components/LpTagsInput.vue.js +1 -1
- package/dist/components/LpTextarea.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/LpTour.vue.js +1 -1
- package/dist/components/MenuTitle.vue.js +1 -1
- package/dist/components/TourCard.vue.js +1 -1
- package/dist/components/dropdown.js +1 -1
- package/dist/composables/useHintSystem.d.ts +65 -0
- package/dist/composables/useHintSystem.js +98 -12
- package/dist/icons-all.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +62 -58
- package/package.json +1 -1
- package/src/components/ContextMenuItems.vue +1 -1
- package/src/components/LpChart.vue +1 -1
- package/src/components/LpCodeBlock.vue +4 -4
- package/src/components/LpColorPicker.vue +4 -4
- package/src/components/LpCommandPalette.vue +36 -14
- package/src/components/LpContextMenu.vue +19 -1
- package/src/components/LpDrawer.vue +1 -1
- package/src/components/LpDropdownMenu.vue +14 -3
- package/src/components/LpHints.vue +7 -1
- package/src/components/LpModal.vue +1 -1
- package/src/components/LpNotificationBell.vue +2 -2
- package/src/components/LpPasswordInput.vue +1 -1
- package/src/components/LpPhoneInput.vue +1 -1
- package/src/components/LpPie.vue +1 -1
- package/src/components/LpSegmented.vue +2 -2
- package/src/components/LpSidebarNav.vue +2 -2
- package/src/components/LpTable.vue +3 -3
- package/src/components/LpTabs.vue +11 -5
- package/src/components/LpToaster.vue +2 -2
- package/src/components/LpTour.vue +3 -0
- package/src/components/MenuTitle.vue +2 -2
- package/src/components/dropdown.ts +1 -1
- package/src/composables/useHintSystem.ts +130 -12
- package/src/index.ts +2 -1
- package/src/kit-icons.ts +1 -1
- package/src/tokens/tokens.css +6 -0
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { ContextMenuContent, ContextMenuPortal, ContextMenuRoot, ContextMenuTrigger } from "reka-ui"
|
|
10
10
|
import { computed } from "vue"
|
|
11
|
+
import { hints } from "../composables/useHints"
|
|
12
|
+
import { HINT_SIGNALS } from "../composables/useHintSystem"
|
|
11
13
|
import ContextMenuItems from "./ContextMenuItems.vue"
|
|
12
14
|
import MenuTitle from "./MenuTitle.vue"
|
|
13
15
|
import { POPOVER_PANEL } from "./dropdown"
|
|
@@ -52,6 +54,11 @@ const props = defineProps<{
|
|
|
52
54
|
/** An icon beside the title, or `titleImage` for a picture (an avatar, a logo). */
|
|
53
55
|
titleIcon?: string
|
|
54
56
|
titleImage?: string
|
|
57
|
+
/**
|
|
58
|
+
* Do not count this element as "has a right-click menu" for hint tips: for
|
|
59
|
+
* menus that only hold generic items (a whole table without row actions).
|
|
60
|
+
*/
|
|
61
|
+
noHint?: boolean
|
|
55
62
|
}>()
|
|
56
63
|
|
|
57
64
|
// With no items the trigger is disabled, so the element keeps the browser's
|
|
@@ -59,6 +66,7 @@ const props = defineProps<{
|
|
|
59
66
|
// per element. `always` overrides this so a late-populated menu is ready on the
|
|
60
67
|
// very first click.
|
|
61
68
|
const enabled = computed(() => props.always || props.items.length > 0)
|
|
69
|
+
const hinted = computed(() => enabled.value && !props.noHint)
|
|
62
70
|
|
|
63
71
|
const PANEL = `${POPOVER_PANEL} z-(--z-popover) min-w-48 p-1 outline-none`
|
|
64
72
|
|
|
@@ -91,6 +99,14 @@ function keepContextMenu(event: Event) {
|
|
|
91
99
|
if (enabled.value) event.stopPropagation()
|
|
92
100
|
}
|
|
93
101
|
|
|
102
|
+
function opened(open: boolean) {
|
|
103
|
+
if (open) hints.use(HINT_SIGNALS.contextMenu)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function leftClick() {
|
|
107
|
+
if (hinted.value) hints.use(HINT_SIGNALS.contextTarget)
|
|
108
|
+
}
|
|
109
|
+
|
|
94
110
|
function returnFocus(event: Event) {
|
|
95
111
|
const saved = field
|
|
96
112
|
field = null
|
|
@@ -106,10 +122,12 @@ function returnFocus(event: Event) {
|
|
|
106
122
|
the reka trigger remounted the wrapped content whenever items arrived
|
|
107
123
|
late, losing its state and focus. A disabled trigger leaves the browser's
|
|
108
124
|
own menu in place. -->
|
|
109
|
-
<ContextMenuRoot>
|
|
125
|
+
<ContextMenuRoot @update:open="opened">
|
|
110
126
|
<ContextMenuTrigger
|
|
111
127
|
as-child
|
|
112
128
|
:disabled="!enabled"
|
|
129
|
+
:data-lp-context-menu="hinted ? '' : undefined"
|
|
130
|
+
@click.capture="leftClick"
|
|
113
131
|
@contextmenu.capture="rememberField"
|
|
114
132
|
@contextmenu="keepContextMenu"
|
|
115
133
|
>
|
|
@@ -552,7 +552,7 @@ const previewStyle = computed(() => {
|
|
|
552
552
|
</DrawerDescription>
|
|
553
553
|
</div>
|
|
554
554
|
<DrawerClose
|
|
555
|
-
class="group flex shrink-0 items-center rounded-
|
|
555
|
+
class="group flex shrink-0 items-center rounded-inner p-1 text-muted outline-none transition-colors duration-[var(--duration-fast)] hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
|
|
556
556
|
:aria-label="text.close"
|
|
557
557
|
>
|
|
558
558
|
<LpIcon
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { hints } from "../composables/useHints"
|
|
3
|
+
import { HINT_SIGNALS } from "../composables/useHintSystem"
|
|
2
4
|
import {
|
|
3
5
|
DropdownMenuContent,
|
|
4
6
|
DropdownMenuItem,
|
|
@@ -20,18 +22,27 @@ export interface MenuItem {
|
|
|
20
22
|
onSelect?: () => void
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
defineProps<{
|
|
25
|
+
const props = defineProps<{
|
|
24
26
|
items: MenuItem[]
|
|
25
27
|
/** A heading naming what the menu acts on; see LpContextMenu's `title`. */
|
|
26
28
|
title?: string
|
|
27
29
|
subtitle?: string
|
|
28
30
|
titleIcon?: string
|
|
29
31
|
titleImage?: string
|
|
32
|
+
/**
|
|
33
|
+
* The same items are also on a right click (a row's "…" next to its context
|
|
34
|
+
* menu). Opening it then counts toward the hint system's right-click tip.
|
|
35
|
+
*/
|
|
36
|
+
rightClickToo?: boolean
|
|
30
37
|
}>()
|
|
38
|
+
|
|
39
|
+
function opened(open: boolean) {
|
|
40
|
+
if (open && props.rightClickToo) hints.use(HINT_SIGNALS.menuButton)
|
|
41
|
+
}
|
|
31
42
|
</script>
|
|
32
43
|
|
|
33
44
|
<template>
|
|
34
|
-
<DropdownMenuRoot>
|
|
45
|
+
<DropdownMenuRoot @update:open="opened">
|
|
35
46
|
<DropdownMenuTrigger as-child>
|
|
36
47
|
<slot name="trigger" />
|
|
37
48
|
</DropdownMenuTrigger>
|
|
@@ -47,7 +58,7 @@ defineProps<{
|
|
|
47
58
|
<template v-for="(item, i) in items" :key="i">
|
|
48
59
|
<DropdownMenuSeparator v-if="item.separatorBefore" class="my-1 h-px bg-line" />
|
|
49
60
|
<DropdownMenuItem
|
|
50
|
-
class="group/item flex cursor-pointer items-center gap-2 rounded-
|
|
61
|
+
class="group/item flex cursor-pointer items-center gap-2 rounded-inner px-2.5 py-1.5 text-sm outline-none transition-[background-color,color] duration-[var(--duration-fast)] data-[disabled]:pointer-events-none data-[disabled]:opacity-45 data-[highlighted]:bg-brand-soft"
|
|
51
62
|
:class="item.danger ? 'text-danger data-[highlighted]:bg-danger-soft data-[highlighted]:text-danger' : 'text-ink data-[highlighted]:text-brand'"
|
|
52
63
|
:disabled="item.disabled"
|
|
53
64
|
@select="item.onSelect?.()"
|
|
@@ -68,6 +68,7 @@ const title = computed(() => {
|
|
|
68
68
|
const o = offer.value
|
|
69
69
|
if (!o) return ""
|
|
70
70
|
if (o.kind === "nudge") return notes.value[o.nudge.feature]?.title ?? ""
|
|
71
|
+
if (o.kind === "tip") return o.tip.title
|
|
71
72
|
if (o.kind === "news") return fill(labels.value.newsTitle)
|
|
72
73
|
return fill(o.kind === "assigned" ? labels.value.assignedTitle : labels.value.tourTitle)
|
|
73
74
|
})
|
|
@@ -76,6 +77,7 @@ const description = computed(() => {
|
|
|
76
77
|
const o = offer.value
|
|
77
78
|
if (!o) return ""
|
|
78
79
|
if (o.kind === "nudge") return o.nudge.text
|
|
80
|
+
if (o.kind === "tip") return o.tip.keys ? `${o.tip.text} ${o.tip.keys}` : o.tip.text
|
|
79
81
|
if (o.kind === "news") return o.items.map((n) => n.text).join(" ")
|
|
80
82
|
return o.kind === "assigned" ? labels.value.assignedText : labels.value.tourText
|
|
81
83
|
})
|
|
@@ -86,6 +88,10 @@ const actions = computed<ActionBarAction[]>(() => {
|
|
|
86
88
|
const answer = (choice: "show" | "later" | "never") => () => props.system.answer(choice)
|
|
87
89
|
const show: ActionBarAction = { label: labels.value.show, icon: "lucide:sparkles", onClick: answer("show") }
|
|
88
90
|
if (o.kind === "nudge") return [{ label: labels.value.noThanks, variant: "ghost", onClick: answer("never") }, show]
|
|
91
|
+
if (o.kind === "tip") {
|
|
92
|
+
const later: ActionBarAction = { label: labels.value.gotIt, variant: o.tip.step ? "ghost" : undefined, onClick: answer("later") }
|
|
93
|
+
return [{ label: labels.value.never, variant: "ghost", onClick: answer("never") }, later, ...(o.tip.step ? [show] : [])]
|
|
94
|
+
}
|
|
89
95
|
if (o.kind === "news") return [{ label: labels.value.gotIt, variant: "ghost", onClick: answer("later") }, show]
|
|
90
96
|
return [
|
|
91
97
|
...(o.kind === "tour" ? [{ label: labels.value.never, variant: "ghost" as const, onClick: answer("never") }] : []),
|
|
@@ -109,7 +115,7 @@ const actions = computed<ActionBarAction[]>(() => {
|
|
|
109
115
|
:open="!!offer"
|
|
110
116
|
:title="title"
|
|
111
117
|
:description="description"
|
|
112
|
-
:icon="offer?.kind === 'news' ? 'lucide:gift' : 'lucide:compass'"
|
|
118
|
+
:icon="offer?.kind === 'news' ? 'lucide:gift' : offer?.kind === 'tip' ? 'lucide:lightbulb' : 'lucide:compass'"
|
|
113
119
|
:actions="actions"
|
|
114
120
|
/>
|
|
115
121
|
</template>
|
|
@@ -350,7 +350,7 @@ watch(
|
|
|
350
350
|
and last in usefulness. Still reachable by Tab and Escape. -->
|
|
351
351
|
<DialogClose
|
|
352
352
|
data-lp-dialog-close
|
|
353
|
-
class="group flex shrink-0 items-center rounded-
|
|
353
|
+
class="group flex shrink-0 items-center rounded-inner p-1 text-muted outline-none transition-colors duration-[var(--duration-fast)] hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
|
|
354
354
|
:aria-label="text.close"
|
|
355
355
|
>
|
|
356
356
|
<LpIcon
|
|
@@ -321,7 +321,7 @@ function timeAgo(iso?: string): string {
|
|
|
321
321
|
<button
|
|
322
322
|
v-if="items.length && handlesClear"
|
|
323
323
|
type="button"
|
|
324
|
-
class="rounded-
|
|
324
|
+
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"
|
|
325
325
|
@click="emit('clear')"
|
|
326
326
|
>
|
|
327
327
|
{{ l.clear }}
|
|
@@ -329,7 +329,7 @@ function timeAgo(iso?: string): string {
|
|
|
329
329
|
<button
|
|
330
330
|
v-if="hasUnread"
|
|
331
331
|
type="button"
|
|
332
|
-
class="rounded-
|
|
332
|
+
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"
|
|
333
333
|
@click="emit('markAllRead')"
|
|
334
334
|
>
|
|
335
335
|
{{ l.markAllRead }}
|
|
@@ -53,7 +53,7 @@ defineExpose({
|
|
|
53
53
|
<template #trailing>
|
|
54
54
|
<button
|
|
55
55
|
type="button"
|
|
56
|
-
class="group rounded-
|
|
56
|
+
class="group rounded-inner p-1 text-muted outline-none transition-colors duration-[var(--duration-fast)] hover:text-ink focus-visible:text-ink"
|
|
57
57
|
:aria-label="visible ? text.hide : text.show"
|
|
58
58
|
@click="visible = !visible"
|
|
59
59
|
>
|
|
@@ -398,7 +398,7 @@ const inputAttrs = computed(() => {
|
|
|
398
398
|
:model-value="query"
|
|
399
399
|
:placeholder="searchPlaceholder"
|
|
400
400
|
auto-focus
|
|
401
|
-
class="h-(--size-control-sm) w-full rounded-
|
|
401
|
+
class="h-(--size-control-sm) w-full rounded-inner border border-line bg-surface-soft px-2.5 text-sm text-ink outline-none placeholder:text-muted focus:border-brand"
|
|
402
402
|
@input="query = ($event.target as HTMLInputElement).value"
|
|
403
403
|
/>
|
|
404
404
|
</div>
|
package/src/components/LpPie.vue
CHANGED
|
@@ -835,7 +835,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
|
835
835
|
v-for="(a, i) in arcs"
|
|
836
836
|
:key="`${i}-${a.label}`"
|
|
837
837
|
type="button"
|
|
838
|
-
class="group flex cursor-pointer items-center gap-2 rounded-
|
|
838
|
+
class="group flex cursor-pointer items-center gap-2 rounded-inner px-1 py-0.5 text-xs outline-none transition-[opacity,background-color] duration-fast focus-visible:ring-2 focus-visible:ring-ring"
|
|
839
839
|
:class="[dimmed(i) ? 'opacity-45' : '', isSelected(i) ? 'bg-surface-soft' : '']"
|
|
840
840
|
:aria-pressed="isSelected(i)"
|
|
841
841
|
@pointerenter="onSlicePointerEnter(i)"
|
|
@@ -95,7 +95,7 @@ const pillTransition = usePillTransition()
|
|
|
95
95
|
:key="opt.value"
|
|
96
96
|
:value="opt.value"
|
|
97
97
|
:aria-label="opt.ariaLabel ?? (opt.label ? undefined : opt.value)"
|
|
98
|
-
class="relative inline-flex h-full items-center justify-center gap-1.5 rounded-
|
|
98
|
+
class="relative inline-flex h-full items-center justify-center gap-1.5 rounded-inner px-3 font-medium outline-none transition-colors duration-[var(--duration-fast)] ease-[var(--ease-emphasized)] focus-visible:ring-2 focus-visible:ring-ring data-[state=on]:text-ink data-[state=off]:text-muted data-[state=off]:hover:text-ink disabled:cursor-not-allowed disabled:opacity-55"
|
|
99
99
|
:class="block ? 'flex-1' : ''"
|
|
100
100
|
>
|
|
101
101
|
<!-- Sliding pill behind the active segment. -->
|
|
@@ -103,7 +103,7 @@ const pillTransition = usePillTransition()
|
|
|
103
103
|
v-if="opt.value === modelValue"
|
|
104
104
|
:layout-id="pillId"
|
|
105
105
|
:transition="pillTransition"
|
|
106
|
-
class="absolute inset-0 z-0 rounded-
|
|
106
|
+
class="absolute inset-0 z-0 rounded-inner border border-line bg-surface-raised shadow-sm"
|
|
107
107
|
/>
|
|
108
108
|
<LpIcon v-if="opt.icon" :name="opt.icon" :size="14" class="relative z-10 shrink-0" />
|
|
109
109
|
<span v-if="opt.label" class="relative z-10">{{ opt.label }}</span>
|
|
@@ -103,8 +103,8 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
|
103
103
|
<div v-if="skeletonHeader" class="flex items-center gap-3 px-1">
|
|
104
104
|
<div class="size-10 shrink-0 animate-pulse rounded-pill bg-surface-soft" />
|
|
105
105
|
<div class="flex-1 space-y-2">
|
|
106
|
-
<div class="h-3.5 w-24 animate-pulse rounded bg-surface-soft" />
|
|
107
|
-
<div class="h-3 w-16 animate-pulse rounded bg-surface-soft/60" />
|
|
106
|
+
<div class="h-3.5 w-24 animate-pulse rounded-small bg-surface-soft" />
|
|
107
|
+
<div class="h-3 w-16 animate-pulse rounded-small bg-surface-soft/60" />
|
|
108
108
|
</div>
|
|
109
109
|
</div>
|
|
110
110
|
<div class="space-y-1">
|
|
@@ -1157,7 +1157,7 @@ const barInsetTop = computed(() =>
|
|
|
1157
1157
|
<!-- The table-wide menu, which is what a right-click on empty header space
|
|
1158
1158
|
reaches: rows and headers carry their own and take the event first, so
|
|
1159
1159
|
this is the fallback rather than a competitor. -->
|
|
1160
|
-
<LpContextMenu :items="contextMenu" always>
|
|
1160
|
+
<LpContextMenu :items="contextMenu" always :no-hint="!rowMenu">
|
|
1161
1161
|
<table ref="tableEl" class="w-full border-collapse text-sm">
|
|
1162
1162
|
<!-- The background belongs on `thead` itself, not only on its row:
|
|
1163
1163
|
`border-collapse` collapses the borders, and with a sticky header the
|
|
@@ -1201,7 +1201,7 @@ const barInsetTop = computed(() =>
|
|
|
1201
1201
|
<LpContextMenu v-if="col.sortable || col.filterable" :items="headerMenu(col)">
|
|
1202
1202
|
<button
|
|
1203
1203
|
type="button"
|
|
1204
|
-
class="group/sort -mx-1 inline-flex items-center gap-1 rounded px-1 outline-none transition-colors duration-[var(--duration-fast)] hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
|
|
1204
|
+
class="group/sort -mx-1 inline-flex items-center gap-1 rounded-small px-1 outline-none transition-colors duration-[var(--duration-fast)] hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
|
|
1205
1205
|
:class="[
|
|
1206
1206
|
col.align === 'right' ? 'flex-row-reverse' : '',
|
|
1207
1207
|
activeSort?.key === col.key ? 'text-ink' : '',
|
|
@@ -1232,7 +1232,7 @@ const barInsetTop = computed(() =>
|
|
|
1232
1232
|
<template #trigger>
|
|
1233
1233
|
<button
|
|
1234
1234
|
type="button"
|
|
1235
|
-
class="ml-0.5 inline-grid size-5 place-items-center rounded align-middle outline-none transition-colors duration-fast hover:bg-surface-raised hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
|
|
1235
|
+
class="ml-0.5 inline-grid size-5 place-items-center rounded-small align-middle outline-none transition-colors duration-fast hover:bg-surface-raised hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
|
|
1236
1236
|
:class="hiddenValues[col.key]?.size ? 'text-brand' : 'opacity-60'"
|
|
1237
1237
|
:aria-label="text.filterColumn.replace('{column}', col.label)"
|
|
1238
1238
|
:data-lp-filter="col.key"
|
|
@@ -38,8 +38,14 @@ const props = withDefaults(
|
|
|
38
38
|
block?: boolean
|
|
39
39
|
/** Accessible name of the tab list, e.g. "Main" for an app's navigation. */
|
|
40
40
|
label?: string
|
|
41
|
+
/**
|
|
42
|
+
* The marker glides under the hovered tab and springs back on leave (the
|
|
43
|
+
* default). False pins it to the selected tab: calmer for a dense header
|
|
44
|
+
* where the pointer crosses the bar on its way elsewhere.
|
|
45
|
+
*/
|
|
46
|
+
hoverFollow?: boolean
|
|
41
47
|
}>(),
|
|
42
|
-
{ variant: "contained", accent: false, block: false },
|
|
48
|
+
{ variant: "contained", accent: false, block: false, hoverFollow: true },
|
|
43
49
|
)
|
|
44
50
|
|
|
45
51
|
const bottom = computed(() => props.variant === "bottom")
|
|
@@ -154,12 +160,12 @@ function releaseFocusAfterPointer(event: PointerEvent) {
|
|
|
154
160
|
? 'min-h-14 min-w-0 flex-col gap-0.5 px-1 py-1.5 text-[11px] font-medium focus-visible:ring-inset'
|
|
155
161
|
: 'shrink-0 gap-1.5 whitespace-nowrap text-sm',
|
|
156
162
|
block && !bottom ? 'flex-1' : '',
|
|
157
|
-
variant === 'underline' ? 'rounded-t-
|
|
158
|
-
variant === 'contained' || variant === 'plain' ? 'rounded-
|
|
163
|
+
variant === 'underline' ? 'rounded-t-inner px-3 pt-1.5 pb-2' : '',
|
|
164
|
+
variant === 'contained' || variant === 'plain' ? 'rounded-inner px-3 py-1.5' : '',
|
|
159
165
|
accent ? 'data-[state=active]:text-brand' : 'data-[state=active]:text-ink',
|
|
160
166
|
hovered === item.value && !bottom ? 'text-ink' : 'text-muted',
|
|
161
167
|
]"
|
|
162
|
-
@pointerenter="hovered = item.value"
|
|
168
|
+
@pointerenter="hoverFollow && (hovered = item.value)"
|
|
163
169
|
@pointerup="releaseFocusAfterPointer"
|
|
164
170
|
>
|
|
165
171
|
<!-- Active marker: a bar under the tab in "underline", over it in
|
|
@@ -180,7 +186,7 @@ function releaseFocusAfterPointer(event: PointerEvent) {
|
|
|
180
186
|
accent ? 'bg-brand' : 'bg-ink',
|
|
181
187
|
]
|
|
182
188
|
: [
|
|
183
|
-
'absolute inset-0 z-0 rounded-
|
|
189
|
+
'absolute inset-0 z-0 rounded-inner shadow-sm',
|
|
184
190
|
accent
|
|
185
191
|
? 'border border-brand/35 bg-brand/12'
|
|
186
192
|
: 'border border-line bg-surface-raised',
|
|
@@ -305,7 +305,7 @@ function runAction(t: ToastItem, action: NonNullable<ToastItem["actions"]>[numbe
|
|
|
305
305
|
<button
|
|
306
306
|
v-if="copyable(t)"
|
|
307
307
|
type="button"
|
|
308
|
-
class="-mt-1 rounded-
|
|
308
|
+
class="-mt-1 rounded-inner p-1 text-muted outline-none transition-colors duration-[var(--duration-fast)] hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
|
|
309
309
|
:aria-label="copiedId === tag(t) ? text.copied : text.copy"
|
|
310
310
|
:title="copiedId === tag(t) ? text.copied : text.copy"
|
|
311
311
|
@click.stop="copyToast(t)"
|
|
@@ -314,7 +314,7 @@ function runAction(t: ToastItem, action: NonNullable<ToastItem["actions"]>[numbe
|
|
|
314
314
|
</button>
|
|
315
315
|
<button
|
|
316
316
|
type="button"
|
|
317
|
-
class="group -mr-1 -mt-1 rounded-
|
|
317
|
+
class="group -mr-1 -mt-1 rounded-inner p-1 text-muted outline-none transition-colors duration-[var(--duration-fast)] hover:text-ink"
|
|
318
318
|
:aria-label="text.dismiss"
|
|
319
319
|
@click.stop="beginClose(t)"
|
|
320
320
|
>
|
|
@@ -270,10 +270,12 @@ watch(
|
|
|
270
270
|
if (value) {
|
|
271
271
|
direction = 1
|
|
272
272
|
document.addEventListener("click", onTargetClick, true)
|
|
273
|
+
document.addEventListener("contextmenu", onTargetClick, true)
|
|
273
274
|
frame ||= requestAnimationFrame(track)
|
|
274
275
|
void show()
|
|
275
276
|
} else {
|
|
276
277
|
document.removeEventListener("click", onTargetClick, true)
|
|
278
|
+
document.removeEventListener("contextmenu", onTargetClick, true)
|
|
277
279
|
end()
|
|
278
280
|
}
|
|
279
281
|
},
|
|
@@ -286,6 +288,7 @@ watch(index, () => {
|
|
|
286
288
|
|
|
287
289
|
onBeforeUnmount(() => {
|
|
288
290
|
document.removeEventListener("click", onTargetClick, true)
|
|
291
|
+
document.removeEventListener("contextmenu", onTargetClick, true)
|
|
289
292
|
if (open.value) end()
|
|
290
293
|
})
|
|
291
294
|
|
|
@@ -18,10 +18,10 @@ defineProps<{
|
|
|
18
18
|
|
|
19
19
|
<template>
|
|
20
20
|
<div class="flex max-w-72 min-w-0 items-center gap-2 px-2.5 pt-1.5 pb-2" aria-hidden="true">
|
|
21
|
-
<img v-if="image" :src="image" alt="" class="size-7 shrink-0 rounded-
|
|
21
|
+
<img v-if="image" :src="image" alt="" class="size-7 shrink-0 rounded-inner object-cover" />
|
|
22
22
|
<span
|
|
23
23
|
v-else-if="icon"
|
|
24
|
-
class="grid size-7 shrink-0 place-items-center rounded-
|
|
24
|
+
class="grid size-7 shrink-0 place-items-center rounded-inner bg-surface-soft text-muted"
|
|
25
25
|
>
|
|
26
26
|
<LpIcon :name="icon" :size="15" />
|
|
27
27
|
</span>
|
|
@@ -49,7 +49,7 @@ export const POPOVER_PANEL = PANEL_SURFACE + " " + POPOVER_ANIM
|
|
|
49
49
|
* stagger. `flex` + gap are included; add justify/extra layout if needed.
|
|
50
50
|
*/
|
|
51
51
|
export const OPTION_ITEM =
|
|
52
|
-
"flex cursor-pointer items-center gap-2 rounded-
|
|
52
|
+
"flex cursor-pointer items-center gap-2 rounded-inner px-2.5 py-1.5 text-sm text-ink outline-none " +
|
|
53
53
|
"animate-[option-in_180ms_var(--ease-emphasized)_both] " +
|
|
54
54
|
"group-data-[side=top]:animate-[option-in-up_180ms_var(--ease-emphasized)_both] " +
|
|
55
55
|
"data-[highlighted]:bg-brand-soft data-[highlighted]:text-brand"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { computed, inject, provide, reactive, watch, type ComputedRef, type InjectionKey, type WatchStopHandle } from "vue"
|
|
1
|
+
import { computed, inject, onBeforeUnmount, provide, reactive, shallowRef, watch, type ComputedRef, type InjectionKey, type WatchStopHandle } from "vue"
|
|
2
2
|
import type { HintNote } from "../components/LpHintLayer.vue"
|
|
3
3
|
import type { TourContext, TourStep } from "../components/LpTour.vue"
|
|
4
4
|
import { anyModalOpen } from "../components/modalDepth"
|
|
@@ -76,6 +76,69 @@ export interface HintNudge {
|
|
|
76
76
|
text: string
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* A better way to do something the user keeps doing the slow way: shown now
|
|
81
|
+
* and then (at most `max` times, `nudgeAgain` apart) and never again once the
|
|
82
|
+
* better way is used. `when` reads usage counters — kit components report
|
|
83
|
+
* their own (see HINT_SIGNALS), and `hints.use(name)` adds any of the app's.
|
|
84
|
+
*/
|
|
85
|
+
export interface HintTip {
|
|
86
|
+
id: string
|
|
87
|
+
title: string
|
|
88
|
+
text: string
|
|
89
|
+
keys?: string
|
|
90
|
+
max?: number
|
|
91
|
+
when: (count: (signal: string) => number) => boolean
|
|
92
|
+
/** What "show me" lights up on the current screen; without it the tip only informs. */
|
|
93
|
+
step?: () => TourStep | null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Usage the kit reports by itself. */
|
|
97
|
+
export const HINT_SIGNALS = {
|
|
98
|
+
/** A context menu was opened (right click or long press). */
|
|
99
|
+
contextMenu: "lp.context-menu",
|
|
100
|
+
/** A "…" menu marked `rightClickToo` was opened: its items were one right click away. */
|
|
101
|
+
menuButton: "lp.menu-button",
|
|
102
|
+
/** A left click on something that also has a context menu. */
|
|
103
|
+
contextTarget: "lp.context-target",
|
|
104
|
+
/** The command palette was opened. */
|
|
105
|
+
palette: "lp.palette",
|
|
106
|
+
/** The current hint page changed: a navigation. */
|
|
107
|
+
navigate: "lp.navigate",
|
|
108
|
+
/** An item's actions were opened in the palette (Tab). */
|
|
109
|
+
paletteActions: "lp.palette-actions",
|
|
110
|
+
} as const
|
|
111
|
+
|
|
112
|
+
/** Right click instead of the "…" button. */
|
|
113
|
+
export function contextMenuTip(text: Partial<Pick<HintTip, "title" | "text"> & { stepText: string }> = {}): HintTip {
|
|
114
|
+
return {
|
|
115
|
+
id: "lp.tip.context-menu",
|
|
116
|
+
title: text.title ?? "Right-click works here",
|
|
117
|
+
text: text.text ?? "Rows and cards open their actions on right-click, even where no … button shows it.",
|
|
118
|
+
when: (count) =>
|
|
119
|
+
count(HINT_SIGNALS.contextMenu) === 0
|
|
120
|
+
&& (count(HINT_SIGNALS.menuButton) >= 5 || count(HINT_SIGNALS.contextTarget) >= 25),
|
|
121
|
+
step: () => {
|
|
122
|
+
const target = [...document.querySelectorAll("[data-lp-context-menu]")].find((el) => {
|
|
123
|
+
const box = el.getBoundingClientRect()
|
|
124
|
+
return box.width > 0 && box.bottom > 0 && box.top < window.innerHeight
|
|
125
|
+
})
|
|
126
|
+
return target ? { target, title: text.title ?? "Right-click works here", text: text.stepText ?? "Right-click this to see what it can do.", try: true } : null
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The palette instead of walking the menu. */
|
|
132
|
+
export function paletteTip(keys = "Ctrl+K", text: Partial<Pick<HintTip, "title" | "text">> = {}): HintTip {
|
|
133
|
+
return {
|
|
134
|
+
id: "lp.tip.palette",
|
|
135
|
+
title: text.title ?? "Jump anywhere",
|
|
136
|
+
text: text.text ?? "Search pages, people and settings from anywhere instead of the menu.",
|
|
137
|
+
keys,
|
|
138
|
+
when: (count) => count(HINT_SIGNALS.navigate) >= 15 && count(HINT_SIGNALS.palette) === 0,
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
79
142
|
export type HintLevel = "seen" | "explained" | "used"
|
|
80
143
|
|
|
81
144
|
export interface HintRecord {
|
|
@@ -113,6 +176,7 @@ export interface HintSystemOptions {
|
|
|
113
176
|
features: Record<string, HintFeature>
|
|
114
177
|
pages?: Record<string, HintPage>
|
|
115
178
|
news?: HintNews[]
|
|
179
|
+
tips?: HintTip[]
|
|
116
180
|
storage?: HintStorage
|
|
117
181
|
/** The current page id, read reactively (map your route to it). */
|
|
118
182
|
page: () => string
|
|
@@ -124,6 +188,7 @@ export type HintOffer =
|
|
|
124
188
|
| { kind: "tour" | "assigned"; page: string }
|
|
125
189
|
| { kind: "news"; page: string; items: HintNews[] }
|
|
126
190
|
| { kind: "nudge"; page: string; nudge: HintNudge }
|
|
191
|
+
| { kind: "tip"; page: string; tip: HintTip }
|
|
127
192
|
|
|
128
193
|
const RANK: Record<HintLevel, number> = { seen: 0, explained: 1, used: 2 }
|
|
129
194
|
const DAY = 24 * 3600 * 1000
|
|
@@ -217,6 +282,8 @@ export function createHintSystem(options: HintSystemOptions) {
|
|
|
217
282
|
const features = options.features
|
|
218
283
|
const pages = options.pages ?? {}
|
|
219
284
|
const news = options.news ?? []
|
|
285
|
+
const tips = options.tips ?? []
|
|
286
|
+
const TIP_MAX = 3
|
|
220
287
|
const storage = options.storage ?? localHintStorage()
|
|
221
288
|
const sessionKey = options.sessionKey ?? "lp-hints:interrupted"
|
|
222
289
|
|
|
@@ -224,6 +291,7 @@ export function createHintSystem(options: HintSystemOptions) {
|
|
|
224
291
|
records: new Map<string, HintRecord>(),
|
|
225
292
|
loaded: false,
|
|
226
293
|
page: "",
|
|
294
|
+
overlay: null as string | null,
|
|
227
295
|
tour: { open: false, step: 0, steps: [] as TourStep[], ids: [] as string[], page: "" },
|
|
228
296
|
offer: null as HintOffer | null,
|
|
229
297
|
candidate: null as (HintNudge & { page: string }) | null,
|
|
@@ -238,6 +306,9 @@ export function createHintSystem(options: HintSystemOptions) {
|
|
|
238
306
|
const pageKey = hintKey.page
|
|
239
307
|
const newsKey = hintKey.news
|
|
240
308
|
const nudgeKey = hintKey.nudge
|
|
309
|
+
const signalKey = (name: string) => `u:${name}`
|
|
310
|
+
const tipKey = (id: string) => `p:${id}`
|
|
311
|
+
const count = (name: string) => state.records.get(signalKey(name))?.count ?? 0
|
|
241
312
|
|
|
242
313
|
function interrupted(): boolean {
|
|
243
314
|
try {
|
|
@@ -390,14 +461,28 @@ export function createHintSystem(options: HintSystemOptions) {
|
|
|
390
461
|
return
|
|
391
462
|
}
|
|
392
463
|
const n = state.candidate
|
|
393
|
-
if (
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
464
|
+
if (n && n.page === page) {
|
|
465
|
+
const seen = state.records.get(nudgeKey(n.id))
|
|
466
|
+
const f = features[n.feature]
|
|
467
|
+
const visible = !!f && (!!f.target || !!f.enter || hints.present(n.feature))
|
|
468
|
+
if (!seen?.dismissed && state.records.get(featureKey(n.feature))?.state !== "used" && visible
|
|
469
|
+
&& !(seen && Date.now() - Date.parse(seen.updatedAt) < timing.nudgeAgain)) {
|
|
470
|
+
state.candidate = null
|
|
471
|
+
spend()
|
|
472
|
+
record(nudgeKey(n.id), { add: 1 })
|
|
473
|
+
state.offer = { kind: "nudge", page, nudge: n }
|
|
474
|
+
return
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
for (const tip of tips) {
|
|
478
|
+
const seen = state.records.get(tipKey(tip.id))
|
|
479
|
+
if (seen?.dismissed || (seen?.count ?? 0) >= (tip.max ?? TIP_MAX)) continue
|
|
480
|
+
if (seen && Date.now() - Date.parse(seen.updatedAt) < timing.nudgeAgain) continue
|
|
481
|
+
if (!tip.when(count)) continue
|
|
482
|
+
spend()
|
|
483
|
+
state.offer = { kind: "tip", page, tip }
|
|
484
|
+
return
|
|
485
|
+
}
|
|
401
486
|
}
|
|
402
487
|
|
|
403
488
|
let stops: (() => void)[] = []
|
|
@@ -405,8 +490,9 @@ export function createHintSystem(options: HintSystemOptions) {
|
|
|
405
490
|
function start(): void {
|
|
406
491
|
if (stops.length) return
|
|
407
492
|
const stopPage: WatchStopHandle = watch(
|
|
408
|
-
options.page,
|
|
409
|
-
(page) => {
|
|
493
|
+
() => state.overlay ?? options.page(),
|
|
494
|
+
(page, previous) => {
|
|
495
|
+
if (previous !== undefined && page !== previous && !state.overlay) record(signalKey(HINT_SIGNALS.navigate), { add: 1 })
|
|
410
496
|
state.page = page
|
|
411
497
|
state.settledAt = Date.now()
|
|
412
498
|
if (state.offer && state.offer.page !== page) state.offer = null
|
|
@@ -428,6 +514,7 @@ export function createHintSystem(options: HintSystemOptions) {
|
|
|
428
514
|
stopPage,
|
|
429
515
|
hints.on("used", (id) => {
|
|
430
516
|
if (features[id]) record(featureKey(id), { state: "used", add: 1 })
|
|
517
|
+
if (id.startsWith("lp.") || !features[id]) record(signalKey(id), { add: 1 })
|
|
431
518
|
}),
|
|
432
519
|
() => clearInterval(poll),
|
|
433
520
|
() => window.removeEventListener("pagehide", flushOnLeave),
|
|
@@ -460,6 +547,16 @@ export function createHintSystem(options: HintSystemOptions) {
|
|
|
460
547
|
nudge(n: HintNudge): void {
|
|
461
548
|
state.candidate = { ...n, page: state.page }
|
|
462
549
|
},
|
|
550
|
+
/**
|
|
551
|
+
* A dialog acts as its own page while open (its dots and tour), then hands
|
|
552
|
+
* back. Pass null on close, and on unmount: a dialog left open by a route
|
|
553
|
+
* change would otherwise keep the page.
|
|
554
|
+
*/
|
|
555
|
+
overlay(page: string | null): void {
|
|
556
|
+
state.overlay = page
|
|
557
|
+
},
|
|
558
|
+
/** How many times a signal was reported (for an app's own tips or reports). */
|
|
559
|
+
count,
|
|
463
560
|
/** The user navigated on purpose (palette, notification): do not interrupt this arrival. */
|
|
464
561
|
markIntent(): void {
|
|
465
562
|
state.intent = true
|
|
@@ -475,6 +572,7 @@ export function createHintSystem(options: HintSystemOptions) {
|
|
|
475
572
|
if (id) record(featureKey(id), { state: "explained" })
|
|
476
573
|
},
|
|
477
574
|
tourEnded(finished: boolean): void {
|
|
575
|
+
if (!state.tour.ids.length) return
|
|
478
576
|
record(pageKey(state.tour.page), finished ? { state: "explained" } : { add: 1 })
|
|
479
577
|
baseline(state.tour.page)
|
|
480
578
|
},
|
|
@@ -486,6 +584,10 @@ export function createHintSystem(options: HintSystemOptions) {
|
|
|
486
584
|
const ids = offer.items.map((n) => n.feature).filter((id): id is string => !!id)
|
|
487
585
|
readNews(offer.page)
|
|
488
586
|
if (choice === "show" && ids.length) startTour(offer.page, ids)
|
|
587
|
+
} else if (offer.kind === "tip") {
|
|
588
|
+
record(tipKey(offer.tip.id), { add: 1, dismissed: choice === "never" })
|
|
589
|
+
const step = choice === "show" ? offer.tip.step?.() : null
|
|
590
|
+
if (step) Object.assign(state.tour, { open: true, step: 0, page: offer.page, ids: [], steps: [step] })
|
|
489
591
|
} else if (offer.kind === "nudge") {
|
|
490
592
|
if (choice === "show") startTour(features[offer.nudge.feature]!.page, [offer.nudge.feature])
|
|
491
593
|
else record(nudgeKey(offer.nudge.id), { dismissed: true })
|
|
@@ -503,10 +605,26 @@ export type HintSystem = ReturnType<typeof createHintSystem>
|
|
|
503
605
|
|
|
504
606
|
const HINT_SYSTEM: InjectionKey<HintSystem> = Symbol.for("lp-hint-system")
|
|
505
607
|
|
|
608
|
+
/*
|
|
609
|
+
* Overlays (the palette, menus) are portalled siblings of the host, not its
|
|
610
|
+
* descendants, so injection alone would never reach them: the mounted host is
|
|
611
|
+
* also kept here.
|
|
612
|
+
*/
|
|
613
|
+
const running = shallowRef<HintSystem | null>(null)
|
|
614
|
+
|
|
506
615
|
export function provideHintSystem(system: HintSystem): void {
|
|
507
616
|
provide(HINT_SYSTEM, system)
|
|
617
|
+
running.value = system
|
|
618
|
+
onBeforeUnmount(() => {
|
|
619
|
+
if (running.value === system) running.value = null
|
|
620
|
+
})
|
|
508
621
|
}
|
|
509
622
|
|
|
510
623
|
export function useHintSystem(): HintSystem | null {
|
|
511
|
-
return inject(HINT_SYSTEM, null)
|
|
624
|
+
return inject(HINT_SYSTEM, null) ?? running.value
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/** The mounted host's system, read reactively from anywhere. */
|
|
628
|
+
export function runningHintSystem(): HintSystem | null {
|
|
629
|
+
return running.value
|
|
512
630
|
}
|
package/src/index.ts
CHANGED
|
@@ -212,7 +212,7 @@ export type { TourContext, TourStep } from "./components/LpTour.vue"
|
|
|
212
212
|
export { holeOf, openContextMenu, panelsAround } from "./components/tour"
|
|
213
213
|
export { default as LpHintLayer } from "./components/LpHintLayer.vue"
|
|
214
214
|
export { default as LpHints } from "./components/LpHints.vue"
|
|
215
|
-
export { createHintSystem, hintKey, hintSummary, localHintStorage, provideHintSystem, useHintSystem } from "./composables/useHintSystem"
|
|
215
|
+
export { HINT_SIGNALS, contextMenuTip, createHintSystem, hintKey, hintSummary, localHintStorage, paletteTip, provideHintSystem, runningHintSystem, useHintSystem } from "./composables/useHintSystem"
|
|
216
216
|
export type {
|
|
217
217
|
HintDelta,
|
|
218
218
|
HintFeature,
|
|
@@ -225,6 +225,7 @@ export type {
|
|
|
225
225
|
HintStorage,
|
|
226
226
|
HintSummary,
|
|
227
227
|
HintSystem,
|
|
228
|
+
HintTip,
|
|
228
229
|
HintSystemOptions,
|
|
229
230
|
HintTiming,
|
|
230
231
|
} from "./composables/useHintSystem"
|