@energie360/ui-library 0.1.17 → 0.1.18

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 (85) hide show
  1. package/components/badge/badge.scss +56 -0
  2. package/components/badge/u-badge.vue +47 -0
  3. package/components/card-contact/card-contact.scss +39 -0
  4. package/components/card-contact/u-card-contact.vue +44 -0
  5. package/components/card-cta-bar/card-cta-bar.scss +4 -0
  6. package/components/card-cta-bar/u-card-cta-bar.vue +24 -0
  7. package/components/card-cta-header/u-card-cta-header.vue +10 -7
  8. package/components/card-footer/u-card-footer.vue +5 -3
  9. package/components/card-group/u-card-group.vue +1 -1
  10. package/components/card-header/card-header.scss +29 -4
  11. package/components/card-header/u-card-header.vue +22 -3
  12. package/components/card-highlight/card-highlight.scss +70 -0
  13. package/components/card-highlight/u-card-highlight.vue +41 -0
  14. package/components/card-price-list/card-price-list.scss +39 -0
  15. package/components/card-price-list/u-card-price-list.vue +37 -0
  16. package/components/card-section/card-section.scss +21 -1
  17. package/components/card-section/u-card-section.vue +9 -1
  18. package/components/data-card/data-card.scss +34 -0
  19. package/components/data-card/u-data-card.vue +49 -0
  20. package/components/data-card-group/data-card-group.scss +12 -0
  21. package/components/data-card-group/u-data-card-group.vue +7 -0
  22. package/components/download-list/download-list.scss +58 -0
  23. package/components/download-list/u-download-list.vue +44 -0
  24. package/components/download-list-item/download-list-item.scss +267 -0
  25. package/components/download-list-item/u-download-list-item.vue +65 -0
  26. package/components/file-upload/file-list.scss +68 -0
  27. package/components/file-upload/file-upload.scss +119 -0
  28. package/components/file-upload/u-file-list.vue +55 -0
  29. package/components/file-upload/u-file-upload.vue +220 -0
  30. package/components/hint/hint.scss +67 -6
  31. package/components/hint/u-hint.vue +11 -1
  32. package/components/index.js +12 -0
  33. package/components/progress-avatar/u-progress-avatar.vue +27 -3
  34. package/components/search-group/search-group.scss +59 -0
  35. package/components/search-group/u-search-group.vue +32 -0
  36. package/components/skeleton-loader/skeleton-loader.scss +39 -0
  37. package/components/skeleton-loader/u-skeleton-loader.vue +28 -0
  38. package/components/table/cell-ctas.scss +1 -7
  39. package/components/table/cell-icon-text.scss +15 -4
  40. package/components/table/table-cell.mixins.scss +3 -2
  41. package/components/table/table-cell.scss +5 -0
  42. package/components/table/table-heading.scss +7 -0
  43. package/components/table/u-cell-ctas.vue +15 -6
  44. package/components/table/u-cell-icon-text.vue +13 -5
  45. package/components/table/u-table-cell.vue +3 -1
  46. package/components/table/u-table-heading.vue +2 -1
  47. package/components/tabs/tabs.scss +10 -1
  48. package/components/tabs/u-tabs.vue +64 -25
  49. package/dist/layout/split.css.map +1 -1
  50. package/elements/button/_button-plain-small-spaceless.scss +10 -0
  51. package/elements/button/button.scss +32 -0
  52. package/elements/button/u-button.vue +47 -4
  53. package/elements/form-field/form-field-base.scss +4 -0
  54. package/elements/select/u-select.vue +6 -6
  55. package/elements/text-field/text-field.scss +15 -0
  56. package/elements/text-field/text-field.types.ts +1 -0
  57. package/elements/text-field/u-text-field.vue +27 -6
  58. package/elements/toggle-switch/toggle-switch-small.scss +10 -0
  59. package/elements/toggle-switch/toggle-switch.scss +25 -21
  60. package/elements/toggle-switch/u-toggle-switch.vue +22 -12
  61. package/i18n/i18n.ts +32 -0
  62. package/layout/container/container.scss +18 -0
  63. package/layout/index.js +2 -0
  64. package/layout/portal/portal.scss +35 -7
  65. package/layout/portal/u-portal.vue +33 -4
  66. package/layout/settings/settings.scss +2 -2
  67. package/layout/tile-grid/tile-grid.scss +13 -0
  68. package/layout/tile-grid/tile-item.scss +31 -0
  69. package/layout/tile-grid/u-tile-grid.vue +7 -0
  70. package/layout/tile-grid/u-tile-item.vue +15 -0
  71. package/modules/content-title/content-title.scss +43 -0
  72. package/modules/content-title/u-content-title.vue +19 -0
  73. package/modules/dialog/dialog.scss +7 -3
  74. package/modules/dialog/u-dialog.vue +6 -1
  75. package/modules/footer/footer.scss +8 -1
  76. package/modules/footer/u-footer.vue +1 -1
  77. package/modules/index.js +2 -0
  78. package/modules/search-filter/search-filter.scss +106 -0
  79. package/modules/search-filter/u-search-filter.vue +54 -0
  80. package/package.json +2 -1
  81. package/utils/array/intersect.js +7 -0
  82. package/utils/functions/breakpoint.js +4 -9
  83. package/utils/functions/format-bytes.js +17 -0
  84. package/utils/global/mime-types.js +8 -0
  85. package/utils/translations/translate.js +10 -2
@@ -0,0 +1,55 @@
1
+ <script setup lang="ts">
2
+ import { UIconButton } from '../../elements'
3
+
4
+ interface FileItem {
5
+ filename: string
6
+ lastModified: number
7
+ name: string
8
+ size: string
9
+ ext: string
10
+ error: boolean
11
+ errorMessage?: string
12
+ }
13
+
14
+ interface Props {
15
+ items: FileItem[]
16
+ }
17
+
18
+ defineProps<Props>()
19
+ defineEmits(['remove-error', 'remove-file'])
20
+ </script>
21
+
22
+ <template>
23
+ <TransitionGroup name="list" tag="ul">
24
+ <li v-for="(item, idx) in items" :key="idx" :class="['file-list__item', { error: item.error }]">
25
+ <div class="file-list__item-content">
26
+ <p>
27
+ <span class="file-list__item-name">{{ item.name }}</span>
28
+ <span>{{ item.size }} &middot; {{ item.ext }}</span>
29
+ </p>
30
+
31
+ <p v-if="item.error && item.errorMessage" class="file-list__item-error">
32
+ {{ item.errorMessage }}
33
+ </p>
34
+ </div>
35
+
36
+ <div class="file-list__file-cta">
37
+ <UIconButton
38
+ v-if="item.error"
39
+ variant="plain"
40
+ icon="close"
41
+ @click="$emit('remove-error', item.filename, item.lastModified)"
42
+ />
43
+
44
+ <UIconButton
45
+ v-else
46
+ variant="plain"
47
+ icon="delete"
48
+ @click="$emit('remove-file', item.filename, item.lastModified)"
49
+ />
50
+ </div>
51
+ </li>
52
+ </TransitionGroup>
53
+ </template>
54
+
55
+ <style scoped lang="scss" src="./file-list.scss"></style>
@@ -0,0 +1,220 @@
1
+ <script setup lang="ts">
2
+ import UFileList from './u-file-list.vue'
3
+ import { UButton, UIcon } from '../../elements'
4
+ import { formatBytes } from '../../utils/functions/format-bytes'
5
+ import { getTranslation } from '../../utils/translations/translate'
6
+ import { mimeTypes } from '../../utils/global/mime-types'
7
+ import { intersect } from '../../utils/array/intersect'
8
+ import { ref, computed, useTemplateRef, useId } from 'vue'
9
+
10
+ interface Props {
11
+ accept?: string[]
12
+ maxFileSize?: number
13
+ maxFiles?: number
14
+ }
15
+
16
+ const { accept = [], maxFileSize = 1024 * 1024, maxFiles = 0 } = defineProps<Props>()
17
+
18
+ const getFilename = (filename: string) => filename.substring(0, filename.lastIndexOf('.'))
19
+ const getExtension = (filename: string) => filename.slice(filename.lastIndexOf('.') + 1)
20
+
21
+ const inputId = `file-upload-${useId()}`
22
+ const dragover = ref(false)
23
+ const uploadDisabled = ref(false)
24
+ const maxFileCountError = ref(false)
25
+ const fileExtensions = computed(() => accept.map((ext) => ext.toUpperCase()).join(', '))
26
+ const acceptStr = computed(() => accept.map((ext) => `.${ext}`).join(','))
27
+ const input = useTemplateRef('input')
28
+ const currentFiles = ref([])
29
+ const declinedFiles = ref([])
30
+
31
+ const displayItems = computed(() => {
32
+ return [
33
+ ...currentFiles.value.map((file) => ({
34
+ name: getFilename(file.name),
35
+ size: formatBytes(file.size),
36
+ ext: getExtension(file.name),
37
+ lastModified: file.lastModified,
38
+ filename: file.name,
39
+ error: false,
40
+ })),
41
+ ...declinedFiles.value.map((file) => ({
42
+ name: getFilename(file.name),
43
+ size: formatBytes(file.size),
44
+ ext: getExtension(file.name),
45
+ lastModified: file.lastModified,
46
+ filename: file.name,
47
+ error: true,
48
+ errorMessage:
49
+ file.size > maxFileSize
50
+ ? getTranslation('fileTooLarge')
51
+ : getTranslation('incorrectFileFormat'),
52
+ })),
53
+ ]
54
+ })
55
+
56
+ const arrayToFileList = (files: []): FileList => {
57
+ const dt = new DataTransfer()
58
+
59
+ files.forEach((file) => {
60
+ dt.items.add(file)
61
+ })
62
+
63
+ return dt.files
64
+ }
65
+
66
+ const handleFiles = (files: FileList) => {
67
+ if (!files.length) {
68
+ return
69
+ }
70
+
71
+ const acceptedFiles = []
72
+ for (const file of files) {
73
+ if (
74
+ accept.length === 0 ||
75
+ (intersect(mimeTypes[file.type] || [], accept).length && file.size <= maxFileSize)
76
+ ) {
77
+ acceptedFiles.push(file)
78
+ } else {
79
+ declinedFiles.value.push(file)
80
+ }
81
+ }
82
+
83
+ if (maxFiles && currentFiles.value.length + acceptedFiles.length > maxFiles) {
84
+ maxFileCountError.value = true
85
+ // Too many files are being added.
86
+ return
87
+ }
88
+
89
+ currentFiles.value.push(...acceptedFiles)
90
+ maxFileCountError.value = false
91
+
92
+ input.value.files = arrayToFileList(currentFiles.value)
93
+
94
+ checkMaxFiles()
95
+ }
96
+
97
+ const checkMaxFiles = () => {
98
+ if (maxFiles && currentFiles.value.length === maxFiles) {
99
+ uploadDisabled.value = true
100
+ }
101
+
102
+ if (maxFiles && currentFiles.value.length < maxFiles) {
103
+ uploadDisabled.value = false
104
+ maxFileCountError.value = false
105
+ }
106
+ }
107
+
108
+ // Event handlers
109
+ const onDrop = (e: DragEvent) => {
110
+ if (uploadDisabled.value) {
111
+ return
112
+ }
113
+
114
+ dragover.value = false
115
+
116
+ handleFiles(e.dataTransfer.files)
117
+ }
118
+
119
+ const onAddFiles = () => {
120
+ handleFiles(input.value.files)
121
+ }
122
+
123
+ const onDragenter = () => {
124
+ if (uploadDisabled.value) {
125
+ return
126
+ }
127
+
128
+ dragover.value = true
129
+ }
130
+
131
+ const onDragleave = () => {
132
+ dragover.value = false
133
+ }
134
+
135
+ const onRemoveFile = (filename: string, lastModified: number) => {
136
+ currentFiles.value = currentFiles.value.filter(
137
+ (file) => file.name !== filename || file.lastModified !== lastModified,
138
+ )
139
+
140
+ input.value.files = arrayToFileList(currentFiles.value)
141
+
142
+ checkMaxFiles()
143
+ }
144
+
145
+ const onRemoveError = (filename: string, lastModified: number) => {
146
+ declinedFiles.value = declinedFiles.value.filter(
147
+ (file) => file.name !== filename || file.lastModified !== lastModified,
148
+ )
149
+ }
150
+ </script>
151
+
152
+ <template>
153
+ <div class="file-upload">
154
+ <div
155
+ :class="['file-upload__drop-zone', { dragover, disabled: uploadDisabled }]"
156
+ @drop.prevent="onDrop"
157
+ @dragover.prevent
158
+ @dragenter.prevent.stop="onDragenter"
159
+ @dragleave.prevent.stop="onDragleave"
160
+ >
161
+ <input
162
+ :id="inputId"
163
+ v-bind="$attrs"
164
+ ref="input"
165
+ class="file-upload__control"
166
+ :disabled="uploadDisabled"
167
+ type="file"
168
+ :accept="acceptStr"
169
+ multiple
170
+ @change="onAddFiles"
171
+ />
172
+
173
+ <div v-if="dragover" class="file-upload__drop-zone-inner-drag">
174
+ <img src="/static/ui-assets/images/doc-upload-inverted.svg" alt="" />
175
+ </div>
176
+
177
+ <div class="file-upload__drop-zone-inner">
178
+ <img
179
+ v-if="uploadDisabled"
180
+ class="no-select"
181
+ src="/static/ui-assets/images/doc-upload-disabled.svg"
182
+ alt=""
183
+ />
184
+ <img v-else class="no-select" src="/static/ui-assets/images/doc-upload.svg" alt="" />
185
+ <p class="no-select type-300-strong label">
186
+ {{ getTranslation('dragFileHereOr') }}
187
+ </p>
188
+
189
+ <label :for="inputId">
190
+ <UButton variant="outlined" as-span :disabled="uploadDisabled">
191
+ {{ getTranslation('chooseFile') }}
192
+ </UButton>
193
+ </label>
194
+
195
+ <p class="no-select type-100 size">
196
+ {{ getTranslation('maxSizePerFile', { $SIZE: formatBytes(maxFileSize) }) }}
197
+ <span v-if="accept.length" class="ext">
198
+ <span class="middot">&middot;</span>
199
+ {{ fileExtensions }}
200
+ </span>
201
+ </p>
202
+ </div>
203
+ </div>
204
+
205
+ <div v-if="displayItems.length" class="file-upload__file-list">
206
+ <UFileList
207
+ v-if="displayItems.length"
208
+ :items="displayItems"
209
+ @remove-file="onRemoveFile"
210
+ @remove-error="onRemoveError"
211
+ />
212
+ </div>
213
+
214
+ <div v-if="maxFileCountError" class="file-upload__max-files-error">
215
+ <UIcon name="report" /> {{ getTranslation('maxFileCountAllowed', { $COUNT: 4 }) }}
216
+ </div>
217
+ </div>
218
+ </template>
219
+
220
+ <style scoped lang="scss" src="./file-upload.scss"></style>
@@ -1,3 +1,5 @@
1
+ // TODO: Styles are a total mess. Clean up!
2
+
1
3
  @use '../../base/abstracts' as a;
2
4
  @use '../../elements/text-link/text-link.scss' as t;
3
5
 
@@ -10,6 +12,14 @@
10
12
  background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 15c-.55 0-1-.45-1-1v-4c0-.55.45-1 1-1s1 .45 1 1v4c0 .55-.45 1-1 1Zm1-8h-2V7h2v2Z' fill='%230096DC'/%3E%3C/svg%3E");
11
13
  }
12
14
 
15
+ .hint__richtext {
16
+ margin-left: var(--e-space-9);
17
+
18
+ @include a.bp(lg) {
19
+ margin-left: 0;
20
+ }
21
+ }
22
+
13
23
  // override some richtext styles.
14
24
  .richtext {
15
25
  a {
@@ -32,6 +42,14 @@
32
42
  background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 21 12 2l11 19H1Zm11-3c.283 0 .52-.096.713-.288A.968.968 0 0 0 13 17a.968.968 0 0 0-.287-.712A.968.968 0 0 0 12 16a.968.968 0 0 0-.713.288A.968.968 0 0 0 11 17c0 .283.096.52.287.712.192.192.43.288.713.288Zm-1-3h2v-5h-2v5Z' fill='%23FF9800'/%3E%3C/svg%3E");
33
43
  }
34
44
 
45
+ .hint__richtext {
46
+ margin-left: var(--e-space-9);
47
+
48
+ @include a.bp(lg) {
49
+ margin-left: 0;
50
+ }
51
+ }
52
+
35
53
  // override some richtext styles.
36
54
  .richtext {
37
55
  a {
@@ -43,6 +61,13 @@
43
61
  }
44
62
  }
45
63
  }
64
+
65
+ .hint__link {
66
+ &:active,
67
+ &:hover {
68
+ text-decoration-color: var(--e-c-signal-02-100);
69
+ }
70
+ }
46
71
  }
47
72
 
48
73
  @mixin type-legal {
@@ -65,6 +90,13 @@
65
90
  margin-left: 0;
66
91
  }
67
92
 
93
+ .hint__link {
94
+ &:active,
95
+ &:hover {
96
+ text-decoration-color: var(--e-c-mono-100);
97
+ }
98
+ }
99
+
68
100
  @include a.bp(lg) {
69
101
  margin-top: calc(4px + 4px + 24px);
70
102
  border: 1px solid var(--e-c-mono-500);
@@ -80,6 +112,24 @@
80
112
  }
81
113
  }
82
114
 
115
+ .hint__link {
116
+ @include t.text-link;
117
+
118
+ display: inline-block;
119
+ color: inherit;
120
+ margin-left: var(--e-space-2);
121
+
122
+ &:active,
123
+ &:hover {
124
+ text-decoration-color: var(--e-c-secondary-05-200);
125
+ }
126
+
127
+ @include a.bp(lg) {
128
+ display: block;
129
+ margin-left: 0;
130
+ }
131
+ }
132
+
83
133
  // Default type
84
134
  @include type-neutral;
85
135
 
@@ -87,6 +137,16 @@
87
137
  padding: var(--e-space-5) var(--e-space-4);
88
138
  border-radius: var(--e-brd-radius-1);
89
139
 
140
+ .hint__label {
141
+ + .hint__richtext {
142
+ margin-top: var(--e-space-1);
143
+
144
+ @include a.bp(lg) {
145
+ margin-top: var(--e-space-2);
146
+ }
147
+ }
148
+ }
149
+
90
150
  &::before {
91
151
  content: '';
92
152
  position: absolute;
@@ -96,15 +156,16 @@
96
156
  height: a.rem(24);
97
157
  }
98
158
 
99
- > p,
100
- .richtext {
101
- margin-left: var(--e-space-9);
102
- }
103
-
104
159
  > p {
105
160
  @include a.type(200, strong);
106
161
 
107
- margin-bottom: var(--e-space-1);
162
+ margin-left: var(--e-space-9);
163
+
164
+ // margin-bottom: var(--e-space-1);
165
+
166
+ // @include a.bp(lg) {
167
+ // margin-bottom: var(--e-space-2);
168
+ // }
108
169
  }
109
170
 
110
171
  @include a.bp(lg) {
@@ -1,22 +1,32 @@
1
1
  <script setup lang="ts">
2
+ import { useSlots } from 'vue'
2
3
  import { URichtext } from '../'
4
+ import { Cta } from '../../elements/types'
3
5
 
4
6
  interface Props {
5
7
  type?: 'neutral' | 'warning' | 'legal'
6
8
  label?: string
9
+ link?: Cta
7
10
  text?: string
8
11
  }
9
12
 
10
13
  const { type = 'neutral' } = defineProps<Props>()
14
+ const slots = useSlots()
11
15
  </script>
12
16
 
13
17
  <template>
14
18
  <div :class="['hint', `hint--${type}`]">
15
19
  <p class="hint__label">
16
20
  <slot name="label">{{ label }}</slot>
21
+
22
+ <a v-if="link" :href="link.href" :target="link.target" class="hint__link">
23
+ {{ link.label }}
24
+ </a>
17
25
  </p>
18
26
 
19
- <URichtext class="hint__richtext" small :text><slot></slot></URichtext>
27
+ <URichtext v-if="text || $slots.default" class="hint__richtext" small :text>
28
+ <slot></slot>
29
+ </URichtext>
20
30
  </div>
21
31
  </template>
22
32
 
@@ -15,6 +15,9 @@ export { default as UCardSection } from './card-section/u-card-section.vue'
15
15
  export { default as UCardTable } from './card-table/u-card-table.vue'
16
16
  export { default as UCardToggleSwitches } from './card-toggle-switches/u-card-toggle-switches.vue'
17
17
  export { default as UCardCtaHeader } from './card-cta-header/u-card-cta-header.vue'
18
+ export { default as UCardCtaBar } from './card-cta-bar/u-card-cta-bar.vue'
19
+ export { default as UCardContact } from './card-contact/u-card-contact.vue'
20
+ export { default as UCardPriceList } from './card-price-list/u-card-price-list.vue'
18
21
 
19
22
  // Collapsible
20
23
  export { default as UCollapsible } from './collapsible/u-collapsible.vue'
@@ -52,3 +55,12 @@ export { default as UWelcome } from './welcome/u-welcome.vue'
52
55
  export { default as UHint } from './hint/u-hint.vue'
53
56
  export { default as UNavigationPanelTile } from './navigation-panel-tile/u-navigation-panel-tile.vue'
54
57
  export { default as UCardInfo } from './card-info/u-card-info.vue'
58
+ export { default as UFileUpload } from './file-upload/u-file-upload.vue'
59
+ export { default as UBadge } from './badge/u-badge.vue'
60
+ export { default as UDownloadListItem } from './download-list-item/u-download-list-item.vue'
61
+ export { default as UDownloadList } from './download-list/u-download-list.vue'
62
+ export { default as UDataCard } from './data-card/u-data-card.vue'
63
+ export { default as UDataCardGroup } from './data-card-group/u-data-card-group.vue'
64
+ export { default as USearchGroup } from './search-group/u-search-group.vue'
65
+ export { default as USkeletonLoader } from './skeleton-loader/u-skeleton-loader.vue'
66
+ export { default as UCardHighlight } from './card-highlight/u-card-highlight.vue'
@@ -1,4 +1,5 @@
1
1
  <script setup lang="ts">
2
+ import { onMounted, useTemplateRef, ref } from 'vue'
2
3
  import { Image } from '../../elements/types'
3
4
  import { UCircularProgress } from '../'
4
5
 
@@ -7,13 +8,36 @@ interface Props {
7
8
  progress: number
8
9
  }
9
10
 
10
- defineProps<Props>()
11
+ const { progress } = defineProps<Props>()
12
+
13
+ const el = useTemplateRef('el')
14
+ const _progress = ref(0)
15
+
16
+ const observer = new IntersectionObserver(
17
+ (entries) => {
18
+ if (entries[0].isIntersecting) {
19
+ observer.disconnect()
20
+
21
+ _progress.value = progress
22
+ }
23
+ },
24
+ {
25
+ rootMargin: '20px',
26
+ threshold: 1,
27
+ },
28
+ )
29
+
30
+ onMounted(() => {
31
+ setTimeout(() => {
32
+ observer.observe(el.value)
33
+ }, 200)
34
+ })
11
35
  </script>
12
36
 
13
37
  <template>
14
- <div class="progress-avatar">
38
+ <div ref="el" class="progress-avatar">
15
39
  <div class="progress-avatar__anim-container">
16
- <UCircularProgress :size="88" :progress />
40
+ <UCircularProgress :size="88" :progress="_progress" />
17
41
  </div>
18
42
 
19
43
  <div class="progress-avatar__image-container">
@@ -0,0 +1,59 @@
1
+ @use '../../base/abstracts' as a;
2
+
3
+ @mixin fields-mobile {
4
+ @include a.bp(lg) {
5
+ display: grid;
6
+ grid-template-columns: 1fr 1fr;
7
+
8
+ > * {
9
+ max-width: none;
10
+ }
11
+ }
12
+
13
+ @include a.bp(m) {
14
+ grid-template-columns: 1fr;
15
+ }
16
+ }
17
+
18
+ .search-group__title {
19
+ @include a.type(200, strong);
20
+
21
+ margin-bottom: var(--e-space-3);
22
+ }
23
+
24
+ .search-group + .search-group {
25
+ margin-top: var(--e-space-8);
26
+ }
27
+
28
+ .search-group.wrap {
29
+ .search-group__fields {
30
+ display: grid;
31
+ gap: var(--e-space-6);
32
+ grid-template-columns: repeat(4, 1fr);
33
+
34
+ > * {
35
+ max-width: none;
36
+ }
37
+
38
+ @include fields-mobile;
39
+ }
40
+ }
41
+
42
+ .search-group__fields {
43
+ display: flex;
44
+ gap: var(--e-space-6);
45
+
46
+ > * {
47
+ flex: 1 1 auto;
48
+ max-width: calc(25% - 18px);
49
+ }
50
+
51
+ @include fields-mobile;
52
+ }
53
+
54
+ .search-group__ctas {
55
+ display: flex;
56
+ gap: var(--e-space-6);
57
+ flex-wrap: wrap;
58
+ margin-top: var(--e-space-10);
59
+ }
@@ -0,0 +1,32 @@
1
+ <script setup lang="ts">
2
+ import { onMounted, ref, useTemplateRef } from 'vue'
3
+
4
+ interface Props {
5
+ title?: string
6
+ }
7
+
8
+ defineProps<Props>()
9
+
10
+ const fieldsEl = useTemplateRef('fields')
11
+ const fieldCount = ref(0)
12
+
13
+ onMounted(() => {
14
+ fieldCount.value = fieldsEl.value.children.length
15
+ })
16
+ </script>
17
+
18
+ <template>
19
+ <fieldset :class="['search-group', { wrap: fieldCount > 5 }]">
20
+ <legend v-if="title" class="search-group__title">{{ title }}</legend>
21
+
22
+ <div ref="fields" class="search-group__fields">
23
+ <slot />
24
+ </div>
25
+
26
+ <div v-if="$slots.ctas" class="search-group__ctas">
27
+ <slot name="ctas"></slot>
28
+ </div>
29
+ </fieldset>
30
+ </template>
31
+
32
+ <style scoped scss src="./search-group.scss"></style>
@@ -0,0 +1,39 @@
1
+ @use '../../base/abstracts' as a;
2
+
3
+ @keyframes skeleton-animation {
4
+ 0% {
5
+ background-color: var(--e-c-primary-01-50);
6
+ }
7
+
8
+ 100% {
9
+ background-color: var(--e-c-primary-01-100);
10
+ }
11
+ }
12
+
13
+ @mixin skeleton-base {
14
+ background-color: var(--e-c-primary-01-50);
15
+ border-radius: var(--e-brd-radius-2);
16
+ width: 100%;
17
+ height: 100%;
18
+ animation-name: skeleton-animation;
19
+ animation-duration: var(--e-trs-duration-slower);
20
+ animation-direction: alternate;
21
+ animation-timing-function: ease-in-out;
22
+ animation-iteration-count: infinite;
23
+ }
24
+
25
+ .skeleton-loader.fit {
26
+ @include skeleton-base;
27
+ }
28
+
29
+ .skeleton-loader.table {
30
+ display: flex;
31
+ flex-direction: column;
32
+ row-gap: var(--e-space-2);
33
+
34
+ > div {
35
+ @include skeleton-base;
36
+
37
+ height: a.rem(52);
38
+ }
39
+ }
@@ -0,0 +1,28 @@
1
+ <script setup lang="ts">
2
+ enum SkeletonType {
3
+ fit = 'fit',
4
+ table = 'table',
5
+ }
6
+
7
+ interface Props {
8
+ type?: SkeletonType
9
+ }
10
+
11
+ const { type = SkeletonType.fit } = defineProps<Props>()
12
+ </script>
13
+
14
+ <template>
15
+ <template v-if="type === SkeletonType.table">
16
+ <div :class="['skeleton-loader', type]">
17
+ <div></div>
18
+ <div></div>
19
+ <div></div>
20
+ </div>
21
+ </template>
22
+
23
+ <template v-if="type === SkeletonType.fit">
24
+ <div class="skeleton-loader fit"></div>
25
+ </template>
26
+ </template>
27
+
28
+ <style scoped lang="scss" src="./skeleton-loader.scss"></style>
@@ -2,11 +2,5 @@
2
2
 
3
3
  .cell-ctas {
4
4
  display: flex;
5
- column-gap: var(--e-space-2);
6
-
7
- .button-label {
8
- @include a.bp(m) {
9
- @include a.visually-hidden;
10
- }
11
- }
5
+ column-gap: var(--e-space-4);
12
6
  }