@ulu/frontend-vue 0.1.0-beta.9 → 0.1.1-beta.2
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.
- package/dist/{breakpoints-BbkGNxxt.js → breakpoints-DfGETUy5.js} +1 -1
- package/dist/frontend-vue.css +1 -1
- package/dist/frontend-vue.js +79 -68
- package/dist/index-94HkwBnP.js +7595 -0
- package/lib/components/collapsible/UluAccordion.vue +71 -53
- package/lib/components/collapsible/UluAccordionGroup.vue +54 -0
- package/lib/components/collapsible/UluCollapsible.vue +144 -0
- package/lib/components/collapsible/UluDropdown.vue +29 -29
- package/lib/components/collapsible/UluOverflowPopover.vue +1 -1
- package/lib/components/elements/UluBadge.vue +51 -28
- package/lib/components/elements/UluBadgeStack.vue +8 -13
- package/lib/components/elements/UluButtonVerbose.vue +119 -0
- package/lib/components/elements/UluCard.vue +1 -1
- package/lib/components/elements/UluDefinitionList.vue +14 -17
- package/lib/components/elements/UluExternalLink.vue +21 -27
- package/lib/components/elements/UluIcon.vue +11 -1
- package/lib/components/elements/UluList.vue +53 -55
- package/lib/components/elements/UluSpokeSpinner.vue +12 -18
- package/lib/components/elements/UluTag.vue +35 -35
- package/lib/components/forms/UluFileDisplay.vue +49 -31
- package/lib/components/forms/UluFormFile.vue +37 -24
- package/lib/components/forms/UluFormMessage.vue +13 -10
- package/lib/components/forms/UluFormSelect.vue +28 -16
- package/lib/components/forms/UluFormText.vue +24 -15
- package/lib/components/forms/UluSearchForm.vue +11 -10
- package/lib/components/forms/UluSelectableMenu.vue +99 -0
- package/lib/components/index.js +4 -3
- package/lib/components/layout/UluTitleRail.vue +18 -0
- package/lib/components/layout/UluWhenBreakpoint.vue +9 -0
- package/lib/components/navigation/UluBreadcrumb.vue +9 -2
- package/lib/components/navigation/UluMenu.vue +8 -3
- package/lib/components/navigation/UluMenuStack.vue +3 -1
- package/lib/components/navigation/UluPager.vue +102 -0
- package/lib/components/systems/facets/ExampleFacetsWithPagination.vue +119 -0
- package/lib/components/systems/facets/UluFacetsFilterLists.vue +91 -0
- package/lib/components/systems/facets/UluFacetsFilterPopovers.vue +125 -0
- package/lib/components/systems/facets/UluFacetsFilterSelects.vue +71 -0
- package/lib/components/systems/facets/UluFacetsHeaderLayout.vue +24 -0
- package/lib/components/systems/facets/UluFacetsList.vue +62 -34
- package/lib/components/systems/facets/UluFacetsResults.vue +63 -0
- package/lib/components/systems/facets/UluFacetsSearch.vue +27 -50
- package/lib/components/systems/facets/UluFacetsSidebarLayout.vue +70 -0
- package/lib/components/systems/facets/UluFacetsSort.vue +45 -0
- package/lib/components/systems/facets/_facets.scss +2 -3
- package/lib/components/systems/facets/_mock-data.js +40 -0
- package/lib/components/systems/facets/useFacets.js +268 -0
- package/lib/components/systems/index.js +13 -2
- package/lib/components/systems/scroll-anchors/UluScrollAnchors.vue +2 -1
- package/lib/components/systems/skeleton/UluShowSkeleton.vue +9 -8
- package/lib/components/systems/skeleton/UluSkeletonContent.vue +39 -43
- package/lib/components/systems/skeleton/UluSkeletonMedia.vue +4 -6
- package/lib/components/systems/skeleton/UluSkeletonText.vue +27 -0
- package/lib/components/systems/slider/UluImageSlideShow.vue +1 -1
- package/lib/components/systems/slider/UluSlideShow.vue +8 -3
- package/lib/components/systems/table-sticky/UluTableSticky.vue +7 -7
- package/lib/components/systems/table-sticky/UluTableStickyTable.vue +3 -3
- package/lib/components/visualizations/UluAnimateNumber.vue +7 -1
- package/lib/components/visualizations/UluProgressBar.vue +148 -74
- package/lib/components/visualizations/UluProgressCircle.vue +159 -0
- package/lib/composables/index.js +3 -1
- package/lib/composables/useDocumentTitle.js +61 -0
- package/lib/composables/usePagination.js +122 -0
- package/lib/index.js +1 -0
- package/lib/plugins/core/index.js +6 -1
- package/lib/plugins/popovers/UluPopover.vue +8 -3
- package/lib/plugins/toast/UluToast.vue +1 -1
- package/lib/plugins/toast/UluToastDisplay.vue +19 -2
- package/lib/utils/dom.js +12 -0
- package/lib/utils/index.js +2 -0
- package/lib/utils/{vue-router.js → router.js} +114 -30
- package/package.json +17 -11
- package/types/components/systems/facets/_mock-data.d.ts +18 -0
- package/types/components/systems/facets/_mock-data.d.ts.map +1 -0
- package/types/components/systems/facets/useFacets.d.ts +39 -0
- package/types/components/systems/facets/useFacets.d.ts.map +1 -0
- package/types/components/systems/index.d.ts +1 -1
- package/types/composables/index.d.ts +2 -0
- package/types/composables/useDocumentTitle.d.ts +22 -0
- package/types/composables/useDocumentTitle.d.ts.map +1 -0
- package/types/composables/usePageTitle.d.ts +19 -0
- package/types/composables/usePageTitle.d.ts.map +1 -0
- package/types/composables/usePagination.d.ts +25 -0
- package/types/composables/usePagination.d.ts.map +1 -0
- package/types/index.d.ts +1 -0
- package/types/plugins/core/index.d.ts.map +1 -1
- package/types/utils/dom.d.ts +1 -0
- package/types/utils/dom.d.ts.map +1 -1
- package/types/utils/index.d.ts +3 -0
- package/types/utils/index.d.ts.map +1 -0
- package/types/utils/router.d.ts +144 -0
- package/types/utils/router.d.ts.map +1 -0
- package/dist/index-D3Uc6T5M.js +0 -6469
- package/lib/components/collapsible/UluCollapsibleRegion.vue +0 -278
- package/lib/components/forms/UluCheckboxMenu.vue +0 -36
- package/lib/components/systems/facets/UluFacets.vue +0 -380
- package/lib/components/systems/skeleton/UluSkeletonTextInline.vue +0 -9
- package/lib/components/visualizations/UluProgressDonut.vue +0 -97
- package/lib/utils/placeholder.js +0 -6
|
@@ -1,94 +1,168 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class="progress-bar__label"
|
|
13
|
-
:class="{
|
|
14
|
-
'type-normal' : small,
|
|
15
|
-
'hidden-visually' : labelHidden,
|
|
16
|
-
}"
|
|
2
|
+
<div :class="componentClasses">
|
|
3
|
+
<div
|
|
4
|
+
v-if="label || $slots.label || $slots.icon || amountInHeader"
|
|
5
|
+
class="progress-bar__header"
|
|
6
|
+
>
|
|
7
|
+
<component
|
|
8
|
+
v-if="label"
|
|
9
|
+
:is="labelElement"
|
|
10
|
+
class="progress-bar__label"
|
|
11
|
+
:class="[classes.label, { 'hidden-visually': labelHidden }]"
|
|
17
12
|
>
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
<slot name="label">
|
|
14
|
+
{{ label }}
|
|
15
|
+
</slot>
|
|
16
|
+
</component>
|
|
17
|
+
<div
|
|
18
|
+
v-if="amountInHeader"
|
|
19
|
+
class="progress-bar__value progress-bar__value--amount"
|
|
20
|
+
>
|
|
21
|
+
<strong class="hidden-visually">Amount:</strong>
|
|
22
|
+
<slot name="valueAmount" :value="amount">{{ formatValue(amount, 'amount') }}</slot>
|
|
23
|
+
</div>
|
|
24
|
+
<div v-if="$slots.icon" class="progress-bar__icon">
|
|
25
|
+
<slot name="icon" />
|
|
23
26
|
</div>
|
|
24
27
|
</div>
|
|
25
28
|
<div class="progress-bar__track">
|
|
26
|
-
<div
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
></div>
|
|
30
|
-
<div
|
|
31
|
-
v-if="deficit"
|
|
29
|
+
<div class="progress-bar__bar" :style="{ width: barWidth }"></div>
|
|
30
|
+
<div
|
|
31
|
+
v-if="deficit > 0"
|
|
32
32
|
class="progress-bar__bar--deficit"
|
|
33
|
-
:style="
|
|
33
|
+
:style="{ width: deficitBarWidth }"
|
|
34
34
|
></div>
|
|
35
35
|
</div>
|
|
36
|
-
<div
|
|
36
|
+
<div
|
|
37
|
+
v-if="!noValues && !amountInHeader && (!loader && !indeterminate)"
|
|
38
|
+
class="progress-bar__values"
|
|
39
|
+
>
|
|
37
40
|
<div class="progress-bar__value progress-bar__value--amount">
|
|
38
41
|
<strong class="hidden-visually">Amount:</strong>
|
|
39
|
-
{{ amount }}
|
|
42
|
+
<slot name="valueAmount" :value="amount">{{ formatValue(amount, 'amount') }}</slot>
|
|
40
43
|
</div>
|
|
41
|
-
<div
|
|
42
|
-
v-if="deficit > 0"
|
|
43
|
-
class="progress-bar__value progress-bar__value--deficit color-status is-danger"
|
|
44
|
-
>
|
|
44
|
+
<div v-if="deficit > 0" class="progress-bar__value progress-bar__value--deficit">
|
|
45
45
|
<strong class="hidden-visually">Deficit: </strong>
|
|
46
|
-
|
|
46
|
+
<slot name="valueDeficit" :value="deficit">-{{ formatValue(deficit, 'deficit') }}</slot>
|
|
47
47
|
</div>
|
|
48
48
|
<div class="progress-bar__value progress-bar__value--total">
|
|
49
49
|
<strong class="hidden-visually">Total:</strong>
|
|
50
|
-
{{ total }}
|
|
50
|
+
<slot name="valueTotal" :value="total">{{ formatValue(total, 'total') }}</slot>
|
|
51
51
|
</div>
|
|
52
52
|
</div>
|
|
53
53
|
</div>
|
|
54
54
|
</template>
|
|
55
55
|
|
|
56
|
-
<script>
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
56
|
+
<script setup>
|
|
57
|
+
import { computed } from "vue";
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A linear progress bar to display progress, with support for various styles and a deficit indicator.
|
|
61
|
+
* @slot icon - A slot for placing an icon in the header, typically to indicate status.
|
|
62
|
+
*/
|
|
63
|
+
const props = defineProps({
|
|
64
|
+
/**
|
|
65
|
+
* The label to display above the progress bar. (or use label slot)
|
|
66
|
+
*/
|
|
67
|
+
label: String,
|
|
68
|
+
/**
|
|
69
|
+
* Hides the label visually, but keeps it for screen readers.
|
|
70
|
+
*/
|
|
71
|
+
labelHidden: Boolean,
|
|
72
|
+
/**
|
|
73
|
+
* Optional classes object (currently only allowing { label } class)
|
|
74
|
+
*/
|
|
75
|
+
classes: {
|
|
76
|
+
type: Object,
|
|
77
|
+
default: () => ({})
|
|
78
|
+
},
|
|
79
|
+
/**
|
|
80
|
+
* Element to use for label
|
|
81
|
+
*/
|
|
82
|
+
labelElement: {
|
|
83
|
+
type: String,
|
|
84
|
+
default: "strong"
|
|
85
|
+
},
|
|
86
|
+
/**
|
|
87
|
+
* The current amount of progress.
|
|
88
|
+
*/
|
|
89
|
+
amount: {
|
|
90
|
+
type: Number,
|
|
91
|
+
default: 0,
|
|
92
|
+
},
|
|
93
|
+
/**
|
|
94
|
+
* The total amount that represents 100% progress.
|
|
95
|
+
*/
|
|
96
|
+
total: {
|
|
97
|
+
type: Number,
|
|
98
|
+
default: 100,
|
|
99
|
+
},
|
|
100
|
+
/**
|
|
101
|
+
* The amount of deficit to display on the bar.
|
|
102
|
+
*/
|
|
103
|
+
deficit: {
|
|
104
|
+
type: Number,
|
|
105
|
+
default: 0,
|
|
106
|
+
},
|
|
107
|
+
/**
|
|
108
|
+
* Renders a smaller version of the progress bar.
|
|
109
|
+
*/
|
|
110
|
+
small: Boolean,
|
|
111
|
+
/**
|
|
112
|
+
* Applies the 'positive' style (e.g., green).
|
|
113
|
+
*/
|
|
114
|
+
positive: Boolean,
|
|
115
|
+
/**
|
|
116
|
+
* Applies the 'negative' style (e.g., red).
|
|
117
|
+
*/
|
|
118
|
+
negative: Boolean,
|
|
119
|
+
/**
|
|
120
|
+
* Applies styles for use as a thin loader.
|
|
121
|
+
*/
|
|
122
|
+
loader: Boolean,
|
|
123
|
+
/**
|
|
124
|
+
* Applies an indeterminate animation for unknown progress.
|
|
125
|
+
*/
|
|
126
|
+
indeterminate: Boolean,
|
|
127
|
+
/**
|
|
128
|
+
* Omit values from output (the numbers below the progress bar)
|
|
129
|
+
*/
|
|
130
|
+
noValues: Boolean,
|
|
131
|
+
/**
|
|
132
|
+
* A function to format the numerical values (amount, deficit, total).
|
|
133
|
+
* Takes the value and type ('amount', 'deficit', 'total') as input and should return a string.
|
|
134
|
+
*/
|
|
135
|
+
formatValue: {
|
|
136
|
+
type: Function,
|
|
137
|
+
default: (value, type) => value,
|
|
138
|
+
},
|
|
139
|
+
/**
|
|
140
|
+
* Will put the amount only in header (there is a headerValue slot it you want to format)
|
|
141
|
+
*/
|
|
142
|
+
amountInHeader: Boolean
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const getCssPercentage = (amount, total) => {
|
|
146
|
+
const percent = total === 0 ? 0 : (amount / total) * 100;
|
|
147
|
+
return `${ percent }%`;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const barWidth = computed(() => {
|
|
151
|
+
if (props.indeterminate) return null; // No value for width
|
|
152
|
+
return getCssPercentage(props.amount, props.total);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const deficitBarWidth = computed(() => getCssPercentage(props.deficit, props.total));
|
|
156
|
+
|
|
157
|
+
const componentClasses = computed(() => {
|
|
158
|
+
return {
|
|
159
|
+
'progress-bar': true,
|
|
160
|
+
'progress-bar--small': props.small,
|
|
161
|
+
'progress-bar--positive': props.positive,
|
|
162
|
+
'progress-bar--negative': props.negative,
|
|
163
|
+
'progress-bar--loader': props.loader,
|
|
164
|
+
'progress-bar--indeterminate': props.indeterminate,
|
|
165
|
+
'type-small': props.small, // From original component, seems to control font size
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
</script>
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="componentClasses">
|
|
3
|
+
<strong class="hidden-visually">{{ label }}</strong>
|
|
4
|
+
<div class="progress-circle__chart">
|
|
5
|
+
<svg class="progress-circle__chart-svg" viewBox="0 0 32 32">
|
|
6
|
+
<circle
|
|
7
|
+
class="progress-circle__chart-track"
|
|
8
|
+
r="16"
|
|
9
|
+
cx="16"
|
|
10
|
+
cy="16"
|
|
11
|
+
/>
|
|
12
|
+
<circle
|
|
13
|
+
class="progress-circle__chart-pie"
|
|
14
|
+
ref="pie"
|
|
15
|
+
r="16"
|
|
16
|
+
cx="16"
|
|
17
|
+
cy="16"
|
|
18
|
+
:style="{ strokeDasharray: endDasharray }"
|
|
19
|
+
/>
|
|
20
|
+
<circle
|
|
21
|
+
class="progress-circle__chart-mask"
|
|
22
|
+
cx="16"
|
|
23
|
+
cy="16"
|
|
24
|
+
/>
|
|
25
|
+
</svg>
|
|
26
|
+
<strong v-if="!showValueOutside && !noValue" class="progress-circle__chart-value">
|
|
27
|
+
<slot name="value" :value="percentage">{{ formatValue(percentage) }}</slot>
|
|
28
|
+
</strong>
|
|
29
|
+
</div>
|
|
30
|
+
<strong v-if="showValueOutside && !noValue" class="progress-circle__value type-small-x">
|
|
31
|
+
<slot name="value" :value="percentage">{{ formatValue(percentage) }}</slot>
|
|
32
|
+
</strong>
|
|
33
|
+
</div>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<script setup>
|
|
37
|
+
import { ref, computed, watch, onMounted } from 'vue';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A circular progress indicator component.
|
|
41
|
+
* @slot value - The value display. Overrides the `formatValue` prop.
|
|
42
|
+
*/
|
|
43
|
+
const props = defineProps({
|
|
44
|
+
/**
|
|
45
|
+
* The label for accessibility (visually hidden).
|
|
46
|
+
*/
|
|
47
|
+
label: {
|
|
48
|
+
type: String,
|
|
49
|
+
default: "Progress"
|
|
50
|
+
},
|
|
51
|
+
/**
|
|
52
|
+
* The progress percentage (0-100).
|
|
53
|
+
*/
|
|
54
|
+
percentage: {
|
|
55
|
+
type: Number,
|
|
56
|
+
default: 0
|
|
57
|
+
},
|
|
58
|
+
/**
|
|
59
|
+
* A function to format the percentage value.
|
|
60
|
+
* Takes the number as input and should return a string.
|
|
61
|
+
*/
|
|
62
|
+
formatValue: {
|
|
63
|
+
type: Function,
|
|
64
|
+
default: (value) => `${value}%`,
|
|
65
|
+
},
|
|
66
|
+
/**
|
|
67
|
+
* Hides the percentage value display.
|
|
68
|
+
*/
|
|
69
|
+
noValue: Boolean,
|
|
70
|
+
/**
|
|
71
|
+
* Renders a smaller version of the component.
|
|
72
|
+
*/
|
|
73
|
+
small: Boolean,
|
|
74
|
+
/**
|
|
75
|
+
* Displays the percentage value outside (to the side) of the circle.
|
|
76
|
+
*/
|
|
77
|
+
outside: Boolean,
|
|
78
|
+
/**
|
|
79
|
+
* Displays the percentage value below the circle.
|
|
80
|
+
*/
|
|
81
|
+
outsideBelow: Boolean,
|
|
82
|
+
/**
|
|
83
|
+
* Sets the status color of the progress circle (e.g., 'low', 'incomplete', 'complete').
|
|
84
|
+
*/
|
|
85
|
+
status: {
|
|
86
|
+
type: String,
|
|
87
|
+
default: ''
|
|
88
|
+
},
|
|
89
|
+
/**
|
|
90
|
+
* Renders the component as a solid pie chart instead of a donut.
|
|
91
|
+
*/
|
|
92
|
+
pieStyle: Boolean,
|
|
93
|
+
/**
|
|
94
|
+
* Removes the center mask, filling the entire circle.
|
|
95
|
+
*/
|
|
96
|
+
noMask: Boolean,
|
|
97
|
+
/**
|
|
98
|
+
* The duration of the animation in milliseconds.
|
|
99
|
+
*/
|
|
100
|
+
duration: {
|
|
101
|
+
type: Number,
|
|
102
|
+
default: 1000 // Matches SCSS animation-duration
|
|
103
|
+
},
|
|
104
|
+
/**
|
|
105
|
+
* The easing function for the animation.
|
|
106
|
+
*/
|
|
107
|
+
easing: {
|
|
108
|
+
type: String,
|
|
109
|
+
default: "ease-in" // Matches SCSS animation-timing
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const pie = ref(null);
|
|
114
|
+
|
|
115
|
+
const normalize = (percentage) => {
|
|
116
|
+
// Added the 1% extra to 100% because sometimes it renders with a tiny gap
|
|
117
|
+
return percentage === 100 ? 101 : percentage;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const animate = (from = 0) => {
|
|
121
|
+
if (!pie.value || !pie.value.animate) return; // No Animation API or element not ready
|
|
122
|
+
|
|
123
|
+
const keyframes = { strokeDasharray: [`${from} 100`, endDasharray.value] };
|
|
124
|
+
pie.value.animate(keyframes, { duration: props.duration, easing: props.easing, fill: "forwards" });
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
watch(() => props.percentage, (newVal, oldVal) => {
|
|
128
|
+
if (newVal !== oldVal) {
|
|
129
|
+
animate(normalize(oldVal));
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const endDasharray = computed(() => {
|
|
134
|
+
return `${normalize(props.percentage)} 100`;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const showValueOutside = computed(() => {
|
|
138
|
+
return props.outside || props.outsideBelow || props.small;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const componentClasses = computed(() => {
|
|
142
|
+
const classes = {
|
|
143
|
+
'progress-circle': true,
|
|
144
|
+
'progress-circle--small': props.small,
|
|
145
|
+
'progress-circle--pie': props.pieStyle,
|
|
146
|
+
'progress-circle--outside': showValueOutside.value,
|
|
147
|
+
'progress-circle--outside-below': props.outsideBelow,
|
|
148
|
+
'progress-circle--no-mask': props.noMask,
|
|
149
|
+
};
|
|
150
|
+
if (props.status) {
|
|
151
|
+
classes[`progress-circle--${props.status}`] = true;
|
|
152
|
+
}
|
|
153
|
+
return classes;
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
onMounted(() => {
|
|
157
|
+
animate();
|
|
158
|
+
});
|
|
159
|
+
</script>
|
package/lib/composables/index.js
CHANGED
|
@@ -8,4 +8,6 @@ export { useIcon } from './useIcon.js';
|
|
|
8
8
|
export { useModifiers } from './useModifiers.js';
|
|
9
9
|
export { useWindowResize } from './useWindowResize.js';
|
|
10
10
|
export { useRequiredInject } from './useRequiredInject.js';
|
|
11
|
-
export { useBreakpointManager } from './useBreakpointManager.js';
|
|
11
|
+
export { useBreakpointManager } from './useBreakpointManager.js';
|
|
12
|
+
export { usePagination } from './usePagination.js';
|
|
13
|
+
export { useDocumentTitle } from './useDocumentTitle.js';
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { reactive, watchEffect, onUnmounted, unref, computed } from "vue";
|
|
2
|
+
import { useHead as defaultUseHead } from "@unhead/vue";
|
|
3
|
+
import { useRoute as defaultUseRoute } from "vue-router";
|
|
4
|
+
import { getRouteTitle } from "../utils/router.js";
|
|
5
|
+
|
|
6
|
+
// A reactive map to store component-defined titles.
|
|
7
|
+
const componentTitles = reactive({});
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A composable to manage the document title.
|
|
11
|
+
*
|
|
12
|
+
* When called with a `title` option, it sets a dynamic title for the current page.
|
|
13
|
+
* This is for use within specific components.
|
|
14
|
+
*
|
|
15
|
+
* When called without a `title` option (typically in App.vue), it manages the
|
|
16
|
+
* document title for the whole app, using titles from components or route meta.
|
|
17
|
+
*
|
|
18
|
+
* @param {object} options
|
|
19
|
+
* @param {import('vue').Ref<string> | string} [options.title] - The dynamic title to set for the current page.
|
|
20
|
+
* @param {string} [options.titleTemplate='%s'] - The template for the document title, e.g., '%s | My Site'.
|
|
21
|
+
* @param {Function} [options.useRoute=defaultUseRoute] - Injectable `useRoute` for testing.
|
|
22
|
+
* @param {Function} [options.useHead=defaultUseHead] - Injectable `useHead` for testing.
|
|
23
|
+
*/
|
|
24
|
+
export function useDocumentTitle(options = {}) {
|
|
25
|
+
const {
|
|
26
|
+
title,
|
|
27
|
+
titleTemplate = "%s",
|
|
28
|
+
useRoute = defaultUseRoute,
|
|
29
|
+
useHead = defaultUseHead,
|
|
30
|
+
} = options;
|
|
31
|
+
|
|
32
|
+
const route = useRoute();
|
|
33
|
+
const path = route.path;
|
|
34
|
+
|
|
35
|
+
// --- Setter Mode ---
|
|
36
|
+
// If a title is provided, we're in "setter" mode, used within a component.
|
|
37
|
+
if (title !== undefined) {
|
|
38
|
+
watchEffect(() => {
|
|
39
|
+
componentTitles[path] = unref(title);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
onUnmounted(() => {
|
|
43
|
+
delete componentTitles[path];
|
|
44
|
+
});
|
|
45
|
+
return; // End execution for setter mode.
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// --- Manager Mode ---
|
|
49
|
+
// If no title is provided, we're in "manager" mode, used in App.vue.
|
|
50
|
+
const documentTitle = computed(() => {
|
|
51
|
+
const titleFromComponent = componentTitles[route.path];
|
|
52
|
+
const titleFromMeta = getRouteTitle(route, route);
|
|
53
|
+
const resolvedTitle = titleFromComponent || titleFromMeta;
|
|
54
|
+
|
|
55
|
+
return resolvedTitle ? titleTemplate.replace("%s", resolvedTitle) : "App";
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
useHead({
|
|
59
|
+
title: documentTitle,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { computed, watch } from "vue";
|
|
2
|
+
import { useRoute, useRouter } from "vue-router";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A Vue composable for handling pagination logic.
|
|
6
|
+
* It interacts with vue-router to keep the current page in the URL query string.
|
|
7
|
+
*
|
|
8
|
+
* @param {import('vue').Ref<Array<any>>} items - A ref containing the full list of items to be paginated.
|
|
9
|
+
* @param {number} itemsPerPage - The number of items to display per page.
|
|
10
|
+
* @returns {{
|
|
11
|
+
* currentPage: import('vue').ComputedRef<number>,
|
|
12
|
+
* totalPages: import('vue').ComputedRef<number>,
|
|
13
|
+
* paginatedItems: import('vue').ComputedRef<Array<any>>,
|
|
14
|
+
* pagerItems: import('vue').ComputedRef<object|null>,
|
|
15
|
+
* pagerEllipses: import('vue').ComputedRef<{previous: boolean, next: boolean}>
|
|
16
|
+
* }} - An object containing reactive properties for pagination.
|
|
17
|
+
*/
|
|
18
|
+
export function usePagination(items, itemsPerPage) {
|
|
19
|
+
const route = useRoute();
|
|
20
|
+
const router = useRouter();
|
|
21
|
+
|
|
22
|
+
const currentPage = computed(() => {
|
|
23
|
+
const page = parseInt(route.query.page || '1', 10);
|
|
24
|
+
return isNaN(page) || page < 1 ? 1 : page;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const totalPages = computed(() => {
|
|
28
|
+
if (!items.value || items.value.length === 0) return 1;
|
|
29
|
+
return Math.ceil(items.value.length / itemsPerPage);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
watch(totalPages, (newTotalPages) => {
|
|
33
|
+
if (currentPage.value > newTotalPages) {
|
|
34
|
+
router.push({ query: { ...route.query, page: newTotalPages } });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const paginatedItems = computed(() => {
|
|
39
|
+
const start = (currentPage.value - 1) * itemsPerPage;
|
|
40
|
+
const end = start + itemsPerPage;
|
|
41
|
+
return items.value.slice(start, end);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const pagerItems = computed(() => {
|
|
45
|
+
if (totalPages.value <= 1) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const items = {
|
|
50
|
+
pages: {}
|
|
51
|
+
};
|
|
52
|
+
const page = currentPage.value;
|
|
53
|
+
const total = totalPages.value;
|
|
54
|
+
const maxPagesToShow = 5;
|
|
55
|
+
|
|
56
|
+
const createRoute = (p) => {
|
|
57
|
+
return { query: { ...route.query, page: p } };
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
if (page > 1) {
|
|
61
|
+
items.first = { href: createRoute(1) };
|
|
62
|
+
items.previous = { href: createRoute(page - 1) };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (page < total) {
|
|
66
|
+
items.next = { href: createRoute(page + 1) };
|
|
67
|
+
items.last = { href: createRoute(total) };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let startPage, endPage;
|
|
71
|
+
if (total <= maxPagesToShow) {
|
|
72
|
+
startPage = 1;
|
|
73
|
+
endPage = total;
|
|
74
|
+
} else {
|
|
75
|
+
const maxPagesBeforeCurrent = Math.floor(maxPagesToShow / 2);
|
|
76
|
+
const maxPagesAfterCurrent = Math.ceil(maxPagesToShow / 2) - 1;
|
|
77
|
+
if (page <= maxPagesBeforeCurrent) {
|
|
78
|
+
startPage = 1;
|
|
79
|
+
endPage = maxPagesToShow;
|
|
80
|
+
} else if (page + maxPagesAfterCurrent >= total) {
|
|
81
|
+
startPage = total - maxPagesToShow + 1;
|
|
82
|
+
endPage = total;
|
|
83
|
+
} else {
|
|
84
|
+
startPage = page - maxPagesBeforeCurrent;
|
|
85
|
+
endPage = page + maxPagesAfterCurrent;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (let i = startPage; i <= endPage; i++) {
|
|
90
|
+
items.pages[i] = { href: createRoute(i) };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return items;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const pagerEllipses = computed(() => {
|
|
97
|
+
const ellipses = { previous: false, next: false };
|
|
98
|
+
if (!pagerItems.value || !pagerItems.value.pages) return ellipses;
|
|
99
|
+
|
|
100
|
+
const pageKeys = Object.keys(pagerItems.value.pages).map(Number);
|
|
101
|
+
if (pageKeys.length === 0) return ellipses;
|
|
102
|
+
|
|
103
|
+
const firstPageInPager = Math.min(...pageKeys);
|
|
104
|
+
const lastPageInPager = Math.max(...pageKeys);
|
|
105
|
+
|
|
106
|
+
if (firstPageInPager > 1) {
|
|
107
|
+
ellipses.previous = true;
|
|
108
|
+
}
|
|
109
|
+
if (lastPageInPager < totalPages.value) {
|
|
110
|
+
ellipses.next = true;
|
|
111
|
+
}
|
|
112
|
+
return ellipses;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
currentPage,
|
|
117
|
+
totalPages,
|
|
118
|
+
paginatedItems,
|
|
119
|
+
pagerItems,
|
|
120
|
+
pagerEllipses
|
|
121
|
+
};
|
|
122
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -21,7 +21,12 @@ const defaults = {
|
|
|
21
21
|
resizeVertical: "fas fa-grip-lines",
|
|
22
22
|
resizeBoth: "fas fa-grip",
|
|
23
23
|
ellipsis: "fas fa-ellipsis",
|
|
24
|
-
pathSeparator: "fas fa-chevron-right"
|
|
24
|
+
pathSeparator: "fas fa-chevron-right",
|
|
25
|
+
image: "fas fa-image",
|
|
26
|
+
file: "fas fa-file",
|
|
27
|
+
next: "fas fa-chevron-left",
|
|
28
|
+
previous: "fas fa-chevron-right",
|
|
29
|
+
dropdownExpand: "fas fa-caret-down"
|
|
25
30
|
}
|
|
26
31
|
};
|
|
27
32
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
type="button"
|
|
5
5
|
ref="trigger"
|
|
6
6
|
@click="toggle"
|
|
7
|
+
:id="triggerId"
|
|
7
8
|
:disabled="disabled"
|
|
8
9
|
:class="[
|
|
9
10
|
{ [activeClass] : isOpen },
|
|
@@ -14,7 +15,9 @@
|
|
|
14
15
|
:aria-label="triggerAlt"
|
|
15
16
|
v-ulu-tooltip="tooltip ? tooltip : null"
|
|
16
17
|
>
|
|
17
|
-
<slot name="trigger" :isOpen="isOpen"
|
|
18
|
+
<slot name="trigger" :isOpen="isOpen" :toggle="toggle" :close="close">
|
|
19
|
+
{{ triggerText }}
|
|
20
|
+
</slot>
|
|
18
21
|
</button>
|
|
19
22
|
<span
|
|
20
23
|
class="popover"
|
|
@@ -27,7 +30,7 @@
|
|
|
27
30
|
},
|
|
28
31
|
classes.content,
|
|
29
32
|
]"
|
|
30
|
-
:aria-
|
|
33
|
+
:aria-labelledby="triggerId"
|
|
31
34
|
:id="id"
|
|
32
35
|
:style="floatingStyles"
|
|
33
36
|
:data-placement="placement"
|
|
@@ -35,7 +38,7 @@
|
|
|
35
38
|
tabindex="-1"
|
|
36
39
|
>
|
|
37
40
|
<span class="popover__inner">
|
|
38
|
-
<slot
|
|
41
|
+
<slot :isOpen="isOpen" :toggle="toggle" :close="close"/>
|
|
39
42
|
</span>
|
|
40
43
|
<span v-if="$slots.footer" class="popover__footer">
|
|
41
44
|
<slot name="footer" :close="close"/>
|
|
@@ -65,6 +68,7 @@
|
|
|
65
68
|
|
|
66
69
|
const emit = defineEmits(["toggle"]);
|
|
67
70
|
const props = defineProps({
|
|
71
|
+
triggerText: String,
|
|
68
72
|
triggerAlt: String,
|
|
69
73
|
disabled: Boolean,
|
|
70
74
|
tooltip: String,
|
|
@@ -95,6 +99,7 @@
|
|
|
95
99
|
});
|
|
96
100
|
|
|
97
101
|
const id = newUid();
|
|
102
|
+
const triggerId = newUid();
|
|
98
103
|
const config = Object.assign({}, defaults.popover, props.config);
|
|
99
104
|
const isOpen = ref(props.startOpen || false);
|
|
100
105
|
const trigger = ref(null);
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
>
|
|
14
14
|
<div v-if="toast.icon || $slots.icon" class="toast__icon" :class="classes.icon">
|
|
15
15
|
<slot name="icon" :toast="toast">
|
|
16
|
-
<
|
|
16
|
+
<UluIcon v-if="toast.icon" :icon="toast.icon"/>
|
|
17
17
|
</slot>
|
|
18
18
|
</div>
|
|
19
19
|
<div class="toast__content" :class="classes.content">
|