@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.
Files changed (182) hide show
  1. package/dist/chunks/{ContextMenuItems-BtKU7074.js → ContextMenuItems-BY5sTVQF.js} +1 -1
  2. package/dist/chunks/{LayoutCanvas-BkaGXTjx.js → LayoutCanvas-BcGLJVBq.js} +1 -1
  3. package/dist/chunks/{LayoutNode-B3_9E1Xa.js → LayoutNode-BkBn6dvM.js} +1 -1
  4. package/dist/chunks/{LpActionBar-C3hkVsFz.js → LpActionBar-DCN-_pZP.js} +2 -2
  5. package/dist/chunks/{LpAddressInput-NUHmplCd.js → LpAddressInput-Dio6Sqed.js} +7 -7
  6. package/dist/chunks/{LpAlert-B6ilrM7T.js → LpAlert-CXMn1GYE.js} +1 -1
  7. package/dist/chunks/{LpAppShell-BEGjSqFl.js → LpAppShell-Be6T96td.js} +3 -3
  8. package/dist/chunks/{LpAutocomplete-C8IgSMsi.js → LpAutocomplete-hJ_6CHlh.js} +1 -1
  9. package/dist/chunks/{LpAvatar-LTvy0OkE.js → LpAvatar-BxNs23Xu.js} +2 -2
  10. package/dist/chunks/{LpBreadcrumbs-D4TsoFju.js → LpBreadcrumbs-BLzOEgAF.js} +1 -1
  11. package/dist/chunks/{LpButton-BdfZFNqi.js → LpButton-vhlwmpBy.js} +1 -1
  12. package/dist/chunks/{LpCalendar-DrA2McTU.js → LpCalendar-BLoE8Qm7.js} +1 -1
  13. package/dist/chunks/{LpCard-DbjO5yaF.js → LpCard-DhR7u0d8.js} +1 -1
  14. package/dist/chunks/{LpCheckbox-DvVYeOD-.js → LpCheckbox-B_C8sOoT.js} +1 -1
  15. package/dist/chunks/{LpCodeBlock-ca7ANFyW.js → LpCodeBlock-CXsqJwDy.js} +1 -1
  16. package/dist/chunks/{LpColorPicker-D1sQqtXS.js → LpColorPicker-DQ88niXq.js} +2 -2
  17. package/dist/chunks/{LpConfirmDialog-CrbUZgJE.js → LpConfirmDialog-DxlSdjmk.js} +3 -3
  18. package/dist/chunks/{LpConfirmHost-BgMIV8r9.js → LpConfirmHost-BcrmTNu_.js} +1 -1
  19. package/dist/chunks/{LpContextMenu-EF_90u3g.js → LpContextMenu-B1NGfpp1.js} +2 -2
  20. package/dist/chunks/{LpDatePicker-BrdFtSTy.js → LpDatePicker-B-VMHMgy.js} +3 -3
  21. package/dist/chunks/{LpDateRangePicker-Drd7EbIz.js → LpDateRangePicker-B68KPKPl.js} +6 -15
  22. package/dist/chunks/{LpDisclosure-Cc23-JOT.js → LpDisclosure-Ct-VBHxt.js} +1 -1
  23. package/dist/chunks/{LpDrawer-CBFVD624.js → LpDrawer-DSknigxH.js} +5 -7
  24. package/dist/chunks/{LpDropdownMenu-DrpJJBp0.js → LpDropdownMenu-C7oHVlKY.js} +2 -2
  25. package/dist/chunks/{LpEmptyState-BJbZXC8D.js → LpEmptyState-CBwwkkdA.js} +1 -1
  26. package/dist/chunks/{LpFieldNotice-wtieLUTj.js → LpFieldNotice-9hH4ITZa.js} +1 -1
  27. package/dist/chunks/{LpFileTree-3E4w4Rjr.js → LpFileTree-CMA0_rsB.js} +2 -2
  28. package/dist/chunks/{LpFileTreeNode-B4uB8Ks_.js → LpFileTreeNode-B4T258hF.js} +3 -3
  29. package/dist/chunks/{LpFormField-CtVYj6tO.js → LpFormField-BpQ6WJPe.js} +1 -1
  30. package/dist/chunks/LpHintLayer-B5-6FyEf.js +154 -0
  31. package/dist/chunks/LpHints-BqoZXHZk.js +109 -0
  32. package/dist/chunks/{LpIcon-DYtaid0K.js → LpIcon-B5guGKrW.js} +1 -1
  33. package/dist/chunks/{LpInput-D-YGQbEv.js → LpInput-WvyP02ID.js} +2 -2
  34. package/dist/chunks/{LpLightbox-EVXzAq62.js → LpLightbox-Dq-e36uT.js} +1 -1
  35. package/dist/chunks/{LpLogViewer-BfgtXVKm.js → LpLogViewer-DwiYE32z.js} +2 -2
  36. package/dist/chunks/{LpMapPicker-Cc84LaM3.js → LpMapPicker-BH5csaBt.js} +1 -1
  37. package/dist/chunks/{LpMediaGallery-BnlkN_1f.js → LpMediaGallery-B1kDypJ-.js} +2 -2
  38. package/dist/chunks/{LpModal-JNa3B_Lr.js → LpModal-BxYssnXg.js} +1 -1
  39. package/dist/chunks/{LpModalNav-DXBilXr0.js → LpModalNav-Bn2ws64b.js} +4 -4
  40. package/dist/chunks/{LpNotificationBell-DEL-RoHS.js → LpNotificationBell-DJfs4JjQ.js} +4 -4
  41. package/dist/chunks/{LpNumberField-B77O-z28.js → LpNumberField-CJPjmoDz.js} +1 -1
  42. package/dist/chunks/{LpPagination-BcDlo3r_.js → LpPagination-DehAaJLY.js} +1 -1
  43. package/dist/chunks/{LpPasswordInput-eKzQhbQP.js → LpPasswordInput-Bg5uTZbx.js} +2 -2
  44. package/dist/chunks/{LpPhoneInput-BCbrrhgE.js → LpPhoneInput-DStwcbnQ.js} +2 -2
  45. package/dist/chunks/{LpPie-B7ti5qCH.js → LpPie-DeTkbbOK.js} +3 -3
  46. package/dist/chunks/{LpRangeCalendar-DZuaRV0W.js → LpRangeCalendar-D5sDID9W.js} +1 -1
  47. package/dist/chunks/{LpSection-tKEH0jYf.js → LpSection-B19VPYmr.js} +1 -1
  48. package/dist/chunks/{LpSegmented-C09EEam2.js → LpSegmented-D3Eam8sa.js} +1 -1
  49. package/dist/chunks/{LpSelect-DNIL6MB9.js → LpSelect-DynjWK1A.js} +1 -1
  50. package/dist/chunks/{LpSidebar-C2oolgwy.js → LpSidebar-CshW0fqC.js} +3 -3
  51. package/dist/chunks/{LpSidebarNav-C9dHu-2b.js → LpSidebarNav-C9b3i9G2.js} +2 -2
  52. package/dist/chunks/{LpStat-B-7udBRP.js → LpStat-BS2TEXYK.js} +1 -1
  53. package/dist/chunks/{LpStepper-C7XiVKZj.js → LpStepper-BSglWF45.js} +1 -1
  54. package/dist/chunks/{LpTableFilter-CrW7_Rj4.js → LpTableFilter-CwHXVxJt.js} +2 -2
  55. package/dist/chunks/{LpTableOfContents-BUcAfGPQ.js → LpTableOfContents-DYuqkByy.js} +2 -8
  56. package/dist/chunks/{LpTabs-BMSBB78h.js → LpTabs-BBGRHY38.js} +1 -1
  57. package/dist/chunks/{LpTagsInput-D1SuqbhB.js → LpTagsInput-CHbQoAAA.js} +3 -3
  58. package/dist/chunks/{LpTextarea-uuScWo6-.js → LpTextarea-Bz2IWhTH.js} +1 -1
  59. package/dist/chunks/{LpThemeSwitcher-Fz85BCRS.js → LpThemeSwitcher-B_qhSl7q.js} +2 -2
  60. package/dist/chunks/{LpTimeField-Cqe7fXnh.js → LpTimeField-DvGXgdES.js} +1 -1
  61. package/dist/chunks/{LpToaster-DTtWuKvq.js → LpToaster-Bo9Q4QBP.js} +1 -1
  62. package/dist/chunks/{LpTooltip-CXVKcOXT.js → LpTooltip-suFCM34F.js} +2 -2
  63. package/dist/chunks/LpTour-D7m9ZxNk.js +334 -0
  64. package/dist/chunks/{LpUptimeBar-ugcv25xj.js → LpUptimeBar-B4ltCfno.js} +1 -1
  65. package/dist/chunks/{MenuTitle-J3ZDYjpV.js → MenuTitle-DQP6hc2N.js} +1 -1
  66. package/dist/chunks/TourCard-DCHOjr6e.js +100 -0
  67. package/dist/chunks/icons-QqBscG5I.js +43 -0
  68. package/dist/component-names.d.ts +1 -1
  69. package/dist/components/ContextMenuItems.vue.js +1 -1
  70. package/dist/components/LayoutCanvas.vue.js +1 -1
  71. package/dist/components/LayoutNode.vue.js +1 -1
  72. package/dist/components/LpActionBar.vue.js +1 -1
  73. package/dist/components/LpAddressInput.vue.js +1 -1
  74. package/dist/components/LpAlert.vue.js +1 -1
  75. package/dist/components/LpAppShell.vue.js +1 -1
  76. package/dist/components/LpAutocomplete.vue.js +1 -1
  77. package/dist/components/LpAvatar.vue.js +1 -1
  78. package/dist/components/LpBreadcrumbs.vue.js +1 -1
  79. package/dist/components/LpButton.vue.js +1 -1
  80. package/dist/components/LpCalendar.vue.js +1 -1
  81. package/dist/components/LpCard.vue.js +1 -1
  82. package/dist/components/LpChart.vue.js +2 -2
  83. package/dist/components/LpCheckbox.vue.js +1 -1
  84. package/dist/components/LpCodeBlock.vue.js +1 -1
  85. package/dist/components/LpColorPicker.vue.js +1 -1
  86. package/dist/components/LpCommandPalette.vue.js +2 -2
  87. package/dist/components/LpConfirmDialog.vue.js +1 -1
  88. package/dist/components/LpConfirmHost.vue.js +1 -1
  89. package/dist/components/LpContextMenu.vue.js +1 -1
  90. package/dist/components/LpDatePicker.vue.js +1 -1
  91. package/dist/components/LpDateRangePicker.vue.js +1 -1
  92. package/dist/components/LpDisclosure.vue.js +1 -1
  93. package/dist/components/LpDrawer.vue.js +1 -1
  94. package/dist/components/LpDropdownMenu.vue.js +1 -1
  95. package/dist/components/LpDropzone.vue.js +1 -1
  96. package/dist/components/LpEmptyState.vue.js +1 -1
  97. package/dist/components/LpFieldNotice.vue.js +1 -1
  98. package/dist/components/LpFileTree.vue.js +1 -1
  99. package/dist/components/LpFileTreeNode.vue.js +1 -1
  100. package/dist/components/LpFormField.vue.js +1 -1
  101. package/dist/components/LpHintLayer.vue.d.ts +33 -0
  102. package/dist/components/LpHintLayer.vue.js +4 -0
  103. package/dist/components/LpHints.vue.d.ts +20 -0
  104. package/dist/components/LpHints.vue.js +4 -0
  105. package/dist/components/LpIcon.vue.js +1 -1
  106. package/dist/components/LpImageEditor.vue.js +2 -2
  107. package/dist/components/LpInput.vue.js +1 -1
  108. package/dist/components/LpLightbox.vue.js +1 -1
  109. package/dist/components/LpLogViewer.vue.js +1 -1
  110. package/dist/components/LpMapPicker.vue.js +1 -1
  111. package/dist/components/LpMediaGallery.vue.js +1 -1
  112. package/dist/components/LpModal.vue.js +1 -1
  113. package/dist/components/LpModalNav.vue.js +1 -1
  114. package/dist/components/LpNotificationBell.vue.js +1 -1
  115. package/dist/components/LpNumberField.vue.js +1 -1
  116. package/dist/components/LpPagination.vue.js +1 -1
  117. package/dist/components/LpPaletteFacts.vue.js +1 -1
  118. package/dist/components/LpPaletteHeader.vue.js +2 -2
  119. package/dist/components/LpPasswordInput.vue.js +1 -1
  120. package/dist/components/LpPhoneInput.vue.js +1 -1
  121. package/dist/components/LpPie.vue.js +1 -1
  122. package/dist/components/LpRangeCalendar.vue.js +1 -1
  123. package/dist/components/LpSection.vue.js +1 -1
  124. package/dist/components/LpSegmented.vue.js +1 -1
  125. package/dist/components/LpSelect.vue.js +1 -1
  126. package/dist/components/LpShortcutsDialog.vue.js +1 -1
  127. package/dist/components/LpSidebar.vue.js +1 -1
  128. package/dist/components/LpSidebarNav.vue.js +1 -1
  129. package/dist/components/LpStat.vue.js +1 -1
  130. package/dist/components/LpStepper.vue.js +1 -1
  131. package/dist/components/LpTable.vue.js +6 -6
  132. package/dist/components/LpTableFilter.vue.js +1 -1
  133. package/dist/components/LpTableOfContents.vue.js +1 -1
  134. package/dist/components/LpTabs.vue.js +1 -1
  135. package/dist/components/LpTagsInput.vue.js +1 -1
  136. package/dist/components/LpTextarea.vue.js +1 -1
  137. package/dist/components/LpThemeSwitcher.vue.js +1 -1
  138. package/dist/components/LpTimeField.vue.js +1 -1
  139. package/dist/components/LpToaster.vue.js +1 -1
  140. package/dist/components/LpTooltip.vue.js +1 -1
  141. package/dist/components/LpTour.vue.d.ts +98 -0
  142. package/dist/components/LpTour.vue.js +4 -0
  143. package/dist/components/LpUptimeBar.vue.js +1 -1
  144. package/dist/components/MenuTitle.vue.js +1 -1
  145. package/dist/components/TourCard.vue.d.ts +28 -0
  146. package/dist/components/TourCard.vue.js +4 -0
  147. package/dist/components/tour.d.ts +21 -0
  148. package/dist/components/tour.js +38 -0
  149. package/dist/composables/scroll.d.ts +17 -0
  150. package/dist/composables/scroll.js +29 -0
  151. package/dist/composables/useGamepadNav.js +2 -7
  152. package/dist/composables/useHintSystem.d.ts +195 -0
  153. package/dist/composables/useHintSystem.js +334 -0
  154. package/dist/composables/useHints.d.ts +18 -0
  155. package/dist/composables/useHints.js +120 -0
  156. package/dist/composables/useMediaQuery.d.ts +2 -0
  157. package/dist/composables/useMediaQuery.js +18 -0
  158. package/dist/composables/usePaintSelect.js +1 -7
  159. package/dist/icons-all.js +1 -1
  160. package/dist/index.d.ts +13 -0
  161. package/dist/index.js +89 -64
  162. package/package.json +1 -1
  163. package/src/component-names.ts +3 -0
  164. package/src/components/LpDateRangePicker.vue +3 -12
  165. package/src/components/LpDrawer.vue +4 -7
  166. package/src/components/LpHintLayer.vue +148 -0
  167. package/src/components/LpHints.vue +116 -0
  168. package/src/components/LpTableOfContents.vue +2 -14
  169. package/src/components/LpTooltip.vue +2 -4
  170. package/src/components/LpTour.vue +358 -0
  171. package/src/components/TourCard.vue +41 -0
  172. package/src/components/tour.ts +57 -0
  173. package/src/composables/scroll.ts +54 -0
  174. package/src/composables/useGamepadNav.ts +2 -7
  175. package/src/composables/useHintSystem.ts +512 -0
  176. package/src/composables/useHints.ts +151 -0
  177. package/src/composables/useMediaQuery.ts +20 -0
  178. package/src/composables/usePaintSelect.ts +1 -8
  179. package/src/index.ts +27 -0
  180. package/src/kit-icons.ts +1 -1
  181. package/src/tokens/tokens.css +1 -0
  182. 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,2 @@
1
+ import { type Ref } from "vue";
2
+ export declare function useMediaQuery(query: string): Ref<boolean>;
@@ -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
@@ -1,3 +1,3 @@
1
1
  import lucide from "@iconify-json/lucide/icons.json";
2
- import { a as registerIconCollection } from "./chunks/icons-CkA0njzm.js";
2
+ import { a as registerIconCollection } from "./chunks/icons-QqBscG5I.js";
3
3
  registerIconCollection(lucide);
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";