@energie360/ui-library 0.1.21 → 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.
@@ -4,83 +4,77 @@
4
4
  container: card-highlight / inline-size;
5
5
  height: 100%;
6
6
 
7
- @container card-highlight (width >= 900px) {
7
+ @container (width <= 650px) {
8
+ .card-highlight {
9
+ display: flex;
10
+ flex-direction: column;
11
+ }
12
+
8
13
  .card-highlight__image-col {
9
- padding-right: var(--e-space-20);
14
+ justify-content: center;
15
+ }
16
+
17
+ .card-highlight__chip {
18
+ // Forces child element (<UChip>) to be full-width.
19
+ display: grid;
20
+ width: 100%;
10
21
  }
11
22
  }
12
23
  }
13
24
 
14
25
  .card-highlight {
15
26
  position: relative;
16
- display: flex;
27
+ display: grid;
28
+ grid-template-columns: 1fr auto;
29
+ grid-template-rows: 1fr auto;
30
+ grid-column-gap: var(--e-space-4);
31
+ grid-row-gap: var(--e-space-4);
32
+ padding: var(--e-space-6);
17
33
  border: 1px solid var(--e-c-primary-01-100);
18
34
  border-radius: var(--e-brd-radius-2);
19
35
  background-color: var(--e-c-primary-01-50);
20
- padding: var(--e-space-6);
21
- gap: var(--e-space-4);
22
36
  height: 100%;
23
- justify-content: space-between;
37
+ }
24
38
 
25
- @include a.bp(m) {
26
- padding: var(--e-space-5);
27
- }
39
+ .card-highlight__title {
40
+ @include a.type(300, strong);
28
41
 
29
- @include a.bp(s) {
30
- padding: var(--e-space-4);
31
- }
42
+ grid-area: 1 / 1 / 2 / 2;
43
+ color: var(--e-c-secondary-01-950);
44
+ }
45
+
46
+ .card-highlight__image-col {
47
+ grid-area: 1 / 2 / 3 / 3;
48
+ display: flex;
49
+ align-items: center;
32
50
 
33
- @include a.bp(m) {
34
- flex-direction: column;
35
- align-items: flex-start;
51
+ img {
52
+ max-height: a.rem(225);
36
53
  }
37
54
  }
38
55
 
56
+ .card-highlight__content-col {
57
+ grid-area: 2 / 1 / 3 / 2;
58
+ }
59
+
39
60
  .card-highlight__content-col {
40
61
  display: flex;
41
62
  flex-direction: column;
42
63
  align-items: flex-start;
43
64
  row-gap: var(--e-space-4);
44
-
45
- @include a.bp(m) {
46
- width: calc(100% - 40px);
47
- }
48
- }
49
-
50
- .card-highlight__title {
51
- @include a.type(300, strong);
52
-
53
- color: var(--e-c-secondary-01-950);
54
65
  }
55
66
 
56
67
  .card-highlight__text {
57
68
  @include a.type(200);
58
69
 
59
- margin-top: auto;
60
70
  color: var(--e-c-secondary-01-900);
61
71
  }
62
72
 
63
- .card-highlight__image-col {
64
- flex: 1 1 auto;
65
- max-width: 60%;
66
- align-self: center;
67
- display: flex;
68
- justify-content: flex-end;
69
-
70
- img {
71
- max-height: a.rem(225);
72
- }
73
-
74
- @include a.bp(m) {
75
- max-width: none;
76
- }
77
- }
78
-
79
73
  .card-highlight__badge-icon {
80
74
  position: absolute;
81
75
  padding: 5px;
82
- top: var(--e-space-4);
83
- right: var(--e-space-4);
76
+ top: var(--e-space-5);
77
+ right: var(--e-space-5);
84
78
  background-color: var(--e-c-mono-00);
85
79
  color: var(--e-c-secondary-01-950);
86
80
  border-radius: 100%;
@@ -1,12 +1,15 @@
1
1
  <script setup lang="ts">
2
+ import { Chip } from '../chip/u-chip.vue'
2
3
  import { Image } from '../../elements/types'
3
4
  import { UIcon } from '../../elements'
5
+ import { UChip } from '../'
4
6
 
5
7
  interface Props {
6
8
  title?: string
7
9
  text?: string
8
10
  image?: Image
9
11
  badgeIcon?: string
12
+ chip?: Chip
10
13
  }
11
14
 
12
15
  defineProps<Props>()
@@ -15,22 +18,26 @@ defineProps<Props>()
15
18
  <template>
16
19
  <div class="card-highlight-container">
17
20
  <div class="card-highlight">
18
- <div class="card-highlight__content-col">
19
- <h3 v-if="$slots.title || title" class="card-highlight__title">
20
- <slot name="title">{{ title }}</slot>
21
- </h3>
21
+ <h3 v-if="$slots.title || title" class="card-highlight__title">
22
+ <slot name="title">{{ title }}</slot>
23
+ </h3>
24
+
25
+ <div v-if="$slots.image || image" class="card-highlight__image-col" aria-hidden="true">
26
+ <slot name="image">
27
+ <img v-bind="image" />
28
+ </slot>
29
+ </div>
22
30
 
31
+ <div class="card-highlight__content-col">
23
32
  <div v-if="$slots.text || text" class="card-highlight__text">
24
33
  <slot name="text">
25
34
  <div v-html="text"></div>
26
35
  </slot>
27
36
  </div>
28
- </div>
29
37
 
30
- <div v-if="$slots.image || image" class="card-highlight__image-col">
31
- <div>
32
- <slot name="image">
33
- <img v-bind="image" />
38
+ <div v-if="$slots.info || chip" class="card-highlight__chip">
39
+ <slot name="info">
40
+ <UChip v-bind="chip"></UChip>
34
41
  </slot>
35
42
  </div>
36
43
  </div>
@@ -4,8 +4,8 @@
4
4
  background-color: var(--e-c-mono-50);
5
5
  border: 1px solid var(--e-c-mono-200);
6
6
  border-radius: var(--e-brd-radius-2);
7
- padding: var(--e-space-1_5) var(--e-space-2);
8
- display: flex;
7
+ padding: var(--e-space-1_5) var(--e-space-3);
8
+ display: inline-flex;
9
9
  align-items: center;
10
10
  column-gap: var(--e-space-1);
11
11
  }
@@ -1,13 +1,13 @@
1
1
  <script setup lang="ts">
2
2
  import { UIcon } from '../../elements'
3
3
 
4
- interface Props {
4
+ export interface Chip {
5
5
  caption?: string
6
6
  label?: string
7
7
  icon?: string
8
8
  }
9
9
 
10
- defineProps<Props>()
10
+ defineProps<Chip>()
11
11
  </script>
12
12
 
13
13
  <template>
@@ -6,9 +6,9 @@
6
6
  border-radius: var(--e-brd-radius-2);
7
7
  }
8
8
 
9
- .data-card__main {
10
- display: flex;
11
- column-gap: var(--e-space-2);
9
+ .data-card__aside-column {
10
+ float: right;
11
+ margin-left: var(--e-space-2);
12
12
  }
13
13
 
14
14
  .data-card__list {
@@ -15,6 +15,10 @@ defineProps<Props>()
15
15
  <template>
16
16
  <div class="data-card">
17
17
  <div class="data-card__main">
18
+ <div v-if="$slots.aside" class="data-card__aside-column">
19
+ <slot name="aside" />
20
+ </div>
21
+
18
22
  <dl class="data-card__list">
19
23
  <slot>
20
24
  <div v-for="(item, idx) in data" :key="idx">
@@ -23,10 +27,6 @@ defineProps<Props>()
23
27
  </div>
24
28
  </slot>
25
29
  </dl>
26
-
27
- <div v-if="$slots.aside" class="data-card__aside-column">
28
- <slot name="aside" />
29
- </div>
30
30
  </div>
31
31
 
32
32
  <div v-if="$slots.ctas" class="data-card__ctas">
@@ -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'
@@ -1,4 +1,4 @@
1
- @use '../../base/abstracts/' as a;
1
+ @use '../../base/abstracts' as a;
2
2
 
3
3
  @keyframes fade-in-label {
4
4
  0% {
@@ -83,6 +83,10 @@
83
83
 
84
84
  @include a.type(200, strong);
85
85
 
86
+ @include a.bp(lg) {
87
+ width: 100%;
88
+ }
89
+
86
90
  > * {
87
91
  flex: 0 0 auto;
88
92
  }
@@ -154,6 +158,10 @@
154
158
  width: var(--label-width);
155
159
  text-overflow: ellipsis;
156
160
  overflow: hidden;
161
+
162
+ @include a.bp(lg) {
163
+ width: calc(100% - 24px - 8px);
164
+ }
157
165
  }
158
166
 
159
167
  // Animation
@@ -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>
@@ -14,13 +14,21 @@ interface Props {
14
14
  }
15
15
 
16
16
  defineProps<Props>()
17
+
18
+ const isColorKeyword = (str) => Object.values(TableCellIconColor).includes(str)
17
19
  </script>
18
20
 
19
21
  <template>
20
22
  <div class="cell-icon-group">
21
23
  <template v-for="(item, idx) in icons" :key="idx">
22
24
  <UTooltip :title="item.title">
23
- <UIcon :name="item.icon" :class="[`icon-color-${item.color}`]"></UIcon>
25
+ <UIcon
26
+ :name="item.icon"
27
+ :class="{ [`icon-color-${item.color}`]: isColorKeyword(item.color) }"
28
+ :style="{
29
+ color: !isColorKeyword(item.color) ? item.color : false,
30
+ }"
31
+ ></UIcon>
24
32
  </UTooltip>
25
33
  </template>
26
34
  </div>
@@ -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">
@@ -1,25 +1,36 @@
1
1
  <script setup lang="ts">
2
- // import { Image } from '../../elements/types'
2
+ import { Image } from '../../elements/types'
3
3
 
4
4
  interface Props {
5
5
  text?: string
6
- // image: Image
6
+ image?: Image
7
7
  }
8
8
  defineProps<Props>()
9
9
  </script>
10
10
 
11
11
  <template>
12
- <div class="welcome-container">
13
- <div class="welcome-content">
14
- <slot>
15
- <h2 v-if="text">{{ text }}</h2>
12
+ <div class="welcome">
13
+ <div class="welcome__content">
14
+ <h2>
15
+ <slot>{{ text }}</slot>
16
+ </h2>
17
+ </div>
18
+
19
+ <div v-if="$slots.image || image" class="welcome__image" aria-hidden="true">
20
+ <slot name="image">
21
+ <img :src="image.src" :alt="image.alt" />
16
22
  </slot>
17
23
  </div>
18
- <!-- TODO: Add image whenever is ready -->
19
- <!-- <div class="welcome-image">
20
- <img src="path/to/image.jpg" alt="Welcome Image" />
21
- </div> -->
22
24
  </div>
23
25
  </template>
24
26
 
25
27
  <style scoped lang="scss" src="./welcome.scss"></style>
28
+ <style scoped lang="scss">
29
+ .welcome__image {
30
+ :slotted(img) {
31
+ height: 100%;
32
+ width: 100%;
33
+ object-fit: cover;
34
+ }
35
+ }
36
+ </style>
@@ -1,25 +1,48 @@
1
- @use '../../base/abstracts/' as a;
1
+ @use '../../base/abstracts' as a;
2
2
 
3
- .welcome-container {
3
+ .welcome {
4
4
  @include a.type(500, strong);
5
5
 
6
- background-color: var(--e-c-primary-01-100);
7
- border-radius: var(--e-brd-radius-2);
8
- color: var(--e-c-primary-01-950);
6
+ position: relative;
7
+ height: a.rem(94);
9
8
  display: flex;
10
- justify-content: space-between;
11
- padding: var(--e-space-8) var(--e-space-6);
9
+ align-items: center;
10
+ padding: 0 var(--e-space-6);
11
+ background-color: var(--e-c-primary-01-50);
12
+ border: 1px solid var(--e-c-primary-01-100);
13
+ border-radius: var(--e-brd-radius-2);
14
+ color: var(--e-c-secondary-01-950);
15
+ overflow: hidden;
12
16
 
13
- @include a.bp(m) {
17
+ @include a.bp(lg) {
18
+ min-height: a.rem(86);
19
+ height: auto;
14
20
  padding: var(--e-space-3) var(--e-space-4);
15
21
  }
16
22
  }
17
23
 
18
- // .welcome-content {
24
+ .welcome__content {
25
+ max-width: calc(100% - #{a.rem(340)});
26
+
27
+ @include a.bp(lg) {
28
+ max-width: calc(100% - #{a.rem(80)});
29
+ }
30
+ }
19
31
 
20
- // }
32
+ .welcome__image {
33
+ position: absolute;
34
+ top: 0;
35
+ right: 0;
36
+ height: 100%;
37
+ aspect-ratio: 10/3;
21
38
 
22
- // TODO: Add styles for the welcome image whenever is ready
23
- // .welcome-image {
39
+ img {
40
+ height: 100%;
41
+ width: 100%;
42
+ object-fit: cover;
43
+ }
24
44
 
25
- // }
45
+ @include a.bp(lg) {
46
+ display: none;
47
+ }
48
+ }
@@ -6,16 +6,14 @@
6
6
  text-decoration: none;
7
7
  grid-gap: var(--e-space-1);
8
8
 
9
- .icon,
10
- e-icon {
9
+ .icon {
11
10
  --icon-fill-color: var(--e-c-primary-01-700);
12
11
  }
13
12
 
14
13
  &:hover {
15
14
  color: var(--e-c-secondary-01-900);
16
15
 
17
- .icon,
18
- e-icon {
16
+ .icon {
19
17
  --icon-fill-color: var(--e-c-secondary-01-900);
20
18
  }
21
19
  }
@@ -23,8 +21,7 @@
23
21
  &:active {
24
22
  color: var(--e-c-secondary-01-700);
25
23
 
26
- .icon,
27
- e-icon {
24
+ .icon {
28
25
  --icon-fill-color: var(--e-c-secondary-01-700);
29
26
  }
30
27
  }
@@ -34,15 +31,15 @@
34
31
  background: none;
35
32
  color: var(--e-c-mono-500);
36
33
 
37
- .icon,
38
- e-icon {
34
+ .icon {
39
35
  --icon-fill-color: var(--e-c-mono-500);
40
36
  }
41
37
  }
42
38
 
43
39
  &.loading {
44
- .loader,
45
- e-loader {
40
+ pointer-events: none;
41
+
42
+ .loader {
46
43
  --dot-color: var(--e-c-secondary-01-700);
47
44
  }
48
45
  }
@@ -93,6 +93,4 @@ const buttonTag = computed(() => (asSpan ? 'span' : 'button'))
93
93
  </template>
94
94
  </template>
95
95
 
96
- <style lang="scss" scoped>
97
- @use './button.scss';
98
- </style>
96
+ <style lang="scss" scoped src="./button.scss"></style>
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>
@@ -29,16 +29,17 @@ const {
29
29
  const slots = useSlots()
30
30
  const model = defineModel<string>()
31
31
  const emits = defineEmits<{
32
- input: [value: string | null]
32
+ input: [value: string | null] // TODO: Could be obsolete because we get value via model.
33
33
  focus: [target: FocusEvent['target']]
34
34
  focusout: [target: FocusEvent['target']]
35
35
  }>()
36
36
 
37
+ // TODO: Do we need a ref to input? Value is currently written/read via model.
37
38
  const input = useTemplateRef('input')
38
39
 
39
40
  const isFocused = ref(false)
40
41
  const isHovering = ref(false)
41
- const hasValue = ref(false) // only needed for input type 'search'
42
+ const hasValue = ref(!!model.value)
42
43
  const forceFloatLabel = ref(type === TextFieldTypes.date) // input-type 'date' doesn't support placeholder.
43
44
 
44
45
  const spacer = '.&nbsp;'
@@ -60,7 +61,6 @@ const onBlur = () => {
60
61
  }
61
62
 
62
63
  const onInput = () => {
63
- hasValue.value = !!input.value && input.value.value !== ''
64
64
  emits('input', input.value?.value ?? null)
65
65
  }
66
66
 
@@ -111,6 +111,10 @@ watch(
111
111
  }
112
112
  },
113
113
  )
114
+
115
+ watch(model, () => {
116
+ hasValue.value = !!model.value
117
+ })
114
118
  </script>
115
119
 
116
120
  <template>
@@ -12,10 +12,12 @@ interface Props {
12
12
  contentImage?: Image
13
13
  closeBtnLabel?: string
14
14
  autoHeight?: boolean
15
+ modal?: boolean
15
16
  mobileDialogStyle?: 'modal' | 'slideout'
16
17
  }
17
18
 
18
19
  const {
20
+ modal = false,
19
21
  title = '',
20
22
  text = '',
21
23
  headerImage = undefined,
@@ -59,10 +61,12 @@ watch(visible, (newV) => {
59
61
  allowArrowLeftRight: true,
60
62
  })
61
63
 
62
- requestAnimationFrame(() => {
63
- window.addEventListener('keydown', onKeydown)
64
- document.addEventListener('click', onDocumentClick)
65
- })
64
+ if (!modal) {
65
+ requestAnimationFrame(() => {
66
+ window.addEventListener('keydown', onKeydown)
67
+ document.addEventListener('click', onDocumentClick)
68
+ })
69
+ }
66
70
  } else {
67
71
  focusTrapInstance.release()
68
72
 
@@ -84,7 +88,7 @@ watch(visible, (newV) => {
84
88
  'dialog-container',
85
89
  mobileDialogStyle,
86
90
  {
87
- 'has-header-image': !!slots['header-image'] || headerImage,
91
+ 'has-header-image': !!slots['header-image'] || (headerImage?.src && headerImage?.alt),
88
92
  'has-content-image': !!slots['content-image'] || contentImage,
89
93
  },
90
94
  ]"
@@ -124,7 +128,7 @@ watch(visible, (newV) => {
124
128
 
125
129
  <div class="cta-container">
126
130
  <slot name="cta"></slot>
127
- <UButton @click="visible = false">{{ closeBtnLabel }}</UButton>
131
+ <UButton @click="visible = false">{{ closeBtnLabel || getTranslation('close') }}</UButton>
128
132
  </div>
129
133
  </div>
130
134
  </dialog>
package/modules/index.js CHANGED
@@ -5,8 +5,7 @@ export { default as UNavigationToolbarTop } from './navigation-toolbar-top/u-nav
5
5
  export { default as UNavigationToolbarSide } from './navigation-toolbar-side/u-navigation-toolbar-side.vue'
6
6
  export { default as UNavigationPanel } from './navigation-panel/u-navigation-panel.vue'
7
7
  export { default as UProgressIndicator } from './progress-indicator/u-progress-indicator.vue'
8
- export { default as UToast } from './toast/u-toast.vue'
9
- export { toast } from './toast/useToast.ts'
8
+ export { default as UToast, toast } from './toast/u-toast.vue'
10
9
  export { default as UDialog } from './dialog/u-dialog.vue'
11
10
  export { default as USearchFilter } from './search-filter/u-search-filter.vue'
12
11
  export { default as UContentTitle } from './content-title/u-content-title.vue'
@@ -61,5 +61,16 @@
61
61
  @include a.type(200, strong);
62
62
 
63
63
  margin-bottom: var(--e-space-4);
64
+ white-space: nowrap;
65
+ overflow: hidden;
66
+ width: 100%;
67
+ text-overflow: ellipsis;
68
+
69
+ // This is a bit experimental.
70
+ // Because we accept here an HTML string, which can contain <div> or <p>, we'll 'ignore' these tags.
71
+ // This way the text-overflow: ellipsis works as intended.
72
+ * {
73
+ display: contents;
74
+ }
64
75
  }
65
76
  }
@@ -1,3 +1,9 @@
1
+ <script lang="ts">
2
+ import { toast } from './useToast'
3
+
4
+ export { toast }
5
+ </script>
6
+
1
7
  <script setup lang="ts">
2
8
  import UToastMessage from './u-toast-message.vue'
3
9
  import { toasts } from './useToast'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@energie360/ui-library",
3
- "version": "0.1.21",
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
+ }