@leavepulse/ui 0.56.0 → 0.58.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-BtKU7074.js → ContextMenuItems-BY5sTVQF.js} +1 -1
- package/dist/chunks/{LayoutCanvas-BkaGXTjx.js → LayoutCanvas-BcGLJVBq.js} +1 -1
- package/dist/chunks/{LayoutNode-B3_9E1Xa.js → LayoutNode-BkBn6dvM.js} +1 -1
- package/dist/chunks/{LpActionBar-C3hkVsFz.js → LpActionBar-DCN-_pZP.js} +2 -2
- package/dist/chunks/{LpAddressInput-NUHmplCd.js → LpAddressInput-Dio6Sqed.js} +7 -7
- package/dist/chunks/{LpAlert-B6ilrM7T.js → LpAlert-CXMn1GYE.js} +1 -1
- package/dist/chunks/{LpAppShell-BEGjSqFl.js → LpAppShell-Be6T96td.js} +3 -3
- package/dist/chunks/{LpAutocomplete-C8IgSMsi.js → LpAutocomplete-hJ_6CHlh.js} +1 -1
- package/dist/chunks/{LpAvatar-LTvy0OkE.js → LpAvatar-BxNs23Xu.js} +2 -2
- package/dist/chunks/{LpBreadcrumbs-D4TsoFju.js → LpBreadcrumbs-BLzOEgAF.js} +1 -1
- package/dist/chunks/{LpButton-BdfZFNqi.js → LpButton-vhlwmpBy.js} +1 -1
- package/dist/chunks/{LpCalendar-DrA2McTU.js → LpCalendar-BLoE8Qm7.js} +1 -1
- package/dist/chunks/{LpCard-DbjO5yaF.js → LpCard-DhR7u0d8.js} +1 -1
- package/dist/chunks/{LpCheckbox-DvVYeOD-.js → LpCheckbox-B_C8sOoT.js} +1 -1
- package/dist/chunks/{LpCodeBlock-ca7ANFyW.js → LpCodeBlock-CXsqJwDy.js} +1 -1
- package/dist/chunks/{LpColorPicker-D1sQqtXS.js → LpColorPicker-DQ88niXq.js} +2 -2
- package/dist/chunks/{LpConfirmDialog-CrbUZgJE.js → LpConfirmDialog-DxlSdjmk.js} +3 -3
- package/dist/chunks/{LpConfirmHost-BgMIV8r9.js → LpConfirmHost-BcrmTNu_.js} +1 -1
- package/dist/chunks/{LpContextMenu-EF_90u3g.js → LpContextMenu-B1NGfpp1.js} +2 -2
- package/dist/chunks/{LpDatePicker-BrdFtSTy.js → LpDatePicker-B-VMHMgy.js} +3 -3
- package/dist/chunks/{LpDateRangePicker-Drd7EbIz.js → LpDateRangePicker-B68KPKPl.js} +6 -15
- package/dist/chunks/{LpDisclosure-Cc23-JOT.js → LpDisclosure-Ct-VBHxt.js} +1 -1
- package/dist/chunks/{LpDrawer-CBFVD624.js → LpDrawer-DSknigxH.js} +5 -7
- package/dist/chunks/{LpDropdownMenu-DrpJJBp0.js → LpDropdownMenu-C7oHVlKY.js} +2 -2
- package/dist/chunks/{LpEmptyState-BJbZXC8D.js → LpEmptyState-CBwwkkdA.js} +1 -1
- package/dist/chunks/{LpFieldNotice-wtieLUTj.js → LpFieldNotice-9hH4ITZa.js} +1 -1
- package/dist/chunks/{LpFileTree-3E4w4Rjr.js → LpFileTree-CMA0_rsB.js} +2 -2
- package/dist/chunks/{LpFileTreeNode-B4uB8Ks_.js → LpFileTreeNode-B4T258hF.js} +3 -3
- package/dist/chunks/{LpFormField-CtVYj6tO.js → LpFormField-BpQ6WJPe.js} +1 -1
- package/dist/chunks/LpHintLayer-B5-6FyEf.js +154 -0
- package/dist/chunks/LpHints-BqoZXHZk.js +109 -0
- package/dist/chunks/{LpIcon-DYtaid0K.js → LpIcon-B5guGKrW.js} +1 -1
- package/dist/chunks/{LpInput-D-YGQbEv.js → LpInput-WvyP02ID.js} +2 -2
- package/dist/chunks/{LpLightbox-EVXzAq62.js → LpLightbox-Dq-e36uT.js} +1 -1
- package/dist/chunks/{LpLogViewer-BfgtXVKm.js → LpLogViewer-DwiYE32z.js} +2 -2
- package/dist/chunks/{LpMapPicker-Cc84LaM3.js → LpMapPicker-BH5csaBt.js} +1 -1
- package/dist/chunks/{LpMediaGallery-BnlkN_1f.js → LpMediaGallery-B1kDypJ-.js} +2 -2
- package/dist/chunks/{LpModal-JNa3B_Lr.js → LpModal-BxYssnXg.js} +1 -1
- package/dist/chunks/{LpModalNav-DXBilXr0.js → LpModalNav-Bn2ws64b.js} +4 -4
- package/dist/chunks/{LpNotificationBell-DEL-RoHS.js → LpNotificationBell-DJfs4JjQ.js} +4 -4
- package/dist/chunks/{LpNumberField-B77O-z28.js → LpNumberField-CJPjmoDz.js} +1 -1
- package/dist/chunks/{LpPagination-BcDlo3r_.js → LpPagination-DehAaJLY.js} +1 -1
- package/dist/chunks/{LpPasswordInput-eKzQhbQP.js → LpPasswordInput-Bg5uTZbx.js} +2 -2
- package/dist/chunks/{LpPhoneInput-BCbrrhgE.js → LpPhoneInput-DStwcbnQ.js} +2 -2
- package/dist/chunks/{LpPie-B7ti5qCH.js → LpPie-DeTkbbOK.js} +3 -3
- package/dist/chunks/{LpRangeCalendar-DZuaRV0W.js → LpRangeCalendar-D5sDID9W.js} +1 -1
- package/dist/chunks/{LpSection-tKEH0jYf.js → LpSection-B19VPYmr.js} +1 -1
- package/dist/chunks/{LpSegmented-C09EEam2.js → LpSegmented-D3Eam8sa.js} +1 -1
- package/dist/chunks/{LpSelect-DNIL6MB9.js → LpSelect-DynjWK1A.js} +1 -1
- package/dist/chunks/{LpSidebar-C2oolgwy.js → LpSidebar-CshW0fqC.js} +3 -3
- package/dist/chunks/{LpSidebarNav-C9dHu-2b.js → LpSidebarNav-C9b3i9G2.js} +2 -2
- package/dist/chunks/{LpStat-B-7udBRP.js → LpStat-BS2TEXYK.js} +1 -1
- package/dist/chunks/{LpStepper-C7XiVKZj.js → LpStepper-BSglWF45.js} +1 -1
- package/dist/chunks/{LpTableFilter-CrW7_Rj4.js → LpTableFilter-CwHXVxJt.js} +2 -2
- package/dist/chunks/{LpTableOfContents-BUcAfGPQ.js → LpTableOfContents-DYuqkByy.js} +2 -8
- package/dist/chunks/{LpTabs-BMSBB78h.js → LpTabs-BBGRHY38.js} +1 -1
- package/dist/chunks/{LpTagsInput-D1SuqbhB.js → LpTagsInput-CHbQoAAA.js} +3 -3
- package/dist/chunks/{LpTextarea-uuScWo6-.js → LpTextarea-Bz2IWhTH.js} +1 -1
- package/dist/chunks/{LpThemeSwitcher-Fz85BCRS.js → LpThemeSwitcher-B_qhSl7q.js} +2 -2
- package/dist/chunks/{LpTimeField-Cqe7fXnh.js → LpTimeField-DvGXgdES.js} +1 -1
- package/dist/chunks/{LpToaster-DTtWuKvq.js → LpToaster-Bo9Q4QBP.js} +1 -1
- package/dist/chunks/{LpTooltip-CXVKcOXT.js → LpTooltip-suFCM34F.js} +2 -2
- package/dist/chunks/LpTour-D7m9ZxNk.js +334 -0
- package/dist/chunks/{LpUptimeBar-ugcv25xj.js → LpUptimeBar-B4ltCfno.js} +1 -1
- package/dist/chunks/{MenuTitle-J3ZDYjpV.js → MenuTitle-DQP6hc2N.js} +1 -1
- package/dist/chunks/TourCard-DCHOjr6e.js +100 -0
- package/dist/chunks/icons-QqBscG5I.js +43 -0
- package/dist/component-names.d.ts +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 +2 -2
- 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 +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/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.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.d.ts +33 -0
- package/dist/components/LpHintLayer.vue.js +4 -0
- package/dist/components/LpHints.vue.d.ts +20 -0
- package/dist/components/LpHints.vue.js +4 -0
- package/dist/components/LpIcon.vue.js +1 -1
- package/dist/components/LpImageEditor.vue.js +2 -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 +6 -6
- package/dist/components/LpTableFilter.vue.js +1 -1
- package/dist/components/LpTableOfContents.vue.js +1 -1
- 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/LpTooltip.vue.js +1 -1
- package/dist/components/LpTour.vue.d.ts +98 -0
- package/dist/components/LpTour.vue.js +4 -0
- package/dist/components/LpUptimeBar.vue.js +1 -1
- package/dist/components/MenuTitle.vue.js +1 -1
- package/dist/components/TourCard.vue.d.ts +28 -0
- package/dist/components/TourCard.vue.js +4 -0
- package/dist/components/tour.d.ts +21 -0
- package/dist/components/tour.js +38 -0
- package/dist/composables/scroll.d.ts +17 -0
- package/dist/composables/scroll.js +29 -0
- package/dist/composables/useGamepadNav.js +2 -7
- package/dist/composables/useHintSystem.d.ts +195 -0
- package/dist/composables/useHintSystem.js +334 -0
- package/dist/composables/useHints.d.ts +18 -0
- package/dist/composables/useHints.js +120 -0
- package/dist/composables/useMediaQuery.d.ts +2 -0
- package/dist/composables/useMediaQuery.js +18 -0
- package/dist/composables/usePaintSelect.js +1 -7
- package/dist/icons-all.js +1 -1
- package/dist/index.d.ts +13 -0
- package/dist/index.js +89 -64
- package/package.json +1 -1
- package/src/component-names.ts +3 -0
- package/src/components/LpDateRangePicker.vue +3 -12
- package/src/components/LpDrawer.vue +4 -7
- package/src/components/LpHintLayer.vue +148 -0
- package/src/components/LpHints.vue +116 -0
- package/src/components/LpTableOfContents.vue +2 -14
- package/src/components/LpTooltip.vue +2 -4
- package/src/components/LpTour.vue +358 -0
- package/src/components/TourCard.vue +41 -0
- package/src/components/tour.ts +57 -0
- package/src/composables/scroll.ts +54 -0
- package/src/composables/useGamepadNav.ts +2 -7
- package/src/composables/useHintSystem.ts +512 -0
- package/src/composables/useHints.ts +151 -0
- package/src/composables/useMediaQuery.ts +20 -0
- package/src/composables/usePaintSelect.ts +1 -8
- package/src/index.ts +27 -0
- package/src/kit-icons.ts +1 -1
- package/src/tokens/tokens.css +1 -0
- package/dist/chunks/icons-CkA0njzm.js +0 -43
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { provide, reactive, computed, inject, watch } from "vue";
|
|
2
|
+
import { anyModalOpen } from "../components/modalDepth.js";
|
|
3
|
+
import { hints } from "./useHints.js";
|
|
4
|
+
const RANK = { seen: 0, explained: 1, used: 2 };
|
|
5
|
+
const DAY = 24 * 3600 * 1e3;
|
|
6
|
+
const DEFAULT_TIMING = {
|
|
7
|
+
settle: 2500,
|
|
8
|
+
offerAgain: 3 * DAY,
|
|
9
|
+
offersMax: 2,
|
|
10
|
+
freshLimit: 3,
|
|
11
|
+
nudgeAgain: 7 * DAY,
|
|
12
|
+
flush: 2e3
|
|
13
|
+
};
|
|
14
|
+
function localHintStorage(key = "lp-hints") {
|
|
15
|
+
const read = () => {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(localStorage.getItem(key) || "{}");
|
|
18
|
+
} catch {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
load: read,
|
|
24
|
+
save(deltas) {
|
|
25
|
+
const all = read();
|
|
26
|
+
for (const d of deltas) all[d.key] = merge(all[d.key], d);
|
|
27
|
+
try {
|
|
28
|
+
localStorage.setItem(key, JSON.stringify(all));
|
|
29
|
+
} catch {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function merge(record, delta) {
|
|
36
|
+
const base = record ?? { state: "seen", dismissed: false, count: 0, updatedAt: "" };
|
|
37
|
+
return {
|
|
38
|
+
...base,
|
|
39
|
+
state: delta.state && RANK[delta.state] > RANK[base.state] ? delta.state : base.state,
|
|
40
|
+
dismissed: delta.dismissed ?? base.dismissed,
|
|
41
|
+
count: base.count + (delta.add ?? 0),
|
|
42
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const hintKey = {
|
|
46
|
+
feature: (id, rev = 1) => `f:${id}:${rev}`,
|
|
47
|
+
page: (page) => `t:${page}`,
|
|
48
|
+
news: (id) => `c:${id}`,
|
|
49
|
+
nudge: (id) => `n:${id}`
|
|
50
|
+
};
|
|
51
|
+
function hintSummary(records, features, page) {
|
|
52
|
+
const own = Object.entries(features).filter(([, f]) => f.page === page);
|
|
53
|
+
const knows = (id, f) => RANK[records.get(hintKey.feature(id, f.rev))?.state ?? "seen"] >= RANK.explained;
|
|
54
|
+
const tour = records.get(hintKey.page(page));
|
|
55
|
+
return {
|
|
56
|
+
tour: tour && RANK[tour.state] >= RANK.explained ? "done" : tour?.assigned ? "assigned" : tour?.dismissed ? "declined" : tour ? "offered" : "never",
|
|
57
|
+
offers: tour?.count ?? 0,
|
|
58
|
+
known: own.filter(([id, f]) => knows(id, f)).length,
|
|
59
|
+
total: own.length,
|
|
60
|
+
missing: own.filter(([id, f]) => f.required && !knows(id, f)).map(([, f]) => f.title)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function typing() {
|
|
64
|
+
const el = document.activeElement;
|
|
65
|
+
return el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLElement && el.isContentEditable;
|
|
66
|
+
}
|
|
67
|
+
function createHintSystem(options) {
|
|
68
|
+
const timing = { ...DEFAULT_TIMING, ...options.timing };
|
|
69
|
+
const features = options.features;
|
|
70
|
+
const pages = options.pages ?? {};
|
|
71
|
+
const news = options.news ?? [];
|
|
72
|
+
const storage = options.storage ?? localHintStorage();
|
|
73
|
+
const sessionKey = options.sessionKey ?? "lp-hints:interrupted";
|
|
74
|
+
const state = reactive({
|
|
75
|
+
records: /* @__PURE__ */ new Map(),
|
|
76
|
+
loaded: false,
|
|
77
|
+
page: "",
|
|
78
|
+
tour: { open: false, step: 0, steps: [], ids: [], page: "" },
|
|
79
|
+
offer: null,
|
|
80
|
+
candidate: null,
|
|
81
|
+
settledAt: 0,
|
|
82
|
+
intent: false
|
|
83
|
+
});
|
|
84
|
+
const pending = /* @__PURE__ */ new Map();
|
|
85
|
+
let flushTimer;
|
|
86
|
+
const featureKey = (id) => hintKey.feature(id, features[id]?.rev);
|
|
87
|
+
const pageKey = hintKey.page;
|
|
88
|
+
const newsKey = hintKey.news;
|
|
89
|
+
const nudgeKey = hintKey.nudge;
|
|
90
|
+
function interrupted() {
|
|
91
|
+
try {
|
|
92
|
+
return sessionStorage.getItem(sessionKey) === "1";
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function spend() {
|
|
98
|
+
try {
|
|
99
|
+
sessionStorage.setItem(sessionKey, "1");
|
|
100
|
+
} catch {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function flush() {
|
|
105
|
+
clearTimeout(flushTimer);
|
|
106
|
+
flushTimer = void 0;
|
|
107
|
+
if (!pending.size) return;
|
|
108
|
+
const deltas = [...pending.values()];
|
|
109
|
+
pending.clear();
|
|
110
|
+
try {
|
|
111
|
+
await storage.save(deltas);
|
|
112
|
+
} catch {
|
|
113
|
+
for (const d of deltas) if (!pending.has(d.key)) pending.set(d.key, d);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function flushOnLeave() {
|
|
117
|
+
if (!pending.size) return;
|
|
118
|
+
const deltas = [...pending.values()];
|
|
119
|
+
pending.clear();
|
|
120
|
+
if (storage.saveOnLeave) storage.saveOnLeave(deltas);
|
|
121
|
+
else void storage.save(deltas);
|
|
122
|
+
}
|
|
123
|
+
function record(key, delta) {
|
|
124
|
+
state.records.set(key, merge(state.records.get(key), delta));
|
|
125
|
+
const queued = pending.get(key);
|
|
126
|
+
pending.set(key, {
|
|
127
|
+
key,
|
|
128
|
+
state: delta.state ?? queued?.state,
|
|
129
|
+
dismissed: delta.dismissed ?? queued?.dismissed,
|
|
130
|
+
add: (queued?.add ?? 0) + (delta.add ?? 0)
|
|
131
|
+
});
|
|
132
|
+
flushTimer ??= setTimeout(() => void flush(), timing.flush);
|
|
133
|
+
}
|
|
134
|
+
const present = (page) => Object.keys(features).filter((id) => features[id].page === page && hints.present(id));
|
|
135
|
+
function knownBefore(page) {
|
|
136
|
+
const marker = pages[page]?.known;
|
|
137
|
+
if (!marker) return false;
|
|
138
|
+
try {
|
|
139
|
+
return Object.keys(localStorage).some((key) => key.startsWith(marker));
|
|
140
|
+
} catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function baseline(page) {
|
|
145
|
+
for (const id of present(page)) if (!state.records.has(featureKey(id))) record(featureKey(id), { state: "seen" });
|
|
146
|
+
}
|
|
147
|
+
const fresh = computed(() => {
|
|
148
|
+
const page = state.page;
|
|
149
|
+
if (!page || !state.loaded || state.tour.open || !state.records.has(pageKey(page))) return [];
|
|
150
|
+
return present(page).filter((id) => {
|
|
151
|
+
const f = features[id];
|
|
152
|
+
const rec = state.records.get(featureKey(id));
|
|
153
|
+
if (!rec) return !f.conditional || (f.rev ?? 1) > 1;
|
|
154
|
+
return !!f.required && RANK[rec.state] < RANK.explained && !rec.dismissed;
|
|
155
|
+
}).sort((a, b) => Number(!!features[b].required) - Number(!!features[a].required) || (features[a].order ?? 0) - (features[b].order ?? 0)).slice(0, timing.freshLimit);
|
|
156
|
+
});
|
|
157
|
+
const notes = computed(
|
|
158
|
+
() => Object.fromEntries(Object.entries(features).map(([id, f]) => [id, { title: f.title, text: f.text }]))
|
|
159
|
+
);
|
|
160
|
+
const hasTour = (page = state.page) => Object.values(features).some((f) => f.page === page);
|
|
161
|
+
function startTour(page = state.page, only) {
|
|
162
|
+
const entries = Object.entries(features).filter(([id, f]) => f.page === page && (!only || only.includes(id))).sort(([, a], [, b]) => (a.order ?? 0) - (b.order ?? 0));
|
|
163
|
+
if (!entries.length) return;
|
|
164
|
+
state.offer = null;
|
|
165
|
+
Object.assign(state.tour, {
|
|
166
|
+
open: true,
|
|
167
|
+
step: 0,
|
|
168
|
+
page,
|
|
169
|
+
ids: entries.map(([id]) => id),
|
|
170
|
+
steps: entries.map(([id, f]) => ({
|
|
171
|
+
target: f.target ?? id,
|
|
172
|
+
title: f.title,
|
|
173
|
+
text: f.text,
|
|
174
|
+
keys: f.keys,
|
|
175
|
+
try: f.try,
|
|
176
|
+
side: f.side,
|
|
177
|
+
enter: f.enter
|
|
178
|
+
}))
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
const unread = (page) => news.filter((n) => (!page || n.page === page) && !state.records.has(newsKey(n.id)) && (!page || !n.feature || hints.present(n.feature)));
|
|
182
|
+
function readNews(page) {
|
|
183
|
+
for (const n of unread(page)) record(newsKey(n.id), { state: "explained" });
|
|
184
|
+
}
|
|
185
|
+
function considerOffer() {
|
|
186
|
+
const page = state.page;
|
|
187
|
+
const ready = state.loaded && !state.tour.open && !state.offer && !anyModalOpen() && !typing() && Date.now() - state.settledAt > timing.settle;
|
|
188
|
+
if (!page || !ready) return;
|
|
189
|
+
const rec = state.records.get(pageKey(page));
|
|
190
|
+
if (rec?.assigned && RANK[rec.state] < RANK.explained && hasTour(page)) {
|
|
191
|
+
state.offer = { kind: "assigned", page };
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (state.intent || interrupted()) return;
|
|
195
|
+
if (!rec && knownBefore(page)) {
|
|
196
|
+
record(pageKey(page), { state: "seen" });
|
|
197
|
+
baseline(page);
|
|
198
|
+
} else if (!rec || !rec.dismissed && RANK[rec.state] < RANK.explained && rec.count < timing.offersMax && Date.now() - Date.parse(rec.updatedAt || "0") > timing.offerAgain) {
|
|
199
|
+
if (!hasTour(page)) return;
|
|
200
|
+
spend();
|
|
201
|
+
if (!rec) {
|
|
202
|
+
record(pageKey(page), { state: "seen" });
|
|
203
|
+
baseline(page);
|
|
204
|
+
readNews(page);
|
|
205
|
+
}
|
|
206
|
+
state.offer = { kind: "tour", page };
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const items = unread(page);
|
|
210
|
+
if (items.length) {
|
|
211
|
+
spend();
|
|
212
|
+
state.offer = { kind: "news", page, items: items.slice(0, 3) };
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const n = state.candidate;
|
|
216
|
+
if (!n || n.page !== page) return;
|
|
217
|
+
const seen = state.records.get(nudgeKey(n.id));
|
|
218
|
+
if (seen?.dismissed || state.records.get(featureKey(n.feature))?.state === "used" || !hints.present(n.feature)) return;
|
|
219
|
+
if (seen && Date.now() - Date.parse(seen.updatedAt) < timing.nudgeAgain) return;
|
|
220
|
+
state.candidate = null;
|
|
221
|
+
spend();
|
|
222
|
+
record(nudgeKey(n.id), { add: 1 });
|
|
223
|
+
state.offer = { kind: "nudge", page, nudge: n };
|
|
224
|
+
}
|
|
225
|
+
let stops = [];
|
|
226
|
+
function start() {
|
|
227
|
+
if (stops.length) return;
|
|
228
|
+
const stopPage = watch(
|
|
229
|
+
options.page,
|
|
230
|
+
(page) => {
|
|
231
|
+
state.page = page;
|
|
232
|
+
state.settledAt = Date.now();
|
|
233
|
+
if (state.offer && state.offer.page !== page) state.offer = null;
|
|
234
|
+
if (state.candidate && state.candidate.page !== page) state.candidate = null;
|
|
235
|
+
setTimeout(() => state.intent = false, timing.settle * 2);
|
|
236
|
+
},
|
|
237
|
+
{ immediate: true }
|
|
238
|
+
);
|
|
239
|
+
const poll = setInterval(() => {
|
|
240
|
+
try {
|
|
241
|
+
considerOffer();
|
|
242
|
+
} catch (error) {
|
|
243
|
+
console.warn("[hints] disabled after an error:", error);
|
|
244
|
+
stop();
|
|
245
|
+
}
|
|
246
|
+
}, 1e3);
|
|
247
|
+
window.addEventListener("pagehide", flushOnLeave);
|
|
248
|
+
stops = [
|
|
249
|
+
stopPage,
|
|
250
|
+
hints.on("used", (id) => {
|
|
251
|
+
if (features[id]) record(featureKey(id), { state: "used", add: 1 });
|
|
252
|
+
}),
|
|
253
|
+
() => clearInterval(poll),
|
|
254
|
+
() => window.removeEventListener("pagehide", flushOnLeave)
|
|
255
|
+
];
|
|
256
|
+
}
|
|
257
|
+
function stop() {
|
|
258
|
+
for (const s of stops.splice(0)) s();
|
|
259
|
+
flushOnLeave();
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
state,
|
|
263
|
+
fresh,
|
|
264
|
+
notes,
|
|
265
|
+
start,
|
|
266
|
+
stop,
|
|
267
|
+
hasTour,
|
|
268
|
+
startTour,
|
|
269
|
+
async load() {
|
|
270
|
+
state.records.clear();
|
|
271
|
+
try {
|
|
272
|
+
for (const [key, rec] of Object.entries(await storage.load())) state.records.set(key, rec);
|
|
273
|
+
state.loaded = true;
|
|
274
|
+
} catch {
|
|
275
|
+
state.loaded = false;
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
/** Ask for a nudge; it shows when the budget and the page allow. */
|
|
279
|
+
nudge(n) {
|
|
280
|
+
state.candidate = { ...n, page: state.page };
|
|
281
|
+
},
|
|
282
|
+
/** The user navigated on purpose (palette, notification): do not interrupt this arrival. */
|
|
283
|
+
markIntent() {
|
|
284
|
+
state.intent = true;
|
|
285
|
+
},
|
|
286
|
+
unreadNews: (page) => unread(page),
|
|
287
|
+
readNews,
|
|
288
|
+
pageTitle: (page) => pages[page]?.title ?? page,
|
|
289
|
+
readFresh(id) {
|
|
290
|
+
record(featureKey(id), { state: "explained" });
|
|
291
|
+
},
|
|
292
|
+
tourShown(index) {
|
|
293
|
+
const id = state.tour.ids[index];
|
|
294
|
+
if (id) record(featureKey(id), { state: "explained" });
|
|
295
|
+
},
|
|
296
|
+
tourEnded(finished) {
|
|
297
|
+
record(pageKey(state.tour.page), finished ? { state: "explained" } : { add: 1 });
|
|
298
|
+
baseline(state.tour.page);
|
|
299
|
+
},
|
|
300
|
+
answer(choice) {
|
|
301
|
+
const offer = state.offer;
|
|
302
|
+
state.offer = null;
|
|
303
|
+
if (!offer) return;
|
|
304
|
+
if (offer.kind === "news") {
|
|
305
|
+
const ids = offer.items.map((n) => n.feature).filter((id) => !!id);
|
|
306
|
+
readNews(offer.page);
|
|
307
|
+
if (choice === "show" && ids.length) startTour(offer.page, ids);
|
|
308
|
+
} else if (offer.kind === "nudge") {
|
|
309
|
+
if (choice === "show") startTour(features[offer.nudge.feature].page, [offer.nudge.feature]);
|
|
310
|
+
else record(nudgeKey(offer.nudge.id), { dismissed: true });
|
|
311
|
+
} else if (choice === "show") {
|
|
312
|
+
startTour(offer.page);
|
|
313
|
+
} else {
|
|
314
|
+
record(pageKey(offer.page), { add: 1, dismissed: choice === "never" });
|
|
315
|
+
baseline(offer.page);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
const HINT_SYSTEM = /* @__PURE__ */ Symbol.for("lp-hint-system");
|
|
321
|
+
function provideHintSystem(system) {
|
|
322
|
+
provide(HINT_SYSTEM, system);
|
|
323
|
+
}
|
|
324
|
+
function useHintSystem() {
|
|
325
|
+
return inject(HINT_SYSTEM, null);
|
|
326
|
+
}
|
|
327
|
+
export {
|
|
328
|
+
createHintSystem,
|
|
329
|
+
hintKey,
|
|
330
|
+
hintSummary,
|
|
331
|
+
localHintStorage,
|
|
332
|
+
provideHintSystem,
|
|
333
|
+
useHintSystem
|
|
334
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Directive } from "vue";
|
|
2
|
+
export type HintEvent = "seen" | "used" | "hover";
|
|
3
|
+
type Listener = (id: string, el: HTMLElement) => void;
|
|
4
|
+
/** `v-hint="'page.feature'"` on the element that represents a feature. */
|
|
5
|
+
export declare const vHint: Directive<HTMLElement, string | null | undefined>;
|
|
6
|
+
export declare const hints: {
|
|
7
|
+
/** Ids of everything hinted that is in the DOM now. Reactive. */
|
|
8
|
+
ids(): string[];
|
|
9
|
+
/** Whether an element with this id is in the DOM. Reactive. */
|
|
10
|
+
present(id: string): boolean;
|
|
11
|
+
/** The first connected element with this id that has a box, or null. */
|
|
12
|
+
element(id: string): HTMLElement | null;
|
|
13
|
+
/** Subscribe to a signal; returns the unsubscribe. */
|
|
14
|
+
on(event: HintEvent, listener: Listener): () => void;
|
|
15
|
+
/** Report use that is not a click on the element. */
|
|
16
|
+
use(id: string): void;
|
|
17
|
+
};
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { shallowReactive } from "vue";
|
|
2
|
+
const SEEN_MS = 800;
|
|
3
|
+
const SEEN_RATIO = 0.5;
|
|
4
|
+
const mounted = shallowReactive(/* @__PURE__ */ new Map());
|
|
5
|
+
const listeners = { seen: /* @__PURE__ */ new Set(), used: /* @__PURE__ */ new Set(), hover: /* @__PURE__ */ new Set() };
|
|
6
|
+
const idOf = /* @__PURE__ */ new WeakMap();
|
|
7
|
+
const timers = /* @__PURE__ */ new WeakMap();
|
|
8
|
+
const seenOnce = /* @__PURE__ */ new WeakSet();
|
|
9
|
+
function emit(event, id, el) {
|
|
10
|
+
for (const listener of listeners[event]) listener(id, el);
|
|
11
|
+
}
|
|
12
|
+
let observer = null;
|
|
13
|
+
function watchSight(el) {
|
|
14
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
15
|
+
seenOnce.add(el);
|
|
16
|
+
emit("seen", idOf.get(el), el);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
observer ??= new IntersectionObserver(
|
|
20
|
+
(entries) => {
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
const target = entry.target;
|
|
23
|
+
const id = idOf.get(target);
|
|
24
|
+
if (!id || seenOnce.has(target)) continue;
|
|
25
|
+
clearTimeout(timers.get(target));
|
|
26
|
+
if (entry.intersectionRatio >= SEEN_RATIO) {
|
|
27
|
+
timers.set(
|
|
28
|
+
target,
|
|
29
|
+
setTimeout(() => {
|
|
30
|
+
seenOnce.add(target);
|
|
31
|
+
observer?.unobserve(target);
|
|
32
|
+
emit("seen", id, target);
|
|
33
|
+
}, SEEN_MS)
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
{ threshold: [0, SEEN_RATIO] }
|
|
39
|
+
);
|
|
40
|
+
observer.observe(el);
|
|
41
|
+
}
|
|
42
|
+
function onClick(event) {
|
|
43
|
+
const el = event.currentTarget;
|
|
44
|
+
const id = idOf.get(el);
|
|
45
|
+
if (id) emit("used", id, el);
|
|
46
|
+
}
|
|
47
|
+
function onHover(event) {
|
|
48
|
+
const el = event.currentTarget;
|
|
49
|
+
const id = idOf.get(el);
|
|
50
|
+
if (id) emit("hover", id, el);
|
|
51
|
+
}
|
|
52
|
+
function attach(el, id) {
|
|
53
|
+
idOf.set(el, id);
|
|
54
|
+
el.dataset.hint = id;
|
|
55
|
+
const set = mounted.get(id) ?? /* @__PURE__ */ new Set();
|
|
56
|
+
set.add(el);
|
|
57
|
+
mounted.set(id, new Set(set));
|
|
58
|
+
el.addEventListener("click", onClick, { capture: true });
|
|
59
|
+
el.addEventListener("pointerenter", onHover);
|
|
60
|
+
el.addEventListener("focusin", onHover);
|
|
61
|
+
watchSight(el);
|
|
62
|
+
}
|
|
63
|
+
function detach(el) {
|
|
64
|
+
const id = idOf.get(el);
|
|
65
|
+
if (!id) return;
|
|
66
|
+
clearTimeout(timers.get(el));
|
|
67
|
+
observer?.unobserve(el);
|
|
68
|
+
el.removeEventListener("click", onClick, { capture: true });
|
|
69
|
+
el.removeEventListener("pointerenter", onHover);
|
|
70
|
+
el.removeEventListener("focusin", onHover);
|
|
71
|
+
const set = new Set(mounted.get(id));
|
|
72
|
+
set.delete(el);
|
|
73
|
+
if (set.size) mounted.set(id, set);
|
|
74
|
+
else mounted.delete(id);
|
|
75
|
+
idOf.delete(el);
|
|
76
|
+
}
|
|
77
|
+
const vHint = {
|
|
78
|
+
mounted(el, binding) {
|
|
79
|
+
if (binding.value) attach(el, binding.value);
|
|
80
|
+
},
|
|
81
|
+
updated(el, binding) {
|
|
82
|
+
if (binding.value === idOf.get(el)) return;
|
|
83
|
+
detach(el);
|
|
84
|
+
if (binding.value) attach(el, binding.value);
|
|
85
|
+
},
|
|
86
|
+
beforeUnmount(el) {
|
|
87
|
+
detach(el);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const hints = {
|
|
91
|
+
/** Ids of everything hinted that is in the DOM now. Reactive. */
|
|
92
|
+
ids() {
|
|
93
|
+
return [...mounted.keys()];
|
|
94
|
+
},
|
|
95
|
+
/** Whether an element with this id is in the DOM. Reactive. */
|
|
96
|
+
present(id) {
|
|
97
|
+
return mounted.has(id);
|
|
98
|
+
},
|
|
99
|
+
/** The first connected element with this id that has a box, or null. */
|
|
100
|
+
element(id) {
|
|
101
|
+
for (const el of mounted.get(id) ?? []) {
|
|
102
|
+
if (el.isConnected && el.getClientRects().length) return el;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
},
|
|
106
|
+
/** Subscribe to a signal; returns the unsubscribe. */
|
|
107
|
+
on(event, listener) {
|
|
108
|
+
listeners[event].add(listener);
|
|
109
|
+
return () => listeners[event].delete(listener);
|
|
110
|
+
},
|
|
111
|
+
/** Report use that is not a click on the element. */
|
|
112
|
+
use(id) {
|
|
113
|
+
const el = hints.element(id);
|
|
114
|
+
for (const listener of listeners.used) listener(id, el ?? document.body);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
export {
|
|
118
|
+
hints,
|
|
119
|
+
vHint
|
|
120
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ref, onMounted, onBeforeUnmount } from "vue";
|
|
2
|
+
function useMediaQuery(query) {
|
|
3
|
+
const matches = ref(false);
|
|
4
|
+
let list;
|
|
5
|
+
const update = () => {
|
|
6
|
+
matches.value = !!list?.matches;
|
|
7
|
+
};
|
|
8
|
+
onMounted(() => {
|
|
9
|
+
list = window.matchMedia?.(query);
|
|
10
|
+
update();
|
|
11
|
+
list?.addEventListener("change", update);
|
|
12
|
+
});
|
|
13
|
+
onBeforeUnmount(() => list?.removeEventListener("change", update));
|
|
14
|
+
return matches;
|
|
15
|
+
}
|
|
16
|
+
export {
|
|
17
|
+
useMediaQuery
|
|
18
|
+
};
|
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
import { ref, toValue, onMounted, onBeforeUnmount, readonly } from "vue";
|
|
2
2
|
import { useHotkeys } from "./useHotkeys.js";
|
|
3
|
+
import { scrollParent } from "./scroll.js";
|
|
3
4
|
const HOLD_MS = 320;
|
|
4
5
|
const SLOP_PX = 8;
|
|
5
6
|
const EDGE_PX = 40;
|
|
6
7
|
const SCROLL_STEP = 14;
|
|
7
|
-
function scrollParent(el) {
|
|
8
|
-
for (let node = el?.parentElement; node; node = node.parentElement) {
|
|
9
|
-
const { overflowY } = getComputedStyle(node);
|
|
10
|
-
if ((overflowY === "auto" || overflowY === "scroll") && node.scrollHeight > node.clientHeight) return node;
|
|
11
|
-
}
|
|
12
|
-
return null;
|
|
13
|
-
}
|
|
14
8
|
function usePaintSelect(options) {
|
|
15
9
|
const resolve = options.resolve ?? ((raw) => raw);
|
|
16
10
|
let stroke = null;
|
package/dist/icons-all.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -157,6 +157,14 @@ export { default as LpThemeSwitcher } from "./components/LpThemeSwitcher.vue";
|
|
|
157
157
|
export { default as LpTilt } from "./components/LpTilt.vue";
|
|
158
158
|
export { default as LpToaster } from "./components/LpToaster.vue";
|
|
159
159
|
export { default as LpTooltip } from "./components/LpTooltip.vue";
|
|
160
|
+
export { default as LpTour } from "./components/LpTour.vue";
|
|
161
|
+
export type { TourContext, TourStep } from "./components/LpTour.vue";
|
|
162
|
+
export { holeOf, openContextMenu, panelsAround } from "./components/tour";
|
|
163
|
+
export { default as LpHintLayer } from "./components/LpHintLayer.vue";
|
|
164
|
+
export { default as LpHints } from "./components/LpHints.vue";
|
|
165
|
+
export { createHintSystem, hintKey, hintSummary, localHintStorage, provideHintSystem, useHintSystem } from "./composables/useHintSystem";
|
|
166
|
+
export type { HintDelta, HintFeature, HintLevel, HintNews, HintNudge, HintOffer, HintPage, HintRecord, HintStorage, HintSummary, HintSystem, HintSystemOptions, HintTiming, } from "./composables/useHintSystem";
|
|
167
|
+
export type { HintNote } from "./components/LpHintLayer.vue";
|
|
160
168
|
export { default as LpTooltipProvider } from "./components/LpTooltipProvider.vue";
|
|
161
169
|
export { default as LpTopologyCanvas } from "./components/LpTopologyCanvas.vue";
|
|
162
170
|
export type { CanvasViewport, EdgeCategory, EdgeObserved, TopologyEdge, TopologyLane, TopologyNode, } from "./components/LpTopologyCanvas.vue";
|
|
@@ -205,6 +213,11 @@ export type { FieldContext, FieldControlState } from "./composables/useField";
|
|
|
205
213
|
export type { UseInputFilterOptions } from "./composables/useInputFilter";
|
|
206
214
|
export { easeOut, easeTravel, easingToken, prefersReducedMotion } from "./composables/easing";
|
|
207
215
|
export { useReveal } from "./composables/useReveal";
|
|
216
|
+
export { hints, vHint } from "./composables/useHints";
|
|
217
|
+
export type { HintEvent } from "./composables/useHints";
|
|
218
|
+
export { scrollParent, scrollToElement } from "./composables/scroll";
|
|
219
|
+
export type { ScrollParentOptions, ScrollToElementOptions } from "./composables/scroll";
|
|
220
|
+
export { useMediaQuery } from "./composables/useMediaQuery";
|
|
208
221
|
export type { UseReveal, UseRevealOptions } from "./composables/useReveal";
|
|
209
222
|
export { useAsyncTask } from "./composables/useAsyncTask";
|
|
210
223
|
export { useRovingFocus } from "./composables/useRovingFocus";
|