@energie360/ui-library 0.1.22 → 0.1.23

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.
@@ -62,3 +62,4 @@ export { default as USkeletonLoader } from './skeleton-loader/u-skeleton-loader.
62
62
  export { default as UCardHighlight } from './card-highlight/u-card-highlight.vue'
63
63
  export { default as URating } from './rating/u-rating.vue'
64
64
  export { default as UChip } from './chip/u-chip.vue'
65
+ export { default as USlider } from './slider/u-slider.vue'
@@ -0,0 +1,248 @@
1
+ @use '../../base/abstracts' as a;
2
+
3
+ @mixin thumb-reset {
4
+ appearance: none;
5
+ border: none;
6
+ height: var(--slider-thumb-height);
7
+ width: var(--slider-thumb-width);
8
+ border-radius: 100%;
9
+ }
10
+
11
+ .slider {
12
+ --slider-height: 60px;
13
+ --slider-width: 100%;
14
+ --slider-track-height: 4px;
15
+ --slider-upper-fill-color: rgb(150 150 150);
16
+ --slider-lower-fill-color: rgb(0 150 0);
17
+ --slider-thumb-height: 60px;
18
+ --slider-thumb-width: 60px;
19
+
20
+ position: relative;
21
+ display: inline-flex;
22
+ align-items: center;
23
+ column-gap: var(--e-space-4);
24
+ width: var(--slider-width);
25
+ height: var(--slider-height);
26
+
27
+ // Style native input
28
+ input::-moz-range-thumb {
29
+ @include thumb-reset;
30
+ }
31
+
32
+ input::-webkit-slider-thumb {
33
+ @include thumb-reset;
34
+ }
35
+
36
+ // States
37
+ &.is-sliding {
38
+ .slider__dot,
39
+ .slider__dot-wrapper {
40
+ pointer-events: none;
41
+ }
42
+ }
43
+
44
+ &.is-sliding.is-touch {
45
+ .slider__thumb {
46
+ transform: translateX(-50%) translateY(-50%);
47
+ }
48
+ }
49
+ }
50
+
51
+ .slider__min-value,
52
+ .slider__max-value {
53
+ @include a.type(200);
54
+
55
+ flex: 0 0 auto;
56
+ }
57
+
58
+ .slider__slider {
59
+ flex: 1 0 auto;
60
+ position: relative;
61
+ height: 100%;
62
+ }
63
+
64
+ .slider__input {
65
+ position: absolute;
66
+ top: 0;
67
+ left: calc(var(--slider-thumb-width) / -2);
68
+ right: calc(var(--slider-thumb-width) / -2);
69
+ height: 100%;
70
+ pointer-events: none;
71
+
72
+ input {
73
+ width: 100%;
74
+ height: 100%;
75
+ }
76
+ }
77
+
78
+ .slider__control {
79
+ width: 100%;
80
+ height: 100%;
81
+ touch-action: none;
82
+
83
+ &::before {
84
+ content: '';
85
+ position: absolute;
86
+ top: calc(50% - var(--slider-track-height) / 2);
87
+ height: var(--slider-track-height);
88
+ width: 100%;
89
+ background-color: var(--e-c-primary-01-100);
90
+ border-radius: var(--e-brd-radius-1);
91
+ }
92
+ }
93
+
94
+ .slider__dot {
95
+ display: flex;
96
+ align-items: center;
97
+ justify-content: center;
98
+ width: 100%;
99
+ height: 100%;
100
+ border-radius: 100%;
101
+ background-color: var(--e-c-mono-00);
102
+ border: 2px solid var(--e-c-primary-01-500);
103
+ pointer-events: auto;
104
+ cursor: pointer;
105
+
106
+ &::before {
107
+ content: '';
108
+ position: absolute;
109
+ display: block;
110
+ width: a.rem(8);
111
+ height: a.rem(8);
112
+ border-radius: 100%;
113
+ background-color: var(--e-c-primary-01-500);
114
+ }
115
+
116
+ &:hover {
117
+ &::before {
118
+ background-color: var(--e-c-primary-01-700);
119
+ }
120
+ }
121
+
122
+ &:active {
123
+ border-color: var(--e-c-primary-01-700);
124
+ }
125
+
126
+ // Dot variants
127
+ &.upper {
128
+ border-color: var(--e-c-mono-200);
129
+
130
+ &::before {
131
+ width: a.rem(4);
132
+ height: a.rem(4);
133
+ }
134
+
135
+ &:hover {
136
+ &::before {
137
+ width: a.rem(8);
138
+ height: a.rem(8);
139
+ }
140
+ }
141
+
142
+ &:active {
143
+ border-color: var(--e-c-primary-01-700);
144
+ }
145
+ }
146
+
147
+ &.highlight {
148
+ border-color: var(--e-c-mono-700);
149
+
150
+ &::before {
151
+ background-color: var(--e-c-mono-700);
152
+ width: a.rem(8);
153
+ height: a.rem(8);
154
+ }
155
+
156
+ &::after {
157
+ content: '';
158
+ position: absolute;
159
+ display: block;
160
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='9' fill='none'%3E%3Cpath fill='%236B6B6B' d='M6.79 7.985a1 1 0 0 1-1.58 0L.256 1.614A1 1 0 0 1 1.045 0h9.91a1 1 0 0 1 .79 1.614L6.789 7.985Z'/%3E%3C/svg%3E");
161
+ width: 12px;
162
+ height: 9px;
163
+ left: 2px;
164
+ top: -12px;
165
+ }
166
+
167
+ &:active,
168
+ &:hover {
169
+ border-color: var(--e-c-primary-01-500);
170
+
171
+ &::before {
172
+ background-color: var(--e-c-primary-01-500);
173
+ width: a.rem(8);
174
+ height: a.rem(8);
175
+ }
176
+
177
+ &::after {
178
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='9' fill='none'%3E%3Cpath fill='%234BA528' d='M6.79 7.985a1 1 0 0 1-1.58 0L.256 1.614A1 1 0 0 1 1.045 0h9.91a1 1 0 0 1 .79 1.614L6.789 7.985Z'/%3E%3C/svg%3E");
179
+ }
180
+ }
181
+ }
182
+ }
183
+
184
+ .slider__dot-wrapper {
185
+ position: absolute;
186
+ display: block;
187
+ top: -6px;
188
+ width: a.rem(16);
189
+ height: a.rem(16);
190
+ transform: translateX(-50%);
191
+ }
192
+
193
+ .slider__tooltip-placeholder {
194
+ display: block;
195
+ width: 12px;
196
+ height: 12px;
197
+ }
198
+
199
+ .slider__thumb {
200
+ display: flex;
201
+ align-items: center;
202
+ justify-content: center;
203
+ position: relative;
204
+ height: var(--slider-thumb-height);
205
+ width: var(--slider-thumb-width);
206
+ background-color: rgba(var(--e-c-primary-01-200-rgb), 0.5);
207
+ border-radius: 100%;
208
+ top: calc(var(--slider-thumb-height) / -2 + (var(--slider-track-height) / 2));
209
+ transform: translateX(-50%);
210
+ transition: transform var(--e-trs-duration-fastest) var(--e-trs-easing-default);
211
+
212
+ &::before {
213
+ content: '';
214
+ position: absolute;
215
+ background-color: var(--e-c-primary-01-700);
216
+ inset: 6px;
217
+ border-radius: 100%;
218
+ }
219
+ }
220
+
221
+ .slider__thumb-value {
222
+ @include a.type(200, strong);
223
+
224
+ position: relative;
225
+ white-space: nowrap;
226
+ color: var(--e-c-mono-00);
227
+ user-select: none;
228
+ }
229
+
230
+ .slider__track {
231
+ top: calc(50% - var(--slider-track-height) / 2);
232
+ position: relative;
233
+ height: var(--slider-track-height);
234
+ }
235
+
236
+ .slider__lower-fill {
237
+ position: absolute;
238
+ top: 0;
239
+ left: 0;
240
+ height: 4px;
241
+ border-top-left-radius: var(--e-brd-radius-1);
242
+ border-bottom-left-radius: var(--e-brd-radius-1);
243
+ background-color: var(--e-c-primary-01-500);
244
+ }
245
+
246
+ .visually-hidden {
247
+ @include a.visually-hidden;
248
+ }
@@ -0,0 +1,163 @@
1
+ <script setup lang="ts">
2
+ import { watch, useId, useTemplateRef, onMounted, ref } from 'vue'
3
+ import { scaleValue } from '../../utils/math/scale-value'
4
+ import { clamp } from '../../utils/math/clamp'
5
+ import { UTooltip } from '../'
6
+
7
+ interface SliderDot {
8
+ value: number
9
+ isHighlighted?: boolean
10
+ }
11
+
12
+ // TODO: I get an error with importing interface RangeSlider and extending it here. No idea why this happens...
13
+ interface Props {
14
+ name: string
15
+ dots?: SliderDot[]
16
+ label: string
17
+ min?: number
18
+ max?: number
19
+ step?: number
20
+ valuePrefix?: string
21
+ valueSuffix?: string
22
+ hideMinMax?: boolean
23
+ }
24
+
25
+ const {
26
+ dots = [],
27
+ min = 0,
28
+ max = 100,
29
+ step = 1,
30
+ valuePrefix = '',
31
+ valueSuffix = '',
32
+ } = defineProps<Props>()
33
+
34
+ const model = defineModel<number>()
35
+ const emits = defineEmits(['change'])
36
+ const id = useId()
37
+ const isSliding = ref(false)
38
+ const isTouch = ref(false)
39
+ const thumbEl = useTemplateRef('thumb')
40
+ const lowerFillEl = useTemplateRef('lower-fill')
41
+ const controlEl = useTemplateRef('control')
42
+
43
+ const getValueString = (v) => `${valuePrefix}${v}${valueSuffix}`
44
+
45
+ const getLeftPosition = (v) => scaleValue(Number(v), Number(min), Number(max), 0, 100, 2)
46
+ const setThumbPosition = () => {
47
+ const position = getLeftPosition(model.value)
48
+
49
+ thumbEl.value.style.left = `${position}%`
50
+ lowerFillEl.value.style.width = `${position}%`
51
+ }
52
+
53
+ const setModelValueFromLeftPosition = (leftPos) => {
54
+ model.value = scaleValue(leftPos, 0, 100, min, max, 0)
55
+ }
56
+
57
+ onMounted(() => {
58
+ setThumbPosition()
59
+ })
60
+
61
+ watch(model, () => {
62
+ setThumbPosition()
63
+ emits('change', model.value)
64
+ })
65
+
66
+ const onPointerdown = (e: PointerEvent) => {
67
+ if (e.target.closest('.slider__dot')) {
68
+ return
69
+ }
70
+
71
+ isSliding.value = true
72
+
73
+ // Get correct left position
74
+ const leftPos = Math.round(
75
+ ((e.clientX - controlEl.value.getBoundingClientRect().left) / controlEl.value.offsetWidth) *
76
+ 100,
77
+ )
78
+
79
+ isTouch.value = e.pointerType !== 'mouse'
80
+
81
+ setModelValueFromLeftPosition(leftPos)
82
+ controlEl.value.addEventListener('pointermove', onPointermove)
83
+ document.addEventListener('pointerup', onPointerup)
84
+ }
85
+ const onPointermove = (e: PointerEvent) => {
86
+ e.preventDefault()
87
+
88
+ // Get correct left position
89
+ let leftPos = Math.round(
90
+ ((e.clientX - controlEl.value.getBoundingClientRect().left) / controlEl.value.offsetWidth) *
91
+ 100,
92
+ )
93
+
94
+ leftPos = clamp(leftPos, 0, 100)
95
+
96
+ setModelValueFromLeftPosition(leftPos)
97
+ }
98
+ const onPointerup = () => {
99
+ isSliding.value = false
100
+
101
+ controlEl.value.removeEventListener('pointermove', onPointermove)
102
+ document.removeEventListener('pointerup', onPointerup)
103
+ }
104
+ </script>
105
+
106
+ <template>
107
+ <div :class="['slider', { 'is-sliding': isSliding, 'is-touch': isTouch }]">
108
+ <label class="visually-hidden" :for="id">{{ label }}</label>
109
+
110
+ <div v-if="!hideMinMax" class="slider__min-value">
111
+ {{ getValueString(min) }}
112
+ </div>
113
+
114
+ <div class="slider__slider">
115
+ <div class="slider__input">
116
+ <input :id v-model="model" :name type="range" :min :max :step @change="setThumbPosition" />
117
+ </div>
118
+
119
+ <div ref="control" class="slider__control" @pointerdown.prevent="onPointerdown">
120
+ <div class="slider__track">
121
+ <div ref="lower-fill" class="slider__lower-fill"></div>
122
+
123
+ <div
124
+ v-for="(dot, idx) in dots"
125
+ :key="idx"
126
+ class="slider__dot-wrapper"
127
+ :style="{ left: `${getLeftPosition(dot.value)}%` }"
128
+ >
129
+ <UTooltip
130
+ :title="getValueString(dot.value)"
131
+ style="--tooltip-min-width: 0"
132
+ :disabled="isSliding"
133
+ :offset="10"
134
+ :offset-x="1"
135
+ >
136
+ <button
137
+ :class="[
138
+ 'slider__dot',
139
+ `${dot.value < model ? 'lower' : 'upper'}`,
140
+ { highlight: dot.isHighlighted },
141
+ ]"
142
+ type="button"
143
+ @click="model = dot.value"
144
+ >
145
+ <span class="visually-hidden">{{ getValueString(dot.value) }}</span>
146
+ </button>
147
+ </UTooltip>
148
+ </div>
149
+
150
+ <button ref="thumb" type="button" class="slider__thumb" tabindex="-1">
151
+ <span class="slider__thumb-value">{{ getValueString(model) }} </span>
152
+ </button>
153
+ </div>
154
+ </div>
155
+ </div>
156
+
157
+ <div v-if="!hideMinMax" class="slider__max-value">
158
+ {{ getValueString(max) }}
159
+ </div>
160
+ </div>
161
+ </template>
162
+
163
+ <style scoped lang="scss" src="./slider.scss"></style>
@@ -1,13 +1,15 @@
1
- @use '../../base/abstracts/' as a;
1
+ @use '../../base/abstracts' as a;
2
2
 
3
3
  .tooltip {
4
+ --tooltip-min-width: 84px;
5
+
4
6
  pointer-events: none;
5
7
  z-index: a.$layer-tooltip;
6
8
  position: absolute;
7
9
  top: 0;
8
10
  left: 0;
9
11
  max-width: a.rem(360);
10
- min-width: a.rem(84);
12
+ min-width: var(--tooltip-min-width);
11
13
  opacity: 0;
12
14
  transform: translateY(-5px);
13
15
  transition:
@@ -5,9 +5,18 @@ import type { PopoverPositionParams } from './popover'
5
5
 
6
6
  interface Props extends PopoverPositionParams {
7
7
  title: string
8
+ disabled?: boolean
9
+ offsetX?: number
8
10
  }
9
11
 
10
- const { placement = 'top-center', offset = 12, viewportPadding = 20 } = defineProps<Props>()
12
+ const {
13
+ placement = 'top-center',
14
+ offset = 12,
15
+ viewportPadding = 20,
16
+ disabled = false,
17
+ // This optional prop is to potentially fix some slightly off calculations.
18
+ offsetX = 0,
19
+ } = defineProps<Props>()
11
20
 
12
21
  const tooltip = ref<HTMLDivElement | null>(null)
13
22
  const root = ref<HTMLSpanElement | null>(null)
@@ -18,10 +27,18 @@ const isTooltipVisible = ref<boolean>(false)
18
27
  // For mobile and touch devices it's th opposite, pointer events will be fired first.
19
28
  // This way we can detect on what device we are and handle the behaviour accordingly.
20
29
  function hideTooltip() {
30
+ if (disabled) {
31
+ return
32
+ }
33
+
21
34
  isTooltipVisible.value = false
22
35
  }
23
36
 
24
37
  function showTooltip() {
38
+ if (disabled) {
39
+ return
40
+ }
41
+
25
42
  updatePositions()
26
43
  isTooltipVisible.value = true
27
44
  }
@@ -40,7 +57,7 @@ function updatePositions(resetPositions = false) {
40
57
  })
41
58
 
42
59
  tooltip.value!.style.top = resetPositions ? '' : `${top}px`
43
- tooltip.value!.style.left = resetPositions ? '' : `${left}px`
60
+ tooltip.value!.style.left = resetPositions ? '' : `${left + offsetX}px`
44
61
  // Fix pointer position if necessary
45
62
  pointer.value!.style.transform =
46
63
  !resetPositions && leftCorrection ? `translateX(${leftCorrection * -1}px)` : ''
@@ -57,6 +74,7 @@ function updatePositions(resetPositions = false) {
57
74
  ref="tooltip"
58
75
  :class="['tooltip', { 'tooltip--open': isTooltipVisible }]"
59
76
  :aria-hidden="!isTooltipVisible"
77
+ v-bind="$attrs"
60
78
  @transitionend="onTransitionend"
61
79
  >
62
80
  <div class="tooltip__content">
package/elements/index.js CHANGED
@@ -22,3 +22,4 @@ export { default as UToggleSwitch } from './toggle-switch/u-toggle-switch.vue'
22
22
  export { default as UCheckbox } from './checkbox/u-checkbox.vue'
23
23
  export { default as USelectTile } from './select-tile/u-select-tile.vue'
24
24
  export { default as USelectTiles } from './select-tiles/u-select-tiles.vue'
25
+ // export { default as URangeSlider } from './range-slider/u-range-slider.vue'
@@ -0,0 +1,138 @@
1
+ <script setup lang="ts">
2
+ // NOTE: URangeSlider as a base widget was a good idea.
3
+ // But at the moment not really usable.
4
+ // Maybe deprecate it.
5
+
6
+ import { useTemplateRef, onMounted, useId, watch } from 'vue'
7
+ import { scaleValue } from '../../utils/math/scale-value'
8
+
9
+ export interface RangeSlider {
10
+ label: string
11
+ min?: number
12
+ max?: number
13
+ }
14
+
15
+ const { min = 0, max = 100 } = defineProps<RangeSlider>()
16
+
17
+ const id = useId()
18
+ const model = defineModel<number>()
19
+ const thumbEl = useTemplateRef('thumb')
20
+ const lowerFillEl = useTemplateRef('lower-fill')
21
+ let thumbWidth
22
+
23
+ const setThumbPosition = () => {
24
+ const position = scaleValue(Number(model.value), Number(min), Number(max), 0, 100, 2)
25
+ thumbEl.value.style.left = `${position}%`
26
+
27
+ lowerFillEl.value.style.width = `calc(${position}% + ${thumbWidth / 2}px)`
28
+ }
29
+
30
+ onMounted(() => {
31
+ // Store thumb width. We'll judt assume that this won't change during runtime.
32
+ thumbWidth = thumbEl.value.getBoundingClientRect().width
33
+
34
+ setThumbPosition()
35
+ })
36
+
37
+ watch(model, () => {
38
+ setThumbPosition()
39
+ })
40
+ </script>
41
+
42
+ <template>
43
+ <div class="range-slider">
44
+ <label class="visually-hidden" :for="id">{{ label }}</label>
45
+ <div class="range-slider__input">
46
+ <input
47
+ :id
48
+ v-model="model"
49
+ type="range"
50
+ :min
51
+ :max
52
+ @input="setThumbPosition"
53
+ @change="setThumbPosition"
54
+ />
55
+ </div>
56
+ <div class="range-slider__slider">
57
+ <div class="range-slider__track">
58
+ <div ref="lower-fill" class="range-slider__lower-fill"></div>
59
+ <div ref="thumb" class="range-slider__thumb"></div>
60
+ </div>
61
+ </div>
62
+ </div>
63
+ </template>
64
+
65
+ <style lang="scss" scoped>
66
+ @use '../../base/abstracts' as a;
67
+
68
+ .range-slider {
69
+ --range-height: 24px;
70
+ --range-width: 100%;
71
+ --range-border-radius: 0;
72
+ --range-upper-fill-color: rgba(150, 150, 150);
73
+ --range-lower-fill-color: rgb(0, 150, 0);
74
+ --range-thumb-height: 24px;
75
+ --range-thumb-width: 12px;
76
+ --range-thumb-border-radius: 0;
77
+ --range-thumb-background-color: rgb(0, 0, 0);
78
+ --range-thumb-transform: none;
79
+ --range-thumb-transition: none;
80
+
81
+ /* "display: contents" would be better, but it still has some accessiblity issues -> https://ericwbailey.website/published/display-contents-considered-harmful/ */
82
+ position: relative;
83
+ display: inline-flex;
84
+ width: var(--range-width);
85
+ height: var(--range-height);
86
+ }
87
+
88
+ .range-slider__input {
89
+ display: flex;
90
+ width: 100%;
91
+
92
+ input {
93
+ width: 100%;
94
+ }
95
+ }
96
+
97
+ .range-slider__slider {
98
+ position: absolute;
99
+ top: 0;
100
+ left: 0;
101
+ width: 100%;
102
+ height: 100%;
103
+ pointer-events: none;
104
+ background-color: var(--range-upper-fill-color);
105
+ border-radius: var(--range-border-radius);
106
+ }
107
+
108
+ .range-slider__thumb {
109
+ position: relative;
110
+ height: var(--range-thumb-height);
111
+ width: var(--range-thumb-width);
112
+ background-color: var(--range-thumb-background-color);
113
+ border-radius: var(--range-thumb-border-radius);
114
+ transform: var(--range-thumb-transform);
115
+ transition: var(--range-thumb-transition);
116
+ }
117
+
118
+ .range-slider__track {
119
+ position: relative;
120
+ width: calc(100% - var(--range-thumb-width));
121
+ height: 100%;
122
+ }
123
+
124
+ .range-slider__lower-fill {
125
+ position: absolute;
126
+ top: 0;
127
+ left: 0;
128
+ height: 100%;
129
+ border-top-left-radius: var(--range-border-radius);
130
+ border-bottom-left-radius: var(--range-border-radius);
131
+ background-color: var(--range-lower-fill-color);
132
+ min-width: var(--range-thumb-width);
133
+ }
134
+
135
+ .visually-hidden {
136
+ @include a.visually-hidden;
137
+ }
138
+ </style>
@@ -88,7 +88,7 @@ watch(visible, (newV) => {
88
88
  'dialog-container',
89
89
  mobileDialogStyle,
90
90
  {
91
- 'has-header-image': !!slots['header-image'] || headerImage,
91
+ 'has-header-image': !!slots['header-image'] || (headerImage?.src && headerImage?.alt),
92
92
  'has-content-image': !!slots['content-image'] || contentImage,
93
93
  },
94
94
  ]"
@@ -128,7 +128,7 @@ watch(visible, (newV) => {
128
128
 
129
129
  <div class="cta-container">
130
130
  <slot name="cta"></slot>
131
- <UButton @click="visible = false">{{ closeBtnLabel }}</UButton>
131
+ <UButton @click="visible = false">{{ closeBtnLabel || getTranslation('close') }}</UButton>
132
132
  </div>
133
133
  </div>
134
134
  </dialog>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@energie360/ui-library",
3
- "version": "0.1.22",
3
+ "version": "0.1.23",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,13 @@
1
+ export const scaleValue = (
2
+ srcValue,
3
+ srcRangeMin,
4
+ srcRangeMax,
5
+ targetRangeMin,
6
+ targetRangeMax,
7
+ round = 0,
8
+ ) => {
9
+ const dec = Math.pow(10, round)
10
+ const scale = (targetRangeMax - targetRangeMin) / (srcRangeMax - srcRangeMin)
11
+
12
+ return Math.round((targetRangeMin + (srcValue - srcRangeMin) * scale) * dec) / dec
13
+ }