@ulu/frontend-vue 0.1.0-beta.1

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 (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +9 -0
  3. package/dist/breakpoints-ClT9bfZm.js +211 -0
  4. package/dist/frontend-vue.css +1 -0
  5. package/dist/frontend-vue.js +82 -0
  6. package/dist/frontend-vue.umd.cjs +561 -0
  7. package/dist/index-P5Rwl_Dl.js +7263 -0
  8. package/dist/index.es-HlG3u0J5.js +3134 -0
  9. package/lib/_index.scss +14 -0
  10. package/lib/components/_index.scss +6 -0
  11. package/lib/components/collapsible/UluAccordion.vue +82 -0
  12. package/lib/components/collapsible/UluCollapsibleRegion.vue +278 -0
  13. package/lib/components/collapsible/UluDropdown.vue +42 -0
  14. package/lib/components/collapsible/UluModal.vue +384 -0
  15. package/lib/components/collapsible/UluOverflowPopover.vue +52 -0
  16. package/lib/components/collapsible/UluTab.vue +9 -0
  17. package/lib/components/collapsible/UluTabGroup.vue +31 -0
  18. package/lib/components/collapsible/UluTabList.vue +9 -0
  19. package/lib/components/collapsible/UluTabPanel.vue +9 -0
  20. package/lib/components/collapsible/UluTabPanels.vue +9 -0
  21. package/lib/components/elements/UluAlert.vue +81 -0
  22. package/lib/components/elements/UluBadge.vue +58 -0
  23. package/lib/components/elements/UluBadgeStack.vue +27 -0
  24. package/lib/components/elements/UluButton.vue +161 -0
  25. package/lib/components/elements/UluCallout.vue +30 -0
  26. package/lib/components/elements/UluCard.vue +241 -0
  27. package/lib/components/elements/UluDefinitionList.vue +40 -0
  28. package/lib/components/elements/UluExternalLink.vue +47 -0
  29. package/lib/components/elements/UluIcon.vue +108 -0
  30. package/lib/components/elements/UluList.vue +87 -0
  31. package/lib/components/elements/UluMain.vue +5 -0
  32. package/lib/components/elements/UluSpokeSpinner.vue +25 -0
  33. package/lib/components/elements/UluTag.vue +53 -0
  34. package/lib/components/forms/UluCheckboxMenu.vue +36 -0
  35. package/lib/components/forms/UluFileDisplay.vue +39 -0
  36. package/lib/components/forms/UluFormDropzone.vue +62 -0
  37. package/lib/components/forms/UluFormFile.vue +47 -0
  38. package/lib/components/forms/UluFormMessage.vue +20 -0
  39. package/lib/components/forms/UluFormSelect.vue +37 -0
  40. package/lib/components/forms/UluFormText.vue +32 -0
  41. package/lib/components/forms/UluSearchForm.vue +31 -0
  42. package/lib/components/index.js +54 -0
  43. package/lib/components/layout/UluAdaptiveLayout.vue +11 -0
  44. package/lib/components/layout/UluDataGrid.vue +41 -0
  45. package/lib/components/layout/UluTitleRail.vue +56 -0
  46. package/lib/components/layout/UluWhenBreakpoint.vue +86 -0
  47. package/lib/components/navigation/UluBreadcrumb.vue +72 -0
  48. package/lib/components/navigation/UluMenu.vue +105 -0
  49. package/lib/components/navigation/UluMenuStack.vue +49 -0
  50. package/lib/components/navigation/UluNavStrip.vue +48 -0
  51. package/lib/components/navigation/UluSkipLink.vue +5 -0
  52. package/lib/components/systems/facets/UluFacets.vue +380 -0
  53. package/lib/components/systems/facets/UluFacetsList.vue +39 -0
  54. package/lib/components/systems/facets/UluFacetsSearch.vue +67 -0
  55. package/lib/components/systems/facets/_facets.scss +64 -0
  56. package/lib/components/systems/index.js +17 -0
  57. package/lib/components/systems/scroll-anchors/UluScrollAnchors.vue +152 -0
  58. package/lib/components/systems/scroll-anchors/UluScrollAnchorsNav.vue +37 -0
  59. package/lib/components/systems/scroll-anchors/UluScrollAnchorsNavAnimated.vue +124 -0
  60. package/lib/components/systems/scroll-anchors/UluScrollAnchorsSection.vue +63 -0
  61. package/lib/components/systems/scroll-anchors/symbols.js +6 -0
  62. package/lib/components/systems/skeleton/UluShowSkeleton.vue +13 -0
  63. package/lib/components/systems/skeleton/UluSkeletonContent.vue +60 -0
  64. package/lib/components/systems/skeleton/UluSkeletonMedia.vue +11 -0
  65. package/lib/components/systems/skeleton/UluSkeletonTextInline.vue +9 -0
  66. package/lib/components/systems/slider/UluImageSlideShow.vue +75 -0
  67. package/lib/components/systems/slider/UluSlideShow.vue +331 -0
  68. package/lib/components/systems/slider/UluSlideShowSlide.vue +25 -0
  69. package/lib/components/systems/table-sticky/UluTableSticky.vue +793 -0
  70. package/lib/components/systems/table-sticky/UluTableStickyRows.vue +73 -0
  71. package/lib/components/systems/table-sticky/UluTableStickyTable.vue +237 -0
  72. package/lib/components/systems/table-sticky/_table-sticky.scss +185 -0
  73. package/lib/components/utils/UluCondText.vue +28 -0
  74. package/lib/components/utils/UluEmpty.vue +3 -0
  75. package/lib/components/utils/UluEmptyView.vue +3 -0
  76. package/lib/components/utils/UluPlaceholderImage.vue +53 -0
  77. package/lib/components/utils/UluPlaceholderText.vue +25 -0
  78. package/lib/components/utils/UluRouteAnnouncer.vue +83 -0
  79. package/lib/components/visualizations/UluAnimateNumber.vue +32 -0
  80. package/lib/components/visualizations/UluProgressBar.vue +94 -0
  81. package/lib/components/visualizations/UluProgressDonut.vue +97 -0
  82. package/lib/composables/index.js +10 -0
  83. package/lib/composables/useBreakpointManager.js +68 -0
  84. package/lib/composables/useIcon.js +62 -0
  85. package/lib/composables/useModifiers.js +93 -0
  86. package/lib/composables/useWindowResize.js +64 -0
  87. package/lib/index.js +10 -0
  88. package/lib/plugins/_index.scss +7 -0
  89. package/lib/plugins/breakpoints/index.js +47 -0
  90. package/lib/plugins/index.js +11 -0
  91. package/lib/plugins/modals/UluModalsDisplay.vue +59 -0
  92. package/lib/plugins/modals/api.js +76 -0
  93. package/lib/plugins/modals/index.js +60 -0
  94. package/lib/plugins/modals/useModals.js +9 -0
  95. package/lib/plugins/popovers/UluPopover.vue +189 -0
  96. package/lib/plugins/popovers/UluTooltipDisplay.vue +15 -0
  97. package/lib/plugins/popovers/UluTooltipPopover.vue +83 -0
  98. package/lib/plugins/popovers/defaults.js +108 -0
  99. package/lib/plugins/popovers/directive.js +95 -0
  100. package/lib/plugins/popovers/index.js +18 -0
  101. package/lib/plugins/popovers/manager.js +54 -0
  102. package/lib/plugins/popovers/useFollow.js +80 -0
  103. package/lib/plugins/popovers/utils.js +5 -0
  104. package/lib/plugins/toast/UluToast.vue +87 -0
  105. package/lib/plugins/toast/UluToastDisplay.vue +35 -0
  106. package/lib/plugins/toast/_toast.scss +198 -0
  107. package/lib/plugins/toast/defaults.js +30 -0
  108. package/lib/plugins/toast/index.js +17 -0
  109. package/lib/plugins/toast/store.js +71 -0
  110. package/lib/plugins/toast/useToast.js +18 -0
  111. package/lib/settings.js +119 -0
  112. package/lib/utils/dom.js +14 -0
  113. package/lib/utils/placeholder.js +6 -0
  114. package/lib/utils/vue-router.js +219 -0
  115. package/package.json +75 -0
@@ -0,0 +1,32 @@
1
+ <template>
2
+ <span><slot :currentValue="currentValue">{{ currentValue }}</slot></span>
3
+ </template>
4
+
5
+ <script>
6
+ import gsap from "gsap";
7
+ export default {
8
+ name: 'AnimateNumber',
9
+ props: {
10
+ /**
11
+ * Number to animate as it changes
12
+ */
13
+ value: Number
14
+ },
15
+ watch: {
16
+ value() {
17
+ gsap.to(this, {
18
+ tweenValue: this.value,
19
+ onUpdate: () => {
20
+ this.currentValue = Math.ceil(this.tweenValue);
21
+ },
22
+ });
23
+ },
24
+ },
25
+ data() {
26
+ return {
27
+ currentValue: this.value,
28
+ tweenValue: this.value,
29
+ };
30
+ },
31
+ }
32
+ </script>
@@ -0,0 +1,94 @@
1
+ <template>
2
+ <div
3
+ class="progress-bar"
4
+ :class="{
5
+ 'progress-bar--small' : small,
6
+ 'progress-bar--icon-left' : iconOnLeft,
7
+ 'type-small' : small
8
+ }"
9
+ >
10
+ <div class="progress-bar__header">
11
+ <strong
12
+ class="progress-bar__label"
13
+ :class="{
14
+ 'type-normal' : small,
15
+ 'hidden-visually' : labelHidden,
16
+ }"
17
+ >
18
+ {{ label }}
19
+ </strong>
20
+ <div v-if="status" class="progress-bar__icon">
21
+ <StatusIcon :type="status.type" />
22
+ <span class="hidden-visually">{{ status.message }}</span>
23
+ </div>
24
+ </div>
25
+ <div class="progress-bar__track">
26
+ <div
27
+ class="progress-bar__bar"
28
+ :style="`width: ${ percentage }%`"
29
+ ></div>
30
+ <div
31
+ v-if="deficit"
32
+ class="progress-bar__bar--deficit"
33
+ :style="`width: ${ defPercentage }%`"
34
+ ></div>
35
+ </div>
36
+ <div class="progress-bar__values">
37
+ <div class="progress-bar__value progress-bar__value--amount">
38
+ <strong class="hidden-visually">Amount:</strong>
39
+ {{ amount }}
40
+ </div>
41
+ <div
42
+ v-if="deficit > 0"
43
+ class="progress-bar__value progress-bar__value--deficit color-status is-danger"
44
+ >
45
+ <strong class="hidden-visually">Deficit: </strong>
46
+ -{{ deficit }}
47
+ </div>
48
+ <div class="progress-bar__value progress-bar__value--total">
49
+ <strong class="hidden-visually">Total:</strong>
50
+ {{ total }}
51
+ </div>
52
+ </div>
53
+ </div>
54
+ </template>
55
+
56
+ <script>
57
+ export default {
58
+ name: 'ProgressBar',
59
+ props: {
60
+ small: Boolean,
61
+ label: {
62
+ type: String,
63
+ default: "Progress"
64
+ },
65
+ labelHidden: Boolean,
66
+ total: Number,
67
+ deficit: Number,
68
+ amount: Number,
69
+ iconOnLeft: Boolean
70
+ },
71
+ computed: {
72
+ percentage() {
73
+ const { amount, total } = this;
74
+ return amount / total * 100;
75
+ },
76
+ defPercentage() {
77
+ const { deficit, total } = this;
78
+ return deficit ? deficit / total * 100 : 0;
79
+ },
80
+ isComplete() {
81
+ return this.amount >= this.total;
82
+ },
83
+ status() {
84
+ return this.isComplete ? {
85
+ type: "success",
86
+ message: "Item Completed"
87
+ } : this.deficit ? {
88
+ type: "danger",
89
+ message: "Item Has Deficit"
90
+ } : null;
91
+ }
92
+ }
93
+ }
94
+ </script>
@@ -0,0 +1,97 @@
1
+ <template>
2
+ <div
3
+ class="progress-donut"
4
+ :class="{
5
+ 'progress-donut--small' : small,
6
+ 'progress-donut--small-below' : smallBelow,
7
+ 'progress-donut--status-low' : !neutral && percentage < 30,
8
+ 'progress-donut--status-incomplete' : !neutral && percentage >= 30 && percentage < 100,
9
+ 'progress-donut--status-complete' : !neutral && percentage >= 100,
10
+ }"
11
+ >
12
+ <strong class="hidden-visually">Course Progress</strong>
13
+ <div class="progress-donut__chart">
14
+ <!-- Added the 1% extra to 100% becasuse sometimes it renders with a tiny gap -->
15
+ <svg class="progress-donut__chart-svg" viewBox="0 0 32 32">
16
+ <circle
17
+ class="progress-donut__chart-pie"
18
+ ref="pie"
19
+ r="16"
20
+ cx="16"
21
+ cy="16"
22
+ :style="{ strokeDasharray: endDasharray }"
23
+ />
24
+ <circle
25
+ class="progress-donut__chart-mask"
26
+ :r="small ? 7 : 11"
27
+ cx="16"
28
+ cy="16"
29
+ />
30
+ </svg>
31
+ <strong v-if="!small" class="progress-donut__chart-value">
32
+ {{ percentage }}%
33
+ </strong>
34
+ </div>
35
+ <!-- The valus is shown to the side or below when small mode -->
36
+ <strong v-if="small" class="progress-donut__value type-small-x">
37
+ {{ percentage }}%
38
+ </strong>
39
+ </div>
40
+ </template>
41
+
42
+ <script>
43
+ let counter = 0;
44
+ export default {
45
+ name: 'ProgressDonut',
46
+ props: {
47
+ percentage: {
48
+ type: Number,
49
+ default: 0
50
+ },
51
+ small: Boolean,
52
+ smallBelow: Boolean,
53
+ neutral: Boolean,
54
+ duration: {
55
+ type: Number,
56
+ default: 500
57
+ },
58
+ easing: {
59
+ type: String,
60
+ default: "ease-in"
61
+ }
62
+ },
63
+ data() {
64
+ return {
65
+ uid: `progress-donut-${ ++counter }`
66
+ }
67
+ },
68
+ watch: {
69
+ // Need to reanimate if value changes
70
+ percentage(newVal, oldVal) {
71
+ if (newVal !== oldVal) {
72
+ this.animate(this.normalize(oldVal));
73
+ }
74
+ }
75
+ },
76
+ computed: {
77
+ endDasharray() {
78
+ return `${ this.normalize(this.percentage) } 100`;
79
+ }
80
+ },
81
+ methods: {
82
+ normalize(percentage) {
83
+ return percentage === 100 ? 101 : percentage;
84
+ },
85
+ animate(from = 0) {
86
+ const { pie } = this.$refs;
87
+ if (!pie.animate) return; // No Animation API
88
+ const { duration, easing, endDasharray } = this;
89
+ const keyframes = { strokeDasharray: [`${ from } 100`, endDasharray] };
90
+ pie.animate(keyframes, { duration, easing, fill: "forwards" });
91
+ }
92
+ },
93
+ mounted() {
94
+ this.animate();
95
+ }
96
+ }
97
+ </script>
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @module composables/index.js
3
+ * Responsible for exporting all composables
4
+ * - Used in bundle exports
5
+ */
6
+
7
+ export { useIcon } from './useIcon.js';
8
+ export { useModifiers } from './useModifiers.js';
9
+ export { useWindowResize } from './useWindowResize.js';
10
+ export { useBreakpointManager } from './useBreakpointManager.js';
@@ -0,0 +1,68 @@
1
+ import { ref, markRaw } from "vue";
2
+ import { isBrowser } from "@ulu/utils/browser/dom.js";
3
+
4
+ const defaults = {
5
+ /**
6
+ * Set an initial value (value in mounted, SSR)
7
+ */
8
+ initialValue: null,
9
+ /**
10
+ * Function called after init (passed manager)
11
+ */
12
+ onReady: null,
13
+ /**
14
+ * Options sent to CssBreakpoints library
15
+ */
16
+ plugin: {
17
+ customProperty: "--breakpoint"
18
+ }
19
+ };
20
+
21
+ /**
22
+ * Use the CssBreakpoints module in Vue
23
+ * - Normally use only once, unless you have different sets of breakpoints
24
+ * @param {Object} options Configuration options overrides
25
+ * @return {Object} { manager, active, direction } (all are null in SSR environment until init)
26
+ */
27
+ export function useBreakpointManager(options) {
28
+ const config = Object.assign({}, defaults, options);
29
+
30
+ const breakpointManager = ref(null);
31
+ const breakpointActive = ref(config.initialValue);
32
+ const breakpointDirection = ref(null);
33
+
34
+ const init = async () => {
35
+ if (!isBrowser()) return;
36
+
37
+ // Wait for the DOM to be ready before initializing the breakpoint manager
38
+ await new Promise(resolve => {
39
+ if (document.readyState === 'loading') {
40
+ document.addEventListener('DOMContentLoaded', () => resolve());
41
+ } else {
42
+ resolve();
43
+ }
44
+ });
45
+
46
+ const mod = await import("@ulu/frontend/js/ui/breakpoints.js");
47
+ const { BreakpointManager } = mod;
48
+ const manager = markRaw(new BreakpointManager(config.plugin));
49
+
50
+ breakpointManager.value = markRaw(manager);
51
+
52
+ const setValues = () => {
53
+ breakpointActive.value = manager.active;
54
+ breakpointDirection.value = manager.resizeDirection;
55
+ };
56
+
57
+ setValues();
58
+ if (config.onReady) {
59
+ config.onReady(manager);
60
+ }
61
+ manager.onChange(setValues);
62
+ };
63
+
64
+ // Initialize automatically
65
+ init();
66
+
67
+ return { breakpointManager, breakpointActive, breakpointDirection };
68
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Utility composable for handling and formatting icon props for UluIcon.
3
+ * @returns {Object} An object with utility functions { getIconProps, getClassesFromDefinition }
4
+ */
5
+ export function useIcon() {
6
+ const getIconProps = (definition) => {
7
+ if (!definition) return null;
8
+ if (typeof definition === 'object' && !Array.isArray(definition)) {
9
+ // If it's already an object (and not an array), assume it's directly props
10
+ return definition;
11
+ }
12
+ // Otherwise, treat it as the 'icon' prop
13
+ return { icon: definition };
14
+ };
15
+
16
+ /**
17
+ * Converts an icon definition (string, array, or object) into a string of FontAwesome classes.
18
+ * @param {String|Array|Object} definition - The icon definition.
19
+ * @returns {String|null} A string of FontAwesome classes, or null if unable to parse.
20
+ */
21
+ const getClassesFromDefinition = (definition) => {
22
+ if (!definition) {
23
+ return null;
24
+ }
25
+
26
+ // If definition is already a string (e.g., "fas fa-home")
27
+ if (typeof definition === "string") {
28
+ return definition;
29
+ }
30
+
31
+ // If definition is an array (e.g., ["fas", "home"])
32
+ if (Array.isArray(definition)) {
33
+ // FontAwesome's component expects ['fas', 'home'], but for classes,
34
+ // we need to join them. Assuming the first element is the style prefix.
35
+ if (definition.length >= 2) {
36
+ // Common case: ['fas', 'home'] -> 'fas fa-home'
37
+ return `${definition[0]} fa-${definition[1]}`;
38
+ }
39
+ // Handle edge cases or just return the first if it's the only one
40
+ // e.g., ['fas'] might result in just 'fas' which isn't a full icon, but handles the edge
41
+ return definition.join(' ');
42
+ }
43
+
44
+ // If definition is an object (e.g., { icon: ['fas', 'home'] } or { icon: 'fas fa-home' })
45
+ if (typeof definition === 'object' && definition.icon) {
46
+ if (typeof definition.icon === 'string') {
47
+ return definition.icon;
48
+ }
49
+ if (Array.isArray(definition.icon)) {
50
+ if (definition.icon.length >= 2) {
51
+ return `${definition.icon[0]} fa-${definition.icon[1]}`;
52
+ }
53
+ return definition.icon.join(' ');
54
+ }
55
+ }
56
+
57
+ console.warn("useIcon: Unable to parse definition for static FontAwesome classes:", definition);
58
+ return null;
59
+ };
60
+
61
+ return { getIconProps, getClassesFromDefinition };
62
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * @module composables/useModifiers
3
+ * Handles user modifiers prop and internal modifiers for a given component
4
+ */
5
+
6
+ import { computed, toRefs, toValue } from "vue";
7
+ import { normalizeClasses } from "@ulu/utils/templating.js";
8
+
9
+ /**
10
+ * A composable to manage and resolve BEM style modifiers for a component,
11
+ * combining user-passed modifiers with internally derived conditional modifiers.
12
+ *
13
+ * @param {object} options - The options for the composable.
14
+ * @param {object} options.props - The component's props object. (Must contain a 'modifiers' prop if user-passed modifiers are expected)
15
+ * @param {string | import('vue').Ref<string>} options.baseClass - The base CSS class name for the component (e.g., 'modal').
16
+ * Can be a string or a ref to a string.
17
+ * @param {string | string[] | Object.<string, any> | import('vue').ComputedRef<string | string[] | Object.<string, any>>} [options.internal={}] -
18
+ * A flexible input for component's internal modifiers. Can be a string, array of strings/objects, or an object mapping modifier names to conditions.
19
+ * @returns {object} An object containing the computed property `resolvedModifiers`
20
+ *
21
+ * @example
22
+ * // In MyComponent.vue:
23
+ * <template>
24
+ * <div :class="[resolvedModifiers, 'other-class']"></div>
25
+ * </template>
26
+ *
27
+ * <script>
28
+ * import { computed, ref } from 'vue';
29
+ * import { useModifiers } from './composables/useModifiers.js'; // Adjust path
30
+ *
31
+ * export default {
32
+ * props: {
33
+ * variant: String, // e.g., 'primary', 'secondary'
34
+ * isActive: Boolean,
35
+ * modifiers: [String, Array, Object] // User-passed modifiers
36
+ * },
37
+ * setup(props) {
38
+ * const isHovered = ref(false);
39
+ *
40
+ * // Define component-internal modifiers based on props or local state
41
+ * const internalModifiers = computed(() => ({
42
+ * [props.variant]: !!props.variant, // Add 'primary' or 'secondary' if prop exists
43
+ * 'active': props.isActive, // Add 'active' if isActive prop is true
44
+ * 'hovered': isHovered.value, // Add 'hovered' if local state is true
45
+ * 'default': !props.variant && !props.isActive // Add 'default' if no variant/active
46
+ * }));
47
+ *
48
+ * // Use the composable to get the combined modifier classes
49
+ * const { resolvedModifiers } = useModifiers({
50
+ * props: props, // Pass component props for 'modifiers' prop
51
+ * baseClass: 'button', // The BEM block name
52
+ * internal: internalModifiers // The computed internal modifiers
53
+ * });
54
+ *
55
+ * return { resolvedModifiers, isHovered };
56
+ * }
57
+ * };
58
+ * </script>
59
+ *
60
+ * // Resulting class examples for 'my-component':
61
+ * // <MyComponent /> => class="my-component my-component--default"
62
+ * // <MyComponent variant="primary" /> => class="my-component my-component--primary"
63
+ * // <MyComponent isActive /> => class="my-component my-component--active"
64
+ * // <MyComponent modifiers="condensed" /> => class="my-component my-component--default my-component--condensed"
65
+ * // <MyComponent variant="secondary" :isActive="true" modifiers="round" />
66
+ * // => class="my-component my-component--secondary my-component--active my-component--round"
67
+ */
68
+ export function useModifiers({ props, baseClass, internal = {} }) {
69
+ // Use toRefs to destructure props to maintain reactivity for `modifiers`
70
+ // Ensure props has a 'modifiers' property, or handle its absence
71
+ const { modifiers } = toRefs(props);
72
+
73
+ const resolvedModifiers = computed(() => {
74
+ const resolvedBase = toValue(baseClass);
75
+
76
+ // Normalize both userModifiers and internal using the helper
77
+ const userModifiers = normalizeClasses(toValue(modifiers));
78
+ const internalModifiers = normalizeClasses(toValue(internal));
79
+
80
+ if (!resolvedBase) {
81
+ console.warn("useModifiers: Missing 'baseClass' argument, modifiers will not be applied.");
82
+ return '';
83
+ }
84
+
85
+ // Combine all active modifiers into one Set
86
+ const all = new Set([...internalModifiers, ...userModifiers]);
87
+
88
+ // Join all collected modifiers
89
+ return Array.from(all).map(modifier => `${ resolvedBase }--${ modifier }`);
90
+ });
91
+
92
+ return { resolvedModifiers };
93
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Reusable window resize
3
+ * - Future could have request animation frame throttled onResize
4
+ * - Just start and end for now, until thats needed
5
+ */
6
+ import { ref } from "vue";
7
+ import { debounce } from "@ulu/utils/performance.js";
8
+ const resizing = ref(false);
9
+ const callbacks = {
10
+ start: [],
11
+ end: []
12
+ };
13
+ // Method is to add a resize handler just for the first call (start)
14
+ // which removes itself, the end (debounced) will be called after resizing
15
+ // stops at which point it will add the start handler to repeat the process
16
+ // Debounced end handler never needs to be removed.
17
+ function onStart() {
18
+ window.removeEventListener("resize", onStart);
19
+ resizing.value = true;
20
+ callbacks.start.forEach(cb => cb());
21
+ }
22
+ function onEnd() {
23
+ resizing.value = false;
24
+ callbacks.end.forEach(cb => cb());
25
+ window.addEventListener("resize", onStart);
26
+ }
27
+
28
+ // Only allow in browser contexts
29
+ if (!import.meta.env.SSR) {
30
+ window.addEventListener("resize", onStart);
31
+ window.addEventListener("resize", debounce(onEnd, 300));
32
+ }
33
+
34
+ /**
35
+ *
36
+ * @param {Array} array Internal callback array to put callback in
37
+ * @param {*} callback Users callback
38
+ * @returns {Function} Remove Function
39
+ */
40
+ function register(array, callback) {
41
+ array.push(callback);
42
+ return () => {
43
+ const index = array.findIndex(cb => cb === callback);
44
+ if (index > -1) {
45
+ array.splice(index);
46
+ }
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Composable function
52
+ * @return {Object} Contains reactive 'resizing' and two methods for calling callbacks (onResizeStart, onResizeEnd)
53
+ */
54
+ export function useWindowResize() {
55
+ return {
56
+ resizing,
57
+ onResizeStart(callback) {
58
+ return register(callbacks.start, callback);
59
+ },
60
+ onResizeEnd(callback) {
61
+ return register(callbacks.end, callback);
62
+ }
63
+ };
64
+ }
package/lib/index.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @module index.js
3
+ * - Main library entrypoint
4
+ * - Exports everything
5
+ */
6
+
7
+ export * from './plugins/index.js';
8
+ export * from './components/index.js';
9
+ export * from './composables/index.js';
10
+ export * from './settings.js';
@@ -0,0 +1,7 @@
1
+ ////
2
+ /// @group index
3
+ /// Forwards all plugins
4
+ ////
5
+
6
+
7
+ @forward "toast/toast" as toast-*;
@@ -0,0 +1,47 @@
1
+ import { ref, computed } from "vue";
2
+ import { useBreakpointManager } from "../../composables/useBreakpointManager.js";
3
+
4
+ /**
5
+ * Plugin Options
6
+ */
7
+ const defaults = {
8
+ /**
9
+ * Breakpoint for mobile down/max
10
+ */
11
+ breakpointMobile: "small",
12
+ /**
13
+ * Options to pass to useBreakpointManager()
14
+ */
15
+ managerOptions: {}
16
+ };
17
+
18
+ export default function install(app, userOptions) {
19
+ const isMobile = ref(false);
20
+ const options = Object.assign({}, defaults, userOptions);
21
+ const { breakpointMobile } = options;
22
+ const { onReady: userOnReady } = options.managerOptions;
23
+
24
+ // Setup breakpoint manager options but insert our own onReady (create flag for mobile)
25
+ const pluginManagerOptions = {
26
+ onReady(manager) {
27
+ manager.at(breakpointMobile).max({
28
+ on() { isMobile.value = true; },
29
+ off() { isMobile.value = false; }
30
+ });
31
+ if (userOnReady) userOnReady(manager);
32
+ }
33
+ };
34
+
35
+ const managerOptions = Object.assign({}, options.managerOptions, pluginManagerOptions);
36
+
37
+ const {
38
+ breakpointManager,
39
+ breakpointActive,
40
+ breakpointDirection
41
+ } = useBreakpointManager(managerOptions);
42
+
43
+ app.provide("uluBreakpointActive", computed(() => breakpointActive.value));
44
+ app.provide("uluBreakpointDirection", computed(() => breakpointDirection.value));
45
+ app.provide("uluBreakpointManager", computed(() => breakpointManager.value));
46
+ app.provide("uluIsMobile", computed(() => isMobile.value));
47
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @module plugins/index.js
3
+ * Responsible for exporting all plugins
4
+ * - Used in bundle exports
5
+ */
6
+
7
+
8
+ export { default as popoversPlugin } from './popovers/index.js';
9
+ export { default as modalsPlugin } from './modals/index.js';
10
+ export { default as toastPlugin } from './toast/index.js';
11
+ export { default as breakpointsPlugin } from './breakpoints/index.js';
@@ -0,0 +1,59 @@
1
+ <template>
2
+ <component
3
+ v-if="currentModal"
4
+ :is="currentModal.component"
5
+ v-bind="currentProps"
6
+ v-model="open"
7
+ @vue:mounted="modalMounted"
8
+ @vue:unmounted="modalUnmounted"
9
+ />
10
+ </template>
11
+
12
+ <script>
13
+ export default {
14
+ name: "UluModalsDisplay",
15
+ emits: [
16
+ "modal-unmount",
17
+ "modal-mount"
18
+ ],
19
+ data() {
20
+ return {
21
+ open: false
22
+ };
23
+ },
24
+ computed: {
25
+ currentModal() {
26
+ return this.$uluModalsState.data?.active;
27
+ },
28
+ currentProps() {
29
+ return this.$uluModalsState.data?.activeProps;
30
+ }
31
+ },
32
+ watch: {
33
+ // Watch for changes in the global state (e.g., when $uluModals.open() is called)
34
+ currentModal(newValue) {
35
+ if (newValue) {
36
+ this.open = true;
37
+ } else {
38
+ this.open = false;
39
+ }
40
+ },
41
+ // Watch for changes in the local state (e.g., when the modal emits 'update:modelValue')
42
+ open(newValue) {
43
+ if (!newValue && this.currentModal) {
44
+ this.$uluModals.close();
45
+ }
46
+ }
47
+ },
48
+ methods: {
49
+ modalMounted() {
50
+ this.$emit("modal-mount", { modal: this.currentModal });
51
+ },
52
+ modalUnmounted() {
53
+ this.$nextTick(() => {
54
+ this.$emit("modal-unmount");
55
+ });
56
+ }
57
+ }
58
+ };
59
+ </script>