wave-ui 2.48.0 → 3.0.1

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 (48) hide show
  1. package/dist/wave-ui.cjs.js +1 -1
  2. package/dist/wave-ui.css +1 -1
  3. package/dist/wave-ui.es.js +1746 -1535
  4. package/dist/wave-ui.umd.js +1 -1
  5. package/package.json +4 -3
  6. package/src/wave-ui/components/index.js +1 -0
  7. package/src/wave-ui/components/w-accordion.vue +8 -2
  8. package/src/wave-ui/components/w-alert.vue +10 -4
  9. package/src/wave-ui/components/w-app.vue +2 -107
  10. package/src/wave-ui/components/w-badge.vue +7 -3
  11. package/src/wave-ui/components/w-button/button.vue +6 -2
  12. package/src/wave-ui/components/w-card.vue +14 -4
  13. package/src/wave-ui/components/w-checkbox.vue +15 -8
  14. package/src/wave-ui/components/w-confirm.vue +5 -1
  15. package/src/wave-ui/components/w-date-picker.vue +6 -0
  16. package/src/wave-ui/components/w-dialog.vue +9 -3
  17. package/src/wave-ui/components/w-divider.vue +9 -3
  18. package/src/wave-ui/components/w-drawer.vue +9 -3
  19. package/src/wave-ui/components/w-input.vue +4 -2
  20. package/src/wave-ui/components/w-menu.vue +11 -4
  21. package/src/wave-ui/components/w-notification-manager.vue +18 -30
  22. package/src/wave-ui/components/w-notification.vue +7 -1
  23. package/src/wave-ui/components/w-progress.vue +2 -2
  24. package/src/wave-ui/components/w-radio.vue +8 -2
  25. package/src/wave-ui/components/w-rating.vue +11 -3
  26. package/src/wave-ui/components/w-scrollbar.vue +24 -0
  27. package/src/wave-ui/components/w-select.vue +9 -5
  28. package/src/wave-ui/components/w-slider.vue +13 -7
  29. package/src/wave-ui/components/w-steps.vue +14 -4
  30. package/src/wave-ui/components/w-switch.vue +14 -14
  31. package/src/wave-ui/components/w-table.vue +25 -11
  32. package/src/wave-ui/components/w-tabs/index.vue +8 -2
  33. package/src/wave-ui/components/w-tag.vue +15 -10
  34. package/src/wave-ui/components/w-textarea.vue +6 -2
  35. package/src/wave-ui/components/w-timeline.vue +18 -5
  36. package/src/wave-ui/components/w-toolbar.vue +8 -2
  37. package/src/wave-ui/components/w-tooltip.vue +10 -4
  38. package/src/wave-ui/components/w-tree.vue +47 -15
  39. package/src/wave-ui/core.js +120 -91
  40. package/src/wave-ui/scss/_base.scss +53 -2
  41. package/src/wave-ui/scss/_colors.scss +41 -17
  42. package/src/wave-ui/scss/_layout.scss +5 -12
  43. package/src/wave-ui/scss/_mixins.scss +24 -0
  44. package/src/wave-ui/scss/_variables.scss +100 -11
  45. package/src/wave-ui/utils/colors.js +60 -3
  46. package/src/wave-ui/utils/config.js +35 -11
  47. package/src/wave-ui/utils/dynamic-css.js +92 -30
  48. package/src/wave-ui/utils/notification-manager.js +39 -8
@@ -25,7 +25,6 @@ export default {
25
25
  modelValue: { type: [Boolean, Number], default: -1 },
26
26
  color: { type: String },
27
27
  bgColor: { type: String },
28
- dark: { type: Boolean },
29
28
  shadow: { type: Boolean },
30
29
  tile: { type: Boolean },
31
30
  round: { type: Boolean },
@@ -38,7 +37,9 @@ export default {
38
37
  lg: { type: Boolean },
39
38
  xl: { type: Boolean },
40
39
  width: { type: [Number, String] },
41
- height: { type: [Number, String] }
40
+ height: { type: [Number, String] },
41
+ dark: { type: Boolean },
42
+ light: { type: Boolean }
42
43
  },
43
44
 
44
45
  emits: ['input', 'update:modelValue'],
@@ -58,7 +59,8 @@ export default {
58
59
  [this.color]: this.color,
59
60
  [`${this.bgColor}--bg`]: this.bgColor,
60
61
  [`size--${this.presetSize}`]: true,
61
- 'w-tag--dark': this.dark && !this.outline,
62
+ 'w-tag--dark': this.dark,
63
+ 'w-tag--light': this.light,
62
64
  'w-tag--clickable': this.modelValue !== -1,
63
65
  'w-tag--outline': this.outline,
64
66
  'w-tag--no-border': this.noBorder || this.shadow,
@@ -85,14 +87,16 @@ export default {
85
87
  justify-content: center;
86
88
  vertical-align: middle;
87
89
  border-radius: $border-radius;
88
- border: 1px solid rgba(0, 0, 0, 0.08);
89
- background-color: rgba(255, 255, 255, 0.85);
90
+ border: 1px solid rgba(var(--w-contrast-bg-color-rgb), 0.08);
91
+ background-color: rgba(var(--w-base-bg-color-rgb), 0.85);
90
92
  padding-left: 2 * $base-increment;
91
93
  padding-right: 2 * $base-increment;
92
94
  cursor: default;
93
95
  user-select: none;
94
96
 
95
- &--dark {color: rgba(255, 255, 255, 0.95);}
97
+ @include themeable;
98
+
99
+ &--dark {color: rgba(var(--w-base-bg-color-rgb), 0.95);}
96
100
  &--outline {background-color: transparent;border-color: currentColor;}
97
101
  &--no-border {border-color: transparent;}
98
102
  &--round {border-radius: 99em;}
@@ -103,6 +107,7 @@ export default {
103
107
  $font-size: round(0.7 * $base-font-size);
104
108
  font-size: $font-size;
105
109
  line-height: $font-size + 2px;
110
+ padding: round(0.25 * $base-increment) $base-increment;
106
111
  }
107
112
  &.size--sm {
108
113
  $font-size: round(0.82 * $base-font-size);
@@ -150,7 +155,7 @@ export default {
150
155
  }
151
156
 
152
157
  &:hover {
153
- .w-tag__closable {background-color: rgba(0, 0, 0, 0.1);}
158
+ .w-tag__closable {background-color: rgba(var(--w-contrast-bg-color-rgb), 0.1);}
154
159
  }
155
160
 
156
161
  // Overlay to mark the focus and active state.
@@ -171,19 +176,19 @@ export default {
171
176
 
172
177
  // Hover state.
173
178
  &:hover:before {background-color: currentColor;opacity: 0.06;}
174
- &--dark:hover:before {background-color: rgba(255, 255, 255, 0.12);opacity: 1;}
179
+ &--dark:hover:before {background-color: rgba(var(--w-base-bg-color-rgb), 0.12);opacity: 1;}
175
180
  &--outline:hover:before,
176
181
  &--text:hover:before {background-color: currentColor;opacity: 0.12;}
177
182
 
178
183
  // Focus state.
179
184
  &:focus:before {background-color: currentColor;opacity: 0.2;}
180
- &--dark:focus:before {background-color: rgba(255, 255, 255, 0.12);}
185
+ &--dark:focus:before {background-color: rgba(var(--w-base-bg-color-rgb), 0.12);}
181
186
  &--outline:focus:before,
182
187
  &--text:focus:before {background-color: currentColor;opacity: 0.12;}
183
188
 
184
189
  // Active state.
185
190
  &:active:before {background-color: currentColor;opacity: 0.2;}
186
- &--dark:active:before {background-color: rgba(255, 255, 255, 0.2);}
191
+ &--dark:active:before {background-color: rgba(var(--w-base-bg-color-rgb), 0.2);}
187
192
  &--outline:active:before,
188
193
  &--text:active:before {background-color: currentColor;opacity: 0.2;}
189
194
  }
@@ -83,14 +83,15 @@ export default {
83
83
  color: { type: String, default: 'primary' },
84
84
  bgColor: { type: String },
85
85
  labelColor: { type: String, default: 'primary' },
86
- dark: { type: Boolean },
87
86
  outline: { type: Boolean },
88
87
  shadow: { type: Boolean },
89
88
  noAutogrow: { type: Boolean },
90
89
  resizable: { type: Boolean }, // Toggle the HTML built-in bottom right corner resize handle.
91
90
  tile: { type: Boolean },
92
91
  rows: { type: [Number, String], default: 3 },
93
- cols: { type: [Number, String] }
92
+ cols: { type: [Number, String] },
93
+ dark: { type: Boolean },
94
+ light: { type: Boolean }
94
95
  // Props from mixin: name, disabled, readonly, required, tabindex, validators.
95
96
  // Computed from mixin: inputName, isDisabled & isReadonly.
96
97
  },
@@ -132,6 +133,7 @@ export default {
132
133
  [`w-textarea--${this.hasValue ? 'filled' : 'empty'}`]: true,
133
134
  'w-textarea--focused': this.isFocused && !this.isReadonly,
134
135
  'w-textarea--dark': this.dark,
136
+ 'w-textarea--light': this.light,
135
137
  'w-textarea--resizable': this.resizable,
136
138
  'w-textarea--floating-label': this.hasLabel && this.labelPosition === 'inside' && !this.staticLabel,
137
139
  'w-textarea--no-padding': !this.outline && !this.bgColor && !this.shadow,
@@ -228,6 +230,8 @@ $inactive-color: #777;
228
230
  flex-wrap: wrap;
229
231
  font-size: $base-font-size;
230
232
 
233
+ @include themeable;
234
+
231
235
  // textarea wrapper.
232
236
  // ------------------------------------------------------
233
237
  &__textarea-wrap {
@@ -1,5 +1,5 @@
1
1
  <template lang="pug">
2
- ul.w-timeline
2
+ ul.w-timeline(:class="classes")
3
3
  li.w-timeline-item(v-for="(item, i) in items" :key="i")
4
4
  component.w-timeline-item__bullet(
5
5
  :is="item[itemIconKey] || icon ? 'w-icon' : 'div'"
@@ -23,16 +23,29 @@ export default {
23
23
  itemTitleKey: { type: String, default: 'title' },
24
24
  itemContentKey: { type: String, default: 'content' },
25
25
  itemIconKey: { type: String, default: 'icon' },
26
- itemColorKey: { type: String, default: 'color' }
26
+ itemColorKey: { type: String, default: 'color' },
27
+ dark: { type: Boolean },
28
+ light: { type: Boolean }
27
29
  },
28
30
 
29
- emits: []
31
+ emits: [],
32
+
33
+ computed: {
34
+ classes () {
35
+ return {
36
+ 'w-timeline--dark': this.dark,
37
+ 'w-timeline--light': this.light
38
+ }
39
+ }
40
+ }
30
41
  }
31
42
  </script>
32
43
 
33
44
  <style lang="scss">
34
45
  .w-timeline {
35
46
  margin-left: $base-increment;
47
+
48
+ @include themeable;
36
49
  }
37
50
 
38
51
  .w-timeline-item {
@@ -48,7 +61,7 @@ export default {
48
61
  position: absolute;
49
62
  top: 2px;
50
63
  left: 0;
51
- background-color: #fff;
64
+ background-color: $timeline-bullet-color;
52
65
  border-radius: 1em;
53
66
  border: 1px solid currentColor;
54
67
  width: $base-font-size;
@@ -66,7 +79,7 @@ export default {
66
79
  top: 2px;
67
80
  bottom: -2px;
68
81
  left: -1px;
69
- border-left: 2px solid #ddd;
82
+ border-left: 2px solid $timeline-bg-color;
70
83
  }
71
84
  }
72
85
  </style>
@@ -19,7 +19,9 @@ export default {
19
19
  width: { type: [Number, String], default: null },
20
20
  height: { type: [Number, String], default: null },
21
21
  noBorder: { type: Boolean },
22
- shadow: { type: Boolean }
22
+ shadow: { type: Boolean },
23
+ dark: { type: Boolean },
24
+ light: { type: Boolean }
23
25
  },
24
26
 
25
27
  emits: [],
@@ -41,6 +43,8 @@ export default {
41
43
  return {
42
44
  [this.color]: !!this.color,
43
45
  [`${this.bgColor}--bg`]: !!this.bgColor,
46
+ 'w-toolbar--dark': this.dark,
47
+ 'w-toolbar--light': this.light,
44
48
  'w-toolbar--absolute': !!this.absolute,
45
49
  'w-toolbar--fixed': !!this.fixed,
46
50
  [`w-toolbar--${this.bottom ? 'bottom' : 'top'}`]: !this.vertical,
@@ -66,9 +70,11 @@ export default {
66
70
  flex: 1 1 auto;
67
71
  align-items: center;
68
72
  padding: (2 * $base-increment) (3 * $base-increment);
69
- background-color: #fff;
73
+ background-color: $toolbar-bg-color;
70
74
  z-index: 10;
71
75
 
76
+ @include themeable;
77
+
72
78
  &--absolute, &--fixed {top: 0;left: 0;right: 0;}
73
79
  &--absolute {position: absolute;}
74
80
  &--fixed {position: fixed;}
@@ -32,7 +32,9 @@ export default {
32
32
  transition: { type: String },
33
33
  tooltipClass: { type: [String, Object, Array] },
34
34
  persistent: { type: Boolean },
35
- delay: { type: Number }
35
+ delay: { type: Number },
36
+ dark: { type: Boolean },
37
+ light: { type: Boolean }
36
38
  // Other props in the detachable mixin:
37
39
  // detachTo, appendTo, fixed, top, bottom, left, right, alignTop, alignBottom, alignLeft,
38
40
  // alignRight, noPosition, zIndex, activator.
@@ -78,6 +80,8 @@ export default {
78
80
  ...this.tooltipClasses,
79
81
  [`w-tooltip--${this.position}`]: !this.noPosition,
80
82
  [`w-tooltip--align-${this.alignment}`]: !this.noPosition && this.alignment,
83
+ 'w-tooltip--dark': this.dark,
84
+ 'w-tooltip--light': this.light,
81
85
  'w-tooltip--tile': this.tile,
82
86
  'w-tooltip--round': this.round,
83
87
  'w-tooltip--shadow': this.shadow,
@@ -190,7 +194,7 @@ export default {
190
194
  position: absolute;
191
195
  padding: $base-increment round(1.5 * $base-increment);
192
196
  border-radius: $border-radius;
193
- border: 1px solid #ddd;
197
+ border: 1px solid $tooltip-border-color;
194
198
  background-color: $tooltip-bg-color;
195
199
  pointer-events: none;
196
200
  color: $tooltip-color;
@@ -199,6 +203,8 @@ export default {
199
203
  width: max-content; // Not supported in IE11. :/
200
204
  z-index: 100;
201
205
 
206
+ @include themeable;
207
+
202
208
  &--fixed {position: fixed;z-index: 1000;}
203
209
 
204
210
  &--tile {border-radius: 0;}
@@ -218,12 +224,12 @@ export default {
218
224
 
219
225
  // Tooltip without border.
220
226
  &--no-border {
221
- @include triangle(var(--w-tooltip-bg-color), '.w-tooltip', 7px, 0);
227
+ @include triangle($tooltip-bg-color, '.w-tooltip', 7px, 0);
222
228
  }
223
229
 
224
230
  // Tooltip with border.
225
231
  &:not(&--no-border) {
226
- @include triangle(var(--w-tooltip-bg-color), '.w-tooltip', 7px);
232
+ @include triangle($tooltip-bg-color, '.w-tooltip', 7px);
227
233
  }
228
234
  }
229
235
 
@@ -4,22 +4,27 @@ ul.w-tree(:class="classes")
4
4
  v-for="(item, i) in currentDepthItems"
5
5
  :key="i"
6
6
  :class="itemClasses(item)")
7
- .w-tree__item-label(
8
- @click="!disabled && onLabelClick(item, $event)"
9
- @keydown="!disabled && onLabelKeydown(item, $event)"
10
- :tabindex="!disabled && (item.children || item.branch || selectable) && !(unexpandableEmpty && !item.children) ? 0 : null")
7
+ //- The keys `route` & `disabled` are always present in any currentDepthItems.
8
+ component.w-tree__item-label(
9
+ :is="!disabled && !item.disabled && item.route ? (!$router || hasExternalLink(item) ? 'a' : 'router-link') : 'div'"
10
+ v-bind="item.route && { [!$router || hasExternalLink(item) ? 'href' : 'to']: item.route }"
11
+ @click="!disabled && !item.disabled && onLabelClick(item, $event)"
12
+ @keydown="!disabled && !item.disabled && onLabelKeydown(item, $event)"
13
+ :tabindex="!disabled && !item.disabled && (item.children || item.branch || selectable) && !(unexpandableEmpty && !item.children) ? 0 : null")
14
+ //- @click.stop to not follow link if item is a link.
11
15
  w-button.w-tree__item-expand(
12
16
  v-if="(item.children || item.branch) && ((expandOpenIcon && item.open) || expandIcon) && !(unexpandableEmpty && !item.children)"
17
+ @click.stop="!disabled && !item.disabled && onLabelClick(item, $event)"
13
18
  color="inherit"
14
19
  :icon="(item.open && expandOpenIcon) || expandIcon"
15
20
  :icon-props="{ rotate90a: !item.open }"
16
21
  :tabindex="-1"
17
- :disabled="disabled"
22
+ :disabled="disabled || item.disabled"
18
23
  text
19
24
  sm)
20
- slot(name="item-label" :item="item.originalItem" :depth="depth" :open="item.open")
25
+ slot(name="item" :item="item.originalItem" :depth="depth" :open="item.open")
21
26
  w-icon(v-if="itemIcon(item)" class="w-tree__item-icon" :color="item.originalItem[itemIconColorKey] || iconColor") {{ itemIcon(item) }}
22
- span {{ item.label }}
27
+ span(v-html="item.label")
23
28
  span.ml1(v-if="counts && (item.children || item.branch)").
24
29
  ({{ item.originalItem.children?.length || 0 }})
25
30
  component(
@@ -39,16 +44,15 @@ ul.w-tree(:class="classes")
39
44
  @click="$emit('click', $event)"
40
45
  @select="$emit('select', $event)"
41
46
  @update:model-value="$emit('update:model-value', $event)")
42
- template(#item-label="{ item, depth, open }")
43
- slot(name="item-label" :item="item" :depth="depth" :open="open")
47
+ template(#item="{ item, depth, open }")
48
+ slot(name="item" :item="item" :depth="depth" :open="open")
44
49
  </template>
45
50
 
46
51
  <script>
52
+ import { consoleWarn } from '../utils/console'
47
53
  /**
48
- * @todo things to support:
49
- * - items routes
50
- * - icon per item
51
- * - left border?
54
+ * @todo:
55
+ * - option to add a left border.
52
56
  **/
53
57
 
54
58
  export default {
@@ -74,7 +78,10 @@ export default {
74
78
  counts: { type: Boolean },
75
79
  itemIconKey: { type: String, default: 'icon' }, // Support a different icon per item.
76
80
  iconColor: { type: String }, // Applies a color on all the label item icons.
77
- itemIconColorKey: { type: String, default: 'iconColor' } // Applies a specific color on each label item icons.
81
+ itemIconColorKey: { type: String, default: 'iconColor' }, // Applies a specific color on each label item icons.
82
+ itemRouteKey: { type: String, default: 'route' }, // Uses a router link if the item has the `route` key.
83
+ itemDisabledKey: { type: String, default: 'disabled' }, // Disables the item click and selection.
84
+ itemOpenKey: { type: String, default: 'open' } // Open the item by default.
78
85
  },
79
86
 
80
87
  emits: ['update:model-value', 'before-open', 'open', 'before-close', 'close', 'click', 'select'],
@@ -100,6 +107,12 @@ export default {
100
107
  updateCurrentDepthTree (items, oldItems = []) {
101
108
  this.currentDepthItems = []
102
109
 
110
+ if (!Array.isArray(items) && typeof items !== 'object') {
111
+ return consoleWarn(`[w-tree] the tree items must be of type array or object, ${typeof items} received.`, items)
112
+ }
113
+
114
+ if (!Array.isArray(items)) items = [items]
115
+
103
116
  items.forEach((item, i) => {
104
117
  this.currentDepthItems.push({
105
118
  originalItem: item, // Store the original item to return it on event emits.
@@ -107,8 +120,10 @@ export default {
107
120
  label: item.label,
108
121
  children: !!item.children, // The children tree remains available in originalItem.
109
122
  branch: item.branch,
123
+ route: item[this.itemRouteKey],
124
+ disabled: item[this.itemDisabledKey],
110
125
  depth: this.depth,
111
- open: !!(oldItems[i]?.open || this.expandAll)
126
+ open: !!(oldItems[i]?.open || this.expandAll || item[this.itemOpenKey])
112
127
  })
113
128
  })
114
129
  },
@@ -135,6 +150,9 @@ export default {
135
150
  },
136
151
 
137
152
  onLabelClick (item, e) {
153
+ const route = item[this.itemRouteKey]
154
+ if (route && this.$router && !this.hasExternalLink(item)) e.preventDefault()
155
+
138
156
  this.$emit('click', { item: item.originalItem, depth: this.depth, e })
139
157
  if (item.children || (item.branch && !this.unexpandableEmpty)) this.expandDepth(item)
140
158
 
@@ -219,9 +237,14 @@ export default {
219
237
  )
220
238
  },
221
239
 
240
+ hasExternalLink (item) {
241
+ return /^(https?:)?\/\/|mailto:|tel:/.test(item[this.itemRouteKey])
242
+ },
243
+
222
244
  itemClasses (item) {
223
245
  return {
224
246
  [item.children || item.branch ? 'w-tree__item--branch' : 'w-tree__item--leaf']: true,
247
+ 'w-tree__item--disabled': item[this.itemDisabledKey],
225
248
  'w-tree__item--empty': item.branch && !item.children,
226
249
  'w-tree__item--unexpandable': item.branch && !item.children && this.unexpandableEmpty
227
250
  }
@@ -264,6 +287,7 @@ $expand-icon-size: 20px;
264
287
  position: relative;
265
288
  display: inline-flex;
266
289
  align-items: center;
290
+ user-select: none;
267
291
 
268
292
  &:before {
269
293
  content: '';
@@ -274,16 +298,24 @@ $expand-icon-size: 20px;
274
298
  right: - $base-increment - 2px;
275
299
  border-radius: $border-radius;
276
300
  }
301
+ &:hover:before {background-color: rgba($primary, 0.05);}
277
302
  &:focus:before {background-color: rgba($primary, 0.1);}
278
303
  }
279
304
  &__item--leaf &__item-label:before {
280
305
  left: - $base-increment;
281
306
  right: - $base-increment;
282
307
  }
308
+ &__item--disabled &__item-label {opacity: 0.5;}
309
+ &__item--disabled &__item-label:before {display: none;}
283
310
 
284
311
  &__item-expand {margin-right: 2px;}
285
312
 
286
313
  &__item--branch > &__item-label {cursor: pointer;}
314
+ &__item--disabled > &__item-label {
315
+ color: $disabled-color;
316
+ cursor: not-allowed;
317
+ -webkit-tap-highlight-color: transparent;
318
+ }
287
319
  &__item--unexpandable > &__item-label {
288
320
  margin-left: $expand-icon-size + 2px;
289
321
  cursor: auto;
@@ -1,52 +1,73 @@
1
1
  import { reactive, inject } from 'vue'
2
- import config, { mergeConfig } from './utils/config'
3
- import NotificationManager from './utils/notification-manager'
4
- import colors from './utils/colors'
5
- // import * as directives from './directives'
6
-
7
- const shadeColor = (color, amount) => {
8
- return '#' + color.slice(1).match(/../g)
9
- .map(x => (x =+ `0x${x}` + amount, x < 0 ? 0 : ( x > 255 ? 255 : x)).toString(16).padStart(2, 0))
10
- .join('')
2
+ import { mergeConfig } from './utils/config'
3
+ import { injectNotifManagerInDOM, NotificationManager } from './utils/notification-manager'
4
+ import { colorPalette, generateColorShades, flattenColors } from './utils/colors'
5
+ import { injectColorsCSSInDOM, injectCSSInDOM } from './utils/dynamic-css'
6
+ import './scss/index.scss'
7
+
8
+ let mounted = false
9
+ const detectOSDarkMode = $waveui => {
10
+ const matchMedia = window.matchMedia('(prefers-color-scheme: dark)')
11
+ $waveui.preferredTheme = matchMedia.matches ? 'dark' : 'light'
12
+ $waveui.switchTheme($waveui.preferredTheme)
13
+
14
+ matchMedia.addEventListener('change', event => {
15
+ $waveui.preferredTheme = event.matches ? 'dark' : 'light'
16
+ $waveui.switchTheme($waveui.preferredTheme)
17
+ })
11
18
  }
12
19
 
13
- // Keep the notification manager private.
14
- // @todo: find a way to use private fields with Vue 3 proxies.
15
- // https://github.com/tc39/proposal-class-fields/issues/106
16
- // https://github.com/tc39/proposal-class-fields/issues/227
17
- let notificationManager = null
20
+ /**
21
+ * Inject presets into a Vue component props defaults before its registration into the app.
22
+ *
23
+ * @param {Object} component the Vue component to inject presets into.
24
+ * @param {Object} presets the presets to inject. E.g. `{ bgColor: 'green' }`.
25
+ */
26
+ const injectPresets = (component, presets) => {
27
+ for (const preset in presets) {
28
+ component.props[preset].default = presets[preset]
29
+ }
30
+ }
18
31
 
19
32
  export default class WaveUI {
20
- static instance = null
21
- static vueInstance = null // Needed until constructor is called.
22
- // #notificationManager
23
-
24
- // Public breakpoint object. Accessible from this.$waveui.breakpoint.
25
- breakpoint = {
26
- name: '',
27
- xs: false,
28
- sm: false,
29
- md: false,
30
- lg: false,
31
- xl: false
33
+ static #registered = false
34
+
35
+ // Exposed as a global object and also `app.provide`d.
36
+ // Accessible from this.$waveui, or inject('$waveui').
37
+ $waveui = {
38
+ breakpoint: {
39
+ name: '',
40
+ xs: false,
41
+ sm: false,
42
+ md: false,
43
+ lg: false,
44
+ xl: false,
45
+ width: null
46
+ },
47
+ config: {},
48
+ colors: {}, // Object of pairs of color-name => color hex.
49
+ preferredTheme: null, // The user OS preferred theme (light or dark).
50
+ theme: null, // The current theme (light or dark).
51
+ _notificationManager: null,
52
+
53
+ // Callable from this.$waveui.
54
+ notify (...args) {
55
+ this._notificationManager.notify(...args)
56
+ },
57
+
58
+ // Callable from this.$waveui.
59
+ switchTheme (theme) {
60
+ this.theme = theme
61
+ document.documentElement.setAttribute('data-theme', theme)
62
+ document.head.querySelector('#wave-ui-colors')?.remove?.()
63
+ const themeColors = this.config.colors[this.theme]
64
+ injectColorsCSSInDOM(themeColors)
65
+ this.colors = flattenColors(themeColors, colorPalette)
66
+ }
32
67
  }
33
68
 
34
- // A public object containing pairs of color-name => color hex.
35
- // Accessible from anywhere via `this.$waveui.colors`.
36
- // These colors generate the CSS in `w-app` on mounted.
37
- colors = colors.reduce((obj, color) => {
38
- obj[color.label] = color.color
39
- color.shades.forEach(shade => (obj[shade.label] = shade.color))
40
- return obj
41
- }, { ...config.colors, black: '#000', white: '#fff', transparent: 'transparent', inherit: 'inherit' })
42
-
43
- config = {} // Store and expose the config in the $waveui object.
44
-
45
69
  static install (app, options = {}) {
46
70
  // Register directives.
47
- // for (const id in directives) {
48
- // if (directives[id]) app.directive(id, directives[id])
49
- // }
50
71
  app.directive('focus', {
51
72
  // Wait for the next tick to focus the newly mounted element.
52
73
  mounted: el => setTimeout(() => el.focus(), 0)
@@ -62,71 +83,79 @@ export default class WaveUI {
62
83
 
63
84
  // Register a-la-carte components from the given list.
64
85
  const { components = {} } = options || {}
65
- for (let id in components) {
86
+ for (const id in components) {
66
87
  const component = components[id]
88
+ // If presets are defined for this component inject them into the props defaults.
89
+ if (options.presets?.[component.name]) injectPresets(component, options.presets[component.name])
67
90
  app.component(component.name, component)
68
91
  }
69
92
 
70
93
  // Register mixins.
71
- // app.mixin({
72
- // mounted () {
73
- // }
74
- // })
94
+ app.mixin({
95
+ // Add a mixin to capture the first mounted hook, trigger the Wave UI init then unregister the mixin straight away.
96
+ beforeMount () {
97
+ if (!mounted) {
98
+ mounted = true
99
+ const $waveui = inject('$waveui')
100
+ const { config } = $waveui
101
+
102
+ // Add the .w-app class where defined by user or at the root.
103
+ const wApp = document.querySelector(config.on) || document.body
104
+ wApp.classList.add('w-app')
105
+
106
+ let themeColors = config.colors[config.theme]
107
+ if (config.theme === 'auto') {
108
+ detectOSDarkMode($waveui)
109
+ themeColors = config.colors[$waveui.preferredTheme]
110
+ $waveui.colors = flattenColors(themeColors, colorPalette)
111
+ }
112
+ injectColorsCSSInDOM(themeColors)
113
+ injectCSSInDOM($waveui)
114
+ injectNotifManagerInDOM(wApp, components, $waveui)
115
+
116
+ // This mixin must only run once, we can delete it.
117
+ app._context.mixins.find(mixin => mixin.mounted && delete mixin.mounted)
118
+ }
119
+ }
120
+ })
75
121
 
76
- WaveUI.registered = true
122
+ new WaveUI(app, options)
123
+ WaveUI.#registered = true
77
124
  }
78
125
 
79
- // Singleton.
80
126
  constructor (app, options = {}) {
81
- if (WaveUI.instance) return WaveUI.instance
82
-
83
- else {
84
- if (!WaveUI.registered) app.use(WaveUI)
85
- notificationManager = reactive(new NotificationManager())
86
-
87
- // Merge user options into the default config.
88
- mergeConfig(options)
89
-
90
- // Add color shades for each custom color given in options.
91
- if (config.css.colorShades) {
92
- config.colorShades = {}
127
+ if (WaveUI.#registered) {
128
+ console.warn('Wave UI is already instantiated.')
129
+ return
130
+ }
93
131
 
94
- for (let color in config.colors) {
95
- color = { label: color, color: config.colors[color].replace('#', '') }
96
- const col = color.color
97
- if (col.length === 3) color.color = col[0] + '' + col[0] + col[1] + col[1] + col[2] + col[2]
132
+ this.$waveui._notificationManager = new NotificationManager()
133
+
134
+ if (!options.theme) options.theme = 'light'
135
+ // Move colors inside a theme if there are option.colors without theme.
136
+ // E.g. colors: { primary, ... } & not colors: { light { primary, ... }, dark: { primary, ... } })
137
+ if (options.colors) {
138
+ const colors = { ...options.colors }
139
+ if (!options.colors.light) options.colors.light = colors
140
+ if (!options.colors.dark) options.colors.dark = colors
141
+ // Cleanup anything else than themes in config.colors.
142
+ options.colors = { light: options.colors.light, dark: options.colors.dark }
143
+ }
98
144
 
99
- this.colors[color.label] = `#${color.color}`
145
+ // Merge user options into the default config.
146
+ let { components, ...config } = options
147
+ config = this.$waveui.config = mergeConfig(config)
100
148
 
101
- for (let i = 1; i <= 3; i++) {
102
- const lighterColor = shadeColor(`#${color.color}`, i * 40)
103
- const darkerColor = shadeColor(`#${color.color}`, -i * 40)
104
- this.colors[`${color.label}-light${i}`] = lighterColor
105
- this.colors[`${color.label}-dark${i}`] = darkerColor
149
+ // Generates color shades for each color of each theme and store in the config.colors object.
150
+ if (config.css.colorShades) generateColorShades(config)
106
151
 
107
- // Adding the shades to the config object to generate the CSS from w-app.
108
- config.colorShades[`${color.label}-light${i}`] = lighterColor
109
- config.colorShades[`${color.label}-dark${i}`] = darkerColor
110
- }
111
- }
112
- }
152
+ // Make Wave UI reactive and expose the single instance in the app.
153
+ const $waveui = reactive(this.$waveui)
154
+ app.config.globalProperties.$waveui = $waveui
155
+ app.provide('$waveui', $waveui)
113
156
 
114
- this.config = config
115
- this.notify = (...args) => notificationManager.notify(...args)
116
- WaveUI.instance = this
117
-
118
- // Make waveui reactive and expose the single instance in Vue.
119
- app.config.globalProperties.$waveui = reactive(this)
120
- app.provide('$waveui', WaveUI.instance)
157
+ if (config.theme !== 'auto') {
158
+ this.$waveui.colors = flattenColors(config.colors[config.theme], colorPalette)
121
159
  }
122
160
  }
123
-
124
- notify (...args) {
125
- notificationManager.notify(...args)
126
- }
127
161
  }
128
-
129
- /**
130
- * Returns the WaveUI instance. Equivalent to using `$waveui` inside templates.
131
- */
132
- export const useWaveUI = () => inject('$waveui')