@leavepulse/ui 0.74.2 → 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.
Files changed (52) hide show
  1. package/README.md +44 -0
  2. package/dist/assets/{LpShortcutsDialog-BUp_BX4s.css → LpShortcutsDialog-C6uzPeMf.css} +7 -7
  3. package/dist/assets/{LpTable-DHvorDiU.css → LpTable-Bv-EcQs7.css} +19 -19
  4. package/dist/assets/LpTabs-CJ0rYQbx.css +18 -0
  5. package/dist/chunks/{LpAddressInput-Al3HT5ey.js → LpAddressInput-De_LTYM7.js} +1 -1
  6. package/dist/chunks/{LpAppShell-CpSSm5WT.js → LpAppShell-Cpy_t1U_.js} +1 -1
  7. package/dist/chunks/{LpConfirmDialog-DY2VjDji.js → LpConfirmDialog-CphJR6Q9.js} +4 -2
  8. package/dist/chunks/{LpConfirmHost-DvSZTJ4N.js → LpConfirmHost-QSJ0zl63.js} +1 -1
  9. package/dist/chunks/{LpDrawer-RRamxbLV.js → LpDrawer-DQZixOqJ.js} +13 -0
  10. package/dist/chunks/{LpModal-BnjZ2iem.js → LpModal-C0tGg_D_.js} +13 -0
  11. package/dist/chunks/{LpModalNav-DQpV2kdn.js → LpModalNav-iDDtT9c1.js} +2 -2
  12. package/dist/chunks/{LpSidebar-BoNLKgh-.js → LpSidebar-D6zO8vLm.js} +1 -1
  13. package/dist/components/LpAddressInput.vue.js +1 -1
  14. package/dist/components/LpAppShell.vue.js +1 -1
  15. package/dist/components/LpConfirmDialog.vue.d.ts +4 -0
  16. package/dist/components/LpConfirmDialog.vue.js +1 -1
  17. package/dist/components/LpConfirmHost.vue.js +1 -1
  18. package/dist/components/LpDrawer.vue.d.ts +16 -0
  19. package/dist/components/LpDrawer.vue.js +1 -1
  20. package/dist/components/LpModal.vue.d.ts +9 -0
  21. package/dist/components/LpModal.vue.js +1 -1
  22. package/dist/components/LpModalNav.vue.js +1 -1
  23. package/dist/components/LpShortcutsDialog.vue.d.ts +4 -0
  24. package/dist/components/LpShortcutsDialog.vue.js +6 -4
  25. package/dist/components/LpSidebar.vue.js +1 -1
  26. package/dist/components/LpTable.vue.d.ts +37 -0
  27. package/dist/components/LpTable.vue.js +34 -15
  28. package/dist/components/LpTabs.vue.d.ts +9 -0
  29. package/dist/components/LpTabs.vue.js +18 -4
  30. package/dist/components/tableFilter.d.ts +28 -0
  31. package/dist/components/tableFilter.js +43 -0
  32. package/dist/composables/overlayHistory.d.ts +33 -0
  33. package/dist/composables/overlayHistory.js +237 -0
  34. package/dist/composables/useOverlayHistory.d.ts +45 -0
  35. package/dist/composables/useOverlayHistory.js +84 -0
  36. package/dist/composables/useTableFilterChoice.d.ts +16 -0
  37. package/dist/composables/useTableFilterChoice.js +34 -0
  38. package/dist/index.d.ts +6 -0
  39. package/dist/index.js +18 -8
  40. package/package.json +2 -1
  41. package/src/components/LpConfirmDialog.vue +5 -1
  42. package/src/components/LpDrawer.vue +21 -0
  43. package/src/components/LpModal.vue +21 -1
  44. package/src/components/LpShortcutsDialog.vue +5 -1
  45. package/src/components/LpTable.vue +86 -23
  46. package/src/components/LpTabs.vue +26 -2
  47. package/src/components/tableFilter.ts +66 -0
  48. package/src/composables/overlayHistory.ts +381 -0
  49. package/src/composables/useOverlayHistory.ts +173 -0
  50. package/src/composables/useTableFilterChoice.ts +51 -0
  51. package/src/index.ts +6 -0
  52. package/dist/assets/LpTabs-CDchzoRL.css +0 -18
@@ -25,10 +25,10 @@ import { useResizeObserver } from "../composables/useResizeObserver.js";
25
25
  import { compareValues } from "./tableSort.js";
26
26
  import { _ as _sfc_main$2 } from "../chunks/LpButton-CFb44IjC.js";
27
27
  import { toCsv, downloadFile, fileStem } from "./exportData.js";
28
- import { summarizeShown, parseTableState, serializeTableState } from "./tableFilter.js";
28
+ import { toHiddenSets, sameHidden, summarizeShown, parseTableState, mergeRestored, serializeTableState, fromHiddenSets } from "./tableFilter.js";
29
29
  import { resolveRowMenu } from "./tableRowMenu.js";
30
30
  import { _ as _export_sfc } from "../chunks/_plugin-vue_export-helper-1tPrXgE0.js";
31
- import '../assets/LpTable-DHvorDiU.css';const _hoisted_1 = {
31
+ import '../assets/LpTable-Bv-EcQs7.css';const _hoisted_1 = {
32
32
  key: 0,
33
33
  class: "flex justify-end"
34
34
  };
@@ -89,6 +89,8 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
89
89
  selectable: { type: [Boolean, String], default: false },
90
90
  selected: { default: () => [] },
91
91
  sort: { default: void 0 },
92
+ filters: { default: void 0 },
93
+ filtered: { type: Boolean, default: false },
92
94
  stickyHeader: { type: Boolean },
93
95
  rowMenu: {},
94
96
  sortAscLabel: {},
@@ -105,7 +107,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
105
107
  persist: {},
106
108
  allMatching: { type: Boolean, default: false }
107
109
  },
108
- emits: ["update:selected", "update:sort", "update:allMatching", "rowClick"],
110
+ emits: ["update:selected", "update:sort", "update:filters", "resetFilters", "update:allMatching", "rowClick"],
109
111
  setup(__props, { expose: __expose, emit: __emit }) {
110
112
  const props = __props;
111
113
  const labelSet = useLabels(
@@ -221,7 +223,19 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
221
223
  const height = frameEl.value?.querySelector("table")?.offsetHeight ?? 0;
222
224
  if (height > floor.value) floor.value = height;
223
225
  }
224
- const hiddenValues = ref({});
226
+ const ownHidden = ref({});
227
+ let lastHidden = {};
228
+ const hiddenValues = computed(() => {
229
+ const next = props.filters !== void 0 ? toHiddenSets(props.filters) : ownHidden.value;
230
+ if (sameHidden(next, lastHidden)) return lastHidden;
231
+ lastHidden = next;
232
+ return next;
233
+ });
234
+ function writeHidden(next) {
235
+ if (sameHidden(next, hiddenValues.value)) return;
236
+ ownHidden.value = next;
237
+ emit("update:filters", fromHiddenSets(next));
238
+ }
225
239
  const animating = ref(false);
226
240
  let motionTimer;
227
241
  function withMotion(change) {
@@ -252,21 +266,23 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
252
266
  hidden: () => hiddenValues.value
253
267
  });
254
268
  function valuesOf(col) {
255
- if (props.load) return facets.value[col.key] ?? [...new Set(sourceRows.value.map((row) => row[col.key]))];
256
- return localFacets.distinct(col.key);
269
+ const found = props.load ? facets.value[col.key] ?? [...new Set(sourceRows.value.map((row) => row[col.key]))] : localFacets.distinct(col.key);
270
+ return col.filterValues ? [.../* @__PURE__ */ new Set([...col.filterValues, ...found])] : found;
257
271
  }
258
272
  function showAllValues(key) {
259
- withMotion(() => hiddenValues.value = { ...hiddenValues.value, [key]: /* @__PURE__ */ new Set() });
273
+ writeHidden({ ...hiddenValues.value, [key]: /* @__PURE__ */ new Set() });
260
274
  }
261
275
  function setHidden(key, next) {
262
- withMotion(() => hiddenValues.value = { ...hiddenValues.value, [key]: next });
276
+ writeHidden({ ...hiddenValues.value, [key]: next });
263
277
  }
278
+ watch(hiddenValues, () => withMotion(() => {
279
+ }));
264
280
  function labelOf(col, value) {
265
281
  return col.valueLabel?.(value) ?? String(value ?? "—");
266
282
  }
267
283
  const facetOptions = computed(() => new Map(props.columns.filter((col) => col.filterable).map((col) => {
268
284
  return [col.key, computed(() => {
269
- const counts = props.load ? void 0 : localFacets.counts(col.key);
285
+ const counts = props.load || col.filterCounts === false ? void 0 : localFacets.counts(col.key);
270
286
  return valuesOf(col).map((value) => ({
271
287
  value,
272
288
  label: labelOf(col, value),
@@ -282,7 +298,11 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
282
298
  })
283
299
  );
284
300
  function clearAllFilters() {
285
- withMotion(() => hiddenValues.value = {});
301
+ writeHidden({});
302
+ }
303
+ function resetFilters() {
304
+ clearAllFilters();
305
+ emit("resetFilters");
286
306
  }
287
307
  function filterLabels() {
288
308
  const t = text.value;
@@ -459,9 +479,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
459
479
  ownSort.value = state.sort;
460
480
  emit("update:sort", state.sort);
461
481
  }
462
- if (state.hidden) {
463
- hiddenValues.value = Object.fromEntries(Object.entries(state.hidden).map(([k, v]) => [k, new Set(v)]));
464
- }
482
+ if (state.hidden) writeHidden(mergeRestored(hiddenValues.value, state.hidden));
465
483
  if (state.columns) hiddenColumns.value = new Set(state.columns);
466
484
  }
467
485
  }
@@ -583,6 +601,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
583
601
  );
584
602
  const filteredOut = computed(() => {
585
603
  if (busy.value || failed.value || displayRows.value.length) return false;
604
+ if (props.filtered) return true;
586
605
  if (props.load) return Object.values(hiddenValues.value).some((set) => set.size);
587
606
  return props.rows.length > 0;
588
607
  });
@@ -946,7 +965,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
946
965
  key: 0,
947
966
  variant: "outline",
948
967
  size: "sm",
949
- onClick: clearAllFilters
968
+ onClick: resetFilters
950
969
  }, {
951
970
  default: withCtx(() => [
952
971
  createTextVNode(toDisplayString(text.value.resetFilters), 1)
@@ -1024,7 +1043,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
1024
1043
  };
1025
1044
  }
1026
1045
  });
1027
- const LpTable = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-c1911001"]]);
1046
+ const LpTable = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-d2bafc29"]]);
1028
1047
  export {
1029
1048
  LpTable as default
1030
1049
  };
@@ -52,6 +52,14 @@ type __VLS_Props = {
52
52
  * icon keep their label.
53
53
  */
54
54
  compact?: boolean;
55
+ /**
56
+ * Record each switch as a browser history entry, so Back returns to the
57
+ * previous tab. Unset follows the overlay the tabs sit in (a modal or a
58
+ * drawer with history on, see `provideOverlayHistory`) and is off
59
+ * elsewhere: a page's own tabs belong in its URL. `false` opts out inside
60
+ * an overlay, `true` opts in on a page that keeps no URL of its own.
61
+ */
62
+ history?: boolean;
55
63
  };
56
64
  declare var __VLS_67: {
57
65
  value: string;
@@ -66,6 +74,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
66
74
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
67
75
  "onUpdate:modelValue"?: ((value: string) => any) | undefined;
68
76
  }>, {
77
+ history: boolean;
69
78
  block: boolean;
70
79
  variant: "contained" | "plain" | "underline" | "bottom";
71
80
  accent: boolean;
@@ -3,13 +3,14 @@ import { usePaletteScope, usePaletteCommands } from "../composables/usePaletteCo
3
3
  import { Motion } from "motion-v";
4
4
  import { TabsRoot, TabsList, TabsTrigger, TabsContent } from "reka-ui";
5
5
  import { usePillTransition } from "../composables/usePillTransition.js";
6
+ import { useStepHistory } from "../composables/useOverlayHistory.js";
6
7
  import { prefersReducedMotion } from "../composables/easing.js";
7
8
  import { _ as _sfc_main$1 } from "../chunks/LpContextMenu-rx2tCjwk.js";
8
9
  import { _ as _sfc_main$3 } from "../chunks/LpSkeleton-BB6XIV-w.js";
9
10
  import { _ as _sfc_main$2 } from "../chunks/LpTabFace-DPCwKkH2.js";
10
11
  import { pickTab, BOTTOM_BAR, BOTTOM_ITEM, BOTTOM_MARKER } from "./tabs.js";
11
12
  import { _ as _export_sfc } from "../chunks/_plugin-vue_export-helper-1tPrXgE0.js";
12
- import '../assets/LpTabs-CDchzoRL.css';const _sfc_main = /* @__PURE__ */ defineComponent({
13
+ import '../assets/LpTabs-CJ0rYQbx.css';const _sfc_main = /* @__PURE__ */ defineComponent({
13
14
  __name: "LpTabs",
14
15
  props: {
15
16
  modelValue: {},
@@ -20,7 +21,8 @@ import '../assets/LpTabs-CDchzoRL.css';const _sfc_main = /* @__PURE__ */ defineC
20
21
  block: { type: Boolean, default: false },
21
22
  label: {},
22
23
  hoverFollow: { type: Boolean, default: true },
23
- compact: { type: Boolean, default: false }
24
+ compact: { type: Boolean, default: false },
25
+ history: { type: Boolean, default: void 0 }
24
26
  },
25
27
  emits: ["update:modelValue"],
26
28
  setup(__props, { emit: __emit }) {
@@ -54,10 +56,22 @@ import '../assets/LpTabs-CDchzoRL.css';const _sfc_main = /* @__PURE__ */ defineC
54
56
  row.scrollTo({ left, behavior: prefersReducedMotion() ? "auto" : "smooth" });
55
57
  });
56
58
  });
57
- function select(value) {
59
+ function show(value) {
58
60
  if (props.modelValue === void 0) current.value = value;
59
61
  emit("update:modelValue", value);
60
62
  }
63
+ const { record } = useStepHistory({
64
+ mode: () => props.history,
65
+ set: show,
66
+ has: (value) => props.items.some((item) => item.value === value && !item.disabled)
67
+ });
68
+ function select(value) {
69
+ const from = current.value;
70
+ show(value);
71
+ void nextTick(() => {
72
+ if (from !== void 0 && current.value === value) record(from, value);
73
+ });
74
+ }
61
75
  const pillId = `lp-tab-indicator-${useId()}`;
62
76
  const hovered = ref(null);
63
77
  const pillTransition = usePillTransition();
@@ -195,7 +209,7 @@ import '../assets/LpTabs-CDchzoRL.css';const _sfc_main = /* @__PURE__ */ defineC
195
209
  };
196
210
  }
197
211
  });
198
- const LpTabs = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-9f51a497"]]);
212
+ const LpTabs = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-1d673b52"]]);
199
213
  export {
200
214
  LpTabs as default
201
215
  };
@@ -6,6 +6,34 @@ export declare function hideAllValues(values: readonly unknown[]): Set<unknown>;
6
6
  export declare function invertHidden(values: readonly unknown[], hidden: ReadonlySet<unknown>): Set<unknown>;
7
7
  /** Show these values again, leaving the rest of the filter as it was. */
8
8
  export declare function unhideValues(hidden: ReadonlySet<unknown>, values: readonly unknown[]): Set<unknown>;
9
+ /**
10
+ * A table's header filters as a consumer holds them (v-model:filters on
11
+ * LpTable): per column key, the values hidden. A column that is absent or has
12
+ * an empty list is not filtered. Hidden rather than shown for the same reason
13
+ * as inside the table: a value nobody has seen before shows up.
14
+ */
15
+ export type TableFilters = Record<string, unknown[]>;
16
+ /** The hidden list that leaves only `keep` of `values` showing. */
17
+ export declare function showOnly(values: readonly unknown[], keep: readonly unknown[]): unknown[];
18
+ /** Which of `values` a column's filter still shows. */
19
+ export declare function shownValues(values: readonly unknown[], hidden: readonly unknown[] | undefined): unknown[];
20
+ /**
21
+ * The rows a set of header filters leaves, for the same data drawn somewhere
22
+ * the table is not (cards on a phone, a CSV built by the page).
23
+ */
24
+ export declare function filterRows<Row extends Record<string, unknown>>(rows: readonly Row[], filters: TableFilters): Row[];
25
+ /** Sets per column, from the plain lists a consumer writes. */
26
+ export declare function toHiddenSets(filters: TableFilters): Record<string, Set<unknown>>;
27
+ /** The plain lists a consumer reads, empty columns left out. */
28
+ export declare function fromHiddenSets(hidden: Record<string, ReadonlySet<unknown>>): TableFilters;
29
+ /** Whether two filter states hide the same values, empty columns counting as absent. */
30
+ export declare function sameHidden(a: Record<string, ReadonlySet<unknown>>, b: Record<string, ReadonlySet<unknown>>): boolean;
31
+ /**
32
+ * Persisted filters meeting the ones a parent already set: the parent's
33
+ * explicit state (a deep link, a control above the table) wins per column,
34
+ * and what was remembered fills only the columns it leaves alone.
35
+ */
36
+ export declare function mergeRestored(current: Record<string, ReadonlySet<unknown>>, restored: Record<string, unknown[]>): Record<string, Set<unknown>>;
9
37
  /** Lower-cased and stripped of accents, so "cafe" finds "Café". */
10
38
  export declare function foldText(text: string): string;
11
39
  /** Every word of the query appears somewhere in the label, in any order. */
@@ -13,6 +13,42 @@ function unhideValues(hidden, values) {
13
13
  for (const v of values) next.delete(v);
14
14
  return next;
15
15
  }
16
+ function showOnly(values, keep) {
17
+ return [...onlyValues(values, keep)];
18
+ }
19
+ function shownValues(values, hidden) {
20
+ if (!hidden?.length) return [...values];
21
+ const out = new Set(hidden);
22
+ return values.filter((v) => !out.has(v));
23
+ }
24
+ function filterRows(rows, filters) {
25
+ const active = Object.entries(filters).filter(([, hidden]) => hidden.length).map(([key, hidden]) => [key, new Set(hidden)]);
26
+ if (!active.length) return [...rows];
27
+ return rows.filter((row) => active.every(([key, hidden]) => !hidden.has(row[key])));
28
+ }
29
+ function toHiddenSets(filters) {
30
+ return Object.fromEntries(Object.entries(filters).map(([key, values]) => [key, new Set(values)]));
31
+ }
32
+ function fromHiddenSets(hidden) {
33
+ return Object.fromEntries(Object.entries(hidden).filter(([, set]) => set.size).map(([key, set]) => [key, [...set]]));
34
+ }
35
+ function sameHidden(a, b) {
36
+ const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
37
+ for (const key of keys) {
38
+ const x = a[key];
39
+ const y = b[key];
40
+ if ((x?.size ?? 0) !== (y?.size ?? 0)) return false;
41
+ if (x && y) {
42
+ for (const v of x) if (!y.has(v)) return false;
43
+ }
44
+ }
45
+ return true;
46
+ }
47
+ function mergeRestored(current, restored) {
48
+ const out = Object.fromEntries(Object.entries(current).map(([k, v]) => [k, new Set(v)]));
49
+ for (const [key, values] of Object.entries(restored)) if (!out[key]?.size) out[key] = new Set(values);
50
+ return out;
51
+ }
16
52
  function foldText(text) {
17
53
  return text.normalize("NFKD").replace(new RegExp("\\p{M}", "gu"), "").toLocaleLowerCase();
18
54
  }
@@ -72,13 +108,20 @@ function parseTableState(raw, columnKeys) {
72
108
  }
73
109
  export {
74
110
  countValues,
111
+ filterRows,
75
112
  foldText,
113
+ fromHiddenSets,
76
114
  hideAllValues,
77
115
  invertHidden,
78
116
  matchesQuery,
117
+ mergeRestored,
79
118
  onlyValues,
80
119
  parseTableState,
120
+ sameHidden,
81
121
  serializeTableState,
122
+ showOnly,
123
+ shownValues,
82
124
  summarizeShown,
125
+ toHiddenSets,
83
126
  unhideValues
84
127
  };
@@ -0,0 +1,33 @@
1
+ export interface HistoryEntry {
2
+ /** Back left this entry: undo what it recorded (close the overlay, restore the tab). */
3
+ back: () => void;
4
+ /** Forward came back to it: redo it, and say whether that happened. */
5
+ forward?: () => boolean;
6
+ /** The page moved on to another path while this entry was live. */
7
+ navigated?: () => void;
8
+ /** The component that pushed it is still mounted. */
9
+ alive: boolean;
10
+ }
11
+ /**
12
+ * What the kit needs of a router: vue-router's `listening` switch, or the same
13
+ * flag on any router that skips its popstate handling while it is false.
14
+ */
15
+ export interface HistoryRouter {
16
+ listening: boolean;
17
+ }
18
+ /** The app's router, told not to listen while a step only concerns the kit. */
19
+ export declare function setHistoryRouter(router: HistoryRouter | null): void;
20
+ /** Record an entry on top of the history, under the current address. */
21
+ export declare function pushEntry(entry: HistoryEntry): void;
22
+ /**
23
+ * The thing behind an entry ended without Back (a close button, Escape, the
24
+ * app setting `open` to false, an unmount): take the entry and everything
25
+ * pushed above it out of history, so Back is never spent on a dead step.
26
+ * When the page is not sitting on the kit's entries (another push came on top),
27
+ * the entry stays in place and is skipped when Back or Forward lands on it.
28
+ */
29
+ export declare function dropEntry(entry: HistoryEntry): void;
30
+ /** Whether the entry is live: pushed and not yet popped or dropped. */
31
+ export declare function isLive(entry: HistoryEntry): boolean;
32
+ /** Live entries, bottom first. For tests and devtools. */
33
+ export declare function liveEntries(): number;
@@ -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
+ };