@ulu/frontend-vue 0.5.14 → 0.5.16

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.
@@ -1 +1 @@
1
- {"version":3,"file":"UluScrollAnchors.vue.d.ts","sourceRoot":"","sources":["../../../../lib/components/systems/scroll-anchors/UluScrollAnchors.vue"],"names":[],"mappings":"AAKA;wBAqLqB,uBAAuB,CAAC,OAAO,eAAe,EAAE,oBAAoB,CAAC,OAAO,CAAC,CAAC;;6BAEtE,CAAC,EAAE,CAAC;;;AAbjC;;;;;;;mBAUG"}
1
+ {"version":3,"file":"UluScrollAnchors.vue.d.ts","sourceRoot":"","sources":["../../../../lib/components/systems/scroll-anchors/UluScrollAnchors.vue"],"names":[],"mappings":"AAKA;wBAqMqB,uBAAuB,CAAC,OAAO,eAAe,EAAE,oBAAoB,CAAC,OAAO,CAAC,CAAC;;6BAEtE,CAAC,EAAE,CAAC;;;AAbjC;;;;;;;mBAUG"}
@@ -1,4 +1,4 @@
1
- import { ref as s, provide as n, computed as p, createElementBlock as f, openBlock as m, renderSlot as d } from "vue";
1
+ import { ref as s, provide as l, computed as p, createElementBlock as f, openBlock as m, renderSlot as d } from "vue";
2
2
  import { useScrollAnchors as h } from "./useScrollAnchors.js";
3
3
  const A = {
4
4
  __name: "UluScrollAnchors",
@@ -33,20 +33,28 @@ const A = {
33
33
  /**
34
34
  * Enable debug logging for the IntersectionObserver
35
35
  */
36
- debug: Boolean
36
+ debug: Boolean,
37
+ /**
38
+ * If true, the last section will deactivate when scrolling past its bounding box.
39
+ * By default, the last section remains active until the user scrolls back up.
40
+ */
41
+ deactivateLastItem: {
42
+ type: Boolean,
43
+ default: !1
44
+ }
37
45
  },
38
46
  emits: ["section-change"],
39
- setup(r, { emit: c }) {
40
- const u = r, a = c, e = s([]), t = s(null);
41
- return h({ sections: e, props: u, emit: a, componentElRef: t }), n("uluScrollAnchorsSections", p(() => e.value)), n("uluScrollAnchorsRegister", (o) => {
47
+ setup(c, { emit: r }) {
48
+ const a = c, u = r, e = s([]), n = s(null);
49
+ return h({ sections: e, props: a, emit: u, componentElRef: n }), l("uluScrollAnchorsSections", p(() => e.value)), l("uluScrollAnchorsRegister", (o) => {
42
50
  e.value.push(o);
43
- }), n("uluScrollAnchorsUnregister", (o) => {
44
- const l = e.value.findIndex((i) => i.id === o);
45
- l > -1 && e.value.splice(l, 1);
46
- }), (o, l) => (m(), f("div", {
51
+ }), l("uluScrollAnchorsUnregister", (o) => {
52
+ const t = e.value.findIndex((i) => i.id === o);
53
+ t > -1 && e.value.splice(t, 1);
54
+ }), (o, t) => (m(), f("div", {
47
55
  class: "scroll-anchors",
48
56
  ref_key: "componentEl",
49
- ref: t
57
+ ref: n
50
58
  }, [
51
59
  d(o.$slots, "default")
52
60
  ], 512));
@@ -6,6 +6,9 @@ type __VLS_WithTemplateSlots<T, S> = T & (new () => {
6
6
  declare const __VLS_component: import('vue').DefineComponent<{}, {
7
7
  element: string;
8
8
  railWidth: number;
9
+ trimRailToCenters: boolean;
10
+ railStartOffset: number;
11
+ railEndOffset: number;
9
12
  indicatorWidth: number;
10
13
  indicatorHeight: number;
11
14
  indicatorAlignment: string;
@@ -13,13 +16,16 @@ declare const __VLS_component: import('vue').DefineComponent<{}, {
13
16
  $props: {
14
17
  readonly element?: string | undefined;
15
18
  readonly railWidth?: number | undefined;
19
+ readonly trimRailToCenters?: boolean | undefined;
20
+ readonly railStartOffset?: number | undefined;
21
+ readonly railEndOffset?: number | undefined;
16
22
  readonly indicatorWidth?: number | undefined;
17
23
  readonly indicatorHeight?: number | undefined;
18
24
  readonly indicatorAlignment?: string | undefined;
19
25
  readonly indicatorAlignmentOffset?: number | undefined;
20
26
  };
21
27
  }, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {
22
- indicator: HTMLDivElement;
28
+ listRef: HTMLUListElement;
23
29
  }, any>;
24
30
  type __VLS_TemplateResult = {
25
31
  attrs: Partial<{}>;
@@ -30,7 +36,7 @@ type __VLS_TemplateResult = {
30
36
  }): any;
31
37
  };
32
38
  refs: {
33
- indicator: HTMLDivElement;
39
+ listRef: HTMLUListElement;
34
40
  };
35
41
  rootEl: any;
36
42
  };
@@ -1 +1 @@
1
- {"version":3,"file":"UluScrollAnchorsNavAnimated.vue.d.ts","sourceRoot":"","sources":["../../../../lib/components/systems/scroll-anchors/UluScrollAnchorsNavAnimated.vue"],"names":[],"mappings":"AAsCA;wBA4UqB,uBAAuB,CAAC,OAAO,eAAe,EAAE,oBAAoB,CAAC,OAAO,CAAC,CAAC;;6BAEtE,CAAC,EAAE,CAAC;;;AAXjC;;;;;;;;;;;;;;;;;QAQG"}
1
+ {"version":3,"file":"UluScrollAnchorsNavAnimated.vue.d.ts","sourceRoot":"","sources":["../../../../lib/components/systems/scroll-anchors/UluScrollAnchorsNavAnimated.vue"],"names":[],"mappings":"AAqCA;wBA4eqB,uBAAuB,CAAC,OAAO,eAAe,EAAE,oBAAoB,CAAC,OAAO,CAAC,CAAC;;6BAEtE,CAAC,EAAE,CAAC;;;AAXjC;;;;;;;;;;;;;;;;;;;;;;;QAQG"}
@@ -1,7 +1,7 @@
1
- import { ref as d, computed as N, watch as w, createBlock as $, createCommentVNode as W, unref as f, openBlock as h, resolveDynamicComponent as b, normalizeStyle as _, withCtx as C, createElementVNode as m, createElementBlock as g, Fragment as H, renderList as B, normalizeClass as v, renderSlot as I, createTextVNode as R, toDisplayString as V } from "vue";
2
- import { runAfterFramePaint as z } from "@ulu/utils/browser/performance.js";
3
- import { useScrollAnchorSections as D } from "./useScrollAnchorSections.js";
4
- const E = { class: "scroll-anchors-nav-animated__rail" }, F = ["href"], P = {
1
+ import { ref as c, onMounted as $, onBeforeUnmount as w, computed as N, watch as C, createBlock as z, createCommentVNode as B, unref as p, openBlock as y, resolveDynamicComponent as M, normalizeStyle as g, withCtx as T, createElementVNode as _, createElementBlock as k, Fragment as W, renderList as E, normalizeClass as S, renderSlot as H, createTextVNode as I, toDisplayString as V } from "vue";
2
+ import { runAfterFramePaint as D } from "@ulu/utils/browser/performance.js";
3
+ import { useScrollAnchorSections as F } from "./useScrollAnchorSections.js";
4
+ const L = ["href"], j = {
5
5
  __name: "UluScrollAnchorsNavAnimated",
6
6
  props: {
7
7
  /**
@@ -18,6 +18,26 @@ const E = { class: "scroll-anchors-nav-animated__rail" }, F = ["href"], P = {
18
18
  type: Number,
19
19
  default: 3
20
20
  },
21
+ /**
22
+ * Dynamically trims the rail to span exactly from the center of the first indicator to the center of the last indicator. Disabled by default
23
+ */
24
+ trimRailToCenters: {
25
+ type: Boolean
26
+ },
27
+ /**
28
+ * Pixel offset for the start (top) of the dynamic rail.
29
+ */
30
+ railStartOffset: {
31
+ type: Number,
32
+ default: 0
33
+ },
34
+ /**
35
+ * Pixel offset for the end (bottom) of the dynamic rail.
36
+ */
37
+ railEndOffset: {
38
+ type: Number,
39
+ default: 0
40
+ },
21
41
  /**
22
42
  * The width of the indicator, defaults to railWidth
23
43
  */
@@ -48,71 +68,96 @@ const E = { class: "scroll-anchors-nav-animated__rail" }, F = ["href"], P = {
48
68
  default: 0
49
69
  }
50
70
  },
51
- setup(o) {
52
- const i = o, n = D(), p = d({}), c = d(!1), S = d(null), e = N(() => {
53
- if (!n || !n.value || !n.value.length)
54
- return !1;
55
- const t = n.value.findIndex((x) => x.active);
56
- if (t === -1)
71
+ setup(u) {
72
+ const l = u, a = F(), f = c(null), x = c({}), d = c(!1), h = c(0);
73
+ let s = null;
74
+ $(() => {
75
+ f.value && (s = new ResizeObserver(() => {
76
+ h.value++;
77
+ }), s.observe(f.value));
78
+ }), w(() => {
79
+ s && (s.disconnect(), s = null);
80
+ });
81
+ function v(e) {
82
+ const t = x.value[e];
83
+ if (!t) return null;
84
+ const { offsetTop: n, offsetHeight: r } = t, o = l.indicatorHeight != null, R = l.indicatorWidth ?? l.railWidth, b = o ? l.indicatorHeight : r;
85
+ let m = n;
86
+ return l.indicatorAlignment === "center" && (m = n + r / 2 - b / 2), m += l.indicatorAlignmentOffset, { y: m, height: b, width: R };
87
+ }
88
+ const i = N(() => {
89
+ if (h.value, !a || !a.value || !a.value.length)
57
90
  return !1;
58
- const l = p.value[t];
59
- if (!l) return !1;
60
- const { offsetTop: a, offsetHeight: r } = l, s = i.indicatorHeight != null, A = i.indicatorWidth ?? i.railWidth, y = s ? i.indicatorHeight : r;
61
- let u = a;
62
- return i.indicatorAlignment === "center" && (u = a + r / 2 - y / 2), u += i.indicatorAlignmentOffset, { y: u, height: y, width: A };
91
+ const e = a.value.findIndex((t) => t.active);
92
+ return e === -1 ? !1 : v(e) || !1;
93
+ }), A = N(() => {
94
+ if (h.value, !l.trimRailToCenters) return {};
95
+ if (!a || !a.value || a.value.length < 1) return {};
96
+ const e = v(0), t = v(a.value.length - 1);
97
+ if (!e || !t) return {};
98
+ let n = e.y + e.height / 2, r = t.y + t.height / 2;
99
+ n += l.railStartOffset, r += l.railEndOffset;
100
+ const o = Math.max(0, r - n);
101
+ return {
102
+ "--ulu-sa-nav-rail-top": `${n}px`,
103
+ "--ulu-sa-nav-rail-height": `${o}px`
104
+ };
63
105
  });
64
- w(e, (t) => {
65
- t && !c.value && z(() => {
66
- c.value = !0;
106
+ C(i, (e) => {
107
+ e && !d.value && D(() => {
108
+ d.value = !0;
67
109
  });
68
110
  });
69
- function k(t, l) {
70
- l && (p.value[t] = l);
111
+ function O(e, t) {
112
+ t && (x.value[e] = t);
71
113
  }
72
- return (t, l) => f(n) && f(n).length ? (h(), $(b(o.element), {
114
+ return (e, t) => p(a) && p(a).length ? (y(), z(M(u.element), {
73
115
  key: 0,
74
116
  class: "scroll-anchors__nav scroll-anchors__nav--animated scroll-anchors-nav-animated",
75
- style: _({ "--ulu-sa-nav-rail-width": `${o.railWidth}px` })
117
+ style: g({ "--ulu-sa-nav-rail-width": `${u.railWidth}px` })
76
118
  }, {
77
- default: C(() => [
78
- m("ul", E, [
79
- (h(!0), g(H, null, B(f(n), (a, r) => (h(), g("li", {
119
+ default: T(() => [
120
+ _("ul", {
121
+ class: "scroll-anchors-nav-animated__rail",
122
+ ref_key: "listRef",
123
+ ref: f,
124
+ style: g(A.value)
125
+ }, [
126
+ (y(!0), k(W, null, E(p(a), (n, r) => (y(), k("li", {
80
127
  key: r,
81
- class: v({ "is-active": a.active })
128
+ class: S({ "is-active": n.active })
82
129
  }, [
83
- m("a", {
84
- class: v({ "is-active": a.active }),
130
+ _("a", {
131
+ class: S({ "is-active": n.active }),
85
132
  ref_for: !0,
86
- ref: (s) => k(r, s),
87
- href: `#${a.titleId}`
133
+ ref: (o) => O(r, o),
134
+ href: `#${n.titleId}`
88
135
  }, [
89
- I(t.$slots, "default", {
90
- item: a,
136
+ H(e.$slots, "default", {
137
+ item: n,
91
138
  index: r
92
139
  }, () => [
93
- R(V(a.title), 1)
140
+ I(V(n.title), 1)
94
141
  ])
95
- ], 10, F)
142
+ ], 10, L)
96
143
  ], 2))), 128))
97
- ]),
98
- m("div", {
99
- class: v(["scroll-anchors-nav-animated__indicator", {
100
- "scroll-anchors-nav-animated__indicator--can-transition": c.value
144
+ ], 4),
145
+ _("div", {
146
+ class: S(["scroll-anchors-nav-animated__indicator", {
147
+ "scroll-anchors-nav-animated__indicator--can-transition": d.value
101
148
  }]),
102
- ref_key: "indicator",
103
- ref: S,
104
- style: _({
105
- opacity: e.value ? "1" : "0",
106
- transform: `translateY(${e.value ? e.value.y : 0}px)`,
107
- height: `${e.value ? e.value.height : 0}px`,
108
- width: `${e.value ? e.value.width : 0}px`
149
+ style: g({
150
+ opacity: i.value ? "1" : "0",
151
+ transform: `translateY(${i.value ? i.value.y : 0}px)`,
152
+ height: `${i.value ? i.value.height : 0}px`,
153
+ width: `${i.value ? i.value.width : 0}px`
109
154
  })
110
155
  }, null, 6)
111
156
  ]),
112
157
  _: 3
113
- }, 8, ["style"])) : W("", !0);
158
+ }, 8, ["style"])) : B("", !0);
114
159
  }
115
160
  };
116
161
  export {
117
- P as default
162
+ j as default
118
163
  };
@@ -1 +1 @@
1
- {"version":3,"file":"useScrollAnchors.d.ts","sourceRoot":"","sources":["../../../../lib/components/systems/scroll-anchors/useScrollAnchors.js"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,4EAFW;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,WAAW;IAAC,cAAc,EAAE,MAAM,CAAA;CAAC,QA0KnF"}
1
+ {"version":3,"file":"useScrollAnchors.d.ts","sourceRoot":"","sources":["../../../../lib/components/systems/scroll-anchors/useScrollAnchors.js"],"names":[],"mappings":"AAIA;;;;;;GAMG;AACH,4EAFW;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,WAAW;IAAC,cAAc,EAAE,MAAM,CAAA;CAAC,QAsTnF"}
@@ -1,95 +1,138 @@
1
- import { onMounted as T, onUnmounted as Y, watch as F, nextTick as O } from "vue";
2
- import { getScrollParent as C } from "@ulu/utils/browser/dom.js";
3
- function D({ sections: i, props: e, emit: h, componentElRef: I }) {
4
- let r = null;
5
- function v(t) {
6
- return i.value.findIndex(({ element: l }) => t === l);
1
+ import { onMounted as P, onUnmounted as U, watch as Y, nextTick as L } from "vue";
2
+ import { getScrollParent as M } from "@ulu/utils/browser/dom.js";
3
+ import { debounce as j } from "@ulu/utils/performance.js";
4
+ function J({ sections: r, props: t, emit: T, componentElRef: m }) {
5
+ let a = null, O = 0, k = "down", d = null, x = !0;
6
+ function g(e) {
7
+ return r.value.findIndex(({ element: n }) => e === n);
7
8
  }
8
- function w(t = null, l = "down") {
9
- i.value.forEach((c) => {
10
- c !== t && (c.active && (c.inactiveFrom = l === "down" ? "forward" : "reverse", c.activeFrom = null), c.active = !1);
9
+ function B(e, n, s = "down") {
10
+ if (!e) return;
11
+ const v = s === "down" ? "forward" : "reverse";
12
+ n ? (e.active = !0, e.inactiveFrom = null, e.activeFrom = v) : (e.active && (e.inactiveFrom = v, e.activeFrom = null), e.active = !1);
13
+ }
14
+ function $(e = null, n = "down") {
15
+ r.value.forEach((s) => {
16
+ s !== e && B(s, !1, n);
11
17
  });
12
18
  }
13
- function A() {
14
- let t = 0, l = !0;
15
- const c = (a) => {
16
- const { root: S } = r, d = S ? S.scrollTop : document.documentElement.scrollTop || window.scrollY;
17
- if (e.debug && (console.group("useScrollAnchors: onObserve"), console.log("Observer:", r), console.log("Last/Current Y:", `${t}/${d}`), console.log("Entries:", a.map((n) => ({ el: n.target, is: n.isIntersecting })))), l && e.firstItemActive) {
18
- e.debug && console.log("Initial observation, respecting `firstItemActive`."), l = !1, t = d, e.debug && console.groupEnd();
19
- return;
20
- }
21
- l = !1;
22
- const s = d > t ? "down" : "up";
23
- t = d, e.debug && console.log(`Scroll direction: ${s}`);
24
- const f = a.filter((n) => n.isIntersecting);
25
- if (e.debug && console.log("Intersecting entries:", f.map((n) => n.target)), f.length > 0) {
26
- f.sort((u, m) => v(u.target) - v(m.target));
27
- const n = s === "down" ? f[f.length - 1] : f[0];
28
- e.debug && console.log("Chosen target entry:", n.target);
29
- const o = i.value[v(n.target)];
30
- o && !o.active && (e.debug && console.log("Activating section:", o.title), O(() => {
31
- w(o, s), o.active = !0, o.inactiveFrom = null, o.activeFrom = s === "down" ? "forward" : "reverse", h("section-change", { section: o, sections: i.value, active: !0 });
32
- }));
19
+ function h(e, n) {
20
+ e && !e.active && (t.debug && console.log("Activate:", e.title), L(() => {
21
+ $(e, n), B(e, !0, n), T("section-change", { section: e, sections: r.value, active: !0 });
22
+ }));
23
+ }
24
+ function I(e, n) {
25
+ const s = r.value.find((v) => v.active);
26
+ s && (t.debug && n && console.log(n, s.title), L(() => {
27
+ $(null, e), T("section-change", { section: s, sections: r.value, active: !1 });
28
+ }));
29
+ }
30
+ function y() {
31
+ let e = null;
32
+ return t.observerOptions && t.observerOptions.root !== void 0 ? e = t.observerOptions.root : m.value && (e = M(m.value), e === document.scrollingElement && (e = null)), e || window;
33
+ }
34
+ const A = j(() => {
35
+ t.debug && console.log("New Observer (debounced/check)"), a && (a.disconnect(), a = null), E(), w();
36
+ }, 100);
37
+ function C() {
38
+ D(), d = y(), d && d.addEventListener("scroll", A, { passive: !0 });
39
+ }
40
+ function D() {
41
+ d && (d.removeEventListener("scroll", A), d = null);
42
+ }
43
+ function E() {
44
+ x = !0;
45
+ const e = (u) => {
46
+ const { root: R } = a, S = R ? R.scrollTop : document.documentElement.scrollTop || window.scrollY;
47
+ let i = k;
48
+ S > O ? i = "down" : S < O && (i = "up"), t.debug && (console.groupCollapsed(`Scroll: ${O} -> ${S} (${i})`), console.table(u.map((o) => ({
49
+ el: o.target.id || o.target.tagName,
50
+ int: o.isIntersecting,
51
+ ratio: o.intersectionRatio.toFixed(2)
52
+ })))), O = S, k = i;
53
+ const b = u.filter((o) => o.isIntersecting);
54
+ if (b.length > 0) {
55
+ b.sort((c, f) => g(c.target) - g(f.target));
56
+ const o = i === "down" ? b[b.length - 1] : b[0];
57
+ t.debug && console.log("Target:", o.target.id || o.target.tagName);
58
+ const l = r.value[g(o.target)];
59
+ h(l, i);
60
+ } else if (x) {
61
+ t.debug && console.log("Fallback: bounds");
62
+ let o = -1;
63
+ if (u.forEach((l) => {
64
+ const c = l.rootBounds ? l.rootBounds.top : 0;
65
+ if (l.boundingClientRect.top <= c + 1) {
66
+ const f = g(l.target);
67
+ f > o && (o = f);
68
+ }
69
+ }), o > -1) {
70
+ const l = o === r.value.length - 1, c = r.value[o];
71
+ if (l && t.deactivateLastItem) {
72
+ const f = u.find((H) => H.target === c.element), F = f.rootBounds ? f.rootBounds.bottom : window.innerHeight;
73
+ f && f.boundingClientRect.bottom < F ? I(i, "Deactivate (last):") : h(c, i);
74
+ } else
75
+ h(c, i);
76
+ } else if (t.debug && console.log("Fallback: top"), !t.firstItemActive)
77
+ I(i, "Deactivate (top):");
78
+ else {
79
+ const l = r.value[0];
80
+ h(l, i);
81
+ }
33
82
  } else {
34
- e.debug && console.log("No intersecting entries. Checking edge cases.");
35
- const n = i.value.find((o) => o.active);
36
- if (n) {
37
- const o = a.find((u) => u.target === n.element);
38
- if (o && !o.isIntersecting) {
39
- const u = v(o.target), m = u === 0, y = u === i.value.length - 1;
40
- (m && s === "up" && !e.firstItemActive || y && s === "down") && (e.debug && console.log("Deactivating section at edge:", n.title), O(() => {
41
- w(null, s), h("section-change", { section: n, sections: i.value, active: !1 });
42
- }));
83
+ t.debug && console.log("Check edges");
84
+ const o = r.value.find((l) => l.active);
85
+ if (o) {
86
+ const l = u.find((c) => c.target === o.element);
87
+ if (l && !l.isIntersecting) {
88
+ const c = g(l.target), f = c === 0, F = c === r.value.length - 1;
89
+ (f && i === "up" && !t.firstItemActive || F && i === "down" && t.deactivateLastItem) && I(i, "Deactivate (edge):");
43
90
  }
44
91
  }
45
92
  }
46
- e.debug && console.groupEnd();
93
+ x = !1, t.debug && console.groupEnd();
47
94
  };
48
- let g = null;
49
- e.observerOptions && e.observerOptions.root !== void 0 ? g = e.observerOptions.root : I.value && (g = C(I.value), g === document.scrollingElement && (g = null));
50
- let E = {
95
+ let n = null;
96
+ t.observerOptions && t.observerOptions.root !== void 0 ? n = t.observerOptions.root : m.value && (n = M(m.value), n === document.scrollingElement && (n = null));
97
+ let s = {
51
98
  rootMargin: "-25% 0px -55% 0px",
52
99
  threshold: 0
53
100
  };
54
- if (e.snapOffset !== !1 && e.snapOffset !== void 0) {
55
- const a = e.snapOffset === !0 ? 20 : Number(e.snapOffset);
56
- E.rootMargin = `-${a}% 0px -${99 - a}% 0px`;
101
+ if (t.snapOffset !== !1 && t.snapOffset !== void 0) {
102
+ const u = t.snapOffset === !0 ? 20 : Number(t.snapOffset);
103
+ s.rootMargin = `-${u}% 0px -${99 - u}% 0px`;
57
104
  }
58
- const $ = {
59
- ...E,
60
- ...e.observerOptions || {},
61
- root: g
105
+ const v = {
106
+ ...s,
107
+ ...t.observerOptions || {},
108
+ root: n
62
109
  };
63
- r = new IntersectionObserver(c, $);
110
+ a = new IntersectionObserver(e, v);
64
111
  }
65
- function b() {
66
- r && (r.disconnect(), i.value.forEach(({ element: t }) => {
67
- t && r.observe(t);
112
+ function w() {
113
+ a && (a.disconnect(), r.value.forEach(({ element: e }) => {
114
+ e && a.observe(e);
68
115
  }));
69
116
  }
70
- function x() {
71
- r && (r.disconnect(), r = null);
117
+ function N() {
118
+ a && (a.disconnect(), a = null);
72
119
  }
73
- T(() => {
74
- if (e.firstItemActive && i.value.length > 0) {
75
- const t = i.value[0];
76
- t && (t.active = !0);
77
- }
78
- A(), b();
79
- }), Y(() => {
80
- x();
81
- }), F(() => i.value.length, () => {
82
- O(() => {
83
- b();
120
+ P(() => {
121
+ E(), w(), C();
122
+ }), U(() => {
123
+ N(), D(), A.cancel();
124
+ }), Y(() => r.value.length, () => {
125
+ L(() => {
126
+ w();
84
127
  });
85
- }), F(
86
- () => [e.snapOffset, e.observerOptions],
128
+ }), Y(
129
+ () => [t.snapOffset, t.observerOptions],
87
130
  () => {
88
- x(), A(), b();
131
+ N(), E(), w(), C();
89
132
  },
90
133
  { deep: !0 }
91
134
  );
92
135
  }
93
136
  export {
94
- D as useScrollAnchors
137
+ J as useScrollAnchors
95
138
  };
@@ -39,7 +39,15 @@
39
39
  /**
40
40
  * Enable debug logging for the IntersectionObserver
41
41
  */
42
- debug: Boolean
42
+ debug: Boolean,
43
+ /**
44
+ * If true, the last section will deactivate when scrolling past its bounding box.
45
+ * By default, the last section remains active until the user scrolls back up.
46
+ */
47
+ deactivateLastItem: {
48
+ type: Boolean,
49
+ default: false
50
+ }
43
51
  });
44
52
 
45
53
  const emit = defineEmits(["section-change"]);
@@ -5,7 +5,7 @@
5
5
  class="scroll-anchors__nav scroll-anchors__nav--animated scroll-anchors-nav-animated"
6
6
  :style="{ '--ulu-sa-nav-rail-width': `${ railWidth }px` }"
7
7
  >
8
- <ul class="scroll-anchors-nav-animated__rail">
8
+ <ul class="scroll-anchors-nav-animated__rail" ref="listRef" :style="railStyles">
9
9
  <li
10
10
  v-for="(item, index) in sections" :key="index"
11
11
  :class="{ 'is-active' : item.active }"
@@ -26,7 +26,6 @@
26
26
  :class="{
27
27
  'scroll-anchors-nav-animated__indicator--can-transition' : indicatorAnimReady
28
28
  }"
29
- ref="indicator"
30
29
  :style="{
31
30
  opacity: indicatorStyles ? '1' : '0',
32
31
  transform: `translateY(${ indicatorStyles ? indicatorStyles.y : 0 }px)`,
@@ -38,9 +37,9 @@
38
37
  </template>
39
38
 
40
39
  <script setup>
41
- import { ref, computed, watch } from 'vue';
40
+ import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue";
42
41
  import { runAfterFramePaint } from "@ulu/utils/browser/performance.js";
43
- import { useScrollAnchorSections } from './useScrollAnchorSections.js';
42
+ import { useScrollAnchorSections } from "./useScrollAnchorSections.js";
44
43
 
45
44
  const props = defineProps({
46
45
  /**
@@ -57,6 +56,26 @@
57
56
  type: Number,
58
57
  default: 3
59
58
  },
59
+ /**
60
+ * Dynamically trims the rail to span exactly from the center of the first indicator to the center of the last indicator. Disabled by default
61
+ */
62
+ trimRailToCenters: {
63
+ type: Boolean
64
+ },
65
+ /**
66
+ * Pixel offset for the start (top) of the dynamic rail.
67
+ */
68
+ railStartOffset: {
69
+ type: Number,
70
+ default: 0
71
+ },
72
+ /**
73
+ * Pixel offset for the end (bottom) of the dynamic rail.
74
+ */
75
+ railEndOffset: {
76
+ type: Number,
77
+ default: 0
78
+ },
60
79
  /**
61
80
  * The width of the indicator, defaults to railWidth
62
81
  */
@@ -76,7 +95,7 @@
76
95
  */
77
96
  indicatorAlignment: {
78
97
  type: String,
79
- default: 'center' // options: center, top
98
+ default: "center" // options: center, top
80
99
  },
81
100
  /**
82
101
  * Pixel offset for the indicator's vertical alignment
@@ -87,22 +106,42 @@
87
106
  }
88
107
  });
89
108
 
109
+ // State from the scroll anchor system
90
110
  const sections = useScrollAnchorSections();
91
111
 
112
+ // Template refs for the list and individual links
113
+ const listRef = ref(null);
92
114
  const linkRefs = ref({});
115
+
116
+ // Flag to enable CSS transitions only after initial placement
93
117
  const indicatorAnimReady = ref(false);
94
- const indicator = ref(null);
95
118
 
96
- const indicatorStyles = computed(() => {
97
- if (!sections || !sections.value || !sections.value.length) {
98
- return false;
119
+ // Resize observer to recalculate metrics on layout shifts
120
+ const resizeTrigger = ref(0);
121
+ let resizeObserver = null;
122
+
123
+ onMounted(() => {
124
+ if (listRef.value) {
125
+ resizeObserver = new ResizeObserver(() => {
126
+ resizeTrigger.value++;
127
+ });
128
+ resizeObserver.observe(listRef.value);
99
129
  }
100
- const activeIndex = sections.value.findIndex(s => s.active);
101
- if (activeIndex === -1) {
102
- return false;
130
+ });
131
+
132
+ onBeforeUnmount(() => {
133
+ if (resizeObserver) {
134
+ resizeObserver.disconnect();
135
+ resizeObserver = null;
103
136
  }
104
- const link = linkRefs.value[activeIndex];
105
- if (!link) return false; // Link might not be rendered yet
137
+ });
138
+
139
+ /**
140
+ * Helper to calculate the target position/size of the indicator for a specific item
141
+ */
142
+ function getIndicatorMetrics(index) {
143
+ const link = linkRefs.value[index];
144
+ if (!link) return null;
106
145
 
107
146
  const { offsetTop, offsetHeight } = link;
108
147
  const isStatic = props.indicatorHeight != null;
@@ -110,15 +149,54 @@
110
149
  const height = isStatic ? props.indicatorHeight : offsetHeight;
111
150
 
112
151
  let y = offsetTop; // Default to 'top' alignment
113
- if (props.indicatorAlignment === 'center') {
152
+ if (props.indicatorAlignment === "center") {
114
153
  y = offsetTop + (offsetHeight / 2) - (height / 2);
115
154
  }
116
155
 
117
156
  y += props.indicatorAlignmentOffset;
118
157
 
119
158
  return { y, height, width };
159
+ }
160
+
161
+ // Active indicator styles
162
+ const indicatorStyles = computed(() => {
163
+ resizeTrigger.value; // Re-evaluate on resize
164
+ if (!sections || !sections.value || !sections.value.length) {
165
+ return false;
166
+ }
167
+ const activeIndex = sections.value.findIndex(s => s.active);
168
+ if (activeIndex === -1) {
169
+ return false;
170
+ }
171
+ return getIndicatorMetrics(activeIndex) || false;
172
+ });
173
+
174
+ // Background rail styles (trimmed to start/end of indicators)
175
+ const railStyles = computed(() => {
176
+ resizeTrigger.value; // Re-evaluate on resize
177
+ if (!props.trimRailToCenters) return {};
178
+ if (!sections || !sections.value || sections.value.length < 1) return {};
179
+
180
+ const firstMetrics = getIndicatorMetrics(0);
181
+ const lastMetrics = getIndicatorMetrics(sections.value.length - 1);
182
+
183
+ if (!firstMetrics || !lastMetrics) return {};
184
+
185
+ let top = firstMetrics.y + (firstMetrics.height / 2);
186
+ let bottom = lastMetrics.y + (lastMetrics.height / 2);
187
+
188
+ top += props.railStartOffset;
189
+ bottom += props.railEndOffset;
190
+
191
+ const height = Math.max(0, bottom - top);
192
+
193
+ return {
194
+ "--ulu-sa-nav-rail-top": `${top}px`,
195
+ "--ulu-sa-nav-rail-height": `${height}px`
196
+ };
120
197
  });
121
198
 
199
+ // Allow transition after initial styles are applied
122
200
  watch(indicatorStyles, (val) => {
123
201
  if (val && !indicatorAnimReady.value) {
124
202
  runAfterFramePaint(() => {
@@ -127,9 +205,10 @@
127
205
  }
128
206
  });
129
207
 
208
+ // Helper to store link template refs
130
209
  function addLinkRef(index, el) {
131
210
  if (el) {
132
211
  linkRefs.value[index] = el;
133
212
  }
134
213
  }
135
- </script>
214
+ </script>
@@ -49,8 +49,18 @@ $config: (
49
49
  position: relative;
50
50
  }
51
51
  #{ $prefix }__rail {
52
- border-left: var(--ulu-sa-nav-rail-width, 3px) solid color.get(get("rail-border-color"));
53
52
  padding-left: get("rail-padding");
53
+
54
+ &::before {
55
+ content: "";
56
+ position: absolute;
57
+ left: 0;
58
+ top: var(--ulu-sa-nav-rail-top, 0);
59
+ height: var(--ulu-sa-nav-rail-height, 100%);
60
+ width: var(--ulu-sa-nav-rail-width, 3px);
61
+ background-color: color.get(get("rail-border-color"));
62
+ z-index: 0;
63
+ }
54
64
  }
55
65
  #{ $prefix }__indicator {
56
66
  position: absolute;
@@ -58,6 +68,7 @@ $config: (
58
68
  left: 0;
59
69
  background-color: color.get(get("indicator-color"));
60
70
  clip-path: get("indicator-clip-path");
71
+ z-index: 1;
61
72
  }
62
73
  #{ $prefix }__indicator--can-transition {
63
74
  transition-property: height, transform, width;
@@ -1,5 +1,6 @@
1
1
  import { onMounted, onUnmounted, nextTick, watch } from "vue";
2
2
  import { getScrollParent } from "@ulu/utils/browser/dom.js";
3
+ import { debounce } from "@ulu/utils/performance.js";
3
4
 
4
5
  /**
5
6
  * The main composable that contains the core "engine" for the Scroll Anchors system.
@@ -10,96 +11,236 @@ import { getScrollParent } from "@ulu/utils/browser/dom.js";
10
11
  */
11
12
  export function useScrollAnchors({ sections, props, emit, componentElRef }) {
12
13
  let observer = null;
14
+ let lastScrollY = 0;
15
+ let lastScrollDirection = 'down';
16
+ let scrollRoot = null;
17
+ let isObserverRefresh = true;
13
18
 
19
+ /**
20
+ * Helper to quickly get the index of a section by its DOM element
21
+ */
14
22
  function getSectionIndex(el) {
15
23
  return sections.value.findIndex(({ element }) => el === element);
16
24
  }
17
25
 
26
+ /**
27
+ * Standardizes state assignments (active, forward/reverse animations) for a specific section
28
+ */
29
+ function setSectionState(section, isActive, scrollDirection = 'down') {
30
+ if (!section) return;
31
+ const direction = scrollDirection === 'down' ? 'forward' : 'reverse';
32
+ if (isActive) {
33
+ section.active = true;
34
+ section.inactiveFrom = null;
35
+ section.activeFrom = direction;
36
+ } else {
37
+ if (section.active) {
38
+ section.inactiveFrom = direction;
39
+ section.activeFrom = null;
40
+ }
41
+ section.active = false;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Loops through all sections and forces them to inactive, optionally skipping one target
47
+ */
18
48
  function removeActive(except = null, scrollDirection = 'down') {
19
49
  sections.value.forEach(s => {
20
50
  if (s !== except) {
21
- if (s.active) {
22
- s.inactiveFrom = scrollDirection === 'down' ? 'forward' : 'reverse';
23
- s.activeFrom = null;
24
- }
25
- s.active = false;
51
+ setSectionState(s, false, scrollDirection);
26
52
  }
27
53
  });
28
54
  }
29
55
 
56
+ /**
57
+ * Safe wrapper to clear other sections, make a new target active, and emit the change event
58
+ */
59
+ function setSectionActive(section, scrollDirection) {
60
+ if (section && !section.active) {
61
+ if (props.debug) console.log("Activate:", section.title);
62
+ nextTick(() => {
63
+ removeActive(section, scrollDirection);
64
+ setSectionState(section, true, scrollDirection);
65
+ emit("section-change", { section, sections: sections.value, active: true });
66
+ });
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Clears the currently active item and emits a deactivation event (used at top/bottom scroll edges)
72
+ */
73
+ function deactivateAll(scrollDirection, debugMsg) {
74
+ const activeSection = sections.value.find(s => s.active);
75
+ if (activeSection) {
76
+ if (props.debug && debugMsg) console.log(debugMsg, activeSection.title);
77
+ nextTick(() => {
78
+ removeActive(null, scrollDirection);
79
+ emit("section-change", { section: activeSection, sections: sections.value, active: false });
80
+ });
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Resolves the scrolling container element based on options, DOM hierarchy, or defaults to window
86
+ */
87
+ function getScrollRoot() {
88
+ let root = null;
89
+ if (props.observerOptions && props.observerOptions.root !== undefined) {
90
+ root = props.observerOptions.root;
91
+ } else if (componentElRef.value) {
92
+ root = getScrollParent(componentElRef.value);
93
+ if (root === document.scrollingElement) {
94
+ root = null;
95
+ }
96
+ }
97
+ return root || window;
98
+ }
99
+
100
+ /**
101
+ * Debounced callback attached to the scroll listener.
102
+ * Rebuilding the observer ensures a fresh set of entries representing the true layout when scrolling stops.
103
+ */
104
+ const refreshObserver = debounce(() => {
105
+ if (props.debug) console.log("New Observer (debounced/check)");
106
+ if (observer) {
107
+ observer.disconnect();
108
+ observer = null;
109
+ }
110
+ createObserver();
111
+ observeItems();
112
+ }, 100);
113
+
114
+ function setupScrollListener() {
115
+ removeScrollListener();
116
+ scrollRoot = getScrollRoot();
117
+ if (scrollRoot) {
118
+ scrollRoot.addEventListener('scroll', refreshObserver, { passive: true });
119
+ }
120
+ }
121
+
122
+ function removeScrollListener() {
123
+ if (scrollRoot) {
124
+ scrollRoot.removeEventListener('scroll', refreshObserver);
125
+ scrollRoot = null;
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Primary logic core. Creates the IntersectionObserver and handles state changes.
131
+ */
30
132
  function createObserver() {
31
- let lastScrollY = 0;
32
- let isInitialObservation = true;
133
+ isObserverRefresh = true;
33
134
 
34
135
  const onObserve = (entries) => {
35
136
  const { root } = observer;
36
137
  const currentScrollY = root ? root.scrollTop : document.documentElement.scrollTop || window.scrollY;
37
138
 
38
- if (props.debug) {
39
- console.group("useScrollAnchors: onObserve");
40
- console.log("Observer:", observer);
41
- console.log("Last/Current Y:", `${ lastScrollY }/${ currentScrollY }`);
42
- console.log("Entries:", entries.map(e => ({ el: e.target, is: e.isIntersecting })));
139
+ // Determine the direction of the scroll
140
+ let scrollDirection = lastScrollDirection;
141
+ if (currentScrollY > lastScrollY) {
142
+ scrollDirection = 'down';
143
+ } else if (currentScrollY < lastScrollY) {
144
+ scrollDirection = 'up';
43
145
  }
44
146
 
45
- if (isInitialObservation && props.firstItemActive) {
46
- if (props.debug) console.log("Initial observation, respecting `firstItemActive`.");
47
- isInitialObservation = false;
48
- lastScrollY = currentScrollY;
49
- if (props.debug) console.groupEnd();
50
- return;
147
+ if (props.debug) {
148
+ console.groupCollapsed(`Scroll: ${lastScrollY} -> ${currentScrollY} (${scrollDirection})`);
149
+ console.table(entries.map(e => ({
150
+ el: e.target.id || e.target.tagName,
151
+ int: e.isIntersecting,
152
+ ratio: e.intersectionRatio.toFixed(2)
153
+ })));
51
154
  }
52
- isInitialObservation = false;
53
155
 
54
- const scrollDirection = currentScrollY > lastScrollY ? 'down' : 'up';
55
156
  lastScrollY = currentScrollY;
56
- if (props.debug) console.log(`Scroll direction: ${scrollDirection}`);
157
+ lastScrollDirection = scrollDirection;
57
158
 
58
159
  const intersectingEntries = entries.filter(entry => entry.isIntersecting);
59
- if (props.debug) console.log("Intersecting entries:", intersectingEntries.map(e => e.target));
60
160
 
161
+ // Strategy 1: Standard intersection detection
61
162
  if (intersectingEntries.length > 0) {
62
163
  intersectingEntries.sort((a, b) => getSectionIndex(a.target) - getSectionIndex(b.target));
63
164
 
165
+ // Choose the most appropriate intersecting entry based on scroll direction
64
166
  const targetEntry = scrollDirection === 'down'
65
167
  ? intersectingEntries[intersectingEntries.length - 1]
66
168
  : intersectingEntries[0];
67
- if (props.debug) console.log("Chosen target entry:", targetEntry.target);
169
+
170
+ if (props.debug) console.log("Target:", targetEntry.target.id || targetEntry.target.tagName);
68
171
 
69
172
  const sectionToActivate = sections.value[getSectionIndex(targetEntry.target)];
70
-
71
- if (sectionToActivate && !sectionToActivate.active) {
72
- if (props.debug) console.log("Activating section:", sectionToActivate.title);
73
- nextTick(() => {
74
- removeActive(sectionToActivate, scrollDirection);
75
- sectionToActivate.active = true;
76
- sectionToActivate.inactiveFrom = null;
77
- sectionToActivate.activeFrom = scrollDirection === 'down' ? 'forward' : 'reverse';
78
- emit("section-change", { section: sectionToActivate, sections: sections.value, active: true });
79
- });
80
- }
173
+ setSectionActive(sectionToActivate, scrollDirection);
81
174
  } else {
82
- if (props.debug) console.log("No intersecting entries. Checking edge cases.");
83
- const activeSection = sections.value.find(s => s.active);
84
- if (activeSection) {
85
- const entryForActive = entries.find(e => e.target === activeSection.element);
86
- if (entryForActive && !entryForActive.isIntersecting) {
87
- const index = getSectionIndex(entryForActive.target);
88
- const isFirst = index === 0;
89
- const isLast = index === sections.value.length - 1;
90
- if ((isFirst && scrollDirection === 'up' && !props.firstItemActive) || (isLast && scrollDirection === 'down')) {
91
- if (props.debug) console.log("Deactivating section at edge:", activeSection.title);
92
- nextTick(() => {
93
- removeActive(null, scrollDirection);
94
- emit("section-change", { section: activeSection, sections: sections.value, active: false });
95
- });
175
+ // Strategy 2: Absolute positioning fallback (fired automatically on fresh observer load)
176
+ // Used to instantly catch sections we skipped entirely during warp-speed scrolling.
177
+ if (isObserverRefresh) {
178
+ if (props.debug) console.log("Fallback: bounds");
179
+
180
+ let highestAboveIndex = -1;
181
+ entries.forEach(entry => {
182
+ const rootTop = entry.rootBounds ? entry.rootBounds.top : 0;
183
+ // +1 buffer for fractional pixels, check if element is above the reading zone
184
+ if (entry.boundingClientRect.top <= rootTop + 1) {
185
+ const idx = getSectionIndex(entry.target);
186
+ if (idx > highestAboveIndex) {
187
+ highestAboveIndex = idx;
188
+ }
189
+ }
190
+ });
191
+
192
+ // Activate the last section that is above the reading zone viewport
193
+ if (highestAboveIndex > -1) {
194
+ const isLast = highestAboveIndex === sections.value.length - 1;
195
+ const targetSection = sections.value[highestAboveIndex];
196
+
197
+ // Edge case: User scrolled past the bottom of the very last section into a footer
198
+ if (isLast && props.deactivateLastItem) {
199
+ const lastEntry = entries.find(e => e.target === targetSection.element);
200
+ const rootBottom = lastEntry.rootBounds ? lastEntry.rootBounds.bottom : window.innerHeight;
201
+ if (lastEntry && lastEntry.boundingClientRect.bottom < rootBottom) {
202
+ deactivateAll(scrollDirection, "Deactivate (last):");
203
+ } else {
204
+ setSectionActive(targetSection, scrollDirection);
205
+ }
206
+ } else {
207
+ setSectionActive(targetSection, scrollDirection);
208
+ }
209
+ } else {
210
+ // No elements are above the reading zone viewport, we are above the first item
211
+ if (props.debug) console.log("Fallback: top");
212
+ if (!props.firstItemActive) {
213
+ deactivateAll(scrollDirection, "Deactivate (top):");
214
+ } else {
215
+ const firstSection = sections.value[0];
216
+ setSectionActive(firstSection, scrollDirection);
217
+ }
218
+ }
219
+ } else {
220
+ // Strategy 3: Real-time edge deactivation
221
+ // Fires as user slowly scrolls out of the bounds of the first or last items
222
+ if (props.debug) console.log("Check edges");
223
+ const activeSection = sections.value.find(s => s.active);
224
+ if (activeSection) {
225
+ const entryForActive = entries.find(e => e.target === activeSection.element);
226
+ if (entryForActive && !entryForActive.isIntersecting) {
227
+ const index = getSectionIndex(entryForActive.target);
228
+ const isFirst = index === 0;
229
+ const isLast = index === sections.value.length - 1;
230
+ if ((isFirst && scrollDirection === 'up' && !props.firstItemActive) || (isLast && scrollDirection === 'down' && props.deactivateLastItem)) {
231
+ deactivateAll(scrollDirection, "Deactivate (edge):");
232
+ }
96
233
  }
97
234
  }
98
235
  }
99
236
  }
237
+
238
+ // Observer refresh cycle completed
239
+ isObserverRefresh = false;
100
240
  if (props.debug) console.groupEnd();
101
241
  };
102
242
 
243
+ // Calculate options and margins for the reading zone
103
244
  let root = null;
104
245
  if (props.observerOptions && props.observerOptions.root !== undefined) {
105
246
  root = props.observerOptions.root;
@@ -147,32 +288,32 @@ export function useScrollAnchors({ sections, props, emit, componentElRef }) {
147
288
  }
148
289
 
149
290
  onMounted(() => {
150
- if (props.firstItemActive && sections.value.length > 0) {
151
- const first = sections.value[0];
152
- if (first) {
153
- first.active = true;
154
- }
155
- }
156
291
  createObserver();
157
292
  observeItems();
293
+ setupScrollListener();
158
294
  });
159
295
 
160
296
  onUnmounted(() => {
161
297
  destroyObserver();
298
+ removeScrollListener();
299
+ refreshObserver.cancel();
162
300
  });
163
301
 
302
+ // Re-observe items dynamically if the section list grows/shrinks
164
303
  watch(() => sections.value.length, () => {
165
304
  nextTick(() => {
166
305
  observeItems();
167
306
  });
168
307
  });
169
308
 
309
+ // Hot swap the observer if the developer changes options or snap settings
170
310
  watch(
171
311
  () => [props.snapOffset, props.observerOptions],
172
312
  () => {
173
313
  destroyObserver();
174
314
  createObserver();
175
315
  observeItems();
316
+ setupScrollListener();
176
317
  },
177
318
  { deep: true }
178
319
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ulu/frontend-vue",
3
- "version": "0.5.14",
3
+ "version": "0.5.16",
4
4
  "description": "A modular, tree-shakeable Vue 3 component library for the Ulu Frontend theming system, plus general utilities for Vue development",
5
5
  "type": "module",
6
6
  "files": [
@@ -65,7 +65,7 @@
65
65
  "@fortawesome/vue-fontawesome": "^3.0.8",
66
66
  "@headlessui/vue": "^1.7.23",
67
67
  "@portabletext/vue": "^1.0.14",
68
- "@ulu/frontend": "^0.4.11",
68
+ "@ulu/frontend": "^0.5.0",
69
69
  "@ulu/utils": "^0.0.34",
70
70
  "@unhead/vue": "^2.0.11",
71
71
  "fuse.js": "^6.6.2",