@leavepulse/ui 0.75.0 → 0.76.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +47 -0
  2. package/dist/assets/{LpShortcutsDialog-BUp_BX4s.css → LpShortcutsDialog-C6uzPeMf.css} +7 -7
  3. package/dist/assets/LpTabs-CJ0rYQbx.css +18 -0
  4. package/dist/chunks/{LpAddressInput-Al3HT5ey.js → LpAddressInput-De_LTYM7.js} +1 -1
  5. package/dist/chunks/{LpAppShell-CpSSm5WT.js → LpAppShell-Cpy_t1U_.js} +1 -1
  6. package/dist/chunks/{LpConfirmDialog-DY2VjDji.js → LpConfirmDialog-CphJR6Q9.js} +4 -2
  7. package/dist/chunks/{LpConfirmHost-DvSZTJ4N.js → LpConfirmHost-QSJ0zl63.js} +1 -1
  8. package/dist/chunks/{LpDrawer-RRamxbLV.js → LpDrawer-DQZixOqJ.js} +13 -0
  9. package/dist/chunks/{LpModal-BnjZ2iem.js → LpModal-C0tGg_D_.js} +13 -0
  10. package/dist/chunks/{LpModalNav-DQpV2kdn.js → LpModalNav-iDDtT9c1.js} +2 -2
  11. package/dist/chunks/{LpSidebar-BoNLKgh-.js → LpSidebar-D6zO8vLm.js} +1 -1
  12. package/dist/components/LpAddressInput.vue.js +1 -1
  13. package/dist/components/LpAppShell.vue.js +1 -1
  14. package/dist/components/LpConfirmDialog.vue.d.ts +4 -0
  15. package/dist/components/LpConfirmDialog.vue.js +1 -1
  16. package/dist/components/LpConfirmHost.vue.js +1 -1
  17. package/dist/components/LpDrawer.vue.d.ts +16 -0
  18. package/dist/components/LpDrawer.vue.js +1 -1
  19. package/dist/components/LpModal.vue.d.ts +9 -0
  20. package/dist/components/LpModal.vue.js +1 -1
  21. package/dist/components/LpModalNav.vue.js +1 -1
  22. package/dist/components/LpShortcutsDialog.vue.d.ts +4 -0
  23. package/dist/components/LpShortcutsDialog.vue.js +6 -4
  24. package/dist/components/LpSidebar.vue.js +1 -1
  25. package/dist/components/LpTabs.vue.d.ts +9 -0
  26. package/dist/components/LpTabs.vue.js +18 -4
  27. package/dist/composables/overlayHistory.d.ts +33 -0
  28. package/dist/composables/overlayHistory.js +250 -0
  29. package/dist/composables/useOverlayHistory.d.ts +45 -0
  30. package/dist/composables/useOverlayHistory.js +84 -0
  31. package/dist/index.d.ts +3 -0
  32. package/dist/index.js +12 -8
  33. package/package.json +2 -1
  34. package/src/components/LpConfirmDialog.vue +5 -1
  35. package/src/components/LpDrawer.vue +21 -0
  36. package/src/components/LpModal.vue +21 -1
  37. package/src/components/LpShortcutsDialog.vue +5 -1
  38. package/src/components/LpTabs.vue +26 -2
  39. package/src/composables/overlayHistory.ts +403 -0
  40. package/src/composables/useOverlayHistory.ts +173 -0
  41. package/src/index.ts +3 -0
  42. package/dist/assets/LpTabs-CDchzoRL.css +0 -18
@@ -0,0 +1,250 @@
1
+ const KEY = "__lpOverlay";
2
+ const SETTLE_MS = 300;
3
+ const STATE_KEY = /* @__PURE__ */ Symbol.for("@leavepulse/ui:overlay-history");
4
+ const g = globalThis;
5
+ const fresh = {
6
+ router: null,
7
+ replaying: false,
8
+ popping: null,
9
+ installed: false,
10
+ live: [],
11
+ closed: /* @__PURE__ */ new Map(),
12
+ dead: /* @__PURE__ */ new Map(),
13
+ silent: 0,
14
+ keep: null,
15
+ waiting: [],
16
+ href: "",
17
+ serial: 0,
18
+ internal: false,
19
+ settle: 0
20
+ };
21
+ const s = g[STATE_KEY] ??= fresh;
22
+ function markOf(state) {
23
+ if (!state || typeof state !== "object") return null;
24
+ const mark = state[KEY];
25
+ if (!mark || typeof mark !== "object") return null;
26
+ const { token, depth } = mark;
27
+ return typeof token === "string" && typeof depth === "number" ? { token, depth } : null;
28
+ }
29
+ function pathOf(href) {
30
+ try {
31
+ return new URL(href, location.href).pathname;
32
+ } catch {
33
+ return href;
34
+ }
35
+ }
36
+ function own(run) {
37
+ s.internal = true;
38
+ try {
39
+ return run();
40
+ } finally {
41
+ s.internal = false;
42
+ }
43
+ }
44
+ function silentGo(delta) {
45
+ if (delta === 0) return;
46
+ s.silent++;
47
+ mute();
48
+ own(() => history.go(delta));
49
+ }
50
+ function whenSettled(run) {
51
+ if (s.silent > 0) s.waiting.push(run);
52
+ else run();
53
+ }
54
+ function flushWaiting() {
55
+ if (s.silent > 0) return;
56
+ const queued = s.waiting.splice(0);
57
+ for (const run of queued) run();
58
+ }
59
+ function current() {
60
+ return markOf(history.state);
61
+ }
62
+ function onTop() {
63
+ const top = s.live[s.live.length - 1];
64
+ return !!top && current()?.token === top.token;
65
+ }
66
+ function leavePage() {
67
+ const gone = s.live.splice(0);
68
+ for (const item of gone.reverse()) {
69
+ bury(item);
70
+ item.entry.navigated?.();
71
+ }
72
+ s.closed.clear();
73
+ }
74
+ function handlePop(event) {
75
+ const before = s.href;
76
+ s.href = location.href;
77
+ if (s.silent > 0) {
78
+ s.silent--;
79
+ const keep = s.silent === 0 ? s.keep : null;
80
+ if (s.silent === 0) s.keep = null;
81
+ if (keep && keep.href !== location.href) restore(keep, event);
82
+ if (before === location.href) event.stopImmediatePropagation();
83
+ flushWaiting();
84
+ return;
85
+ }
86
+ window.clearTimeout(s.settle);
87
+ const sameAddress = before === location.href;
88
+ const mark = markOf(event.state);
89
+ const at = mark ? s.live.findIndex((item) => item.token === mark.token) : -1;
90
+ if (at >= 0) {
91
+ closeAbove(at);
92
+ if (sameAddress) event.stopImmediatePropagation();
93
+ return;
94
+ }
95
+ const reopen = mark ? s.closed.get(mark.token) : void 0;
96
+ if (reopen && sameAddress) {
97
+ event.stopImmediatePropagation();
98
+ if (reopen.entry.alive && reopen.entry.forward?.()) {
99
+ s.closed.delete(reopen.token);
100
+ s.live.push(reopen);
101
+ return;
102
+ }
103
+ silentGo(-1);
104
+ return;
105
+ }
106
+ if (mark && (s.dead.has(mark.token) || reopen)) {
107
+ if (sameAddress) event.stopImmediatePropagation();
108
+ if (!sameAddress) {
109
+ silentGo(-mark.depth);
110
+ return;
111
+ }
112
+ const depth = mark.depth;
113
+ own(() => history.forward());
114
+ s.settle = window.setTimeout(() => {
115
+ if (markOf(history.state)?.token === mark.token) silentGo(-depth);
116
+ else mute();
117
+ }, SETTLE_MS);
118
+ return;
119
+ }
120
+ if (s.live.length === 0) return;
121
+ if (sameAddress) {
122
+ closeAbove(-1);
123
+ event.stopImmediatePropagation();
124
+ } else if (pathOf(before) !== pathOf(location.href)) {
125
+ leavePage();
126
+ }
127
+ }
128
+ function restore(keep, event) {
129
+ const mark = markOf(event.state);
130
+ const base = keep.state && typeof keep.state === "object" ? { ...keep.state } : {};
131
+ delete base[KEY];
132
+ own(() => history.replaceState(mark ? { ...base, [KEY]: mark } : base, "", keep.href));
133
+ s.href = location.href;
134
+ event.stopImmediatePropagation();
135
+ }
136
+ const DEAD_LIMIT = 200;
137
+ function bury(item) {
138
+ s.dead.set(item.token, item);
139
+ if (s.dead.size > DEAD_LIMIT) s.dead.delete(s.dead.keys().next().value);
140
+ }
141
+ function closeAbove(index) {
142
+ const popped = s.live.splice(index + 1);
143
+ for (const item of popped.reverse()) {
144
+ s.closed.set(item.token, item);
145
+ if (item.entry.alive) item.entry.back();
146
+ }
147
+ }
148
+ function onPop(event) {
149
+ if (s.replaying) return;
150
+ s.popping = { muted: !!s.router && !s.router.listening, before: s.href };
151
+ handlePop(event);
152
+ if (event.cancelBubble) {
153
+ s.popping = null;
154
+ mute();
155
+ }
156
+ }
157
+ function afterPop(event) {
158
+ if (s.replaying || !s.popping) return;
159
+ const { muted, before } = s.popping;
160
+ s.popping = null;
161
+ if (muted && before !== location.href) replay(event.state);
162
+ mute();
163
+ }
164
+ function replay(state) {
165
+ if (!s.router) return;
166
+ s.router.listening = true;
167
+ s.replaying = true;
168
+ try {
169
+ window.dispatchEvent(new PopStateEvent("popstate", { state }));
170
+ } finally {
171
+ s.replaying = false;
172
+ }
173
+ }
174
+ function mute() {
175
+ if (s.popping) return;
176
+ if (s.router) s.router.listening = !(current() || s.silent > 0);
177
+ }
178
+ function setHistoryRouter(router) {
179
+ if (s.router && s.router !== router) s.router.listening = true;
180
+ s.router = router;
181
+ if (typeof window === "undefined") return;
182
+ window.removeEventListener("popstate", afterPop);
183
+ window.addEventListener("popstate", afterPop);
184
+ mute();
185
+ }
186
+ function install() {
187
+ if (s.installed || typeof window === "undefined") return;
188
+ s.installed = true;
189
+ s.href = location.href;
190
+ window.addEventListener("popstate", onPop, { capture: true });
191
+ window.removeEventListener("popstate", afterPop);
192
+ window.addEventListener("popstate", afterPop);
193
+ const push = history.pushState.bind(history);
194
+ const replace = history.replaceState.bind(history);
195
+ history.pushState = function(data, unused, url) {
196
+ const before = location.href;
197
+ push(data, unused, url);
198
+ s.href = location.href;
199
+ if (s.internal) return;
200
+ s.closed.clear();
201
+ if (pathOf(before) !== pathOf(location.href)) leavePage();
202
+ mute();
203
+ };
204
+ history.replaceState = function(data, unused, url) {
205
+ const mark = s.internal ? null : current();
206
+ const keep = mark && data && typeof data === "object" && !markOf(data) ? { ...data, [KEY]: mark } : data;
207
+ replace(keep, unused, url);
208
+ s.href = location.href;
209
+ };
210
+ }
211
+ function pushEntry(entry) {
212
+ if (typeof window === "undefined") return;
213
+ install();
214
+ whenSettled(() => {
215
+ if (!entry.alive) return;
216
+ const token = `${Date.now().toString(36)}-${(s.serial++).toString(36)}`;
217
+ const depth = (current()?.depth ?? 0) + 1;
218
+ const base = history.state && typeof history.state === "object" ? history.state : {};
219
+ own(() => history.pushState({ ...base, [KEY]: { token, depth } }, "", location.href));
220
+ s.href = location.href;
221
+ s.closed.clear();
222
+ s.live.push({ entry, token, depth });
223
+ mute();
224
+ });
225
+ }
226
+ function dropEntry(entry) {
227
+ const at = s.live.findIndex((item) => item.entry === entry);
228
+ if (at === -1) return;
229
+ const positioned = onTop();
230
+ const removed = s.live.splice(at);
231
+ if (positioned) {
232
+ s.keep = { href: location.href, state: history.state };
233
+ silentGo(-removed.length);
234
+ return;
235
+ }
236
+ for (const item of removed) bury(item);
237
+ }
238
+ function isLive(entry) {
239
+ return s.live.some((item) => item.entry === entry);
240
+ }
241
+ function liveEntries() {
242
+ return s.live.length;
243
+ }
244
+ export {
245
+ dropEntry,
246
+ isLive,
247
+ liveEntries,
248
+ pushEntry,
249
+ setHistoryRouter
250
+ };
@@ -0,0 +1,45 @@
1
+ import { type MaybeRefOrGetter } from "vue";
2
+ import { type HistoryRouter } from "./overlayHistory";
3
+ export type OverlayHistoryMode = boolean | "reopen";
4
+ export interface ProvideOverlayHistoryOptions {
5
+ /**
6
+ * The app's router, so a Back or Forward that only closes an overlay does
7
+ * not reach it as a navigation (vue-router: the router instance). Its
8
+ * popstate listener runs before the kit's, so without this it sees each of
9
+ * those steps as a visit to the page it is already on: guards and
10
+ * `afterEach` run again and the route object is replaced, though nothing
11
+ * remounts. One router per page; the latest one given is used.
12
+ */
13
+ router?: HistoryRouter;
14
+ }
15
+ /** Turn browser history on for every overlay below the calling component. */
16
+ export declare function provideOverlayHistory(mode?: MaybeRefOrGetter<OverlayHistoryMode>, options?: ProvideOverlayHistoryOptions): void;
17
+ export interface OverlayHistoryOptions {
18
+ /** The overlay's own `history` prop; `undefined` follows `provideOverlayHistory`. */
19
+ mode: () => OverlayHistoryMode | undefined;
20
+ open: () => boolean;
21
+ /** Back left the entry: close the way the overlay's own ways out do. */
22
+ close: () => Promise<unknown>;
23
+ /** Forward under `"reopen"`: ask the app to open it again (`update:open` true). */
24
+ reopen: () => void;
25
+ /** The page went to another path: close without asking, the place it belonged to is gone. */
26
+ navigated: () => void;
27
+ }
28
+ /** Wire an overlay's `open` to a history entry. */
29
+ export declare function useOverlayHistory(options: OverlayHistoryOptions): void;
30
+ export interface StepHistoryOptions {
31
+ /** The component's own `history` prop; `undefined` follows the overlay it sits in. */
32
+ mode: () => boolean | undefined;
33
+ /** Show this value without recording it. */
34
+ set: (value: string) => void;
35
+ /** Whether the value can still be shown (the tab exists and is enabled). */
36
+ has: (value: string) => boolean;
37
+ }
38
+ /**
39
+ * Steps inside an overlay (a tab switch) as history entries. Outside an overlay
40
+ * it is off unless asked for: a page's own tabs belong in its URL, where the
41
+ * app's router already keeps them.
42
+ */
43
+ export declare function useStepHistory(options: StepHistoryOptions): {
44
+ record: (from: string, to: string) => void;
45
+ };
@@ -0,0 +1,84 @@
1
+ import { inject, computed, toValue, watch, onBeforeUnmount, provide, nextTick } from "vue";
2
+ import { isLive, pushEntry, dropEntry, setHistoryRouter } from "./overlayHistory.js";
3
+ const MODE = /* @__PURE__ */ Symbol("lp-overlay-history");
4
+ const SCOPE = /* @__PURE__ */ Symbol("lp-overlay-history-scope");
5
+ function provideOverlayHistory(mode = true, options = {}) {
6
+ provide(MODE, mode);
7
+ if (options.router) setHistoryRouter(options.router);
8
+ }
9
+ function useOverlayHistory(options) {
10
+ const inherited = inject(MODE, false);
11
+ const mode = computed(() => options.mode() ?? toValue(inherited));
12
+ const entry = {
13
+ alive: true,
14
+ back: () => {
15
+ void options.close().then(
16
+ () => nextTick(() => {
17
+ if (entry.alive && options.open() && !isLive(entry)) pushEntry(entry);
18
+ })
19
+ );
20
+ },
21
+ forward: () => {
22
+ if (mode.value !== "reopen") return false;
23
+ options.reopen();
24
+ void nextTick(() => {
25
+ if (!options.open() && isLive(entry)) dropEntry(entry);
26
+ });
27
+ return true;
28
+ },
29
+ navigated: () => {
30
+ if (options.open()) options.navigated();
31
+ }
32
+ };
33
+ watch(
34
+ () => [options.open(), mode.value !== false],
35
+ ([open, enabled]) => {
36
+ if (open && enabled) {
37
+ if (!isLive(entry)) pushEntry(entry);
38
+ } else if (isLive(entry)) {
39
+ dropEntry(entry);
40
+ }
41
+ },
42
+ { immediate: true }
43
+ );
44
+ onBeforeUnmount(() => {
45
+ entry.alive = false;
46
+ if (isLive(entry)) dropEntry(entry);
47
+ });
48
+ provide(SCOPE, { active: () => mode.value !== false && options.open() && isLive(entry) });
49
+ }
50
+ function useStepHistory(options) {
51
+ const scope = inject(SCOPE, null);
52
+ const enabled = () => {
53
+ const own = options.mode();
54
+ if (own === false) return false;
55
+ if (scope) return scope.active();
56
+ return own === true;
57
+ };
58
+ const entries = [];
59
+ function record(from, to) {
60
+ if (from === to || !enabled()) return;
61
+ const entry = {
62
+ alive: true,
63
+ back: () => options.set(from),
64
+ forward: () => {
65
+ if (!enabled() || !options.has(to)) return false;
66
+ options.set(to);
67
+ return true;
68
+ }
69
+ };
70
+ entries.splice(0, entries.length, ...entries.filter(isLive), entry);
71
+ pushEntry(entry);
72
+ }
73
+ onBeforeUnmount(() => {
74
+ for (const entry of entries) entry.alive = false;
75
+ const first = entries.find(isLive);
76
+ if (first) dropEntry(first);
77
+ });
78
+ return { record };
79
+ }
80
+ export {
81
+ provideOverlayHistory,
82
+ useOverlayHistory,
83
+ useStepHistory
84
+ };
package/dist/index.d.ts CHANGED
@@ -219,6 +219,9 @@ export type { UseGamepadNavOptions } from "./composables/useGamepadNav";
219
219
  export { focusableIn, nextInDirection } from "./composables/spatialNav";
220
220
  export type { Box, Direction } from "./composables/spatialNav";
221
221
  export { provideGlass, useGlass } from "./composables/useGlass";
222
+ export { provideOverlayHistory, useOverlayHistory, useStepHistory } from "./composables/useOverlayHistory";
223
+ export type { OverlayHistoryMode, OverlayHistoryOptions, ProvideOverlayHistoryOptions, StepHistoryOptions } from "./composables/useOverlayHistory";
224
+ export type { HistoryRouter } from "./composables/overlayHistory";
222
225
  export { LP_CONFIG, lpConfig, mergeConfig, provideLpConfig, useLabels, useLinkComponent, useLpConfig } from "./composables/useLpConfig";
223
226
  export type { KitStorage, LabelScope, LpConfig } from "./composables/useLpConfig";
224
227
  export { browserStorage, useKitStorage, usePersisted } from "./composables/usePersisted";
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { _ } from "./chunks/LayoutCanvas-CNOAUk7D.js";
2
- import { _ as _2 } from "./chunks/LpAddressInput-Al3HT5ey.js";
2
+ import { _ as _2 } from "./chunks/LpAddressInput-De_LTYM7.js";
3
3
  import { _ as _3 } from "./chunks/LpMapPicker-DRGgh89B.js";
4
4
  import { OSM_TILES, createNominatimProvider, emptyAddress, formatAddress, project, tileUrl, unproject } from "./components/address.js";
5
5
  import { _ as _4 } from "./chunks/LpActionBar-BP1ld7-S.js";
6
6
  import { _ as _5 } from "./chunks/LpAlert-n5XPAZfv.js";
7
- import { _ as _6 } from "./chunks/LpAppShell-CpSSm5WT.js";
7
+ import { _ as _6 } from "./chunks/LpAppShell-Cpy_t1U_.js";
8
8
  import { _ as _7 } from "./chunks/LpAutocomplete-DOu7FN5P.js";
9
9
  import { _ as _8 } from "./chunks/LpAvatar-Cg6Az3wz.js";
10
10
  import { _ as _9 } from "./chunks/LpBadge-wUYLN4U9.js";
@@ -30,8 +30,8 @@ import { default as default6 } from "./components/LpPaletteStats.vue.js";
30
30
  import { default as default7 } from "./components/LpPaletteSwatches.vue.js";
31
31
  import { default as default8 } from "./components/LpKeys.vue.js";
32
32
  import { comboKeys, comboSteps } from "./components/keys.js";
33
- import { _ as _18 } from "./chunks/LpConfirmDialog-DY2VjDji.js";
34
- import { _ as _19 } from "./chunks/LpConfirmHost-DvSZTJ4N.js";
33
+ import { _ as _18 } from "./chunks/LpConfirmDialog-CphJR6Q9.js";
34
+ import { _ as _19 } from "./chunks/LpConfirmHost-QSJ0zl63.js";
35
35
  import { confirm, useConfirm } from "./components/confirm.js";
36
36
  import { _ as _20 } from "./chunks/LpContextMenu-rx2tCjwk.js";
37
37
  import { _ as _21 } from "./chunks/LpDatePicker-CA0QTfBC.js";
@@ -40,7 +40,7 @@ import { _ as _23 } from "./chunks/LpRangeCalendar-CYYUYqJ7.js";
40
40
  import { clampRange, dayAllowed, defaultDatePresets, defaultRangePresets, formatRange, normalizeRange, sameRange } from "./components/dateRange.js";
41
41
  import { _ as _24 } from "./chunks/LpDisclosure-Cpa7Hc5T.js";
42
42
  import { _ as _25 } from "./chunks/LpDivider-CUX46KR0.js";
43
- import { _ as _26 } from "./chunks/LpDrawer-RRamxbLV.js";
43
+ import { _ as _26 } from "./chunks/LpDrawer-DQZixOqJ.js";
44
44
  import { _ as _27 } from "./chunks/LpDropdownMenu-Dl7Z4ZhS.js";
45
45
  import { default as default9 } from "./components/LpDropzone.vue.js";
46
46
  import { _ as _28 } from "./chunks/LpEmptyState-EQHiRYhG.js";
@@ -59,8 +59,8 @@ import { _ as _36 } from "./chunks/LpLink-Krkjhe_y.js";
59
59
  import { _ as _37 } from "./chunks/LpLogViewer-D25jM2_I.js";
60
60
  import { countLevels, matchPositions, matchesTerm, ringWindow, stepMatch } from "./components/logSearch.js";
61
61
  import { _ as _38 } from "./chunks/LpMediaGallery-Bkram-MO.js";
62
- import { _ as _39 } from "./chunks/LpModal-BnjZ2iem.js";
63
- import { _ as _40 } from "./chunks/LpModalNav-DQpV2kdn.js";
62
+ import { _ as _39 } from "./chunks/LpModal-C0tGg_D_.js";
63
+ import { _ as _40 } from "./chunks/LpModalNav-iDDtT9c1.js";
64
64
  import { _ as _41 } from "./chunks/LpNotificationBell-3244LUVc.js";
65
65
  import { _ as _42 } from "./chunks/LpNumberField-e0IBkWU1.js";
66
66
  import { _ as _43 } from "./chunks/LpTimeField-BawSVsmt.js";
@@ -86,7 +86,7 @@ import { _ as _60 } from "./chunks/LpShift-CCq7dofl.js";
86
86
  import { _ as _61 } from "./chunks/LpSwap-C96c0LWh.js";
87
87
  import { _ as _62 } from "./chunks/LpHintExample-9Py0wc-L.js";
88
88
  import { default as default11 } from "./components/LpShortcutsDialog.vue.js";
89
- import { _ as _63 } from "./chunks/LpSidebar-BoNLKgh-.js";
89
+ import { _ as _63 } from "./chunks/LpSidebar-D6zO8vLm.js";
90
90
  import { _ as _64 } from "./chunks/LpContextSwitcher-Dwh6dyID.js";
91
91
  import { _ as _65 } from "./chunks/LpSkeleton-BB6XIV-w.js";
92
92
  import { _ as _66 } from "./chunks/LpSlider-DxYoCJJO.js";
@@ -134,6 +134,7 @@ import { anyModalOpen, topModalElement, useModalLayer } from "./components/modal
134
134
  import { useGamepadNav } from "./composables/useGamepadNav.js";
135
135
  import { focusableIn, nextInDirection } from "./composables/spatialNav.js";
136
136
  import { provideGlass, useGlass } from "./composables/useGlass.js";
137
+ import { provideOverlayHistory, useOverlayHistory, useStepHistory } from "./composables/useOverlayHistory.js";
137
138
  import { LP_CONFIG, lpConfig, mergeConfig, provideLpConfig, useLabels, useLinkComponent, useLpConfig } from "./composables/useLpConfig.js";
138
139
  import { browserStorage, useKitStorage, usePersisted } from "./composables/usePersisted.js";
139
140
  import { useCloseGuard } from "./composables/useCloseGuard.js";
@@ -571,6 +572,7 @@ export {
571
572
  provideGlass,
572
573
  provideHintSystem,
573
574
  provideLpConfig,
575
+ provideOverlayHistory,
574
576
  pseudoLocalize,
575
577
  pushRecent,
576
578
  quotaSegments,
@@ -670,6 +672,7 @@ export {
670
672
  useModalLayer,
671
673
  useNow,
672
674
  useOptimisticToggle,
675
+ useOverlayHistory,
673
676
  usePaintSelect,
674
677
  usePaletteCommands,
675
678
  u3 as usePaletteTheme,
@@ -685,6 +688,7 @@ export {
685
688
  useSettled,
686
689
  useShift,
687
690
  useSpotlight,
691
+ useStepHistory,
688
692
  useTableFilterChoice,
689
693
  u as useTheme,
690
694
  u2 as useThemeStudio,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leavepulse/ui",
3
- "version": "0.75.0",
3
+ "version": "0.76.1",
4
4
  "type": "module",
5
5
  "description": "LeavePulse token-driven Vue component kit.",
6
6
  "repository": {
@@ -101,6 +101,7 @@
101
101
  "vite-plugin-lib-inject-css": "^2.2.2",
102
102
  "vue": "3.5.31",
103
103
  "vue-component-meta": "3.3.5",
104
+ "vue-router": "4.6.4",
104
105
  "vue-tsc": "^3.0.0"
105
106
  },
106
107
  "dependencies": {
@@ -5,6 +5,7 @@ import { useLabels } from "../composables/useLpConfig"
5
5
  import LpButton from "./LpButton.vue"
6
6
  import LpIcon from "./LpIcon.vue"
7
7
  import LpModal from "./LpModal.vue"
8
+ import type { OverlayHistoryMode } from "../composables/useOverlayHistory"
8
9
 
9
10
  export type ConfirmLabels = {
10
11
  confirm: string
@@ -54,9 +55,11 @@ const props = withDefaults(
54
55
  action?: () => unknown
55
56
  /** Frosted glass panel, passed to LpModal. Follows `provideGlass` when unset. */
56
57
  glass?: boolean
58
+ /** Browser history for the dialog, passed to LpModal. Follows `provideOverlayHistory` when unset. */
59
+ history?: OverlayHistoryMode
57
60
  labels?: Partial<ConfirmLabels>
58
61
  }>(),
59
- { glass: undefined },
62
+ { glass: undefined, history: undefined },
60
63
  )
61
64
 
62
65
  const emit = defineEmits<{
@@ -128,6 +131,7 @@ function dismiss(open: boolean) {
128
131
  <template>
129
132
  <LpModal
130
133
  :glass="glass"
134
+ :history="history"
131
135
  :open="open"
132
136
  :title="title"
133
137
  :description="description"
@@ -37,6 +37,7 @@ import { CLOSE_ICON } from "./dropdown"
37
37
  import { prefersReducedMotion } from "../composables/easing"
38
38
  import { useCloseGuard, type CloseGuard } from "../composables/useCloseGuard"
39
39
  import { useGlass } from "../composables/useGlass"
40
+ import { useOverlayHistory, type OverlayHistoryMode } from "../composables/useOverlayHistory"
40
41
  import { useLabels } from "../composables/useLpConfig"
41
42
  import { usePersisted } from "../composables/usePersisted"
42
43
  import LpIcon from "./LpIcon.vue"
@@ -132,9 +133,17 @@ const props = withDefaults(
132
133
  glass?: boolean
133
134
  /** Accessible name for a drawer without a visible `title`. */
134
135
  ariaLabel?: string
136
+ /**
137
+ * Browser Back closes it, and opening it adds a history entry. Unset
138
+ * follows `provideOverlayHistory` (off without one); `"reopen"` also
139
+ * opens it again on Forward; `false` keeps it out of history, for an
140
+ * overlay the app already drives from its route.
141
+ */
142
+ history?: OverlayHistoryMode
135
143
  labels?: Partial<{ close: string }>
136
144
  }>(),
137
145
  {
146
+ history: undefined,
138
147
  side: "right",
139
148
  size: "sm",
140
149
  dismissible: true,
@@ -214,6 +223,18 @@ async function requestClose() {
214
223
  // ways out do. Handed to the body and footer slots, and exposed.
215
224
  defineExpose({ close: requestClose })
216
225
 
226
+ useOverlayHistory({
227
+ mode: () => props.history,
228
+ open: () => !!props.open,
229
+ close: async () => {
230
+ if (!(await allowClose())) return false
231
+ emit("update:open", false)
232
+ return true
233
+ },
234
+ reopen: () => emit("update:open", true),
235
+ navigated: () => emit("update:open", false),
236
+ })
237
+
217
238
  // The header's close button is vaul's own and would slide the panel away at
218
239
  // once; with a guard it asks first and closes through the same path as above.
219
240
  function guardClose(event: MouseEvent) {
@@ -11,6 +11,7 @@ import {
11
11
  import { computed, onBeforeUnmount, ref, useSlots, watch } from "vue"
12
12
  import { useCloseGuard, type CloseGuard } from "../composables/useCloseGuard"
13
13
  import { useGlass } from "../composables/useGlass"
14
+ import { useOverlayHistory, type OverlayHistoryMode } from "../composables/useOverlayHistory"
14
15
  import { useLabels } from "../composables/useLpConfig"
15
16
  import { useShift } from "../composables/useShift"
16
17
  import { useModalLayer } from "./modalDepth"
@@ -93,9 +94,16 @@ const props = withDefaults(
93
94
  * form's state, `:guard="form.dirty"`. See `useCloseGuard`.
94
95
  */
95
96
  guard?: CloseGuard
97
+ /**
98
+ * Browser Back closes it, and opening it adds a history entry. Unset
99
+ * follows `provideOverlayHistory` (off without one); `"reopen"` also
100
+ * opens it again on Forward; `false` keeps it out of history, for an
101
+ * overlay the app already drives from its route.
102
+ */
103
+ history?: OverlayHistoryMode
96
104
  labels?: Partial<{ close: string }>
97
105
  }>(),
98
- { size: "md", autoFocus: "auto", glass: undefined, dismissible: true, guard: undefined },
106
+ { size: "md", autoFocus: "auto", glass: undefined, dismissible: true, guard: undefined, history: undefined },
99
107
  )
100
108
 
101
109
  const text = useLabels("modal", { close: "Close" }, () => props.labels)
@@ -120,6 +128,18 @@ async function onOpenChange(next: boolean) {
120
128
  function close() {
121
129
  void onOpenChange(false)
122
130
  }
131
+
132
+ useOverlayHistory({
133
+ mode: () => props.history,
134
+ open: () => !!props.open,
135
+ close: async () => {
136
+ if (!(await allowClose())) return false
137
+ emit("update:open", false)
138
+ return true
139
+ },
140
+ reopen: () => emit("update:open", true),
141
+ navigated: () => emit("update:open", false),
142
+ })
123
143
  defineExpose({ close })
124
144
 
125
145
  function onEscape(event: KeyboardEvent) {
@@ -18,6 +18,7 @@
18
18
  import { computed } from "vue"
19
19
  import LpKeys from "./LpKeys.vue"
20
20
  import LpModal from "./LpModal.vue"
21
+ import type { OverlayHistoryMode } from "../composables/useOverlayHistory"
21
22
  import {
22
23
  activeShortcuts,
23
24
  mergeShortcutGroups,
@@ -36,6 +37,8 @@ const props = withDefaults(
36
37
  description?: string
37
38
  /** Frosted glass panel, passed to LpModal. Follows `provideGlass` when unset. */
38
39
  glass?: boolean
40
+ /** Browser history for the dialog, passed to LpModal. Follows `provideOverlayHistory` when unset. */
41
+ history?: OverlayHistoryMode
39
42
  /**
40
43
  * A key that toggles the reference, e.g. "F1". Bound only while the dialog
41
44
  * is mounted, also inside inputs and over other overlays, and the browser's
@@ -43,7 +46,7 @@ const props = withDefaults(
43
46
  */
44
47
  hotkey?: string
45
48
  }>(),
46
- { open: false, groups: () => [], live: false, title: undefined, glass: undefined },
49
+ { open: false, groups: () => [], live: false, title: undefined, glass: undefined, history: undefined },
47
50
  )
48
51
 
49
52
  const text = useLabels("shortcuts", {
@@ -75,6 +78,7 @@ useHotkeys(() =>
75
78
  <template>
76
79
  <LpModal
77
80
  :glass="props.glass"
81
+ :history="props.history"
78
82
  :open="props.open"
79
83
  size="lg"
80
84
  :title="props.title ?? text.title"