webcake-ui-kit 1.1.38 → 1.2.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 (68) hide show
  1. package/package.json +8 -2
  2. package/src/components/checkbox/Checkbox.vue +1 -1
  3. package/src/components/drawer/Drawer.vue +261 -0
  4. package/src/components/drawer/drawer.css +162 -0
  5. package/src/components/dropdown/Dropdown.vue +15 -9
  6. package/src/components/hover-card/HoverCard.vue +159 -0
  7. package/src/components/hover-card/hover-card.css +10 -0
  8. package/src/components/input-counter/InputCounter.vue +250 -0
  9. package/src/components/input-counter/input-counter.css +209 -0
  10. package/src/components/menu/Menu.vue +4 -2
  11. package/src/components/pagination/Pagination.vue +12 -3
  12. package/src/components/popover/Popover.vue +144 -0
  13. package/src/components/popover/popover.css +3 -0
  14. package/src/components/table-v2/ColumnDragHandle.vue +103 -0
  15. package/src/components/table-v2/Table.vue +299 -0
  16. package/src/components/table-v2/TableColGroup.vue +55 -0
  17. package/src/components/table-v2/TableEmpty.vue +10 -0
  18. package/src/components/table-v2/TableLoading.vue +25 -0
  19. package/src/components/table-v2/TableResizeObserver.vue +93 -0
  20. package/src/components/table-v2/TableWrapper.vue +193 -0
  21. package/src/components/table-v2/body/DragPreview.vue +65 -0
  22. package/src/components/table-v2/body/MeasureCell.vue +42 -0
  23. package/src/components/table-v2/body/TableBody.vue +126 -0
  24. package/src/components/table-v2/body/TableCell.vue +108 -0
  25. package/src/components/table-v2/body/TableRow.vue +73 -0
  26. package/src/components/table-v2/composables/useColumns.js +19 -0
  27. package/src/components/table-v2/composables/useSorter.js +12 -0
  28. package/src/components/table-v2/composables/useSticky.js +31 -0
  29. package/src/components/table-v2/constants.js +2 -0
  30. package/src/components/table-v2/context/createTableContext.js +79 -0
  31. package/src/components/table-v2/header/TableHeader.vue +15 -0
  32. package/src/components/table-v2/header/TableHeaderCell.vue +89 -0
  33. package/src/components/table-v2/header/TableHeaderRow.vue +107 -0
  34. package/src/components/table-v2/style/table-body.css +0 -0
  35. package/src/components/table-v2/style/table-header.css +0 -0
  36. package/src/components/table-v2/style/table-loading.css +19 -0
  37. package/src/components/table-v2/style/table-wrapper.css +287 -0
  38. package/src/components/table-v2/style/table.css +5 -0
  39. package/src/icons/BoxShadow.vue +35 -0
  40. package/src/icons/Carousel.vue +34 -0
  41. package/src/icons/CloudSync2.vue +42 -0
  42. package/src/icons/CornerBottomLeft.vue +26 -0
  43. package/src/icons/CornerBottomRight.vue +26 -0
  44. package/src/icons/CornerTopLeft.vue +26 -0
  45. package/src/icons/CornerTopRight.vue +26 -0
  46. package/src/icons/DistanceRight.vue +30 -0
  47. package/src/icons/DistanceTop.vue +30 -0
  48. package/src/icons/DropShadow.vue +35 -0
  49. package/src/icons/Fire.vue +26 -0
  50. package/src/icons/GlobeAccess.vue +32 -0
  51. package/src/icons/InnerShadow.vue +33 -0
  52. package/src/icons/Livestream.vue +54 -0
  53. package/src/icons/PaddingBottom.vue +30 -0
  54. package/src/icons/PaddingLeft.vue +30 -0
  55. package/src/icons/PaddingRight.vue +30 -0
  56. package/src/icons/PaddingTop.vue +30 -0
  57. package/src/icons/SkewHeight.vue +32 -0
  58. package/src/icons/SkewWidth.vue +32 -0
  59. package/src/icons/SquareDashedBottomSolid.vue +26 -0
  60. package/src/icons/SquareDashedLeftSolid.vue +26 -0
  61. package/src/icons/SquareDashedRightSolid.vue +26 -0
  62. package/src/icons/StrokeLine.vue +23 -0
  63. package/src/icons/Topline.vue +26 -0
  64. package/src/icons/index.js +25 -0
  65. package/src/index.js +5 -0
  66. package/src/styles/.omc/state/agent-replay-645326b7-372b-463d-ab45-0adaafe31a51.jsonl +2 -0
  67. package/src/styles/.omc/state/subagent-tracking.json +7 -0
  68. package/src/utils/common.js +34 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webcake-ui-kit",
3
- "version": "1.1.38",
3
+ "version": "1.2.1",
4
4
  "description": "UI Kit for Vue 2 && 3 - Pure Options API",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.js",
@@ -30,14 +30,18 @@
30
30
  "./WkDatePicker": "./src/components/date-picker/DatePicker.vue",
31
31
  "./WkDialog": "./src/components/dialog/Dialog.vue",
32
32
  "./WkDivider": "./src/components/divider/Divider.vue",
33
+ "./WkDrawer": "./src/components/drawer/Drawer.vue",
33
34
  "./WkDropdown": "./src/components/dropdown/Dropdown.vue",
34
35
  "./WkEmpty": "./src/components/empty/Empty.vue",
35
36
  "./WkEmptyIcon": "./src/components/empty-icon/EmptyIcon.vue",
36
37
  "./WkField": "./src/components/field/Field.vue",
38
+ "./WkHoverCard": "./src/components/hover-card/HoverCard.vue",
37
39
  "./WkInput": "./src/components/input/Input.vue",
40
+ "./WkInputCounter": "./src/components/input-counter/InputCounter.vue",
38
41
  "./WkInputNumber": "./src/components/input-number/InputNumber.vue",
39
42
  "./WkMenu": "./src/components/menu/Menu.vue",
40
43
  "./WkPagination": "./src/components/pagination/Pagination.vue",
44
+ "./WkPopover": "./src/components/popover/Popover.vue",
41
45
  "./WkProgress": "./src/components/progress/Progress.vue",
42
46
  "./WkRadio": "./src/components/radio/Radio.vue",
43
47
  "./WkRadioGroup": "./src/components/radio-group/RadioGroup.vue",
@@ -52,6 +56,7 @@
52
56
  "./WkSwitch": "./src/components/switch/Switch.vue",
53
57
  "./WkSwitchGroup": "./src/components/switch-group/SwitchGroup.vue",
54
58
  "./WkTable": "./src/components/table/Table.vue",
59
+ "./WkTableV2": "./src/components/table-v2/Table.vue",
55
60
  "./WkTabs": "./src/components/tabs/Tabs.vue",
56
61
  "./WkTag": "./src/components/tag/Tag.vue",
57
62
  "./WkTextarea": "./src/components/textarea/Textarea.vue",
@@ -139,6 +144,7 @@
139
144
  },
140
145
  "homepage": "https://ui.webcake.io",
141
146
  "dependencies": {
142
- "dayjs": "^1.11.21"
147
+ "dayjs": "^1.11.21",
148
+ "vuedraggable": "^2.24.3"
143
149
  }
144
150
  }
@@ -10,7 +10,7 @@
10
10
  @change="onChange"
11
11
  />
12
12
  <span class="ui-checkbox__box" aria-hidden="true">
13
- <WkiCheck v-if="isChecked" :size="16" />
13
+ <WkiCheck v-if="isChecked" :size="14" />
14
14
  </span>
15
15
  </span>
16
16
  </template>
@@ -0,0 +1,261 @@
1
+ <template>
2
+ <div v-if="hasOpened" v-show="isVisible" class="ui-drawer-root" :style="rootStyle">
3
+ <transition name="ui-drawer-fade">
4
+ <div v-if="isOpen && mask" class="ui-drawer-mask" :style="maskStyle" @click="onMaskClick"></div>
5
+ </transition>
6
+ <transition :name="transitionName" appear @after-leave="onAfterLeave">
7
+ <div
8
+ v-if="isOpen"
9
+ ref="panel"
10
+ :class="['ui-drawer', `ui-drawer--${placement}`]"
11
+ :style="panelStyle"
12
+ tabindex="-1"
13
+ role="dialog"
14
+ aria-modal="true"
15
+ @keydown.esc="onEscape"
16
+ >
17
+ <div
18
+ v-if="showHandle"
19
+ class="ui-drawer__handle"
20
+ aria-hidden="true"
21
+ @mousedown="onHandlePointerDown"
22
+ @touchstart="onHandlePointerDown"
23
+ style="cursor: grab"
24
+ >
25
+ <span class="ui-drawer__handle-bar"></span>
26
+ </div>
27
+ <div class="ui-drawer__body">
28
+ <slot></slot>
29
+ </div>
30
+ </div>
31
+ </transition>
32
+ </div>
33
+ </template>
34
+
35
+ <script>
36
+ import { acquire, release } from '../../floating/portal-root.js'
37
+ import { bumpTo, nextZIndex } from '../../floating/layer-manager.js'
38
+
39
+ const PLACEMENTS = ['bottom', 'top', 'left', 'right']
40
+
41
+ export default {
42
+ name: 'Drawer',
43
+ model: { prop: 'open', event: 'change' },
44
+ props: {
45
+ open: { type: Boolean, default: false },
46
+ modelValue: { type: Boolean, default: undefined },
47
+ placement: {
48
+ type: String,
49
+ default: 'bottom',
50
+ validator: v => PLACEMENTS.includes(v)
51
+ },
52
+ // Cross-axis size: height for top/bottom, width for left/right.
53
+ size: { type: [Number, String], default: '' },
54
+ showHandle: { type: Boolean, default: true },
55
+ mask: { type: Boolean, default: true },
56
+ maskClosable: { type: Boolean, default: true },
57
+ keyboard: { type: Boolean, default: true },
58
+ blur: { type: [Boolean, Number, String], default: false },
59
+ zIndex: { type: [Number, String], default: 1000 }
60
+ },
61
+ emits: ['change', 'update:modelValue', 'open', 'close', 'after-close'],
62
+ data() {
63
+ return {
64
+ hasOpened: false,
65
+ isVisible: false,
66
+ activeZIndex: 0,
67
+ isDragging: false,
68
+ dragSize: null
69
+ }
70
+ },
71
+ computed: {
72
+ isOpen() {
73
+ return this.modelValue !== undefined ? this.modelValue : this.open
74
+ },
75
+ isHorizontal() {
76
+ return this.placement === 'left' || this.placement === 'right'
77
+ },
78
+ transitionName() {
79
+ return `ui-drawer-slide-${this.placement}`
80
+ },
81
+ rootStyle() {
82
+ return { zIndex: this.activeZIndex || this.zIndex }
83
+ },
84
+ maskStyle() {
85
+ if (!this.blur) return {}
86
+ const val =
87
+ this.blur === true
88
+ ? '8px'
89
+ : typeof this.blur === 'number' || !isNaN(Number(this.blur))
90
+ ? `${this.blur}px`
91
+ : this.blur
92
+ return { backdropFilter: `blur(${val})`, WebkitBackdropFilter: `blur(${val})` }
93
+ },
94
+ panelStyle() {
95
+ if (this.dragSize !== null) {
96
+ const v = `${this.dragSize}px`
97
+ return Object.assign({}, this.isHorizontal ? { width: v } : { height: v }, { transition: 'none' })
98
+ }
99
+ if (this.size === '' || this.size === null) return {}
100
+ const v = typeof this.size === 'number' || !isNaN(Number(this.size)) ? `${this.size}px` : this.size
101
+ return this.isHorizontal ? { width: v } : { height: v }
102
+ }
103
+ },
104
+ watch: {
105
+ isOpen: {
106
+ handler(v) {
107
+ if (v) {
108
+ this.isVisible = true
109
+ this.onOpen()
110
+ } else {
111
+ this.onClose()
112
+ }
113
+ }
114
+ }
115
+ },
116
+ mounted() {
117
+ this._prevFocus = null
118
+ this._prevOverflow = ''
119
+ this._prevPadding = ''
120
+ if (typeof document !== 'undefined' && this.$el) {
121
+ this._drawerPortalRoot = acquire()
122
+ if (this._drawerPortalRoot) this._drawerPortalRoot.appendChild(this.$el)
123
+ }
124
+ if (typeof this.$on === 'function') {
125
+ // eslint-disable-next-line vue/no-deprecated-events-api
126
+ this.$on('hook:beforeDestroy', this.cleanup)
127
+ }
128
+ if (this.isOpen) {
129
+ this.isVisible = true
130
+ this.onOpen()
131
+ }
132
+ },
133
+ beforeUnmount() {
134
+ this.cleanup()
135
+ },
136
+ // eslint-disable-next-line vue/no-deprecated-destroyed-lifecycle, vue/no-dupe-keys
137
+ beforeDestroy() {
138
+ this.cleanup()
139
+ },
140
+ methods: {
141
+ setOpen(v) {
142
+ this.$emit('change', v)
143
+ this.$emit('update:modelValue', v)
144
+ },
145
+ close() {
146
+ this.setOpen(false)
147
+ },
148
+ onMaskClick(e) {
149
+ if (this.maskClosable) this.close(e)
150
+ },
151
+ onHandlePointerDown(e) {
152
+ if (!this.$refs.panel) return
153
+ this.isDragging = true
154
+ const rect = this.$refs.panel.getBoundingClientRect()
155
+ this._initialSize = this.isHorizontal ? rect.width : rect.height
156
+ this._startClientX = e.type.includes('touch') ? e.touches[0].clientX : e.clientX
157
+ this._startClientY = e.type.includes('touch') ? e.touches[0].clientY : e.clientY
158
+ this.dragSize = this._initialSize
159
+
160
+ document.addEventListener('mousemove', this.onPointerMove)
161
+ document.addEventListener('touchmove', this.onPointerMove, { passive: false })
162
+ document.addEventListener('mouseup', this.onPointerUp)
163
+ document.addEventListener('touchend', this.onPointerUp)
164
+ },
165
+ onPointerMove(e) {
166
+ if (!this.isDragging) return
167
+ e.preventDefault()
168
+
169
+ const clientX = e.type.includes('touch') ? e.touches[0].clientX : e.clientX
170
+ const clientY = e.type.includes('touch') ? e.touches[0].clientY : e.clientY
171
+
172
+ let delta = 0
173
+ if (this.placement === 'bottom') {
174
+ delta = this._startClientY - clientY
175
+ } else if (this.placement === 'top') {
176
+ delta = clientY - this._startClientY
177
+ } else if (this.placement === 'right') {
178
+ delta = this._startClientX - clientX
179
+ } else if (this.placement === 'left') {
180
+ delta = clientX - this._startClientX
181
+ }
182
+
183
+ this.dragSize = Math.max(0, Math.min(this._initialSize, this._initialSize + delta))
184
+ },
185
+ onPointerUp() {
186
+ if (!this.isDragging) return
187
+ this.isDragging = false
188
+
189
+ document.removeEventListener('mousemove', this.onPointerMove)
190
+ document.removeEventListener('touchmove', this.onPointerMove)
191
+ document.removeEventListener('mouseup', this.onPointerUp)
192
+ document.removeEventListener('touchend', this.onPointerUp)
193
+
194
+ if (this.dragSize < this._initialSize * (2 / 3)) {
195
+ this.close()
196
+ }
197
+
198
+ this.dragSize = null
199
+ },
200
+ onEscape(e) {
201
+ if (this.keyboard) this.close(e)
202
+ },
203
+ onOpen() {
204
+ this.hasOpened = true
205
+ bumpTo(this.zIndex)
206
+ this.activeZIndex = nextZIndex()
207
+ this.lockBody()
208
+ if (typeof document !== 'undefined') {
209
+ this._prevFocus = document.activeElement
210
+ }
211
+ this.$emit('open')
212
+ this.$nextTick(() => {
213
+ if (this.$refs.panel && this.$refs.panel.focus) {
214
+ this.$refs.panel.focus()
215
+ }
216
+ })
217
+ },
218
+ onClose() {
219
+ this.unlockBody()
220
+ if (this._prevFocus && typeof this._prevFocus.focus === 'function') {
221
+ this._prevFocus.focus()
222
+ }
223
+ this._prevFocus = null
224
+ this.$emit('close')
225
+ },
226
+ onAfterLeave() {
227
+ if (!this.isOpen) {
228
+ this.isVisible = false
229
+ }
230
+ this.$emit('after-close')
231
+ },
232
+ lockBody() {
233
+ if (typeof document === 'undefined' || typeof window === 'undefined') return
234
+ const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth
235
+ this._prevOverflow = document.body.style.overflow
236
+ this._prevPadding = document.body.style.paddingRight
237
+ document.body.style.overflow = 'hidden'
238
+ if (scrollbarWidth > 0) {
239
+ document.body.style.paddingRight = `${scrollbarWidth}px`
240
+ }
241
+ },
242
+ unlockBody() {
243
+ if (typeof document === 'undefined') return
244
+ document.body.style.overflow = this._prevOverflow || ''
245
+ document.body.style.paddingRight = this._prevPadding || ''
246
+ },
247
+ cleanup() {
248
+ this.unlockBody()
249
+ if (this.$el && this.$el.parentNode) {
250
+ this.$el.parentNode.removeChild(this.$el)
251
+ }
252
+ if (this._drawerPortalRoot) {
253
+ release()
254
+ this._drawerPortalRoot = null
255
+ }
256
+ }
257
+ }
258
+ }
259
+ </script>
260
+
261
+ <style src="./drawer.css" scoped></style>
@@ -0,0 +1,162 @@
1
+ /* ── overlay ──────────────────────────────────────────────────────────── */
2
+ .ui-drawer-root {
3
+ position: fixed;
4
+ inset: 0;
5
+ }
6
+
7
+ .ui-drawer-mask {
8
+ position: absolute;
9
+ inset: 0;
10
+ background: var(--backdrop);
11
+ }
12
+
13
+ /* ── panel ────────────────────────────────────────────────────────────── */
14
+ .ui-drawer {
15
+ position: absolute;
16
+ display: flex;
17
+ flex-direction: column;
18
+ background: var(--card);
19
+ box-shadow: var(--shadow-xl);
20
+ overflow: hidden;
21
+ outline: none;
22
+ pointer-events: auto;
23
+ box-sizing: border-box;
24
+ border-radius: var(--rounded-3xl);
25
+ margin: 8px;
26
+ }
27
+
28
+ /* Bottom sheet (default) — spans full width, docked to the bottom edge. */
29
+ .ui-drawer--bottom {
30
+ left: 0;
31
+ right: 0;
32
+ bottom: 0;
33
+ max-height: calc(100% - var(--spacing-4xl));
34
+ }
35
+
36
+ .ui-drawer--top {
37
+ left: 0;
38
+ right: 0;
39
+ top: 0;
40
+ max-height: calc(100% - var(--spacing-4xl));
41
+ }
42
+
43
+ .ui-drawer--left {
44
+ top: 0;
45
+ bottom: 0;
46
+ left: 0;
47
+ width: 378px;
48
+ max-width: calc(100% - var(--spacing-4xl));
49
+ }
50
+
51
+ .ui-drawer--right {
52
+ top: 0;
53
+ bottom: 0;
54
+ right: 0;
55
+ width: 378px;
56
+ max-width: calc(100% - var(--spacing-4xl));
57
+ }
58
+
59
+ /* ── handle ───────────────────────────────────────────────────────────── */
60
+ .ui-drawer__handle {
61
+ display: flex;
62
+ align-items: center;
63
+ justify-content: center;
64
+ flex-shrink: 0;
65
+ padding-top: var(--spacing-xs);
66
+ padding-bottom: var(--spacing-sm);
67
+ }
68
+
69
+ .ui-drawer__handle-bar {
70
+ width: 50px;
71
+ height: 3px;
72
+ border-radius: var(--radius-2);
73
+ background: var(--accent-2);
74
+ }
75
+
76
+ /* Side drawers get a vertical grab bar aligned to the exposed edge. */
77
+ .ui-drawer--left .ui-drawer__handle,
78
+ .ui-drawer--right .ui-drawer__handle {
79
+ position: absolute;
80
+ top: 0;
81
+ bottom: 0;
82
+ align-items: center;
83
+ padding-top: 0;
84
+ padding-bottom: 0;
85
+ }
86
+
87
+ .ui-drawer--left .ui-drawer__handle {
88
+ right: 0;
89
+ padding-right: var(--spacing-xs);
90
+ padding-left: var(--spacing-sm);
91
+ }
92
+
93
+ .ui-drawer--right .ui-drawer__handle {
94
+ left: 0;
95
+ padding-left: var(--spacing-xs);
96
+ padding-right: var(--spacing-sm);
97
+ }
98
+
99
+ .ui-drawer--left .ui-drawer__handle-bar,
100
+ .ui-drawer--right .ui-drawer__handle-bar {
101
+ width: 3px;
102
+ height: 50px;
103
+ }
104
+
105
+ /* ── body ─────────────────────────────────────────────────────────────── */
106
+ .ui-drawer__body {
107
+ flex: 1 1 auto;
108
+ min-height: 0;
109
+ overflow-y: auto;
110
+ -webkit-overflow-scrolling: touch;
111
+ font-family: var(--paragraph-small-font-family);
112
+ font-size: var(--paragraph-small-font-size);
113
+ line-height: var(--paragraph-small-line-height);
114
+ color: var(--secondary-fg);
115
+ }
116
+
117
+ /* ── transitions ──────────────────────────────────────────────────────── */
118
+ .ui-drawer-fade-enter-active,
119
+ .ui-drawer-fade-leave-active {
120
+ transition: opacity 0.25s ease;
121
+ }
122
+
123
+ .ui-drawer-fade-enter,
124
+ .ui-drawer-fade-enter-from,
125
+ .ui-drawer-fade-leave-to {
126
+ opacity: 0;
127
+ }
128
+
129
+ .ui-drawer-slide-bottom-enter-active,
130
+ .ui-drawer-slide-bottom-leave-active,
131
+ .ui-drawer-slide-top-enter-active,
132
+ .ui-drawer-slide-top-leave-active,
133
+ .ui-drawer-slide-left-enter-active,
134
+ .ui-drawer-slide-left-leave-active,
135
+ .ui-drawer-slide-right-enter-active,
136
+ .ui-drawer-slide-right-leave-active {
137
+ transition: transform 0.28s cubic-bezier(0.32, 0.72, 0, 1);
138
+ }
139
+
140
+ .ui-drawer-slide-bottom-enter,
141
+ .ui-drawer-slide-bottom-enter-from,
142
+ .ui-drawer-slide-bottom-leave-to {
143
+ transform: translateY(100%);
144
+ }
145
+
146
+ .ui-drawer-slide-top-enter,
147
+ .ui-drawer-slide-top-enter-from,
148
+ .ui-drawer-slide-top-leave-to {
149
+ transform: translateY(-100%);
150
+ }
151
+
152
+ .ui-drawer-slide-left-enter,
153
+ .ui-drawer-slide-left-enter-from,
154
+ .ui-drawer-slide-left-leave-to {
155
+ transform: translateX(-100%);
156
+ }
157
+
158
+ .ui-drawer-slide-right-enter,
159
+ .ui-drawer-slide-right-enter-from,
160
+ .ui-drawer-slide-right-leave-to {
161
+ transform: translateX(100%);
162
+ }
@@ -24,15 +24,21 @@
24
24
  <slot></slot>
25
25
  </span>
26
26
  </template>
27
- <DropdownMenuItems
28
- v-if="hasItems"
29
- :items="items"
30
- :value="value"
31
- @select="onItemSelect"
32
- :size="size"
33
- :showChecked="showChecked"
34
- />
35
- <slot v-else name="overlay"></slot>
27
+ <div
28
+ style="display: contents"
29
+ @mouseenter="triggerHasHover && handleMouseEnter()"
30
+ @mouseleave="triggerHasHover && handleMouseLeave()"
31
+ >
32
+ <DropdownMenuItems
33
+ v-if="hasItems"
34
+ :items="items"
35
+ :value="value"
36
+ @select="onItemSelect"
37
+ :size="size"
38
+ :showChecked="showChecked"
39
+ />
40
+ <slot v-else name="overlay"></slot>
41
+ </div>
36
42
  </Menu>
37
43
  </template>
38
44
 
@@ -0,0 +1,159 @@
1
+ <template>
2
+ <Menu
3
+ :open="isOpen"
4
+ :placement="placement"
5
+ :offset="offset"
6
+ :auto-focus="false"
7
+ :persistent="!triggerHasClick"
8
+ :close-on-esc="closeOnEsc"
9
+ :disabled="disabled"
10
+ :anchor-width="anchorWidth"
11
+ :width="width"
12
+ :overlay-class-name="overlayClassName"
13
+ :overlay-style="overlayStyle"
14
+ @change="onOpenChange"
15
+ @open="$emit('open')"
16
+ @close="$emit('close')"
17
+ >
18
+ <template #trigger="{ triggerRef }">
19
+ <span
20
+ :ref="triggerRef"
21
+ class="ui-hover-card__anchor"
22
+ @click="triggerHasClick && toggleClick()"
23
+ @mouseenter="triggerHasHover && onOpenIntent()"
24
+ @mouseleave="triggerHasHover && onCloseIntent()"
25
+ @focusin="onOpenIntent()"
26
+ @focusout="onCloseIntent()"
27
+ >
28
+ <slot name="trigger"></slot>
29
+ </span>
30
+ </template>
31
+ <div
32
+ class="ui-hover-card__panel"
33
+ @mouseenter="triggerHasHover && onOpenIntent()"
34
+ @mouseleave="triggerHasHover && onCloseIntent()"
35
+ >
36
+ <slot></slot>
37
+ </div>
38
+ </Menu>
39
+ </template>
40
+
41
+ <script>
42
+ import Menu from '../menu/Menu.vue'
43
+
44
+ const PLACEMENTS = [
45
+ 'top',
46
+ 'top-start',
47
+ 'top-end',
48
+ 'bottom',
49
+ 'bottom-start',
50
+ 'bottom-end',
51
+ 'left',
52
+ 'left-start',
53
+ 'left-end',
54
+ 'right',
55
+ 'right-start',
56
+ 'right-end'
57
+ ]
58
+
59
+ export default {
60
+ name: 'HoverCard',
61
+ components: { Menu },
62
+ model: { prop: 'open', event: 'change' },
63
+ props: {
64
+ open: { type: Boolean, default: false },
65
+ modelValue: { type: Boolean, default: undefined },
66
+ placement: {
67
+ type: String,
68
+ default: 'bottom',
69
+ validator: v => PLACEMENTS.indexOf(v) !== -1
70
+ },
71
+ offset: { type: Number, default: 8 },
72
+ // 'hover', 'click', or both
73
+ trigger: {
74
+ type: Array,
75
+ default: function () {
76
+ return ['hover']
77
+ }
78
+ },
79
+ openDelay: { type: Number, default: 200 },
80
+ closeDelay: { type: Number, default: 150 },
81
+ closeOnEsc: { type: Boolean, default: true },
82
+ disabled: { type: Boolean, default: false },
83
+ anchorWidth: { type: Boolean, default: false },
84
+ width: { type: [String, Number], default: null },
85
+ overlayClassName: { type: [String, Array, Object], default: null },
86
+ overlayStyle: { type: [Object, Array], default: null }
87
+ },
88
+ emits: ['change', 'update:modelValue', 'open', 'close'],
89
+ data() {
90
+ return {
91
+ isOpen: this.modelValue !== undefined ? this.modelValue : this.open
92
+ }
93
+ },
94
+ computed: {
95
+ triggerHasClick() {
96
+ return this.trigger.indexOf('click') !== -1
97
+ },
98
+ triggerHasHover() {
99
+ return this.trigger.indexOf('hover') !== -1
100
+ }
101
+ },
102
+ watch: {
103
+ open(v) {
104
+ if (this.modelValue === undefined) this.isOpen = v
105
+ },
106
+ modelValue(v) {
107
+ if (v !== undefined) this.isOpen = v
108
+ }
109
+ },
110
+ methods: {
111
+ onOpenChange(v) {
112
+ this.isOpen = v
113
+ this.$emit('change', v)
114
+ this.$emit('update:modelValue', v)
115
+ },
116
+ _clearTimers() {
117
+ if (this._openTimer) {
118
+ clearTimeout(this._openTimer)
119
+ this._openTimer = null
120
+ }
121
+ if (this._closeTimer) {
122
+ clearTimeout(this._closeTimer)
123
+ this._closeTimer = null
124
+ }
125
+ },
126
+ toggleClick() {
127
+ if (this.disabled) return
128
+ this._clearTimers()
129
+ this.onOpenChange(!this.isOpen)
130
+ },
131
+ onOpenIntent() {
132
+ if (this.disabled) return
133
+ this._clearTimers()
134
+ if (this.isOpen) return
135
+ this._openTimer = setTimeout(() => {
136
+ this._openTimer = null
137
+ this.onOpenChange(true)
138
+ }, this.openDelay)
139
+ },
140
+ onCloseIntent() {
141
+ this._clearTimers()
142
+ if (!this.isOpen) return
143
+ this._closeTimer = setTimeout(() => {
144
+ this._closeTimer = null
145
+ this.onOpenChange(false)
146
+ }, this.closeDelay)
147
+ }
148
+ },
149
+ beforeUnmount() {
150
+ this._clearTimers()
151
+ },
152
+ // eslint-disable-next-line vue/no-deprecated-destroyed-lifecycle, vue/no-dupe-keys
153
+ beforeDestroy() {
154
+ this._clearTimers()
155
+ }
156
+ }
157
+ </script>
158
+
159
+ <style src="./hover-card.css" scoped></style>
@@ -0,0 +1,10 @@
1
+ .ui-hover-card__anchor {
2
+ display: inline-flex;
3
+ }
4
+
5
+ .ui-hover-card__panel {
6
+ display: flex;
7
+ flex-direction: column;
8
+ gap: var(--spacing-sm);
9
+ padding: var(--spacing-md);
10
+ }