@ulu/frontend-vue 0.6.23 → 0.6.24

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 (67) hide show
  1. package/dist/components/collapsible/UluAccordion.vue.d.ts +2 -2
  2. package/dist/components/collapsible/UluModal.vue.d.ts +4 -4
  3. package/dist/components/collapsible/UluTabGroup.vue.d.ts +2 -2
  4. package/dist/components/elements/UluAlert.vue.d.ts +2 -2
  5. package/dist/components/elements/UluBadge.vue.d.ts +4 -4
  6. package/dist/components/elements/UluButton.vue.d.ts +10 -10
  7. package/dist/components/elements/UluButtonVerbose.vue.d.ts +4 -4
  8. package/dist/components/elements/UluCaptionedFigure.vue.d.ts +2 -2
  9. package/dist/components/elements/UluCard.vue.d.ts +4 -4
  10. package/dist/components/elements/UluList.vue.d.ts +2 -2
  11. package/dist/components/elements/UluSlider.vue.d.ts +2 -2
  12. package/dist/components/elements/UluTag.vue.d.ts +2 -2
  13. package/dist/components/forms/UluSearchForm.vue.d.ts +2 -2
  14. package/dist/components/layout/UluTitleRail.vue.d.ts +2 -2
  15. package/dist/components/systems/facets/UluFacetsList.vue.d.ts +2 -2
  16. package/dist/components/systems/facets/UluFacetsSearch.vue.d.ts +2 -2
  17. package/dist/components/utils/UluAction.vue.d.ts +2 -2
  18. package/dist/components/visualizations/UluProgressBar.vue.d.ts +2 -2
  19. package/dist/components/visualizations/UluProgressCircle.vue.d.ts +2 -2
  20. package/dist/composables/useUluFloating.d.ts.map +1 -1
  21. package/dist/composables/useUluFloating.js +26 -25
  22. package/dist/index.js +258 -240
  23. package/dist/mcp-data.json +1047 -381
  24. package/dist/plugins/index.d.ts +2 -1
  25. package/dist/plugins/popovers/UluPopover.vue.d.ts +188 -7
  26. package/dist/plugins/popovers/UluPopover.vue.d.ts.map +1 -1
  27. package/dist/plugins/popovers/UluPopover.vue.js +82 -133
  28. package/dist/plugins/popovers/UluPopoverBase.vue.d.ts +58 -0
  29. package/dist/plugins/popovers/UluPopoverBase.vue.d.ts.map +1 -0
  30. package/dist/plugins/popovers/UluPopoverBase.vue.js +146 -0
  31. package/dist/plugins/popovers/UluTooltipPopover.vue.d.ts +1 -4
  32. package/dist/plugins/popovers/UluTooltipPopover.vue.d.ts.map +1 -1
  33. package/dist/plugins/popovers/UluTooltipPopover.vue.js +24 -45
  34. package/dist/plugins/popovers/index.d.ts +4 -0
  35. package/dist/plugins/popovers/index.d.ts.map +1 -1
  36. package/dist/plugins/popovers/index.js +54 -49
  37. package/dist/plugins/tour/UluTourContent.vue.d.ts +10 -0
  38. package/dist/plugins/tour/UluTourContent.vue.d.ts.map +1 -0
  39. package/dist/plugins/tour/UluTourContent.vue.js +39 -0
  40. package/dist/plugins/tour/UluTourDisplay.vue.d.ts +100 -0
  41. package/dist/plugins/tour/UluTourDisplay.vue.d.ts.map +1 -0
  42. package/dist/plugins/tour/UluTourDisplay.vue.js +133 -0
  43. package/dist/plugins/tour/UluTourPager.vue.d.ts +3 -0
  44. package/dist/plugins/tour/UluTourPager.vue.d.ts.map +1 -0
  45. package/dist/plugins/tour/UluTourPager.vue.js +31 -0
  46. package/dist/plugins/tour/api.d.ts +52 -0
  47. package/dist/plugins/tour/api.d.ts.map +1 -0
  48. package/dist/plugins/tour/api.js +52 -0
  49. package/dist/plugins/tour/index.d.ts +7 -0
  50. package/dist/plugins/tour/index.d.ts.map +1 -0
  51. package/dist/plugins/tour/index.js +14 -0
  52. package/dist/plugins/tour/useTour.d.ts +8 -0
  53. package/dist/plugins/tour/useTour.d.ts.map +1 -0
  54. package/dist/plugins/tour/useTour.js +8 -0
  55. package/lib/composables/useUluFloating.js +2 -0
  56. package/lib/plugins/index.js +3 -3
  57. package/lib/plugins/popovers/UluPopover.vue +42 -125
  58. package/lib/plugins/popovers/UluPopoverBase.vue +197 -0
  59. package/lib/plugins/popovers/UluTooltipPopover.vue +11 -33
  60. package/lib/plugins/popovers/index.js +15 -5
  61. package/lib/plugins/tour/UluTourContent.vue +37 -0
  62. package/lib/plugins/tour/UluTourDisplay.vue +182 -0
  63. package/lib/plugins/tour/UluTourPager.vue +36 -0
  64. package/lib/plugins/tour/api.js +83 -0
  65. package/lib/plugins/tour/index.js +29 -0
  66. package/lib/plugins/tour/useTour.js +10 -0
  67. package/package.json +1 -1
@@ -0,0 +1,197 @@
1
+ <template>
2
+ <span
3
+ class="popover"
4
+ ref="contentEl"
5
+ :style="floatingStyles"
6
+ :data-placement="placement"
7
+ :class="[
8
+ {
9
+ 'popover--fixed': isFixedStrategy,
10
+ 'is-active': isOpen
11
+ },
12
+ resolvedModifiers
13
+ ]"
14
+ @keydown.esc="handleEsc"
15
+ tabindex="-1"
16
+ >
17
+ <span class="popover__inner">
18
+ <slot :isOpen="isOpen" :close="close" />
19
+ </span>
20
+ <span v-if="$slots.footer" class="popover__footer">
21
+ <slot name="footer" :isOpen="isOpen" :close="close" />
22
+ </span>
23
+ <span
24
+ v-if="resolvedConfig.arrow"
25
+ class="popover__arrow"
26
+ ref="contentArrow"
27
+ :style="arrowStyles"
28
+ data-ulu-popover-arrow
29
+ ></span>
30
+ </span>
31
+ </template>
32
+
33
+ <script setup>
34
+ import { ref, computed, watch, onUnmounted, nextTick } from 'vue';
35
+ import { useUluFloating } from '../../composables/useUluFloating.js';
36
+ import { useModifiers } from '../../composables/useModifiers.js';
37
+ import { wasClickOutside } from '@ulu/utils/browser/dom.js';
38
+
39
+ const emit = defineEmits(['close']);
40
+
41
+ const props = defineProps({
42
+ /**
43
+ * Controls the open/active state of the popover
44
+ */
45
+ isOpen: {
46
+ type: Boolean,
47
+ default: false
48
+ },
49
+ /**
50
+ * Close popover when click is outside
51
+ */
52
+ clickOutsideCloses: {
53
+ type: Boolean,
54
+ default: true
55
+ },
56
+ /**
57
+ * Close popover when escape key is pressed
58
+ */
59
+ escapeCloses: {
60
+ type: Boolean,
61
+ default: true
62
+ },
63
+ /**
64
+ * Direct focus when open/closing popover
65
+ */
66
+ directFocus: {
67
+ type: Function,
68
+ default: ({ isOpen, trigger, content }) => {
69
+ if (isOpen && content) {
70
+ content.focus({ preventScroll: true });
71
+ } else if (!isOpen && trigger && trigger instanceof HTMLElement) {
72
+ // Note: using nextTick prevents scroll jumping in some cases
73
+ trigger.focus({ preventScroll: true });
74
+ }
75
+ }
76
+ },
77
+ /**
78
+ * The target element for the popover to float alongside
79
+ */
80
+ trigger: {
81
+ type: Object,
82
+ default: null
83
+ },
84
+ /**
85
+ * Floating UI configuration
86
+ */
87
+ config: {
88
+ type: Object,
89
+ default: () => ({})
90
+ },
91
+ /**
92
+ * Modifiers (to add any modifier classes based on base class [ie. 'large'])
93
+ */
94
+ modifiers: [String, Array, Object]
95
+ });
96
+
97
+ const contentEl = ref(null);
98
+
99
+ const { resolvedModifiers } = useModifiers({ props, baseClass: "popover" });
100
+
101
+ const resolvedConfig = computed(() => props.config || {});
102
+
103
+ const { floatingStyles, placement, arrowStyles, update, isFixedStrategy, contentArrow } = useUluFloating(
104
+ computed(() => props.trigger),
105
+ contentEl,
106
+ resolvedConfig
107
+ );
108
+
109
+ const close = () => {
110
+ emit('close');
111
+ };
112
+
113
+ const handleEsc = (event) => {
114
+ if (props.isOpen && props.escapeCloses) {
115
+ event.preventDefault();
116
+ close();
117
+ }
118
+ };
119
+
120
+ let outsideHandler = null;
121
+
122
+ const destroyOutsideClick = () => {
123
+ if (outsideHandler) {
124
+ document.removeEventListener("click", outsideHandler);
125
+ outsideHandler = null;
126
+ }
127
+ };
128
+
129
+ const addOutsideClick = () => {
130
+ destroyOutsideClick();
131
+ if (props.clickOutsideCloses) {
132
+ outsideHandler = (event) => {
133
+ if (!props.isOpen || !contentEl.value) return;
134
+
135
+ // Ignore clicks on elements that have been removed from the DOM
136
+ if (!document.body.contains(event.target)) return;
137
+
138
+ // If the click happened on a DOM node physically inside the popover,
139
+ // it is NOT an outside click, even if floating-ui instantly moved
140
+ // the popover across the screen causing `wasClickOutside` (bounding rect check) to fail.
141
+ if (contentEl.value.contains(event.target)) return;
142
+
143
+ if (wasClickOutside(contentEl.value, event)) {
144
+ // If the click is on the trigger itself, we let the trigger handle toggling
145
+ if (props.trigger instanceof HTMLElement && props.trigger.contains(event.target)) {
146
+ return;
147
+ }
148
+ close();
149
+ }
150
+ };
151
+ // Defer so the click that opened the popover doesn't instantly close it
152
+ setTimeout(() => {
153
+ if (outsideHandler) {
154
+ document.addEventListener("click", outsideHandler);
155
+ }
156
+ }, 0);
157
+ }
158
+ };
159
+
160
+ watch(() => props.isOpen, (newVal) => {
161
+ if (newVal) {
162
+ update();
163
+ addOutsideClick();
164
+ if (props.directFocus) {
165
+ nextTick(() => {
166
+ props.directFocus({ isOpen: true, trigger: props.trigger, content: contentEl.value });
167
+ });
168
+ }
169
+ } else {
170
+ destroyOutsideClick();
171
+ if (props.directFocus) {
172
+ nextTick(() => {
173
+ props.directFocus({ isOpen: false, trigger: props.trigger, content: contentEl.value });
174
+ });
175
+ }
176
+ }
177
+ }, { immediate: true });
178
+
179
+ onUnmounted(() => {
180
+ destroyOutsideClick();
181
+ });
182
+
183
+ defineExpose({
184
+ /**
185
+ * Emits the close event
186
+ */
187
+ close,
188
+ /**
189
+ * Manually trigger a floating UI position update
190
+ */
191
+ update,
192
+ /**
193
+ * The internal root popover element reference
194
+ */
195
+ content: contentEl
196
+ });
197
+ </script>
@@ -1,39 +1,25 @@
1
- <!-- NOTE: Need to rename classes when moving this into the library -->
2
1
  <template>
3
- <span
4
- class="popover popover--tooltip is-active"
5
- ref="content"
2
+ <UluPopoverBase
3
+ class="popover--tooltip is-active"
6
4
  :id="TOOLTIP_ID"
7
- :data-placement="placement"
8
- :class="[
9
- {
10
- 'popover--fixed' : isFixedStrategy
11
- },
12
- resolvedConfig.class
13
- ]"
14
- :style="floatingStyles"
5
+ :trigger="trigger"
6
+ :config="resolvedConfig"
7
+ :class="resolvedConfig.class"
15
8
  >
16
- <span v-if="resolvedConfig.isHtml" class="popover__inner" v-html="resolvedConfig.content">
17
- </span>
18
- <span v-else class="popover__inner">
9
+ <span v-if="resolvedConfig.isHtml" v-html="resolvedConfig.content"></span>
10
+ <template v-else>
19
11
  <component v-if="resolvedConfig.component" :is="resolvedConfig.component" v-bind="resolvedConfig.componentProps"/>
20
12
  <template v-else>
21
13
  {{ resolvedConfig.content }}
22
14
  </template>
23
- </span>
24
- <span
25
- v-if="resolvedConfig.arrow"
26
- class="popover__arrow"
27
- ref="contentArrow"
28
- :style="arrowStyles"
29
- ></span>
30
- </span>
15
+ </template>
16
+ </UluPopoverBase>
31
17
  </template>
32
18
 
33
19
  <script setup>
34
- import { ref, toRef, computed } from "vue";
35
- import { useUluFloating } from "../../composables/useUluFloating.js";
20
+ import { computed } from "vue";
36
21
  import { TOOLTIP_ID } from "./index.js";
22
+ import UluPopoverBase from "./UluPopoverBase.vue";
37
23
 
38
24
  const props = defineProps({
39
25
  config: Object,
@@ -43,13 +29,5 @@
43
29
  }
44
30
  });
45
31
 
46
- const content = ref(null);
47
32
  const resolvedConfig = computed(() => props.config);
48
- const {
49
- floatingStyles,
50
- placement,
51
- arrowStyles,
52
- contentArrow,
53
- isFixedStrategy
54
- } = useUluFloating(toRef(props, 'trigger'), content, resolvedConfig);
55
33
  </script>
@@ -4,6 +4,7 @@
4
4
  import { reactive, markRaw } from 'vue';
5
5
  import UluTooltipDisplay from './UluTooltipDisplay.vue';
6
6
  import UluPopover from "./UluPopover.vue";
7
+ import UluPopoverBase from "./UluPopoverBase.vue";
7
8
  import defaults from './defaults.js';
8
9
  import { useTooltip } from './useTooltip.js';
9
10
 
@@ -73,9 +74,12 @@ export default function install(app, userOptions = {}) {
73
74
  config: {},
74
75
  });
75
76
  const showTooltip = (triggerEl, configObj) => {
77
+ const isDOMElement = triggerEl instanceof Element;
78
+
76
79
  // If a teleport target isn't already specified in the config,
77
80
  // try to find a parent dialog to teleport to automatically.
78
- if (triggerEl && !configObj.teleportTo) {
81
+ // (Note: triggerEl might be a virtual element object from useTooltipFollow, not a real DOM element)
82
+ if (isDOMElement && !configObj.teleportTo) {
79
83
  const dialog = triggerEl.closest("dialog");
80
84
  if (dialog) {
81
85
  configObj.teleportTo = dialog;
@@ -84,12 +88,12 @@ export default function install(app, userOptions = {}) {
84
88
 
85
89
  // If a tooltip is already active for a different element, remove its attribute.
86
90
  if (tooltipState.trigger && tooltipState.trigger !== triggerEl) {
87
- if (tooltipState.trigger?.removeAttribute) {
91
+ if (tooltipState.trigger instanceof Element) {
88
92
  tooltipState.trigger.removeAttribute('aria-describedby');
89
93
  }
90
94
  }
91
95
 
92
- if (triggerEl?.setAttribute) {
96
+ if (isDOMElement) {
93
97
  triggerEl.setAttribute('aria-describedby', TOOLTIP_ID);
94
98
  }
95
99
 
@@ -97,8 +101,9 @@ export default function install(app, userOptions = {}) {
97
101
  tooltipState.config = configObj;
98
102
  tooltipState.visible = true;
99
103
  };
104
+
100
105
  const hideTooltip = () => {
101
- if (tooltipState.trigger?.removeAttribute) {
106
+ if (tooltipState.trigger instanceof Element) {
102
107
  tooltipState.trigger.removeAttribute('aria-describedby');
103
108
  }
104
109
  tooltipState.visible = false;
@@ -114,6 +119,7 @@ export default function install(app, userOptions = {}) {
114
119
  // Register components and directives
115
120
  app.component('UluTooltipDisplay', UluTooltipDisplay);
116
121
  app.component('UluPopover', UluPopover);
122
+ app.component('UluPopoverBase', UluPopoverBase);
117
123
 
118
124
  const elementHandlers = new WeakMap();
119
125
  const popoverDefaults = mergedOptions.popover;
@@ -197,4 +203,8 @@ export default function install(app, userOptions = {}) {
197
203
  }
198
204
  }
199
205
  });
200
- }
206
+ }
207
+ export { default as UluPopover } from './UluPopover.vue';
208
+ export { default as UluPopoverBase } from './UluPopoverBase.vue';
209
+ export { default as UluTooltipDisplay } from './UluTooltipDisplay.vue';
210
+ export { default as UluTooltipPopover } from './UluTooltipPopover.vue';
@@ -0,0 +1,37 @@
1
+ <template>
2
+ <div class="tour-content crop-margins">
3
+ <component
4
+ v-if="step?.title"
5
+ :is="isModal ? 'h2' : 'strong'"
6
+ :class="isModal ? 'h3' : 'h4'"
7
+ class="display-block no-margin"
8
+ >
9
+ {{ step.title }}
10
+ </component>
11
+ <UluRule />
12
+ <p v-if="step?.content">
13
+ {{ step.content }}
14
+ </p>
15
+ </div>
16
+ </template>
17
+
18
+ <script setup>
19
+ import UluRule from "../../components/elements/UluRule.vue";
20
+
21
+ defineProps({
22
+ /**
23
+ * The tour step configuration object
24
+ */
25
+ step: {
26
+ type: Object,
27
+ required: true
28
+ },
29
+ /**
30
+ * Adjusts the typography to match a modal context rather than a popover context
31
+ */
32
+ isModal: {
33
+ type: Boolean,
34
+ default: false
35
+ }
36
+ });
37
+ </script>
@@ -0,0 +1,182 @@
1
+ <template>
2
+ <Teleport to="body">
3
+ <template v-if="tourState.active">
4
+
5
+ <!-- 1. FULL COMPONENT TAKEOVER -->
6
+ <component
7
+ v-if="currentStep?.component"
8
+ :is="currentStep.component"
9
+ v-bind="currentStep.componentProps"
10
+ />
11
+
12
+ <!-- 2. RENDER MODAL STEP -->
13
+ <UluModal
14
+ v-else-if="!currentStep?.target"
15
+ :modelValue="true"
16
+ v-bind="resolvedModalProps"
17
+ @close="api.stop()"
18
+ >
19
+ <component
20
+ v-if="currentStep?.contentComponent"
21
+ :is="currentStep.contentComponent"
22
+ v-bind="currentStep.componentProps"
23
+ />
24
+ <UluTourContent v-else :step="currentStep" :is-modal="true" />
25
+
26
+ <template #footer v-if="!currentStep?.hideFooter">
27
+ <UluTourPager />
28
+ </template>
29
+ </UluModal>
30
+
31
+ <!-- 3. RENDER POPOVER STEP -->
32
+ <UluPopoverBase
33
+ v-else
34
+ ref="popoverBase"
35
+ :trigger="targetEl"
36
+ :config="resolvedConfig"
37
+ :style="{ zIndex: 9999 }"
38
+ :isOpen="true"
39
+ @close="api.stop()"
40
+ v-bind="resolvedPopoverProps"
41
+ >
42
+ <component
43
+ v-if="currentStep?.contentComponent"
44
+ :is="currentStep.contentComponent"
45
+ v-bind="currentStep.componentProps"
46
+ />
47
+ <UluTourContent v-else :step="currentStep" :is-modal="false" />
48
+
49
+ <template #footer v-if="!currentStep?.hideFooter">
50
+ <UluTourPager />
51
+ </template>
52
+ </UluPopoverBase>
53
+
54
+ <!-- OPTIONAL HIGHLIGHT BACKDROP -->
55
+ <div
56
+ v-if="currentStep?.target && currentStep?.highlight && targetRect"
57
+ class="tour-highlight-backdrop"
58
+ :style="highlightStyles"
59
+ @click="api.stop()"
60
+ ></div>
61
+ </template>
62
+ </Teleport>
63
+ </template>
64
+
65
+ <script setup>
66
+ import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue';
67
+ import UluModal from '../../components/collapsible/UluModal.vue';
68
+ import UluPopoverBase from '../popovers/UluPopoverBase.vue';
69
+ import UluTourPager from './UluTourPager.vue';
70
+ import UluTourContent from './UluTourContent.vue';
71
+ import { useTour } from './useTour.js';
72
+
73
+ const { api, state: tourState } = useTour();
74
+
75
+ const currentStep = computed(() => tourState.active?.steps[tourState.stepIndex]);
76
+
77
+ // Floating UI Setup for Popovers
78
+ const targetEl = ref(null);
79
+ const popoverBase = ref(null);
80
+
81
+ const resolvedConfig = computed(() => ({
82
+ placement: currentStep.value?.placement || 'bottom',
83
+ arrow: true,
84
+ offset: 8
85
+ }));
86
+
87
+ // Prop Resolution
88
+ const resolvedModalProps = computed(() => {
89
+ return {
90
+ ...(tourState.active?.modalProps || {}),
91
+ ...(currentStep.value?.modalProps || {})
92
+ };
93
+ });
94
+
95
+ const resolvedPopoverProps = computed(() => {
96
+ return {
97
+ modifiers: 'large',
98
+ ...(tourState.active?.popoverProps || {}),
99
+ ...(currentStep.value?.popoverProps || {})
100
+ };
101
+ });
102
+
103
+ // Highlight Backdrop Setup
104
+ const targetRect = ref(null);
105
+
106
+ const updateTargetRect = () => {
107
+ if (targetEl.value) {
108
+ const rect = targetEl.value.getBoundingClientRect();
109
+ targetRect.value = {
110
+ top: rect.top,
111
+ left: rect.left,
112
+ width: rect.width,
113
+ height: rect.height
114
+ };
115
+ } else {
116
+ targetRect.value = null;
117
+ }
118
+ };
119
+
120
+ const highlightStyles = computed(() => {
121
+ if (!targetRect.value) return {};
122
+ const t = targetRect.value;
123
+ return {
124
+ position: 'fixed',
125
+ top: 0,
126
+ left: 0,
127
+ width: '100vw',
128
+ height: '100vh',
129
+ pointerEvents: 'auto',
130
+ zIndex: 9998,
131
+ background: 'rgba(0,0,0,0.5)',
132
+ clipPath: `polygon(
133
+ 0% 0%,
134
+ 100% 0%,
135
+ 100% 100%,
136
+ 0% 100%,
137
+ 0% ${ t.top }px,
138
+ ${ t.left }px ${ t.top }px,
139
+ ${ t.left }px ${ t.top + t.height }px,
140
+ ${ t.left + t.width }px ${ t.top + t.height }px,
141
+ ${ t.left + t.width }px ${ t.top }px,
142
+ ${ t.left }px ${ t.top }px,
143
+ 0% ${ t.top }px
144
+ )`
145
+ };
146
+ });
147
+
148
+ // Watch for step changes to update floating UI target
149
+ watch(currentStep, async (step) => {
150
+ if (step?.target) {
151
+ await nextTick();
152
+ const el = document.querySelector(step.target);
153
+ if (el) {
154
+ targetEl.value = el;
155
+ // UluPopoverBase's isOpen watcher automatically calls update(),
156
+ // but since isOpen stays strictly true here when switching between popover steps,
157
+ // we might need to manually trigger an update if the target changed.
158
+ if (popoverBase.value) {
159
+ popoverBase.value.update();
160
+ }
161
+ if (step.highlight) {
162
+ updateTargetRect();
163
+ }
164
+ } else {
165
+ console.warn(`Tour target not found: ${step.target}`);
166
+ }
167
+ } else {
168
+ targetEl.value = null;
169
+ targetRect.value = null;
170
+ }
171
+ }, { immediate: true });
172
+
173
+ onMounted(() => {
174
+ window.addEventListener('resize', updateTargetRect);
175
+ window.addEventListener('scroll', updateTargetRect);
176
+ });
177
+
178
+ onUnmounted(() => {
179
+ window.removeEventListener('resize', updateTargetRect);
180
+ window.removeEventListener('scroll', updateTargetRect);
181
+ });
182
+ </script>
@@ -0,0 +1,36 @@
1
+ <template>
2
+ <div class="tour-pager layout-flex-justified">
3
+ <UluButton
4
+ @click="api.prev()"
5
+ text="Previous"
6
+ :disabled="state.stepIndex === 0"
7
+ icon="type:previous"
8
+ iconBefore
9
+ small
10
+ secondary
11
+ transparent
12
+ />
13
+ <UluButton
14
+ primary
15
+ :icon="isLastStep ? null : 'type:next'"
16
+ @click="api.next()"
17
+ :text="isLastStep ? 'Finish' : 'Next'"
18
+ small
19
+ />
20
+ </div>
21
+ </template>
22
+
23
+ <script setup>
24
+ import { computed } from 'vue';
25
+ import { useTour } from './useTour.js';
26
+ import UluButton from '../../components/elements/UluButton.vue';
27
+
28
+ const { api, state } = useTour();
29
+
30
+ const isLastStep = computed(() => {
31
+ return state.active && state.stepIndex === state.active.steps.length - 1;
32
+ });
33
+ const isFirstStep = computed(() => {
34
+ return state.active && state.stepIndex === 0;
35
+ });
36
+ </script>
@@ -0,0 +1,83 @@
1
+ import { markRaw } from "vue";
2
+
3
+ /**
4
+ * @typedef {Object} TourStep
5
+ * @property {String} [target] - The CSS selector for the target element. If omitted, step renders as a modal.
6
+ * @property {String} [title] - The title text for the step.
7
+ * @property {String} [content] - The content text for the step.
8
+ * @property {Object} [component] - A Vue component to completely replace the modal/popover wrapper and layout.
9
+ * @property {Object} [contentComponent] - A Vue component to replace the step text content, keeping the modal/popover wrappers and default footer.
10
+ * @property {Object} [componentProps] - Props to bind to the custom component.
11
+ * @property {Boolean} [hideFooter] - If true, hides the default tour footer (Next/Back buttons).
12
+ * @property {String} [placement] - Floating UI placement string (e.g. 'bottom', 'right').
13
+ * @property {Boolean} [highlight] - If true, darkens the backdrop around the target element.
14
+ * @property {Object} [modalProps] - Props to pass to the modal (if target is omitted).
15
+ * @property {Object} [popoverProps] - Props to pass to the popover (if target is present).
16
+ * @property {Function} [onEnter] - Async hook called before the step is rendered.
17
+ */
18
+
19
+ /**
20
+ * Creates the tour API methods used to control the tour lifecycle
21
+ * @param {Object} state - The reactive state object
22
+ * @returns {Object} Tour API object
23
+ */
24
+ export const createApi = (state) => ({
25
+ /**
26
+ * Starts a new tour
27
+ * @param {Object} tour - The tour configuration object
28
+ * @param {TourStep[]} tour.steps - Array of steps for the tour
29
+ * @param {String} [tour.name] - Optional name for the tour
30
+ * @param {Object} [tour.modalProps] - Global props applied to all modal steps in this tour (can be overridden by step)
31
+ * @param {Object} [tour.popoverProps] - Global props applied to all popover steps in this tour (can be overridden by step)
32
+ */
33
+ async start(tour) {
34
+ if (!tour?.steps?.length) return;
35
+ await this._runHooks(tour.steps[0]);
36
+ state.active = markRaw(tour);
37
+ state.stepIndex = 0;
38
+ },
39
+ /**
40
+ * Advances the tour to the next step, or stops if it is the last step
41
+ */
42
+ async next() {
43
+ const active = state.active;
44
+ if (!active) return;
45
+
46
+ if (state.stepIndex < active.steps.length - 1) {
47
+ const nextIndex = state.stepIndex + 1;
48
+ await this._runHooks(active.steps[nextIndex]);
49
+ state.stepIndex = nextIndex;
50
+ } else {
51
+ this.stop();
52
+ }
53
+ },
54
+ /**
55
+ * Returns the tour to the previous step
56
+ */
57
+ async prev() {
58
+ const active = state.active;
59
+ if (!active) return;
60
+
61
+ if (state.stepIndex > 0) {
62
+ const prevIndex = state.stepIndex - 1;
63
+ await this._runHooks(active.steps[prevIndex]);
64
+ state.stepIndex = prevIndex;
65
+ }
66
+ },
67
+ /**
68
+ * Stops and closes the current tour
69
+ */
70
+ stop() {
71
+ state.active = null;
72
+ state.stepIndex = 0;
73
+ },
74
+ /**
75
+ * Internal method to run asynchronous lifecycle hooks on a step
76
+ * @param {Object} step - The tour step configuration
77
+ */
78
+ async _runHooks(step) {
79
+ if (step && typeof step.onEnter === 'function') {
80
+ await step.onEnter();
81
+ }
82
+ }
83
+ });
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @module tourPlugin
3
+ */
4
+ import { reactive } from 'vue';
5
+ import UluTourDisplay from './UluTourDisplay.vue';
6
+ import { createApi } from './api.js';
7
+ import { useTour } from './useTour.js';
8
+
9
+ export default function install(app) {
10
+ const state = reactive({
11
+ active: null,
12
+ stepIndex: 0
13
+ });
14
+
15
+ const api = createApi(state);
16
+
17
+ app.component("UluTourDisplay", UluTourDisplay);
18
+
19
+ app.config.globalProperties.$uluTour = api;
20
+ app.config.globalProperties.$uluTourState = state;
21
+
22
+ app.provide('uluTour', api);
23
+ app.provide('uluTourState', state);
24
+ }
25
+
26
+ export { useTour };
27
+ export { default as UluTourDisplay } from './UluTourDisplay.vue';
28
+ export { default as UluTourPager } from './UluTourPager.vue';
29
+ export { default as UluTourContent } from './UluTourContent.vue';