@leavepulse/ui 0.75.0 → 0.76.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/README.md +44 -0
- package/dist/assets/{LpShortcutsDialog-BUp_BX4s.css → LpShortcutsDialog-C6uzPeMf.css} +7 -7
- package/dist/assets/LpTabs-CJ0rYQbx.css +18 -0
- package/dist/chunks/{LpAddressInput-Al3HT5ey.js → LpAddressInput-De_LTYM7.js} +1 -1
- package/dist/chunks/{LpAppShell-CpSSm5WT.js → LpAppShell-Cpy_t1U_.js} +1 -1
- package/dist/chunks/{LpConfirmDialog-DY2VjDji.js → LpConfirmDialog-CphJR6Q9.js} +4 -2
- package/dist/chunks/{LpConfirmHost-DvSZTJ4N.js → LpConfirmHost-QSJ0zl63.js} +1 -1
- package/dist/chunks/{LpDrawer-RRamxbLV.js → LpDrawer-DQZixOqJ.js} +13 -0
- package/dist/chunks/{LpModal-BnjZ2iem.js → LpModal-C0tGg_D_.js} +13 -0
- package/dist/chunks/{LpModalNav-DQpV2kdn.js → LpModalNav-iDDtT9c1.js} +2 -2
- package/dist/chunks/{LpSidebar-BoNLKgh-.js → LpSidebar-D6zO8vLm.js} +1 -1
- package/dist/components/LpAddressInput.vue.js +1 -1
- package/dist/components/LpAppShell.vue.js +1 -1
- package/dist/components/LpConfirmDialog.vue.d.ts +4 -0
- package/dist/components/LpConfirmDialog.vue.js +1 -1
- package/dist/components/LpConfirmHost.vue.js +1 -1
- package/dist/components/LpDrawer.vue.d.ts +16 -0
- package/dist/components/LpDrawer.vue.js +1 -1
- package/dist/components/LpModal.vue.d.ts +9 -0
- package/dist/components/LpModal.vue.js +1 -1
- package/dist/components/LpModalNav.vue.js +1 -1
- package/dist/components/LpShortcutsDialog.vue.d.ts +4 -0
- package/dist/components/LpShortcutsDialog.vue.js +6 -4
- package/dist/components/LpSidebar.vue.js +1 -1
- package/dist/components/LpTabs.vue.d.ts +9 -0
- package/dist/components/LpTabs.vue.js +18 -4
- package/dist/composables/overlayHistory.d.ts +33 -0
- package/dist/composables/overlayHistory.js +237 -0
- package/dist/composables/useOverlayHistory.d.ts +45 -0
- package/dist/composables/useOverlayHistory.js +84 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +12 -8
- package/package.json +2 -1
- package/src/components/LpConfirmDialog.vue +5 -1
- package/src/components/LpDrawer.vue +21 -0
- package/src/components/LpModal.vue +21 -1
- package/src/components/LpShortcutsDialog.vue +5 -1
- package/src/components/LpTabs.vue +26 -2
- package/src/composables/overlayHistory.ts +381 -0
- package/src/composables/useOverlayHistory.ts +173 -0
- package/src/index.ts +3 -0
- package/dist/assets/LpTabs-CDchzoRL.css +0 -18
|
@@ -0,0 +1,237 @@
|
|
|
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
|
+
waiting: [],
|
|
15
|
+
href: "",
|
|
16
|
+
serial: 0,
|
|
17
|
+
internal: false,
|
|
18
|
+
settle: 0
|
|
19
|
+
};
|
|
20
|
+
const s = g[STATE_KEY] ??= fresh;
|
|
21
|
+
function markOf(state) {
|
|
22
|
+
if (!state || typeof state !== "object") return null;
|
|
23
|
+
const mark = state[KEY];
|
|
24
|
+
if (!mark || typeof mark !== "object") return null;
|
|
25
|
+
const { token, depth } = mark;
|
|
26
|
+
return typeof token === "string" && typeof depth === "number" ? { token, depth } : null;
|
|
27
|
+
}
|
|
28
|
+
function pathOf(href) {
|
|
29
|
+
try {
|
|
30
|
+
return new URL(href, location.href).pathname;
|
|
31
|
+
} catch {
|
|
32
|
+
return href;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function own(run) {
|
|
36
|
+
s.internal = true;
|
|
37
|
+
try {
|
|
38
|
+
return run();
|
|
39
|
+
} finally {
|
|
40
|
+
s.internal = false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function silentGo(delta) {
|
|
44
|
+
if (delta === 0) return;
|
|
45
|
+
s.silent++;
|
|
46
|
+
mute();
|
|
47
|
+
own(() => history.go(delta));
|
|
48
|
+
}
|
|
49
|
+
function whenSettled(run) {
|
|
50
|
+
if (s.silent > 0) s.waiting.push(run);
|
|
51
|
+
else run();
|
|
52
|
+
}
|
|
53
|
+
function flushWaiting() {
|
|
54
|
+
if (s.silent > 0) return;
|
|
55
|
+
const queued = s.waiting.splice(0);
|
|
56
|
+
for (const run of queued) run();
|
|
57
|
+
}
|
|
58
|
+
function current() {
|
|
59
|
+
return markOf(history.state);
|
|
60
|
+
}
|
|
61
|
+
function onTop() {
|
|
62
|
+
const top = s.live[s.live.length - 1];
|
|
63
|
+
return !!top && current()?.token === top.token;
|
|
64
|
+
}
|
|
65
|
+
function leavePage() {
|
|
66
|
+
const gone = s.live.splice(0);
|
|
67
|
+
for (const item of gone.reverse()) {
|
|
68
|
+
bury(item);
|
|
69
|
+
item.entry.navigated?.();
|
|
70
|
+
}
|
|
71
|
+
s.closed.clear();
|
|
72
|
+
}
|
|
73
|
+
function handlePop(event) {
|
|
74
|
+
const before = s.href;
|
|
75
|
+
s.href = location.href;
|
|
76
|
+
if (s.silent > 0) {
|
|
77
|
+
s.silent--;
|
|
78
|
+
if (before === location.href) event.stopImmediatePropagation();
|
|
79
|
+
flushWaiting();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
window.clearTimeout(s.settle);
|
|
83
|
+
const sameAddress = before === location.href;
|
|
84
|
+
const mark = markOf(event.state);
|
|
85
|
+
const at = mark ? s.live.findIndex((item) => item.token === mark.token) : -1;
|
|
86
|
+
if (at >= 0) {
|
|
87
|
+
closeAbove(at);
|
|
88
|
+
if (sameAddress) event.stopImmediatePropagation();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const reopen = mark ? s.closed.get(mark.token) : void 0;
|
|
92
|
+
if (reopen && sameAddress) {
|
|
93
|
+
event.stopImmediatePropagation();
|
|
94
|
+
if (reopen.entry.alive && reopen.entry.forward?.()) {
|
|
95
|
+
s.closed.delete(reopen.token);
|
|
96
|
+
s.live.push(reopen);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
silentGo(-1);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (mark && (s.dead.has(mark.token) || reopen)) {
|
|
103
|
+
if (sameAddress) event.stopImmediatePropagation();
|
|
104
|
+
if (!sameAddress) {
|
|
105
|
+
silentGo(-mark.depth);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const depth = mark.depth;
|
|
109
|
+
own(() => history.forward());
|
|
110
|
+
s.settle = window.setTimeout(() => {
|
|
111
|
+
if (markOf(history.state)?.token === mark.token) silentGo(-depth);
|
|
112
|
+
else mute();
|
|
113
|
+
}, SETTLE_MS);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (s.live.length === 0) return;
|
|
117
|
+
if (sameAddress) {
|
|
118
|
+
closeAbove(-1);
|
|
119
|
+
event.stopImmediatePropagation();
|
|
120
|
+
} else if (pathOf(before) !== pathOf(location.href)) {
|
|
121
|
+
leavePage();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const DEAD_LIMIT = 200;
|
|
125
|
+
function bury(item) {
|
|
126
|
+
s.dead.set(item.token, item);
|
|
127
|
+
if (s.dead.size > DEAD_LIMIT) s.dead.delete(s.dead.keys().next().value);
|
|
128
|
+
}
|
|
129
|
+
function closeAbove(index) {
|
|
130
|
+
const popped = s.live.splice(index + 1);
|
|
131
|
+
for (const item of popped.reverse()) {
|
|
132
|
+
s.closed.set(item.token, item);
|
|
133
|
+
if (item.entry.alive) item.entry.back();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function onPop(event) {
|
|
137
|
+
if (s.replaying) return;
|
|
138
|
+
s.popping = { muted: !!s.router && !s.router.listening, before: s.href };
|
|
139
|
+
handlePop(event);
|
|
140
|
+
if (event.cancelBubble) {
|
|
141
|
+
s.popping = null;
|
|
142
|
+
mute();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function afterPop(event) {
|
|
146
|
+
if (s.replaying || !s.popping) return;
|
|
147
|
+
const { muted, before } = s.popping;
|
|
148
|
+
s.popping = null;
|
|
149
|
+
if (muted && before !== location.href) replay(event.state);
|
|
150
|
+
mute();
|
|
151
|
+
}
|
|
152
|
+
function replay(state) {
|
|
153
|
+
if (!s.router) return;
|
|
154
|
+
s.router.listening = true;
|
|
155
|
+
s.replaying = true;
|
|
156
|
+
try {
|
|
157
|
+
window.dispatchEvent(new PopStateEvent("popstate", { state }));
|
|
158
|
+
} finally {
|
|
159
|
+
s.replaying = false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function mute() {
|
|
163
|
+
if (s.popping) return;
|
|
164
|
+
if (s.router) s.router.listening = !(current() || s.silent > 0);
|
|
165
|
+
}
|
|
166
|
+
function setHistoryRouter(router) {
|
|
167
|
+
if (s.router && s.router !== router) s.router.listening = true;
|
|
168
|
+
s.router = router;
|
|
169
|
+
if (typeof window === "undefined") return;
|
|
170
|
+
window.removeEventListener("popstate", afterPop);
|
|
171
|
+
window.addEventListener("popstate", afterPop);
|
|
172
|
+
mute();
|
|
173
|
+
}
|
|
174
|
+
function install() {
|
|
175
|
+
if (s.installed || typeof window === "undefined") return;
|
|
176
|
+
s.installed = true;
|
|
177
|
+
s.href = location.href;
|
|
178
|
+
window.addEventListener("popstate", onPop, { capture: true });
|
|
179
|
+
window.removeEventListener("popstate", afterPop);
|
|
180
|
+
window.addEventListener("popstate", afterPop);
|
|
181
|
+
const push = history.pushState.bind(history);
|
|
182
|
+
const replace = history.replaceState.bind(history);
|
|
183
|
+
history.pushState = function(data, unused, url) {
|
|
184
|
+
const before = location.href;
|
|
185
|
+
push(data, unused, url);
|
|
186
|
+
s.href = location.href;
|
|
187
|
+
if (s.internal) return;
|
|
188
|
+
s.closed.clear();
|
|
189
|
+
if (pathOf(before) !== pathOf(location.href)) leavePage();
|
|
190
|
+
mute();
|
|
191
|
+
};
|
|
192
|
+
history.replaceState = function(data, unused, url) {
|
|
193
|
+
const mark = s.internal ? null : current();
|
|
194
|
+
const keep = mark && data && typeof data === "object" && !markOf(data) ? { ...data, [KEY]: mark } : data;
|
|
195
|
+
replace(keep, unused, url);
|
|
196
|
+
s.href = location.href;
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function pushEntry(entry) {
|
|
200
|
+
if (typeof window === "undefined") return;
|
|
201
|
+
install();
|
|
202
|
+
whenSettled(() => {
|
|
203
|
+
if (!entry.alive) return;
|
|
204
|
+
const token = `${Date.now().toString(36)}-${(s.serial++).toString(36)}`;
|
|
205
|
+
const depth = (current()?.depth ?? 0) + 1;
|
|
206
|
+
const base = history.state && typeof history.state === "object" ? history.state : {};
|
|
207
|
+
own(() => history.pushState({ ...base, [KEY]: { token, depth } }, "", location.href));
|
|
208
|
+
s.href = location.href;
|
|
209
|
+
s.closed.clear();
|
|
210
|
+
s.live.push({ entry, token, depth });
|
|
211
|
+
mute();
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
function dropEntry(entry) {
|
|
215
|
+
const at = s.live.findIndex((item) => item.entry === entry);
|
|
216
|
+
if (at === -1) return;
|
|
217
|
+
const positioned = onTop();
|
|
218
|
+
const removed = s.live.splice(at);
|
|
219
|
+
if (positioned) {
|
|
220
|
+
silentGo(-removed.length);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
for (const item of removed) bury(item);
|
|
224
|
+
}
|
|
225
|
+
function isLive(entry) {
|
|
226
|
+
return s.live.some((item) => item.entry === entry);
|
|
227
|
+
}
|
|
228
|
+
function liveEntries() {
|
|
229
|
+
return s.live.length;
|
|
230
|
+
}
|
|
231
|
+
export {
|
|
232
|
+
dropEntry,
|
|
233
|
+
isLive,
|
|
234
|
+
liveEntries,
|
|
235
|
+
pushEntry,
|
|
236
|
+
setHistoryRouter
|
|
237
|
+
};
|
|
@@ -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-
|
|
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-
|
|
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-
|
|
34
|
-
import { _ as _19 } from "./chunks/LpConfirmHost-
|
|
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-
|
|
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-
|
|
63
|
-
import { _ as _40 } from "./chunks/LpModalNav-
|
|
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-
|
|
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.
|
|
3
|
+
"version": "0.76.0",
|
|
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"
|
|
@@ -4,6 +4,7 @@ import { Motion } from "motion-v"
|
|
|
4
4
|
import { TabsContent, TabsList, TabsRoot, TabsTrigger } from "reka-ui"
|
|
5
5
|
import { computed, nextTick, ref, useId, watch } from "vue"
|
|
6
6
|
import { usePillTransition } from "../composables/usePillTransition"
|
|
7
|
+
import { useStepHistory } from "../composables/useOverlayHistory"
|
|
7
8
|
import { prefersReducedMotion } from "../composables/easing"
|
|
8
9
|
import LpContextMenu, { type ContextMenuItemDef } from "./LpContextMenu.vue"
|
|
9
10
|
import LpSkeleton from "./LpSkeleton.vue"
|
|
@@ -65,8 +66,16 @@ const props = withDefaults(
|
|
|
65
66
|
* icon keep their label.
|
|
66
67
|
*/
|
|
67
68
|
compact?: boolean
|
|
69
|
+
/**
|
|
70
|
+
* Record each switch as a browser history entry, so Back returns to the
|
|
71
|
+
* previous tab. Unset follows the overlay the tabs sit in (a modal or a
|
|
72
|
+
* drawer with history on, see `provideOverlayHistory`) and is off
|
|
73
|
+
* elsewhere: a page's own tabs belong in its URL. `false` opts out inside
|
|
74
|
+
* an overlay, `true` opts in on a page that keeps no URL of its own.
|
|
75
|
+
*/
|
|
76
|
+
history?: boolean
|
|
68
77
|
}>(),
|
|
69
|
-
{ variant: "contained", accent: false, block: false, hoverFollow: true, compact: false },
|
|
78
|
+
{ variant: "contained", accent: false, block: false, hoverFollow: true, compact: false, history: undefined },
|
|
70
79
|
)
|
|
71
80
|
|
|
72
81
|
const bottom = computed(() => props.variant === "bottom")
|
|
@@ -110,11 +119,26 @@ watch(current, () => {
|
|
|
110
119
|
})
|
|
111
120
|
})
|
|
112
121
|
|
|
113
|
-
function
|
|
122
|
+
function show(value: string) {
|
|
114
123
|
if (props.modelValue === undefined) current.value = value
|
|
115
124
|
emit("update:modelValue", value)
|
|
116
125
|
}
|
|
117
126
|
|
|
127
|
+
const { record } = useStepHistory({
|
|
128
|
+
mode: () => props.history,
|
|
129
|
+
set: show,
|
|
130
|
+
has: (value) => props.items.some((item) => item.value === value && !item.disabled),
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
function select(value: string) {
|
|
134
|
+
const from = current.value
|
|
135
|
+
show(value)
|
|
136
|
+
// Recorded once the switch took: a bound value the app refused is not a step.
|
|
137
|
+
void nextTick(() => {
|
|
138
|
+
if (from !== undefined && current.value === value) record(from, value)
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
118
142
|
// The pill sits under the hovered tab, falling back to the active one
|
|
119
143
|
// (UAProject header behaviour). motion-v shared layoutId animates the move.
|
|
120
144
|
// The id must be unique per instance — a shared constant makes the pill fly
|