webcake-ui-kit 1.0.28 → 1.0.30

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.
@@ -0,0 +1,224 @@
1
+ <template>
2
+ <div class="ui-menu-host">
3
+ <slot name="trigger" v-bind="triggerSlotProps"></slot>
4
+ <div ref="portal" class="ui-menu-portal">
5
+ <transition name="ui-menu" @after-leave="onAfterLeave">
6
+ <div
7
+ v-if="isVisible"
8
+ ref="floating"
9
+ :id="menuUid"
10
+ class="ui-menu"
11
+ :class="floatingClasses"
12
+ :style="floatingStyle"
13
+ tabindex="-1"
14
+ role="menu"
15
+ @keydown="_kmOnKeydown"
16
+ >
17
+ <div class="ui-menu__content">
18
+ <slot></slot>
19
+ </div>
20
+ </div>
21
+ </transition>
22
+ </div>
23
+ </div>
24
+ </template>
25
+
26
+ <script>
27
+ import { acquire, release } from '../../floating/portal-root.js'
28
+ import { nextZIndex } from '../../floating/layer-manager.js'
29
+ import positionMixin from '../../floating/position-mixin.js'
30
+ import clickOutsideMixin from '../../floating/click-outside-mixin.js'
31
+ import keyboardMixin from '../../floating/keyboard-mixin.js'
32
+ import focusMixin from '../../floating/focus-mixin.js'
33
+
34
+ let _uid = 0
35
+ const PLACEMENTS = [
36
+ 'top',
37
+ 'top-start',
38
+ 'top-end',
39
+ 'bottom',
40
+ 'bottom-start',
41
+ 'bottom-end',
42
+ 'left',
43
+ 'left-start',
44
+ 'left-end',
45
+ 'right',
46
+ 'right-start',
47
+ 'right-end'
48
+ ]
49
+
50
+ export default {
51
+ name: 'Menu',
52
+ mixins: [keyboardMixin, focusMixin, clickOutsideMixin, positionMixin],
53
+ model: { prop: 'open', event: 'change' },
54
+ provide() {
55
+ return { menu: this }
56
+ },
57
+ props: {
58
+ open: { type: Boolean, default: false },
59
+ modelValue: { type: Boolean, default: undefined },
60
+ placement: {
61
+ type: String,
62
+ default: 'bottom-start',
63
+ validator: v => PLACEMENTS.indexOf(v) !== -1
64
+ },
65
+ offset: { type: Number, default: 4 },
66
+ closeOnSelect: { type: Boolean, default: true },
67
+ closeOnEsc: { type: Boolean, default: true },
68
+ disabled: { type: Boolean, default: false },
69
+ anchorWidth: { type: Boolean, default: false },
70
+ width: { type: [String, Number], default: null }
71
+ },
72
+ emits: ['change', 'update:modelValue', 'open', 'close', 'select'],
73
+ data() {
74
+ return {
75
+ menuUid: 'wk-menu-' + ++_uid,
76
+ isVisible: false,
77
+ zIndex: 0
78
+ }
79
+ },
80
+ computed: {
81
+ isOpen() {
82
+ return this.modelValue !== undefined ? this.modelValue : this.open
83
+ },
84
+ triggerSlotProps() {
85
+ return {
86
+ toggle: this.toggle,
87
+ open: this.openMenu,
88
+ close: this.closeMenu,
89
+ isOpen: this.isOpen,
90
+ attrs: this.triggerAttrs,
91
+ triggerRef: this._setTriggerEl
92
+ }
93
+ },
94
+ triggerAttrs() {
95
+ return {
96
+ 'aria-haspopup': 'menu',
97
+ 'aria-expanded': this.isOpen ? 'true' : 'false',
98
+ 'aria-controls': this.isOpen ? this.menuUid : null
99
+ }
100
+ },
101
+ floatingClasses() {
102
+ return [this.pmResolvedPlacement && 'ui-menu--' + this.pmResolvedPlacement]
103
+ },
104
+ floatingStyle() {
105
+ const style = {
106
+ top: this.pmTop + 'px',
107
+ left: this.pmLeft + 'px',
108
+ zIndex: this.zIndex || null
109
+ }
110
+ if (this.anchorWidth && this.pmAnchorWidth) {
111
+ style.minWidth = this.pmAnchorWidth + 'px'
112
+ } else if (this.width) {
113
+ style.minWidth = typeof this.width === 'number' ? `${this.width}px` : this.width
114
+ }
115
+ return style
116
+ }
117
+ },
118
+ watch: {
119
+ isOpen(v) {
120
+ if (v) this.handleOpen()
121
+ else this.handleClose()
122
+ }
123
+ },
124
+ mounted() {
125
+ if (typeof document !== 'undefined' && this.$refs.portal) {
126
+ const root = acquire()
127
+ if (root) {
128
+ root.appendChild(this.$refs.portal)
129
+ this._portalAcquired = true
130
+ }
131
+ }
132
+ if (typeof this.$on === 'function') {
133
+ // eslint-disable-next-line vue/no-deprecated-events-api
134
+ this.$on('hook:beforeDestroy', this._cleanupAll)
135
+ }
136
+ if (!this._triggerEl && this.$el) {
137
+ const firstChild = this.$el.firstElementChild
138
+ if (firstChild && !firstChild.classList.contains('ui-menu-portal')) {
139
+ this._triggerEl = firstChild
140
+ }
141
+ }
142
+ if (this.isOpen) this.handleOpen()
143
+ },
144
+ beforeUnmount() {
145
+ this._cleanupAll()
146
+ },
147
+ methods: {
148
+ setOpenState(v) {
149
+ this.$emit('change', v)
150
+ this.$emit('update:modelValue', v)
151
+ },
152
+ toggle() {
153
+ if (this.disabled) return
154
+ this.setOpenState(!this.isOpen)
155
+ },
156
+ openMenu() {
157
+ if (this.disabled || this.isOpen) return
158
+ this.setOpenState(true)
159
+ },
160
+ closeMenu() {
161
+ if (!this.isOpen) return
162
+ this.setOpenState(false)
163
+ },
164
+ selectItem(value) {
165
+ this.$emit('select', value)
166
+ if (this.closeOnSelect) this.closeMenu()
167
+ },
168
+ setActiveItem(el) {
169
+ const items = this._kmListItems()
170
+ const idx = items.indexOf(el)
171
+ if (idx !== -1) this.kmActiveIndex = idx
172
+ },
173
+ _setTriggerEl(el) {
174
+ this._triggerEl = el || null
175
+ },
176
+ handleOpen() {
177
+ this.isVisible = true
178
+ this.zIndex = nextZIndex()
179
+ this._fmCapture()
180
+ this._kmReset()
181
+ this.$emit('open')
182
+ this.$nextTick(() => {
183
+ const float = this.$refs.floating
184
+ if (!float) return
185
+ this._coSetElements(this._triggerEl, float)
186
+ this._kmSetFloatEl(float)
187
+ this._pmAttach(this._triggerEl, float)
188
+ this._coAttach()
189
+ this._fmFocus(float)
190
+ })
191
+ },
192
+ handleClose() {
193
+ this._pmDetach()
194
+ this._coDetach()
195
+ this.isVisible = false
196
+ this.$emit('close')
197
+ },
198
+ onAfterLeave() {
199
+ this._fmRestore()
200
+ },
201
+ _kmOnEscape() {
202
+ if (this.closeOnEsc) this.closeMenu()
203
+ },
204
+ _kmOnTab() {
205
+ this.closeMenu()
206
+ },
207
+ _coOnOutside() {
208
+ this.closeMenu()
209
+ },
210
+ _cleanupAll() {
211
+ this._pmDetach()
212
+ this._coDetach()
213
+ const portal = this.$refs.portal
214
+ if (portal && portal.parentNode) portal.parentNode.removeChild(portal)
215
+ if (this._portalAcquired) {
216
+ release()
217
+ this._portalAcquired = false
218
+ }
219
+ }
220
+ }
221
+ }
222
+ </script>
223
+
224
+ <style src="./menu.css" scoped></style>
@@ -0,0 +1,64 @@
1
+ .ui-menu-host {
2
+ display: contents;
3
+ }
4
+
5
+ .ui-menu-portal {
6
+ position: absolute;
7
+ top: 0;
8
+ left: 0;
9
+ width: 0;
10
+ height: 0;
11
+ pointer-events: none;
12
+ }
13
+
14
+ .ui-menu {
15
+ position: fixed;
16
+ background: var(--card);
17
+ border: 1px solid var(--border-primary);
18
+ border-radius: var(--rounded-xl);
19
+ box-shadow: var(--shadow-md);
20
+ display: flex;
21
+ flex-direction: column;
22
+ min-width: 176px;
23
+ max-width: 320px;
24
+ outline: none;
25
+ pointer-events: auto;
26
+ box-sizing: border-box;
27
+ }
28
+
29
+ .ui-menu__content {
30
+ display: flex;
31
+ flex-direction: column;
32
+ gap: var(--spacing-2xs);
33
+ max-height: 320px;
34
+ overflow-y: auto;
35
+ border-radius: var(--rounded-lg);
36
+ }
37
+
38
+ .ui-menu__content::-webkit-scrollbar {
39
+ width: 6px;
40
+ }
41
+ .ui-menu__content::-webkit-scrollbar-thumb {
42
+ background: var(--border-primary);
43
+ border-radius: 3px;
44
+ }
45
+
46
+ /* Open/close animation — class names cover both Vue 2 and Vue 3 transition naming. */
47
+ .ui-menu-enter-active,
48
+ .ui-menu-leave-active {
49
+ transition:
50
+ opacity 0.12s ease,
51
+ transform 0.12s ease;
52
+ }
53
+ .ui-menu-enter,
54
+ .ui-menu-enter-from,
55
+ .ui-menu-leave-to {
56
+ opacity: 0;
57
+ transform: scale(0.96);
58
+ }
59
+ .ui-menu-enter-to,
60
+ .ui-menu-leave,
61
+ .ui-menu-leave-from {
62
+ opacity: 1;
63
+ transform: scale(1);
64
+ }
@@ -1,73 +1,72 @@
1
1
  <template>
2
- <div
3
- class="ui-select"
4
- :class="[
5
- `ui-select--${size}`,
6
- {
7
- 'ui-select--open': isOpen,
8
- 'ui-select--disabled': disabled,
9
- 'ui-select--error': error,
10
- 'ui-select--loading': loading
11
- }
12
- ]"
13
- tabindex="0"
14
- @click="toggle"
15
- @keydown.esc="close"
16
- @keydown.enter.prevent="toggle"
17
- @keydown.space.prevent="toggle"
2
+ <Menu
3
+ :open="isOpen"
4
+ :disabled="disabled || loading"
5
+ placement="bottom-start"
6
+ :offset="4"
7
+ :anchor-width="true"
8
+ :close-on-select="false"
9
+ @change="onMenuChange"
10
+ @close="onMenuClose"
18
11
  >
19
- <span class="ui-select__left">
20
- <span v-if="hasIconSlot" class="ui-select__icon">
21
- <slot name="icon" />
22
- </span>
23
- <span v-if="prepend" class="ui-select__prepend">{{ prepend }}</span>
24
- <span class="ui-select__value" :class="{ 'ui-select__value--placeholder': !selectedLabel }">
25
- {{ selectedLabel || placeholder }}
26
- </span>
27
- </span>
28
-
29
- <spinner v-if="loading" type="mirrored" size="sm" />
30
- <span v-else class="ui-select__chevron">
31
- <ChevronDownIcon :size="16" />
32
- </span>
33
-
34
- <div ref="dropdown" v-show="isOpen" class="ui-select__dropdown" :style="dropdownStyle" @click.stop>
35
- <div v-if="canScrollUp" class="ui-select__scroll-indicator ui-select__scroll-indicator--up">
36
- <ChevronUpIcon :size="16" />
37
- </div>
12
+ <template #trigger="{ toggle, isOpen: open, attrs, triggerRef }">
38
13
  <div
39
- ref="options"
40
- class="ui-select__options"
41
- :style="listHeight ? { maxHeight: listHeight + 'px' } : {}"
42
- @scroll="updateScrollIndicators"
14
+ :ref="triggerRef"
15
+ class="ui-select"
16
+ :class="[
17
+ `ui-select--${size}`,
18
+ {
19
+ 'ui-select--open': open,
20
+ 'ui-select--disabled': disabled,
21
+ 'ui-select--error': error,
22
+ 'ui-select--loading': loading
23
+ }
24
+ ]"
25
+ tabindex="0"
26
+ v-bind="attrs"
27
+ @click="toggle"
28
+ @keydown.enter.prevent="toggle"
29
+ @keydown.space.prevent="toggle"
43
30
  >
44
- <slot>
45
- <select-option
46
- v-for="opt in normalizedOptions"
47
- :key="opt.value"
48
- :value="opt.value"
49
- :label="opt.label"
50
- :disabled="opt.disabled"
51
- />
52
- </slot>
53
- </div>
54
- <div v-if="canScrollDown" class="ui-select__scroll-indicator ui-select__scroll-indicator--down">
55
- <ChevronDownIcon :size="16" />
31
+ <span class="ui-select__left">
32
+ <span v-if="hasIconSlot" class="ui-select__icon">
33
+ <slot name="icon" />
34
+ </span>
35
+ <span v-if="prepend" class="ui-select__prepend">{{ prepend }}</span>
36
+ <span class="ui-select__value" :class="{ 'ui-select__value--placeholder': !selectedLabel }">
37
+ {{ selectedLabel || placeholder }}
38
+ </span>
39
+ </span>
40
+ <Spinner v-if="loading" type="mirrored" size="sm" />
41
+ <span v-else class="ui-select__chevron">
42
+ <ChevronDownIcon :size="16" />
43
+ </span>
56
44
  </div>
45
+ </template>
46
+ <div class="ui-select__list" :style="listStyle">
47
+ <slot>
48
+ <SelectOption
49
+ v-for="opt in normalizedOptions"
50
+ :key="opt.value"
51
+ :value="opt.value"
52
+ :label="opt.label"
53
+ :disabled="opt.disabled"
54
+ />
55
+ </slot>
57
56
  </div>
58
- </div>
57
+ </Menu>
59
58
  </template>
60
59
 
61
60
  <script>
62
61
  import ChevronDownIcon from '../../icons/ChevronDownIcon.vue'
63
- import ChevronUpIcon from '../../icons/ChevronUpIcon.vue'
62
+ import Menu from '../menu/Menu.vue'
64
63
  import SelectOption from '../select-option/SelectOption.vue'
65
64
  import Spinner from '../spinner/Spinner.vue'
66
65
 
67
66
  export default {
68
67
  name: 'Select',
69
68
 
70
- components: { SelectOption, Spinner, ChevronDownIcon, ChevronUpIcon },
69
+ components: { Menu, SelectOption, Spinner, ChevronDownIcon },
71
70
 
72
71
  provide() {
73
72
  return { select: this }
@@ -83,7 +82,7 @@ export default {
83
82
  type: String,
84
83
  default: 'default',
85
84
  validator: function (v) {
86
- return ['xs', 'sm', 'default', 'lg'].includes(v)
85
+ return ['xs', 'sm', 'default', 'lg'].indexOf(v) !== -1
87
86
  }
88
87
  },
89
88
  value: {
@@ -125,24 +124,7 @@ export default {
125
124
  data() {
126
125
  return {
127
126
  isOpen: false,
128
- canScrollUp: false,
129
- canScrollDown: false,
130
- labelCache: {},
131
- dropdownStyle: {}
132
- }
133
- },
134
-
135
- watch: {
136
- isOpen(val) {
137
- if (val) {
138
- this.$nextTick(() => {
139
- this.positionDropdown()
140
- this.updateScrollIndicators()
141
- })
142
- } else {
143
- this.canScrollUp = false
144
- this.canScrollDown = false
145
- }
127
+ labelCache: {}
146
128
  }
147
129
  },
148
130
 
@@ -162,76 +144,25 @@ export default {
162
144
  },
163
145
  hasIconSlot() {
164
146
  return !!((this.$scopedSlots && this.$scopedSlots['icon']) || this.$slots['icon'])
147
+ },
148
+ listStyle() {
149
+ return { maxHeight: this.listHeight + 'px' }
165
150
  }
166
151
  },
167
152
 
168
- mounted() {
169
- document.addEventListener('click', this.handleOutsideClick)
170
- window.addEventListener('scroll', this.handleScroll, true)
171
- window.addEventListener('resize', this.handleResize)
172
- if (typeof document !== 'undefined' && this.$refs.dropdown) {
173
- document.body.appendChild(this.$refs.dropdown)
174
- }
175
- if (typeof this.$on === 'function') {
176
- // eslint-disable-next-line vue/no-deprecated-events-api
177
- this.$on('hook:beforeDestroy', this.cleanup)
178
- }
179
- },
180
-
181
- beforeUnmount() {
182
- this.cleanup()
183
- },
184
-
185
153
  methods: {
186
- toggle() {
187
- if (this.disabled || this.loading) return
188
- this.isOpen = !this.isOpen
154
+ onMenuChange(v) {
155
+ this.isOpen = v
189
156
  },
190
- close() {
157
+ onMenuClose() {
191
158
  this.isOpen = false
192
159
  },
193
160
  select(val) {
194
161
  this.$emit('change', val)
195
- this.close()
162
+ this.isOpen = false
196
163
  },
197
164
  registerOption(value, label) {
198
165
  this.labelCache = Object.assign({}, this.labelCache, { [value]: label })
199
- },
200
- positionDropdown() {
201
- if (!this.$el || !this.$refs.dropdown) return
202
- var rect = this.$el.getBoundingClientRect()
203
- this.dropdownStyle = {
204
- position: 'fixed',
205
- top: rect.bottom + 4 + 'px',
206
- left: rect.left + 'px',
207
- width: rect.width + 'px',
208
- zIndex: 1050
209
- }
210
- },
211
- updateScrollIndicators() {
212
- const el = this.$refs.options
213
- if (!el) return
214
- this.canScrollUp = el.scrollTop > 0
215
- this.canScrollDown = el.scrollTop + el.clientHeight < el.scrollHeight
216
- },
217
- handleOutsideClick(e) {
218
- if (!this.$el.contains(e.target) && !(this.$refs.dropdown && this.$refs.dropdown.contains(e.target))) {
219
- this.close()
220
- }
221
- },
222
- handleScroll() {
223
- if (this.isOpen) this.positionDropdown()
224
- },
225
- handleResize() {
226
- if (this.isOpen) this.positionDropdown()
227
- },
228
- cleanup() {
229
- document.removeEventListener('click', this.handleOutsideClick)
230
- window.removeEventListener('scroll', this.handleScroll, true)
231
- window.removeEventListener('resize', this.handleResize)
232
- if (this.$refs.dropdown && this.$refs.dropdown.parentNode === document.body) {
233
- document.body.removeChild(this.$refs.dropdown)
234
- }
235
166
  }
236
167
  }
237
168
  }
@@ -110,23 +110,6 @@
110
110
  transform: rotate(180deg);
111
111
  }
112
112
 
113
- .ui-select__spinner {
114
- display: flex;
115
- align-items: center;
116
- justify-content: center;
117
- width: 16px;
118
- height: 16px;
119
- flex-shrink: 0;
120
- color: var(--muted-fg);
121
- animation: ui-select-spin 0.75s linear infinite;
122
- }
123
-
124
- @keyframes ui-select-spin {
125
- to {
126
- transform: rotate(360deg);
127
- }
128
- }
129
-
130
113
  /* — sizes — */
131
114
  .ui-select--lg {
132
115
  min-height: 40px;
@@ -163,45 +146,20 @@
163
146
  line-height: var(--paragraph-mini-line-height);
164
147
  }
165
148
 
166
- /* Dropdown — position/size set inline via JS (portal to body) */
167
- .ui-select__dropdown {
168
- background: var(--input);
169
- border: 1px solid var(--border-primary);
170
- border-radius: var(--radius-10);
171
- box-shadow: var(--shadow-md);
172
- overflow: hidden;
149
+ /* List wrapper inside the portal'd Menu. Width is anchored to trigger via the
150
+ Menu component's anchorWidth prop (computes min-width). */
151
+ .ui-select__list {
173
152
  display: flex;
174
153
  flex-direction: column;
175
- scrollbar-width: none;
176
- -ms-overflow-style: none;
177
- }
178
-
179
- .ui-select__dropdown .ui-select__options::-webkit-scrollbar {
180
- display: none;
181
- }
182
-
183
- .ui-select__options {
184
154
  overflow-y: auto;
185
155
  padding: var(--spacing-2xs);
156
+ scrollbar-width: thin;
186
157
  }
187
158
 
188
- .ui-select__scroll-indicator {
189
- position: absolute;
190
- left: 0;
191
- right: 0;
192
- display: flex;
193
- align-items: center;
194
- justify-content: center;
195
- flex-shrink: 0;
196
- color: var(--muted-fg);
197
- background: var(--input);
198
- padding: var(--spacing-2xs) 0;
199
- }
200
-
201
- .ui-select__scroll-indicator--up {
202
- top: 0;
159
+ .ui-select__list::-webkit-scrollbar {
160
+ width: 6px;
203
161
  }
204
-
205
- .ui-select__scroll-indicator--down {
206
- bottom: 0;
162
+ .ui-select__list::-webkit-scrollbar-thumb {
163
+ background: var(--border-primary);
164
+ border-radius: 3px;
207
165
  }
@@ -1,16 +1,16 @@
1
1
  <template>
2
2
  <div
3
3
  class="ui-select-option"
4
- :class="{
5
- 'ui-select-option--selected': isSelected,
6
- 'ui-select-option--disabled': isDisabled,
7
- 'ui-select-option--large': size === 'large',
8
- 'ui-select-option--destructive': variant === 'destructive'
9
- }"
10
- tabindex="0"
4
+ :class="[
5
+ size === 'large' ? 'ui-select-option--large' : null,
6
+ variant === 'destructive' ? 'ui-select-option--destructive' : null,
7
+ isDisabled ? 'ui-select-option--disabled' : null,
8
+ isSelected ? 'ui-select-option--selected' : null
9
+ ]"
10
+ role="option"
11
+ :aria-selected="isSelected ? 'true' : 'false'"
12
+ :aria-disabled="isDisabled ? 'true' : null"
11
13
  @click="handleClick"
12
- @keydown.enter="handleClick"
13
- @keydown.space.prevent="handleClick"
14
14
  >
15
15
  <slot>{{ label || value }}</slot>
16
16
  </div>
@@ -55,25 +55,21 @@ export default {
55
55
 
56
56
  emits: [],
57
57
 
58
- mounted() {
59
- if (this.select && this.select.registerOption) {
60
- this.select.registerOption(this.value, this.label || this.value)
61
- }
62
- },
63
-
64
58
  computed: {
65
59
  isSelected() {
66
- return this.select ? this.select.value === this.value : false
60
+ return !!(this.select && this.select.value !== undefined && this.select.value === this.value)
67
61
  },
68
62
  isDisabled() {
69
- return this.disabled || (this.select ? this.select.disabled : false)
63
+ return this.disabled || !!(this.select && this.select.disabled)
70
64
  }
71
65
  },
72
66
 
73
67
  methods: {
74
68
  handleClick() {
75
- if (this.isDisabled || !this.select) return
76
- this.select.select(this.value)
69
+ if (this.isDisabled) return
70
+ if (this.select && typeof this.select.select === 'function') {
71
+ this.select.select(this.value)
72
+ }
77
73
  }
78
74
  }
79
75
  }