wave-ui 2.28.0 → 2.31.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wave-ui",
3
- "version": "2.28.0",
3
+ "version": "2.31.0",
4
4
  "description": "An emerging UI framework for Vue.js & Vue 3 with only the bright side. :sunny:",
5
5
  "author": "Antoni Andre <antoniandre.web@gmail.com>",
6
6
  "main": "./dist/wave-ui.umd.js",
@@ -43,29 +43,32 @@
43
43
  "*.vue"
44
44
  ],
45
45
  "devDependencies": {
46
- "@babel/core": "^7.16.0",
47
- "@babel/eslint-parser": "^7.16.3",
48
- "@babel/plugin-proposal-class-properties": "^7.16.0",
46
+ "@babel/core": "^7.17.0",
47
+ "@babel/eslint-parser": "^7.17.0",
48
+ "@babel/plugin-proposal-class-properties": "^7.16.7",
49
49
  "@mdi/font": "^5.9.55",
50
- "@vitejs/plugin-vue": "^1.9.4",
50
+ "@vitejs/plugin-vue": "^1.10.2",
51
51
  "@vue/compiler-sfc": "3.1.5",
52
- "autoprefixer": "^10.4.0",
53
- "axios": "^0.21.4",
52
+ "autoprefixer": "^10.4.2",
53
+ "axios": "^0.25.0",
54
54
  "eslint": "^7.32.0",
55
55
  "font-awesome": "^4.7.0",
56
- "gsap": "^3.8.0",
56
+ "gsap": "^3.9.1",
57
57
  "ionicons": "^4.6.3",
58
58
  "material-design-icons": "^3.0.1",
59
59
  "rollup-plugin-delete": "^2.0.0",
60
- "sass": "^1.43.4",
60
+ "sass": "^1.49.7",
61
61
  "simple-syntax-highlighter": "^2.2.0",
62
62
  "splitpanes": "^3.0.6",
63
- "vite": "^2.6.14",
63
+ "vite": "^2.7.13",
64
64
  "vite-plugin-pug": "^0.3.0",
65
- "vue": "^3.2.26",
65
+ "vue": "^3.2.29",
66
66
  "vue-cal": "^4.2.0",
67
67
  "vue-router": "^4.0.12",
68
68
  "vueperslides": "^3.3.2",
69
69
  "vuex": "^4.0.2"
70
+ },
71
+ "dependencies": {
72
+ "postcss": "^8.4.6"
70
73
  }
71
74
  }
@@ -19,7 +19,8 @@
19
19
  :tabindex="-1"
20
20
  text
21
21
  @keypress.stop
22
- @click.stop="!item._disabled && toggleItem(item, $event)")
22
+ @click.stop="!item._disabled && toggleItem(item, $event)"
23
+ :class="{ 'w-accordion__expand-icon--expanded': item._expanded }")
23
24
  //- Title.
24
25
  slot(
25
26
  v-if="$slots[`item-title.${item.id || i + 1}`]"
@@ -34,7 +35,8 @@
34
35
  :icon="(item._expanded && collapseIcon) || expandIcon"
35
36
  text
36
37
  @keypress.stop
37
- @click.stop="!item._disabled && toggleItem(item, $event)")
38
+ @click.stop="!item._disabled && toggleItem(item, $event)"
39
+ :class="{ 'w-accordion__expand-icon--expanded': item._expanded }")
38
40
  //- Content.
39
41
  w-transition-expand(y)
40
42
  .w-accordion__item-content(v-if="item._expanded" :class="contentClass")
@@ -148,8 +150,8 @@ export default {
148
150
  margin-right: $base-increment;
149
151
 
150
152
  .w-accordion--rotate-icon & {@include default-transition;}
151
- .w-accordion--rotate-icon .w-accordion__item--expanded & {transform: rotate(-180deg);}
152
- .w-accordion--rotate-icon.w-accordion--icon-right .w-accordion__item--expanded & {transform: rotate(180deg);}
153
+ &--expanded {transform: rotate(-180deg);}
154
+ .w-accordion--icon-right &--expanded {transform: rotate(180deg);}
153
155
 
154
156
  .w-icon:before {font-size: 1.1em;}
155
157
  }
@@ -229,7 +229,6 @@ export default {
229
229
  &--icon-outside &__icon {
230
230
  position: absolute;
231
231
  opacity: 1;
232
- // top: 2 * $base-increment - 1px; // Needed for IE 11, but dropping support.
233
232
  left: 1px;
234
233
  z-index: 1;
235
234
  transform: translateX(-50%);
@@ -40,7 +40,7 @@ export default {
40
40
  classes () {
41
41
  return {
42
42
  'd-block': this.block,
43
- 'row': this.row,
43
+ row: this.row,
44
44
  'align-center': this.alignCenter,
45
45
  'align-end': this.alignEnd,
46
46
  'justify-center': this.justifyCenter,
@@ -2,23 +2,23 @@
2
2
  .w-confirm
3
3
  w-menu(v-model="showPopup" v-bind="wMenuProps")
4
4
  template(#activator="{ on }")
5
- w-button.w-confirm__button(v-on="on" v-bind="buttonProps")
5
+ w-button.w-confirm__button(v-bind="{ ...$attrs, ...buttonProps, ...on }")
6
6
  slot
7
7
  w-flex(:column="!inline" align-center)
8
8
  div
9
- slot(name="question") Are you sure?
9
+ slot(name="question") {{ question }}
10
10
  .w-flex.justify-end(:class="inline ? 'ml2' : 'mt2'")
11
11
  w-button.mr2(
12
- v-if="!noCancel"
13
- v-bind="cancelButton"
12
+ v-if="cancel !== false"
13
+ v-bind="cancelButtonProps"
14
14
  :bg-color="(cancelButton || {}).bgColor || 'error'"
15
15
  @click="onCancel")
16
- slot(name="cancel") Cancel
16
+ slot(name="cancel") {{ cancelButton.label }}
17
17
  w-button(
18
- v-bind="confirmButton"
18
+ v-bind="confirmButtonProps"
19
19
  :bg-color="(confirmButton || {}).bgColor || 'success'"
20
20
  @click="onConfirm")
21
- slot(name="confirm") Confirm
21
+ slot(name="confirm") {{ confirmButton.label }}
22
22
  </template>
23
23
 
24
24
  <script>
@@ -30,11 +30,16 @@ export default {
30
30
  color: { type: String },
31
31
  icon: { type: String },
32
32
  mainButton: { type: Object }, // Allow passing down an object of props to the w-button component.
33
+ question: { type: String, default: 'Are you sure?' },
33
34
 
34
35
  // Cancel & confirm buttons props.
35
- noCancel: { type: Boolean }, // Removes the cancel button.
36
- cancelButton: { type: [Boolean, Object] }, // Allow passing down an object of props to the w-button component.
37
- confirmButton: { type: Object }, // Allow passing down an object of props to the w-button component.
36
+ // Allow passing down an object of props to the w-button component.
37
+ // If a string is given, that will be the label of the button.
38
+ // If false, no cancel button.
39
+ cancel: { type: [Boolean, Object, String], default: undefined },
40
+ // Allow passing down an object of props to the w-button component.
41
+ // If a string is given, that will be the label of the button.
42
+ confirm: { type: [Object, String] },
38
43
 
39
44
  // global menu props.
40
45
  inline: { type: Boolean }, // The layout inside the menu.
@@ -63,6 +68,26 @@ export default {
63
68
  }),
64
69
 
65
70
  computed: {
71
+ cancelButton () {
72
+ let button = { label: typeof this.cancel === 'string' ? this.cancel : 'Cancel' }
73
+ if (typeof this.cancel === 'object') button = Object.assign({}, button, this.cancel)
74
+ return button
75
+ },
76
+ // Props to pass down to the w-button component.
77
+ cancelButtonProps () {
78
+ const { label, ...props } = this.cancelButton // Everything except label.
79
+ return props
80
+ },
81
+ confirmButton () {
82
+ let button = { label: typeof this.confirm === 'string' ? this.confirm : 'Confirm' }
83
+ if (typeof this.confirm === 'object') button = Object.assign({}, button, this.confirm)
84
+ return button
85
+ },
86
+ // Props to pass down to the w-button component.
87
+ confirmButtonProps () {
88
+ const { label, ...props } = this.confirmButton // Everything except label.
89
+ return props
90
+ },
66
91
  wMenuProps () {
67
92
  return {
68
93
  top: this.top,
@@ -26,6 +26,7 @@ export default {
26
26
  justifyEnd: { type: Boolean },
27
27
  justifySpaceBetween: { type: Boolean },
28
28
  justifySpaceAround: { type: Boolean },
29
+ justifySpaceEvenly: { type: Boolean },
29
30
  basisZero: { type: Boolean },
30
31
  gap: { type: Number, default: 0 }
31
32
  },
@@ -48,6 +49,7 @@ export default {
48
49
  'justify-end': this.justifyEnd,
49
50
  'justify-space-between': this.justifySpaceBetween,
50
51
  'justify-space-around': this.justifySpaceAround,
52
+ 'justify-space-evenly': this.justifySpaceEvenly,
51
53
  'basis-zero': this.basisZero,
52
54
  [`w-flex--gap${this.gap}`]: this.gap
53
55
  }
@@ -12,9 +12,16 @@ component(
12
12
  template(v-else)
13
13
  //- Left label.
14
14
  template(v-if="labelPosition === 'left'")
15
- label.w-input__label.w-input__label--left.w-form-el-shakable(v-if="$slots.default" :for="`w-input--${_.uid}`")
15
+ label.w-input__label.w-input__label--left.w-form-el-shakable(
16
+ v-if="$slots.default"
17
+ :for="`w-input--${_.uid}`"
18
+ :class="validationClasses")
16
19
  slot
17
- label.w-input__label.w-input__label--left.w-form-el-shakable(v-else-if="label" :for="`w-input--${_.uid}`" v-html="label")
20
+ label.w-input__label.w-input__label--left.w-form-el-shakable(
21
+ v-else-if="label"
22
+ :for="`w-input--${_.uid}`"
23
+ :class="validationClasses"
24
+ v-html="label")
18
25
 
19
26
  //- Input wrapper.
20
27
  .w-input__input-wrap(:class="inputWrapClasses")
@@ -23,8 +30,10 @@ component(
23
30
  tag="label"
24
31
  :for="`w-input--${_.uid}`"
25
32
  @click="$emit('click:inner-icon-left', $event)") {{ innerIconLeft }}
33
+ //- All types of input except file.
26
34
  input.w-input__input(
27
35
  v-if="type !== 'file'"
36
+ ref="input"
28
37
  v-model="inputValue"
29
38
  v-on="listeners"
30
39
  @input="onInput"
@@ -45,8 +54,10 @@ component(
45
54
  :required="required || null"
46
55
  :tabindex="tabindex || null"
47
56
  v-bind="attrs")
48
- template(v-if="type === 'file'")
57
+ //- Input type file.
58
+ template(v-else)
49
59
  input(
60
+ ref="input"
50
61
  :id="`w-input--${_.uid}`"
51
62
  type="file"
52
63
  :name="name || null"
@@ -54,35 +65,45 @@ component(
54
65
  @blur="onBlur"
55
66
  @change="onFileChange"
56
67
  :multiple="multiple || null"
57
- v-bind="attrs")
58
- transition-group.w-input__input.w-input__input--file(tag="label" name="fade" :for="`w-input--${_.uid}`")
68
+ v-bind="attrs"
69
+ :data-progress="overallFilesProgress /* Needed to emit the overallProgress. */")
70
+ transition-group.w-input__input.w-input__input--file(
71
+ tag="label"
72
+ name="fade"
73
+ :for="`w-input--${_.uid}`")
59
74
  span.w-input__no-file(v-if="!inputFiles.length && isFocused" key="no-file")
60
75
  slot(name="no-file")
61
76
  template(v-if="$slots['no-file'] === undefined") No file
62
77
  span(v-for="(file, i) in inputFiles" :key="file.lastModified")
63
78
  | {{ i ? ', ': '' }}
64
79
  span.filename(:key="`${i}b`") {{ file.base }}
65
- | {{ file.extension }}
80
+ | {{ file.extension ? `.${file.extension}` : '' }}
66
81
 
67
82
  template(v-if="labelPosition === 'inside' && showLabelInside")
68
83
  label.w-input__label.w-input__label--inside.w-form-el-shakable(
69
84
  v-if="$slots.default"
70
85
  :for="`w-input--${_.uid}`"
71
- :class="isFocused && { [valid === false ? 'error' : color]: color || valid === false }")
86
+ :class="validationClasses")
72
87
  slot
73
88
  label.w-input__label.w-input__label--inside.w-form-el-shakable(
74
89
  v-else-if="label"
75
90
  :for="`w-input--${_.uid}`"
76
91
  v-html="label"
77
- :class="isFocused && { [valid === false ? 'error' : color]: color || valid === false }")
92
+ :class="validationClasses")
78
93
  w-icon.w-input__icon.w-input__icon--inner-right(
79
94
  v-if="innerIconRight"
80
95
  tag="label"
81
96
  :for="`w-input--${_.uid}`"
82
97
  @click="$emit('click:inner-icon-right', $event)") {{ innerIconRight }}
83
98
 
99
+ w-progress.fill-width(
100
+ v-if="hasLoading || (showProgress && (uploadInProgress || uploadComplete))"
101
+ size="2"
102
+ :color="progressColor || color"
103
+ :model-value="showProgress ? (uploadInProgress || uploadComplete) && overallFilesProgress : loadingValue")
104
+
84
105
  //- Files preview.
85
- label.d-flex(v-if="type === 'file' && inputFiles.length" :for="`w-input--${_.uid}`")
106
+ label.d-flex(v-if="type === 'file' && preview && inputFiles.length" :for="`w-input--${_.uid}`")
86
107
  template(v-for="(file, i) in inputFiles")
87
108
  i.w-icon.wi-spinner.w-icon--spin.size--sm.w-input__file-preview.primary(
88
109
  v-if="file.progress < 100"
@@ -92,20 +113,23 @@ component(
92
113
  :key="`${i}b`"
93
114
  :src="file.preview"
94
115
  alt="")
95
- i.w-icon.wi-file.w-input__file-preview.primary(v-else :key="`${i}c`")
116
+ i.w-icon.w-input__file-preview.primary.size--md(
117
+ v-else
118
+ :key="`${i}c`"
119
+ :class="preview && typeof preview === 'string' ? preview : 'wi-file'")
96
120
 
97
121
  //- Right label.
98
122
  template(v-if="labelPosition === 'right'")
99
123
  label.w-input__label.w-input__label--right.w-form-el-shakable(
100
124
  v-if="$slots.default"
101
- :for="`w-input--${_.uid}`")
125
+ :for="`w-input--${_.uid}`"
126
+ :class="validationClasses")
102
127
  slot
103
128
  label.w-input__label.w-input__label--right.w-form-el-shakable(
104
129
  v-else-if="label"
105
130
  :for="`w-input--${_.uid}`"
131
+ :class="validationClasses"
106
132
  v-html="label")
107
-
108
- w-progress.fill-width(v-if="loading" size="2" :color="progressColor || color")
109
133
  </template>
110
134
 
111
135
  <script>
@@ -144,13 +168,17 @@ export default {
144
168
  shadow: { type: Boolean },
145
169
  tile: { type: Boolean },
146
170
  multiple: { type: Boolean }, // Only for file uploads.
147
- preview: { type: Boolean }, // Only for file uploads.
148
- loading: { type: Boolean }
171
+ preview: { type: [Boolean, String], default: true }, // Only for file uploads.
172
+ loading: { type: [Boolean, Number], default: false }, // If a number is given, it will be the value of the progress.
173
+ showProgress: { type: [Boolean] }, // Only for file uploads.
174
+ // Allow syncing the files 1 way: prefilling a file is not possible.
175
+ // https://stackoverflow.com/questions/16365668/pre-populate-html-form-file-input
176
+ files: { type: Array }
149
177
  // Props from mixin: name, disabled, readonly, required, tabindex, validators.
150
178
  // Computed from mixin: inputName, isDisabled & isReadonly.
151
179
  },
152
180
 
153
- emits: ['input', 'update:modelValue', 'focus', 'blur', 'click:inner-icon-left', 'click:inner-icon-right'],
181
+ emits: ['input', 'update:modelValue', 'focus', 'blur', 'click:inner-icon-left', 'click:inner-icon-right', 'update:overallProgress'],
154
182
 
155
183
  data () {
156
184
  return {
@@ -160,7 +188,8 @@ export default {
160
188
  inputNumberError: false,
161
189
  isFocused: false,
162
190
  inputFiles: [], // For input type file.
163
- fileReader: null // For input type file.
191
+ fileReader: null, // For input type file.
192
+ isAutofilled: false
164
193
  }
165
194
  },
166
195
 
@@ -178,35 +207,69 @@ export default {
178
207
  const { input, focus, blur, ...listeners } = this.$attrs
179
208
  return listeners
180
209
  },
210
+
181
211
  attrs () {
182
212
  // eslint-disable-next-line no-unused-vars
183
213
  const { class: Class, ...htmlAttrs } = this.$attrs
184
214
  return htmlAttrs
185
215
  },
216
+
186
217
  hasValue () {
187
- return (
188
- this.inputValue ||
189
- ['date', 'time'].includes(this.type) ||
190
- (this.type === 'number' && this.inputNumberError) ||
191
- (this.type === 'file' && this.inputFiles.length)
192
- )
218
+ switch (this.type) {
219
+ case 'file': return !!this.inputFiles.length
220
+ case 'number': return this.inputNumberError
221
+ case 'date':
222
+ case 'time':
223
+ return true
224
+ default:
225
+ return this.inputValue || this.inputValue === 0
226
+ }
193
227
  },
194
228
 
195
229
  hasLabel () {
196
230
  return this.label || this.$slots.default
197
231
  },
198
232
 
233
+ hasLoading () {
234
+ return ![undefined, false].includes(this.loading)
235
+ },
236
+
237
+ loadingValue () {
238
+ let value
239
+ if (typeof this.loading === 'number') value = this.loading
240
+ else if (this.loading) {
241
+ value = this.type === 'file' && this.overallFilesProgress ? this.overallFilesProgress : undefined
242
+ }
243
+ return value
244
+ },
245
+
199
246
  showLabelInside () {
200
247
  return !this.staticLabel || (!this.hasValue && !this.placeholder)
201
248
  },
202
249
 
250
+ overallFilesProgress () {
251
+ const progress = this.inputFiles.reduce((total, file) => total + file.progress, 0)
252
+ const total = progress / this.inputFiles.length
253
+ this.$emit('update:overallProgress', this.inputFiles.length ? total : undefined)
254
+
255
+ return total
256
+ },
257
+
258
+ uploadInProgress () {
259
+ return this.overallFilesProgress > 0 && this.overallFilesProgress < 100
260
+ },
261
+
262
+ uploadComplete () {
263
+ return this.overallFilesProgress === 100
264
+ },
265
+
203
266
  classes () {
204
267
  return {
205
268
  'w-input': true,
206
269
  'w-input--file': this.type === 'file',
207
270
  'w-input--disabled': this.isDisabled,
208
271
  'w-input--readonly': this.isReadonly,
209
- [`w-input--${this.hasValue ? 'filled' : 'empty'}`]: true,
272
+ [`w-input--${this.hasValue || this.isAutofilled ? 'filled' : 'empty'}`]: true,
210
273
  'w-input--focused': this.isFocused && !this.isReadonly,
211
274
  'w-input--dark': this.dark,
212
275
  'w-input--floating-label': this.hasLabel && this.labelPosition === 'inside' && !this.staticLabel,
@@ -217,6 +280,12 @@ export default {
217
280
  }
218
281
  },
219
282
 
283
+ validationClasses () {
284
+ return this.isFocused && {
285
+ [this.valid === false ? 'error' : this.color]: this.color || this.valid === false
286
+ }
287
+ },
288
+
220
289
  inputWrapClasses () {
221
290
  return {
222
291
  [this.valid === false ? 'error' : this.color]: this.color || this.valid === false,
@@ -229,7 +298,8 @@ export default {
229
298
  'w-input__input-wrap--underline': !this.outline,
230
299
  'w-input__input-wrap--shadow': this.shadow,
231
300
  'w-input__input-wrap--no-padding': !this.outline && !this.bgColor && !this.shadow && !this.round,
232
- 'w-input__input-wrap--loading': this.loading
301
+ 'w-input__input-wrap--loading': this.loading || (this.showProgress && this.uploadInProgress),
302
+ 'w-input__input-wrap--upload-complete': this.uploadComplete
233
303
  }
234
304
  }
235
305
  },
@@ -254,35 +324,44 @@ export default {
254
324
  // For file input.
255
325
  onFileChange (e) {
256
326
  this.inputFiles = [...e.target.files].map(original => {
257
- const [, base, extension] = original.name.match(/^(.*)(\..*?)$/)
327
+ // `full` if there is no filename but only an extension.
328
+ const [, base = '', extension = '', full = ''] = original.name.match(/^(.*?)\.([^.]*)$|(.*)/)
329
+
258
330
  const file = reactive({
259
331
  name: original.name,
260
- base,
332
+ base: base || full,
261
333
  extension,
262
334
  type: original.type,
263
335
  size: original.size,
264
336
  lastModified: original.lastModified,
265
337
  preview: null,
266
- progress: 0
338
+ progress: 0,
339
+ file: original
267
340
  })
268
341
 
269
- this.filePreview(original, file)
342
+ this.readFile(original, file)
270
343
 
271
344
  return file
272
345
  })
273
346
  this.$emit('update:modelValue', this.inputFiles)
347
+ this.$emit('input', this.inputFiles)
274
348
  },
275
349
 
276
350
  // For file input.
277
- filePreview (original, file) {
351
+ readFile (original, file) {
278
352
  const reader = new FileReader()
279
353
 
354
+ // If the preview prop is a string, the user is setting the preview to an icon and
355
+ // don't need the actual file preview.
356
+ const isPreviewAnIcon = typeof this.preview === 'string'
357
+ const isFileAnImage = original.type && original.type.startsWith('image/')
280
358
  // Check if the file is an image and set a preview image.
281
- if (original.type && original.type.startsWith('image/')) {
359
+ if (this.preview && !isPreviewAnIcon && isFileAnImage) {
282
360
  reader.addEventListener('load', e => {
283
361
  file.preview = e.target.result
284
362
  })
285
363
  }
364
+ else delete file.preview
286
365
 
287
366
  // Used to display a spinner while the file is loading.
288
367
  reader.addEventListener('progress', e => {
@@ -293,9 +372,19 @@ export default {
293
372
  }
294
373
  },
295
374
 
375
+ mounted () {
376
+ // On page load, check if the field is autofilled by the browser.
377
+ // 20211229. Only a problem on Chrome. Firefox ok, Safari always prompts before filling up.
378
+ setTimeout(() => {
379
+ if (this.$refs.input && this.$refs.input.matches(':-webkit-autofill')) this.isAutofilled = true
380
+ }, 400) // Can't be less than 350: time for the browser to autofill.
381
+ },
382
+
296
383
  watch: {
297
384
  modelValue (value) {
298
385
  this.inputValue = value
386
+ // When clearing the field value, also reset the isAutofilled var for the CSS class.
387
+ if (!value && value !== 0) this.isAutofilled = false
299
388
  }
300
389
  }
301
390
  }
@@ -315,6 +404,8 @@ $inactive-color: #777;
315
404
  &--file {
316
405
  flex-wrap: nowrap;
317
406
  align-items: flex-end;
407
+
408
+ span.fade-leave-to {position: absolute;}
318
409
  }
319
410
 
320
411
  // Input field wrapper.
@@ -344,7 +435,10 @@ $inactive-color: #777;
344
435
  &--round {border-radius: 99em;}
345
436
  &--tile {border-radius: initial;}
346
437
  &--shadow {box-shadow: $box-shadow;}
347
- &--loading {border-bottom-color: transparent;}
438
+ &--loading, &--upload-complete {
439
+ border-bottom-color: transparent;
440
+ flex-wrap: wrap;
441
+ }
348
442
  &--loading ~ .w-progress {
349
443
  height: 2px;
350
444
  position: absolute;
@@ -353,7 +447,8 @@ $inactive-color: #777;
353
447
  }
354
448
 
355
449
  .w-input--focused & {border-color: currentColor;}
356
- .w-input--focused &--loading {border-bottom-color: transparent;}
450
+ .w-input--focused &--loading,
451
+ .w-input--focused &--upload-complete {border-bottom-color: transparent;}
357
452
 
358
453
  // Underline.
359
454
  &--underline:after {
@@ -464,6 +559,8 @@ $inactive-color: #777;
464
559
  margin-left: 4px;
465
560
  max-height: 2em;
466
561
  align-self: flex-end;
562
+
563
+ &.w-icon {margin-bottom: 4px;}
467
564
  }
468
565
 
469
566
  // Icons inside.