@ulu/frontend-vue 0.5.15 → 0.6.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 (75) hide show
  1. package/dist/components/elements/UluButton.vue.d.ts +4 -0
  2. package/dist/components/elements/UluButton.vue.d.ts.map +1 -1
  3. package/dist/components/elements/UluButton.vue.js +31 -16
  4. package/dist/components/elements/UluIcon.vue.js +21 -36
  5. package/dist/components/forms/UluFormCheckbox.vue.d.ts +3 -19
  6. package/dist/components/forms/UluFormCheckbox.vue.d.ts.map +1 -1
  7. package/dist/components/forms/UluFormCheckbox.vue.js +10 -31
  8. package/dist/components/forms/UluFormFile.vue.d.ts +3 -25
  9. package/dist/components/forms/UluFormFile.vue.d.ts.map +1 -1
  10. package/dist/components/forms/UluFormFile.vue.js +11 -49
  11. package/dist/components/forms/UluFormItem.vue.d.ts +23 -8
  12. package/dist/components/forms/UluFormItem.vue.d.ts.map +1 -1
  13. package/dist/components/forms/UluFormItem.vue.js +126 -29
  14. package/dist/components/forms/UluFormLabel.vue.d.ts +24 -0
  15. package/dist/components/forms/UluFormLabel.vue.d.ts.map +1 -0
  16. package/dist/components/forms/UluFormLabel.vue.js +34 -0
  17. package/dist/components/forms/UluFormRadio.vue.d.ts +7 -25
  18. package/dist/components/forms/UluFormRadio.vue.d.ts.map +1 -1
  19. package/dist/components/forms/UluFormRadio.vue.js +11 -37
  20. package/dist/components/forms/UluFormSelect.vue.d.ts +7 -23
  21. package/dist/components/forms/UluFormSelect.vue.d.ts.map +1 -1
  22. package/dist/components/forms/UluFormSelect.vue.js +24 -43
  23. package/dist/components/forms/UluFormText.vue.d.ts +5 -23
  24. package/dist/components/forms/UluFormText.vue.d.ts.map +1 -1
  25. package/dist/components/forms/UluFormText.vue.js +10 -38
  26. package/dist/components/forms/UluFormTextarea.vue.d.ts +5 -23
  27. package/dist/components/forms/UluFormTextarea.vue.d.ts.map +1 -1
  28. package/dist/components/forms/UluFormTextarea.vue.js +10 -37
  29. package/dist/components/forms/UluSearchForm.vue.d.ts +24 -3
  30. package/dist/components/forms/UluSearchForm.vue.d.ts.map +1 -1
  31. package/dist/components/forms/UluSearchForm.vue.js +67 -22
  32. package/dist/components/index.d.ts +1 -0
  33. package/dist/components/systems/facets/UluFacetsFilterSelects.vue.d.ts.map +1 -1
  34. package/dist/components/systems/facets/UluFacetsFilterSelects.vue.js +21 -22
  35. package/dist/components/systems/scroll-anchors/UluScrollAnchors.vue.d.ts.map +1 -1
  36. package/dist/components/systems/scroll-anchors/UluScrollAnchors.vue.js +18 -10
  37. package/dist/components/systems/scroll-anchors/UluScrollAnchorsNavAnimated.vue.d.ts +8 -2
  38. package/dist/components/systems/scroll-anchors/UluScrollAnchorsNavAnimated.vue.d.ts.map +1 -1
  39. package/dist/components/systems/scroll-anchors/UluScrollAnchorsNavAnimated.vue.js +92 -47
  40. package/dist/components/systems/scroll-anchors/useScrollAnchors.d.ts.map +1 -1
  41. package/dist/components/systems/scroll-anchors/useScrollAnchors.js +113 -70
  42. package/dist/components/utils/UluAction.vue.d.ts +2 -0
  43. package/dist/components/utils/UluAction.vue.d.ts.map +1 -1
  44. package/dist/components/utils/UluAction.vue.js +9 -5
  45. package/dist/components/visualizations/UluProgressBar.vue.d.ts +2 -2
  46. package/dist/index.js +130 -128
  47. package/dist/plugins/core/index.d.ts.map +1 -1
  48. package/dist/plugins/core/index.js +17 -16
  49. package/dist/utils/props.d.ts +7 -0
  50. package/dist/utils/props.d.ts.map +1 -1
  51. package/dist/utils/props.js +8 -2
  52. package/lib/components/elements/UluButton.vue +18 -3
  53. package/lib/components/elements/UluIcon.vue +8 -26
  54. package/lib/components/forms/UluForm.vue +25 -25
  55. package/lib/components/forms/UluFormCheckbox.vue +11 -25
  56. package/lib/components/forms/UluFormFieldset.vue +6 -6
  57. package/lib/components/forms/UluFormFile.vue +10 -40
  58. package/lib/components/forms/UluFormItem.vue +150 -39
  59. package/lib/components/forms/UluFormLabel.vue +30 -0
  60. package/lib/components/forms/UluFormRadio.vue +15 -34
  61. package/lib/components/forms/UluFormSelect.vue +19 -24
  62. package/lib/components/forms/UluFormText.vue +7 -25
  63. package/lib/components/forms/UluFormTextarea.vue +7 -25
  64. package/lib/components/forms/UluSearchForm.vue +67 -19
  65. package/lib/components/forms/UluSelectableMenu.vue +62 -62
  66. package/lib/components/index.js +1 -0
  67. package/lib/components/systems/facets/UluFacetsFilterSelects.vue +11 -14
  68. package/lib/components/systems/scroll-anchors/UluScrollAnchors.vue +9 -1
  69. package/lib/components/systems/scroll-anchors/UluScrollAnchorsNavAnimated.vue +95 -16
  70. package/lib/components/systems/scroll-anchors/_scroll-anchors-nav-animated.scss +12 -1
  71. package/lib/components/systems/scroll-anchors/useScrollAnchors.js +195 -54
  72. package/lib/components/utils/UluAction.vue +6 -2
  73. package/lib/plugins/core/index.js +2 -1
  74. package/lib/utils/props.js +14 -0
  75. package/package.json +3 -3
@@ -1,32 +1,80 @@
1
1
  <template>
2
- <div class="form-theme search-form type-small">
3
- <div class="search-form__field">
4
- <label class="hidden-visually">Search</label>
5
- <input
6
- class="search-form__input"
7
- type="text"
8
- id="example-input"
9
- :placeholder="placeholder"
10
- >
2
+ <form class="form-theme" @submit.prevent="onSubmit">
3
+ <div class="input-group input-group--joined">
4
+ <div class="input-group__item input-group__item--field">
5
+ <label :for="inputId" class="hidden-visually">{{ label }}</label>
6
+ <input
7
+ type="search"
8
+ :id="inputId"
9
+ class="input-group__input"
10
+ :placeholder="placeholder"
11
+ :value="modelValue"
12
+ @input="$emit('update:modelValue', $event.target.value)"
13
+ >
14
+ </div>
15
+ <div class="input-group__item">
16
+ <slot name="submit">
17
+ <UluButton
18
+ class="input-group__button"
19
+ v-bind="submitButtonProps"
20
+ />
21
+ </slot>
22
+ </div>
11
23
  </div>
12
- <button
13
- class="search-form__submit button button--primary"
14
- aria-label="Submit Search"
15
- >
16
- <UluIcon icon="type:search" />
17
- </button>
18
- </div>
24
+ </form>
19
25
  </template>
20
26
 
21
27
  <script setup>
22
- import UluIcon from "../elements/UluIcon.vue";
23
- defineProps({
28
+ import { computed } from "vue";
29
+ import { newId } from "../../utils/dom.js";
30
+ import UluButton from "../elements/UluButton.vue";
31
+
32
+ const props = defineProps({
33
+ /**
34
+ * The search input value (for v-model).
35
+ */
36
+ modelValue: {
37
+ type: String,
38
+ default: ""
39
+ },
24
40
  /**
25
41
  * The placeholder text for the search input.
26
42
  */
27
43
  placeholder: {
28
44
  type: String,
29
45
  default: "Titles, keyword…"
30
- }
46
+ },
47
+ /**
48
+ * The visually hidden label for the search input.
49
+ */
50
+ label: {
51
+ type: String,
52
+ default: "Search"
53
+ },
54
+ /**
55
+ * Props to pass to the default UluButton component (used for submit button)
56
+ * - Alternately use 'submit' slot
57
+ */
58
+ submitButtonProps: {
59
+ type: Object,
60
+ default: () => ({
61
+ type: "submit",
62
+ primary: true,
63
+ icon: "type:search",
64
+ ariaLabel: "Submit Search"
65
+ })
66
+ },
67
+ /**
68
+ * Optional ID for the input element. If not provided, a unique ID is generated.
69
+ */
70
+ id: String
31
71
  });
72
+
73
+ const emit = defineEmits(["update:modelValue", "submit"]);
74
+
75
+ const inputId = computed(() => props.id || newId());
76
+
77
+ const onSubmit = () => {
78
+ emit("submit", props.modelValue);
79
+ };
32
80
  </script>
@@ -32,75 +32,75 @@
32
32
  </template>
33
33
 
34
34
  <script setup>
35
- import { computed } from 'vue';
35
+ import { computed } from 'vue';
36
36
 
37
- const props = defineProps({
38
- /**
39
- * The legend for the menu.
40
- */
41
- legend: String,
42
- /**
43
- * An array of options for the menu.
44
- */
45
- options: Array,
46
- /**
47
- * Use compact modifier on menu stack
48
- */
49
- compact: Boolean,
50
- /**
51
- * The type of input to use ('checkbox' or 'radio').
52
- */
53
- type: {
54
- type: String,
55
- default: 'checkbox',
56
- },
57
- /**
58
- * The value of the menu (for v-model).
59
- */
60
- modelValue: [String, Array],
61
- /**
62
- * If true, the input elements will be visually hidden.
63
- */
64
- hideInputs: Boolean
65
- });
37
+ const props = defineProps({
38
+ /**
39
+ * The legend for the menu.
40
+ */
41
+ legend: String,
42
+ /**
43
+ * An array of options for the menu.
44
+ */
45
+ options: Array,
46
+ /**
47
+ * Use compact modifier on menu stack
48
+ */
49
+ compact: Boolean,
50
+ /**
51
+ * The type of input to use ('checkbox' or 'radio').
52
+ */
53
+ type: {
54
+ type: String,
55
+ default: 'checkbox',
56
+ },
57
+ /**
58
+ * The value of the menu (for v-model).
59
+ */
60
+ modelValue: [String, Array],
61
+ /**
62
+ * If true, the input elements will be visually hidden.
63
+ */
64
+ hideInputs: Boolean
65
+ });
66
66
 
67
- const emit = defineEmits(['update:modelValue']);
67
+ const emit = defineEmits(['update:modelValue']);
68
68
 
69
- const name = computed(() => props.legend ? props.legend.toLowerCase().replace(/\s+/g, '-') : `menu-${ Math.random().toString(36).substring(7) }`);
70
- const legendId = computed(() => name.value ? `${name.value}-legend` : null);
71
- const groupRole = computed(() => props.type === 'radio' ? 'radiogroup' : 'group');
69
+ const name = computed(() => props.legend ? props.legend.toLowerCase().replace(/\s+/g, '-') : `menu-${ Math.random().toString(36).substring(7) }`);
70
+ const legendId = computed(() => name.value ? `${name.value}-legend` : null);
71
+ const groupRole = computed(() => props.type === 'radio' ? 'radiogroup' : 'group');
72
72
 
73
- const getId = (option) => `${name.value}-${option.uid}`;
73
+ const getId = (option) => `${name.value}-${option.uid}`;
74
74
 
75
- const isChecked = (option) => {
76
- if (props.type === 'radio') {
77
- return props.modelValue === option.uid;
78
- }
79
- if (Array.isArray(props.modelValue)) {
80
- return props.modelValue.includes(option.uid);
81
- }
82
- if (props.type === 'checkbox') {
83
- return option.checked || false;
84
- }
85
- return false;
86
- };
87
-
88
- const handleChange = (option, event) => {
89
- if (props.type === 'radio') {
90
- emit('update:modelValue', option.uid);
91
- } else {
75
+ const isChecked = (option) => {
76
+ if (props.type === 'radio') {
77
+ return props.modelValue === option.uid;
78
+ }
92
79
  if (Array.isArray(props.modelValue)) {
93
- const newValue = [...props.modelValue];
94
- const index = newValue.indexOf(option.uid);
95
- if (index > -1) {
96
- newValue.splice(index, 1);
80
+ return props.modelValue.includes(option.uid);
81
+ }
82
+ if (props.type === 'checkbox') {
83
+ return option.checked || false;
84
+ }
85
+ return false;
86
+ };
87
+
88
+ const handleChange = (option, event) => {
89
+ if (props.type === 'radio') {
90
+ emit('update:modelValue', option.uid);
91
+ } else {
92
+ if (Array.isArray(props.modelValue)) {
93
+ const newValue = [...props.modelValue];
94
+ const index = newValue.indexOf(option.uid);
95
+ if (index > -1) {
96
+ newValue.splice(index, 1);
97
+ } else {
98
+ newValue.push(option.uid);
99
+ }
100
+ emit('update:modelValue', newValue);
97
101
  } else {
98
- newValue.push(option.uid);
102
+ option.checked = event.target.checked;
99
103
  }
100
- emit('update:modelValue', newValue);
101
- } else {
102
- option.checked = event.target.checked;
103
104
  }
104
- }
105
- };
105
+ };
106
106
  </script>
@@ -56,6 +56,7 @@ export { default as UluFormActions } from './forms/UluFormActions.vue';
56
56
  export { default as UluFormCheckbox } from './forms/UluFormCheckbox.vue';
57
57
  export { default as UluFormFieldset } from './forms/UluFormFieldset.vue';
58
58
  export { default as UluFormItem } from './forms/UluFormItem.vue';
59
+ export { default as UluFormLabel } from './forms/UluFormLabel.vue';
59
60
  export { default as UluFormItemsInline } from './forms/UluFormItemsInline.vue';
60
61
  export { default as UluFormRadio } from './forms/UluFormRadio.vue';
61
62
  export { default as UluFormRequiredChar } from './forms/UluFormRequiredChar.vue';
@@ -59,9 +59,6 @@ const props = defineProps({
59
59
  },
60
60
  });
61
61
 
62
- console.log(props);
63
-
64
-
65
62
  const emit = defineEmits(['facet-change']);
66
63
 
67
64
  function onFilterChange(group, event) {
@@ -84,15 +81,15 @@ function onFilterChange(group, event) {
84
81
  </script>
85
82
 
86
83
  <style lang="scss">
87
- .facets-dropdown-filters {
88
- display: flex;
89
- gap: 1rem;
90
- align-items: center;
91
- flex-wrap: wrap;
92
- }
93
- .facets-dropdown-filters__group {
94
- display: flex;
95
- gap: 0.5rem;
96
- align-items: center;
97
- }
84
+ .facets-dropdown-filters {
85
+ display: flex;
86
+ gap: 1rem;
87
+ align-items: center;
88
+ flex-wrap: wrap;
89
+ }
90
+ .facets-dropdown-filters__group {
91
+ display: flex;
92
+ gap: 0.5rem;
93
+ align-items: center;
94
+ }
98
95
  </style>
@@ -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;