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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webcake-ui-kit",
3
- "version": "1.0.28",
3
+ "version": "1.0.30",
4
4
  "description": "UI Kit for Vue 2 && 3 - Pure Options API",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.js",
@@ -28,10 +28,12 @@
28
28
  "./WkCheckboxGroup": "./src/components/checkbox-group/CheckboxGroup.vue",
29
29
  "./WkDialog": "./src/components/dialog/Dialog.vue",
30
30
  "./WkDivider": "./src/components/divider/Divider.vue",
31
+ "./WkDropdown": "./src/components/dropdown/Dropdown.vue",
31
32
  "./WkEmpty": "./src/components/empty/Empty.vue",
32
33
  "./WkEmptyIcon": "./src/components/empty-icon/EmptyIcon.vue",
33
34
  "./WkField": "./src/components/field/Field.vue",
34
35
  "./WkInput": "./src/components/input/Input.vue",
36
+ "./WkMenu": "./src/components/menu/Menu.vue",
35
37
  "./WkPagination": "./src/components/pagination/Pagination.vue",
36
38
  "./WkRadio": "./src/components/radio/Radio.vue",
37
39
  "./WkRadioGroup": "./src/components/radio-group/RadioGroup.vue",
@@ -73,7 +73,7 @@
73
73
  right: 0;
74
74
  bottom: 0;
75
75
  border-radius: var(--rounded-full);
76
- background: var(--positive-500);
76
+ background: var(--green-500);
77
77
  box-shadow: 0 0 0 2px var(--primary-bg);
78
78
  }
79
79
  .ui-avatar--regular .ui-avatar__indicator {
@@ -1,17 +1,20 @@
1
1
  <template>
2
2
  <span :class="['ui-avatar-stack', `ui-avatar-stack--${size}`]">
3
- <Avatar
3
+ <span
4
4
  v-for="(item, idx) in visibleItems"
5
5
  :key="`avatar-${idx}`"
6
- :size="size"
7
- :src="item.src || ''"
8
- :alt="item.alt || ''"
9
- :name="item.name || ''"
10
6
  :class="itemClasses"
11
- />
12
- <Avatar v-if="overflowCount > 0" :size="size" :class="[...itemClasses, 'ui-avatar-stack__overflow']">
13
- <slot name="overflow" v-bind="{ count: overflowCount }">{{ formattedOverflow }}</slot>
14
- </Avatar>
7
+ :style="{ zIndex: totalCount - idx }"
8
+ >
9
+ <slot name="avatar" :avatar="item" :index="idx" :size="size">
10
+ <Avatar :size="size" :src="item.src || ''" :alt="item.alt || ''" :name="item.name || ''" />
11
+ </slot>
12
+ </span>
13
+ <span v-if="overflowCount > 0" :class="itemClasses" :style="{ zIndex: 0 }">
14
+ <Avatar :size="size" class="ui-avatar-stack__overflow">
15
+ <slot name="overflow" v-bind="{ count: overflowCount }">{{ formattedOverflow }}</slot>
16
+ </Avatar>
17
+ </span>
15
18
  </span>
16
19
  </template>
17
20
 
@@ -65,6 +68,9 @@ export default {
65
68
  }
66
69
  return '+' + this.overflowCount
67
70
  },
71
+ totalCount() {
72
+ return this.visibleItems.length + (this.overflowCount > 0 ? 1 : 0)
73
+ },
68
74
  itemClasses() {
69
75
  const classes = ['ui-avatar-stack__item']
70
76
  if (this.animation !== 'none') {
@@ -4,35 +4,36 @@
4
4
  vertical-align: middle;
5
5
  }
6
6
 
7
- /* Each child avatar overlaps the previous one and shows a white ring to
8
- "punch out" of its neighbours. Box-shadow follows border-radius and does
9
- not take up layout space, so the avatar dimensions stay intact. */
10
7
  .ui-avatar-stack__item {
11
8
  position: relative;
12
9
  margin-left: -8px;
13
- box-shadow: 0 0 0 2px var(--primary-bg);
10
+ display: inline-flex;
11
+ align-items: center;
12
+ justify-content: center;
13
+ line-height: 0;
14
+ flex-shrink: 0;
15
+ z-index: 1;
16
+ border-radius: var(--rounded-full);
14
17
  }
18
+
15
19
  .ui-avatar-stack__item:first-child {
16
20
  margin-left: 0;
17
21
  }
18
22
 
19
- /* Stacking order: earlier siblings sit above later ones so the leftmost
20
- avatar's ring is visible over its neighbour. */
21
- .ui-avatar-stack__item {
23
+ /* Ring via pseudo-element so it follows the inner circle regardless of
24
+ how many wrapper elements sit between __item and the avatar (e.g. Tooltip). */
25
+ .ui-avatar-stack__item::after {
26
+ content: '';
27
+ position: absolute;
28
+ top: 0;
29
+ right: 0;
30
+ bottom: 0;
31
+ left: 0;
32
+ border-radius: 50%;
33
+ box-shadow: 0 0 0 2px var(--primary-bg);
34
+ pointer-events: none;
22
35
  z-index: 1;
23
36
  }
24
- .ui-avatar-stack__item:nth-child(1) {
25
- z-index: 5;
26
- }
27
- .ui-avatar-stack__item:nth-child(2) {
28
- z-index: 4;
29
- }
30
- .ui-avatar-stack__item:nth-child(3) {
31
- z-index: 3;
32
- }
33
- .ui-avatar-stack__item:nth-child(4) {
34
- z-index: 2;
35
- }
36
37
 
37
38
  /* Overflow chip — same shape as siblings, slightly darker background. */
38
39
  .ui-avatar-stack__overflow {
@@ -41,10 +42,8 @@
41
42
  }
42
43
 
43
44
  /* ============================ Animations ============================
44
- Three loop animations the consumer opts into via the `animation` prop
45
- on AvatarStack. Each animation is applied to every visible avatar
46
- child (including the "+N" overflow chip). All honour
47
- `prefers-reduced-motion: reduce`. */
45
+ Three loop animations the consumer opts into via the `animation` prop.
46
+ Applied to the wrapper span so the ring pseudo-element animates too. */
48
47
 
49
48
  @keyframes ui-avatar-stack-pulse {
50
49
  0%,
@@ -90,9 +89,12 @@
90
89
  .ui-avatar-stack__item--animation-ring::after {
91
90
  content: '';
92
91
  position: absolute;
93
- inset: -3px;
92
+ top: -3px;
93
+ right: -3px;
94
+ bottom: -3px;
95
+ left: -3px;
94
96
  border-radius: inherit;
95
- border: 2px solid var(--positive-500);
97
+ border: 2px solid var(--green-500);
96
98
  pointer-events: none;
97
99
  animation: ui-avatar-stack-ring 1.6s ease-out infinite;
98
100
  }
@@ -44,11 +44,11 @@
44
44
  color: var(--destructive-inverse-fg);
45
45
  }
46
46
  .ui-badge--info {
47
- background: var(--info-500);
47
+ background: var(--blue-500);
48
48
  color: var(--inverse-fg);
49
49
  }
50
50
  .ui-badge--warning {
51
- background: var(--warning-500);
51
+ background: var(--yellow-500);
52
52
  color: var(--inverse-fg);
53
53
  }
54
54
 
@@ -57,6 +57,11 @@
57
57
  box-shadow: 0 0 0 3px var(--focus-ring);
58
58
  }
59
59
 
60
+ .ui-badge--destructive:focus-visible {
61
+ outline: none;
62
+ box-shadow: 0 0 0 3px var(--focus-ring-error);
63
+ }
64
+
60
65
  .ui-badge__icon {
61
66
  display: inline-flex;
62
67
  align-items: center;
@@ -93,6 +93,7 @@
93
93
  </template>
94
94
 
95
95
  <script>
96
+ import { acquire, release } from '../../floating/portal-root.js'
96
97
  import Button from '../button/Button.vue'
97
98
  import MinusIcon from '../../icons/MinusIcon.vue'
98
99
  import UnfoldVerticalIcon from '../../icons/UnfoldVerticalIcon.vue'
@@ -206,9 +207,9 @@ export default {
206
207
  this._prevFocus = null
207
208
  this._prevOverflow = ''
208
209
  this._prevPadding = ''
209
- //Đẩy dialog ra ngoài body
210
- if (typeof document !== 'undefined' && this.$el && this.$el.parentNode !== document.body) {
211
- document.body.appendChild(this.$el)
210
+ if (typeof document !== 'undefined' && this.$el) {
211
+ this._dialogPortalRoot = acquire()
212
+ if (this._dialogPortalRoot) this._dialogPortalRoot.appendChild(this.$el)
212
213
  }
213
214
  if (typeof this.$on === 'function') {
214
215
  // eslint-disable-next-line vue/no-deprecated-events-api
@@ -234,7 +235,7 @@ export default {
234
235
  this.$emit('cancel', e)
235
236
  this.setOpen(false)
236
237
  },
237
- onWrapMousedown(e) {
238
+ onWrapMousedown() {
238
239
  this._mousedownOnWrap = true
239
240
  },
240
241
  onWrapMouseup(e) {
@@ -296,8 +297,12 @@ export default {
296
297
  },
297
298
  cleanup() {
298
299
  this.unlockBody()
299
- if (this.$el && this.$el.parentNode === document.body) {
300
- document.body.removeChild(this.$el)
300
+ if (this.$el && this.$el.parentNode) {
301
+ this.$el.parentNode.removeChild(this.$el)
302
+ }
303
+ if (this._dialogPortalRoot) {
304
+ release()
305
+ this._dialogPortalRoot = null
301
306
  }
302
307
  }
303
308
  }
@@ -0,0 +1,135 @@
1
+ <template>
2
+ <Menu
3
+ :open="isOpen"
4
+ :placement="placement"
5
+ :offset="4"
6
+ :close-on-esc="closeOnEsc"
7
+ :disabled="disabled"
8
+ @change="onMenuChange"
9
+ @open="$emit('open')"
10
+ @close="$emit('close')"
11
+ :width="width"
12
+ >
13
+ <template #trigger="{ triggerRef }">
14
+ <span
15
+ :ref="triggerRef"
16
+ class="ui-dropdown__anchor"
17
+ @click="triggerHasClick && handleClick()"
18
+ @mouseenter="triggerHasHover && handleMouseEnter()"
19
+ @mouseleave="triggerHasHover && handleMouseLeave()"
20
+ >
21
+ <slot></slot>
22
+ </span>
23
+ </template>
24
+ <DropdownMenuItems v-if="hasItems" :items="items" @select="onItemSelect" />
25
+ <slot v-else name="overlay"></slot>
26
+ </Menu>
27
+ </template>
28
+
29
+ <script>
30
+ import Menu from '../menu/Menu.vue'
31
+ import DropdownMenuItems from './DropdownMenuItems.vue'
32
+
33
+ const PLACEMENTS = [
34
+ 'top',
35
+ 'top-start',
36
+ 'top-end',
37
+ 'bottom',
38
+ 'bottom-start',
39
+ 'bottom-end',
40
+ 'left',
41
+ 'left-start',
42
+ 'left-end',
43
+ 'right',
44
+ 'right-start',
45
+ 'right-end'
46
+ ]
47
+
48
+ export default {
49
+ name: 'Dropdown',
50
+ components: { Menu, DropdownMenuItems },
51
+ model: { prop: 'open', event: 'change' },
52
+ props: {
53
+ open: { type: Boolean, default: false },
54
+ modelValue: { type: Boolean, default: undefined },
55
+ placement: {
56
+ type: String,
57
+ default: 'bottom-start',
58
+ validator: v => PLACEMENTS.indexOf(v) !== -1
59
+ },
60
+ closeOnEsc: { type: Boolean, default: true },
61
+ disabled: { type: Boolean, default: false },
62
+ width: { type: [String, Number], default: null },
63
+ trigger: {
64
+ type: Array,
65
+ default: function () {
66
+ return ['click']
67
+ }
68
+ },
69
+ items: {
70
+ type: Array,
71
+ default: function () {
72
+ return []
73
+ }
74
+ }
75
+ },
76
+ emits: ['change', 'update:modelValue', 'open', 'close', 'select'],
77
+ data() {
78
+ return {
79
+ isOpen: this.modelValue !== undefined ? this.modelValue : this.open
80
+ }
81
+ },
82
+ computed: {
83
+ triggerHasClick() {
84
+ return this.trigger.indexOf('click') !== -1
85
+ },
86
+ triggerHasHover() {
87
+ return this.trigger.indexOf('hover') !== -1
88
+ },
89
+ hasItems() {
90
+ return Array.isArray(this.items) && this.items.length > 0
91
+ }
92
+ },
93
+ watch: {
94
+ open(v) {
95
+ if (this.modelValue === undefined) this.isOpen = v
96
+ },
97
+ modelValue(v) {
98
+ if (v !== undefined) this.isOpen = v
99
+ }
100
+ },
101
+ methods: {
102
+ onMenuChange(v) {
103
+ this.isOpen = v
104
+ this.$emit('change', v)
105
+ this.$emit('update:modelValue', v)
106
+ },
107
+ handleClick() {
108
+ if (this.disabled) return
109
+ this.onMenuChange(!this.isOpen)
110
+ },
111
+ handleMouseEnter() {
112
+ if (this.disabled) return
113
+ if (this._hoverTimer) {
114
+ clearTimeout(this._hoverTimer)
115
+ this._hoverTimer = null
116
+ }
117
+ this.onMenuChange(true)
118
+ },
119
+ handleMouseLeave() {
120
+ this._hoverTimer = setTimeout(() => {
121
+ this.onMenuChange(false)
122
+ }, 80)
123
+ },
124
+ onItemSelect(key) {
125
+ this.$emit('select', key)
126
+ this.onMenuChange(false)
127
+ }
128
+ },
129
+ beforeUnmount() {
130
+ if (this._hoverTimer) clearTimeout(this._hoverTimer)
131
+ }
132
+ }
133
+ </script>
134
+
135
+ <style src="./dropdown.css" scoped></style>
@@ -0,0 +1,128 @@
1
+ <template>
2
+ <div class="ui-dropdown-items">
3
+ <div v-for="item in normalizedItems" :key="item._key" class="ui-dropdown-items__entry">
4
+ <!-- Divider -->
5
+ <div v-if="item._type === 'divider'" class="ui-dropdown-items__divider"></div>
6
+
7
+ <!-- Group: non-clickable label + flat children -->
8
+ <div v-else-if="item._type === 'group'" class="ui-dropdown-items__group">
9
+ <div class="ui-dropdown-items__group-label" :title="item.title || undefined">
10
+ <component :is="item.icon" v-if="item.icon && isComponent(item.icon)" class="ui-dropdown-items__icon" />
11
+ <span>{{ item.label }}</span>
12
+ </div>
13
+ <div
14
+ v-for="child in item.children || []"
15
+ :key="child.key"
16
+ class="ui-dropdown-items__item"
17
+ :class="{ 'ui-dropdown-items__item--disabled': child.disabled }"
18
+ :title="child.title || undefined"
19
+ @click="onItemClick(child)"
20
+ >
21
+ <component :is="child.icon" v-if="child.icon && isComponent(child.icon)" class="ui-dropdown-items__icon" />
22
+ <span class="ui-dropdown-items__label">{{ child.label }}</span>
23
+ <span v-if="child.extra" class="ui-dropdown-items__extra">{{ child.extra }}</span>
24
+ </div>
25
+ </div>
26
+
27
+ <!-- Submenu: non-clickable label + indented flat children -->
28
+ <div v-else-if="item._type === 'submenu'" class="ui-dropdown-items__submenu">
29
+ <div
30
+ class="ui-dropdown-items__submenu-label"
31
+ :class="{ 'ui-dropdown-items__item--disabled': item.disabled }"
32
+ :title="item.title || undefined"
33
+ >
34
+ <component :is="item.icon" v-if="item.icon && isComponent(item.icon)" class="ui-dropdown-items__icon" />
35
+ <span class="ui-dropdown-items__label">{{ item.label }}</span>
36
+ </div>
37
+ <div
38
+ v-for="child in item.children || []"
39
+ :key="child.key"
40
+ class="ui-dropdown-items__item ui-dropdown-items__item--sub"
41
+ :class="{ 'ui-dropdown-items__item--disabled': child.disabled || item.disabled }"
42
+ :title="child.title || undefined"
43
+ @click="onItemClick(child, item)"
44
+ >
45
+ <component :is="child.icon" v-if="child.icon && isComponent(child.icon)" class="ui-dropdown-items__icon" />
46
+ <span class="ui-dropdown-items__label">{{ child.label }}</span>
47
+ <span v-if="child.extra" class="ui-dropdown-items__extra">{{ child.extra }}</span>
48
+ </div>
49
+ </div>
50
+
51
+ <!-- Regular item -->
52
+ <div
53
+ v-else
54
+ class="ui-dropdown-items__item"
55
+ :class="{ 'ui-dropdown-items__item--disabled': item.disabled }"
56
+ :title="item.title || undefined"
57
+ @click="onItemClick(item)"
58
+ >
59
+ <component :is="item.icon" v-if="item.icon && isComponent(item.icon)" class="ui-dropdown-items__icon" />
60
+ <span class="ui-dropdown-items__label">{{ item.label }}</span>
61
+ <span v-if="item.extra" class="ui-dropdown-items__extra">{{ item.extra }}</span>
62
+ </div>
63
+ </div>
64
+ </div>
65
+ </template>
66
+
67
+ <script>
68
+ function skipReactive(val) {
69
+ if (val && typeof val === 'object' && !val.__v_skip) {
70
+ val.__v_skip = true
71
+ }
72
+ }
73
+
74
+ export default {
75
+ name: 'DropdownMenuItems',
76
+
77
+ props: {
78
+ items: {
79
+ type: Array,
80
+ default: function () {
81
+ return []
82
+ }
83
+ }
84
+ },
85
+
86
+ emits: ['select'],
87
+
88
+ computed: {
89
+ normalizedItems() {
90
+ return (this.items || []).map(function (item, i) {
91
+ var type
92
+ if (item.type === 'divider') {
93
+ type = 'divider'
94
+ } else if (item.type === 'group') {
95
+ type = 'group'
96
+ } else if (item.children && item.children.length) {
97
+ type = 'submenu'
98
+ } else {
99
+ type = 'item'
100
+ }
101
+ // Prevent Vue 3 from making component objects reactive (same as markRaw
102
+ // but without importing from 'vue', keeps dual-compat clean).
103
+ skipReactive(item.icon)
104
+ if (item.children)
105
+ item.children.forEach(function (c) {
106
+ skipReactive(c.icon)
107
+ })
108
+ return Object.assign({}, item, {
109
+ _type: type,
110
+ _key: item.key != null ? String(item.key) : 'item-' + i
111
+ })
112
+ })
113
+ }
114
+ },
115
+
116
+ methods: {
117
+ isComponent(val) {
118
+ return val !== null && typeof val === 'object'
119
+ },
120
+ onItemClick(item, parentItem) {
121
+ if (item.disabled || (parentItem && parentItem.disabled)) return
122
+ this.$emit('select', item.key)
123
+ }
124
+ }
125
+ }
126
+ </script>
127
+
128
+ <style src="./dropdown-menu-items.css" scoped></style>
@@ -0,0 +1,116 @@
1
+ .ui-dropdown-items {
2
+ display: flex;
3
+ flex-direction: column;
4
+ padding: var(--spacing-2xs);
5
+ }
6
+
7
+ .ui-dropdown-items__entry {
8
+ display: contents;
9
+ }
10
+
11
+ /* 9px total height: 4px margin + 1px line + 4px margin */
12
+ .ui-dropdown-items__divider {
13
+ height: 1px;
14
+ background: var(--border-primary);
15
+ margin: 4px 0;
16
+ flex-shrink: 0;
17
+ }
18
+
19
+ .ui-dropdown-items__item {
20
+ display: flex;
21
+ align-items: center;
22
+ gap: var(--spacing-sm);
23
+ padding: var(--spacing-7p5) var(--spacing-sm);
24
+ min-height: 36px;
25
+ border-radius: var(--radius-8, 8px);
26
+ font-family: var(--font-family-body);
27
+ font-size: var(--paragraph-small-font-size);
28
+ line-height: var(--paragraph-small-line-height);
29
+ color: var(--primary-fg);
30
+ cursor: pointer;
31
+ user-select: none;
32
+ transition: background 0.1s;
33
+ }
34
+
35
+ .ui-dropdown-items__item:hover:not(.ui-dropdown-items__item--disabled) {
36
+ background: var(--accent-bg);
37
+ }
38
+
39
+ .ui-dropdown-items__item--disabled {
40
+ opacity: 0.4;
41
+ cursor: not-allowed;
42
+ pointer-events: none;
43
+ }
44
+
45
+ .ui-dropdown-items__item--sub {
46
+ padding-left: calc(var(--spacing-sm) + var(--spacing-md));
47
+ }
48
+
49
+ .ui-dropdown-items__group {
50
+ display: flex;
51
+ flex-direction: column;
52
+ }
53
+
54
+ /* Mini label, 500 weight, muted — 5.5px vertical / 8px horizontal */
55
+ .ui-dropdown-items__group-label {
56
+ display: flex;
57
+ align-items: center;
58
+ gap: var(--spacing-xs);
59
+ padding: var(--spacing-5p5) var(--spacing-xs);
60
+ font-family: var(--font-family-body);
61
+ font-size: var(--paragraph-mini-font-size, 12px);
62
+ line-height: var(--paragraph-mini-line-height, 16px);
63
+ color: var(--muted-fg);
64
+ font-weight: 500;
65
+ cursor: default;
66
+ user-select: none;
67
+ }
68
+
69
+ .ui-dropdown-items__submenu {
70
+ display: flex;
71
+ flex-direction: column;
72
+ }
73
+
74
+ .ui-dropdown-items__submenu-label {
75
+ display: flex;
76
+ align-items: center;
77
+ gap: var(--spacing-sm);
78
+ padding: var(--spacing-7p5) var(--spacing-sm);
79
+ min-height: 36px;
80
+ font-family: var(--font-family-body);
81
+ font-size: var(--paragraph-small-font-size);
82
+ line-height: var(--paragraph-small-line-height);
83
+ color: var(--muted-fg);
84
+ cursor: default;
85
+ user-select: none;
86
+ }
87
+
88
+ /* 20px container — icon SVG inside is typically 16px, centred */
89
+ .ui-dropdown-items__icon {
90
+ width: 20px;
91
+ height: 20px;
92
+ flex-shrink: 0;
93
+ display: flex;
94
+ align-items: center;
95
+ justify-content: center;
96
+ color: inherit;
97
+ }
98
+
99
+ .ui-dropdown-items__label {
100
+ flex: 1;
101
+ min-width: 0;
102
+ overflow: hidden;
103
+ text-overflow: ellipsis;
104
+ white-space: nowrap;
105
+ }
106
+
107
+ /* Extra/shortcut text — paragraph small, muted, right-aligned */
108
+ .ui-dropdown-items__extra {
109
+ font-size: var(--paragraph-small-font-size);
110
+ line-height: var(--paragraph-small-line-height);
111
+ color: var(--muted-fg);
112
+ flex-shrink: 0;
113
+ margin-left: auto;
114
+ padding-left: var(--spacing-sm);
115
+ white-space: nowrap;
116
+ }
@@ -0,0 +1,3 @@
1
+ .ui-dropdown__anchor {
2
+ display: inline-block;
3
+ }