ketekny-ui-kit 1.0.16 → 1.0.18
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 +3 -2
- package/src/ui/kDialog.vue +76 -21
- package/src/ui/kInput.vue +8 -3
- package/src/ui/kSelect.vue +234 -223
- package/src/ui/kToggle.vue +18 -6
- package/src/ui/themes/kInput.theme.js +4 -1
- package/tailwind-preset.js +2 -2
- package/tailwind.config.js +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ketekny-ui-kit",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.18",
|
|
5
5
|
"description": "A Vue 3 UI component library with Tailwind CSS styling",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"files": [
|
|
@@ -40,13 +40,14 @@
|
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@primeuix/themes": "^1.1.1",
|
|
42
42
|
"@vuepic/vue-datepicker": "^11.0.2",
|
|
43
|
-
"json-editor-vue": "^0.18.1",
|
|
44
43
|
"he-tree-vue": "^3.1.2",
|
|
44
|
+
"json-editor-vue": "^0.18.1",
|
|
45
45
|
"lucide-vue-next": "^0.511.0",
|
|
46
46
|
"moment": "^2.30.1",
|
|
47
47
|
"primeicons": "^7.0.0",
|
|
48
48
|
"primevue": "^4.3.4",
|
|
49
49
|
"quill": "^2.0.3",
|
|
50
|
+
"reka-ui": "^2.8.2",
|
|
50
51
|
"simple-code-editor": "^2.0.9",
|
|
51
52
|
"vue-router": "^4.6.4",
|
|
52
53
|
"vue3-easy-data-table": "^1.5.47"
|
package/src/ui/kDialog.vue
CHANGED
|
@@ -2,24 +2,32 @@
|
|
|
2
2
|
<div
|
|
3
3
|
v-if="visible"
|
|
4
4
|
:style="{ zIndex: zIndex }"
|
|
5
|
-
class="fixed inset-0 flex items-center justify-center overflow-hidden bg-black
|
|
5
|
+
class="fixed inset-0 flex items-center justify-center overflow-hidden bg-black/50 backdrop-blur-sm"
|
|
6
|
+
@click.self="onBackdropClick"
|
|
6
7
|
>
|
|
7
8
|
<transition name="dialog">
|
|
8
9
|
<div
|
|
9
|
-
|
|
10
|
+
ref="dialogPanel"
|
|
11
|
+
class="relative bg-white shadow-lg overflow-hidden rounded-2xl flex flex-col max-h-[90vh] m-4"
|
|
10
12
|
:class="dialogClasses"
|
|
13
|
+
:role="'dialog'"
|
|
14
|
+
:aria-modal="'true'"
|
|
15
|
+
:aria-labelledby="titleId"
|
|
16
|
+
tabindex="-1"
|
|
11
17
|
v-show="visible"
|
|
12
18
|
>
|
|
13
19
|
<!-- Header -->
|
|
14
|
-
<div class="flex flex-row items-center p-4 text-white bg-
|
|
15
|
-
<div class="text-xl font-semibold !text-white">{{ title }}</div>
|
|
20
|
+
<div class="flex flex-row items-center p-4 text-white bg-sky-800 shrink-0">
|
|
21
|
+
<div :id="titleId" class="text-xl font-semibold !text-white">{{ title }}</div>
|
|
16
22
|
<div class="flex-1" />
|
|
17
|
-
<
|
|
18
|
-
|
|
23
|
+
<button
|
|
24
|
+
type="button"
|
|
25
|
+
class="p-1 text-black transition duration-100 ease-in-out rounded-full cursor-pointer text-secondary hover:bg-white hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70"
|
|
26
|
+
aria-label="Close dialog"
|
|
19
27
|
@click="close"
|
|
20
28
|
>
|
|
21
|
-
<
|
|
22
|
-
</
|
|
29
|
+
<X />
|
|
30
|
+
</button>
|
|
23
31
|
</div>
|
|
24
32
|
|
|
25
33
|
<!-- Fixed content (optional) -->
|
|
@@ -46,6 +54,7 @@ import { X } from 'lucide-vue-next'
|
|
|
46
54
|
|
|
47
55
|
// 🔥 Global z-index tracker
|
|
48
56
|
let dialogZIndexCounter = 1000
|
|
57
|
+
let bodyScrollLockCounter = 0
|
|
49
58
|
|
|
50
59
|
export default {
|
|
51
60
|
name: 'kDialog',
|
|
@@ -54,6 +63,14 @@ export default {
|
|
|
54
63
|
visible: Boolean,
|
|
55
64
|
title: String,
|
|
56
65
|
maximized: Boolean,
|
|
66
|
+
closeOnBackdrop: {
|
|
67
|
+
type: Boolean,
|
|
68
|
+
default: true,
|
|
69
|
+
},
|
|
70
|
+
closeOnEsc: {
|
|
71
|
+
type: Boolean,
|
|
72
|
+
default: true,
|
|
73
|
+
},
|
|
57
74
|
width: {
|
|
58
75
|
type: String,
|
|
59
76
|
default: '',
|
|
@@ -62,39 +79,77 @@ export default {
|
|
|
62
79
|
data() {
|
|
63
80
|
return {
|
|
64
81
|
zIndex: 1000, // Default fallback
|
|
82
|
+
titleId: `dialog-title-${Math.random().toString(36).slice(2, 10)}`,
|
|
83
|
+
previousActiveElement: null,
|
|
65
84
|
}
|
|
66
85
|
},
|
|
67
86
|
watch: {
|
|
68
|
-
visible
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
87
|
+
visible: {
|
|
88
|
+
immediate: true,
|
|
89
|
+
handler(val) {
|
|
90
|
+
if (val) {
|
|
91
|
+
dialogZIndexCounter += 1
|
|
92
|
+
this.zIndex = dialogZIndexCounter
|
|
93
|
+
this.previousActiveElement = document.activeElement
|
|
94
|
+
this.lockBodyScroll()
|
|
95
|
+
document.addEventListener('keydown', this.handleKeydown)
|
|
96
|
+
this.$nextTick(() => {
|
|
97
|
+
this.$refs.dialogPanel?.focus()
|
|
98
|
+
})
|
|
99
|
+
} else {
|
|
100
|
+
this.unlockBodyScroll()
|
|
101
|
+
document.removeEventListener('keydown', this.handleKeydown)
|
|
102
|
+
this.$nextTick(() => {
|
|
103
|
+
this.previousActiveElement?.focus?.()
|
|
104
|
+
this.previousActiveElement = null
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
},
|
|
73
108
|
},
|
|
74
109
|
},
|
|
75
|
-
// mounted() {
|
|
76
|
-
// // ✅ Assign a new z-index when component is created
|
|
77
|
-
// dialogZIndexCounter += 1
|
|
78
|
-
// this.zIndex = dialogZIndexCounter
|
|
79
|
-
// },
|
|
80
110
|
computed: {
|
|
81
111
|
computedWidth() {
|
|
82
|
-
return
|
|
112
|
+
return 'w-[600px] max-w-[calc(100vw-2rem)]'
|
|
83
113
|
},
|
|
84
114
|
dialogClasses() {
|
|
85
115
|
if (this.width) {
|
|
86
|
-
return this.
|
|
116
|
+
return this.width
|
|
87
117
|
} else if (this.maximized) {
|
|
88
|
-
return 'max-w-none h-[90vh]'
|
|
118
|
+
return 'w-[calc(100vw-2rem)] max-w-none h-[90vh]'
|
|
89
119
|
} else {
|
|
90
120
|
return this.computedWidth + ' max-h-[90vh]'
|
|
91
121
|
}
|
|
92
122
|
},
|
|
93
123
|
},
|
|
94
124
|
methods: {
|
|
125
|
+
onBackdropClick() {
|
|
126
|
+
if (!this.closeOnBackdrop) return
|
|
127
|
+
this.close()
|
|
128
|
+
},
|
|
129
|
+
handleKeydown(event) {
|
|
130
|
+
if (!this.visible || !this.closeOnEsc) return
|
|
131
|
+
if (event.key === 'Escape') {
|
|
132
|
+
event.preventDefault()
|
|
133
|
+
this.close()
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
lockBodyScroll() {
|
|
137
|
+
bodyScrollLockCounter += 1
|
|
138
|
+
if (bodyScrollLockCounter > 1) return
|
|
139
|
+
document.body.style.overflow = 'hidden'
|
|
140
|
+
},
|
|
141
|
+
unlockBodyScroll() {
|
|
142
|
+
bodyScrollLockCounter = Math.max(0, bodyScrollLockCounter - 1)
|
|
143
|
+
if (bodyScrollLockCounter > 0) return
|
|
144
|
+
document.body.style.overflow = ''
|
|
145
|
+
},
|
|
95
146
|
close() {
|
|
96
147
|
this.$emit('update:visible', false)
|
|
97
148
|
},
|
|
98
149
|
},
|
|
150
|
+
beforeUnmount() {
|
|
151
|
+
this.unlockBodyScroll()
|
|
152
|
+
document.removeEventListener('keydown', this.handleKeydown)
|
|
153
|
+
},
|
|
99
154
|
}
|
|
100
155
|
</script>
|
package/src/ui/kInput.vue
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="w-full text-primary/90">
|
|
3
|
-
<label
|
|
3
|
+
<label
|
|
4
|
+
v-if="label != null"
|
|
5
|
+
:for="inputId"
|
|
6
|
+
class="inputLabel"
|
|
7
|
+
:class="hasError ? theme.labelError : disabled ? theme.labelDisabled : theme.label">
|
|
4
8
|
{{ label }}
|
|
5
9
|
</label>
|
|
6
10
|
<div class="relative">
|
|
@@ -22,11 +26,12 @@
|
|
|
22
26
|
:aria-invalid="hasError ? 'true' : 'false'"
|
|
23
27
|
:aria-describedby="describedById"
|
|
24
28
|
/>
|
|
25
|
-
<component v-if="iconComponent" :is="iconComponent" :class="theme.trailingIcon" />
|
|
29
|
+
<component v-if="iconComponent" :is="iconComponent" :class="[theme.trailingIcon, disabled ? theme.trailingIconDisabled : '']" />
|
|
26
30
|
<div v-if="isPassword" :class="theme.passwordToggle">
|
|
27
31
|
<button
|
|
28
32
|
type="button"
|
|
29
|
-
:
|
|
33
|
+
:disabled="disabled"
|
|
34
|
+
:class="[theme.passwordToggleButton, disabled ? theme.passwordToggleButtonDisabled : '']"
|
|
30
35
|
:aria-label="showPassword ? 'Hide password' : 'Show password'"
|
|
31
36
|
:aria-pressed="showPassword ? 'true' : 'false'"
|
|
32
37
|
@click="showPassword = !showPassword">
|
package/src/ui/kSelect.vue
CHANGED
|
@@ -1,95 +1,101 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="relative w-full">
|
|
3
|
-
<!-- Label -->
|
|
4
3
|
<label v-if="label != null" :for="computedId" class="inputLabel" :class="hasError ? 'text-red-500' : 'text-gray-700'">
|
|
5
4
|
{{ label }}
|
|
6
5
|
</label>
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
:aria-disabled="disabled.toString()"
|
|
18
|
-
:aria-controls="dropdownId"
|
|
19
|
-
:role="'combobox'"
|
|
20
|
-
:tabindex="disabled ? -1 : 0"
|
|
21
|
-
>
|
|
22
|
-
<span class="block pr-10 truncate">{{ selectedLabel || placeholder }}</span>
|
|
7
|
+
<SelectRoot v-model="internalValue" v-model:open="open" :disabled="disabled">
|
|
8
|
+
<SelectTrigger as-child>
|
|
9
|
+
<div
|
|
10
|
+
:id="computedId"
|
|
11
|
+
class="relative w-full px-3 py-2 text-gray-700 bg-white border rounded-lg cursor-pointer trigger-box"
|
|
12
|
+
:class="[defaultStyle, hasError ? errorStyle : '', disabled ? disabledStyle : '']"
|
|
13
|
+
:aria-label="label || 'Select option'"
|
|
14
|
+
>
|
|
15
|
+
<SelectValue :placeholder="placeholder" class="block pr-10 truncate" />
|
|
23
16
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
<!-- Dropdown icon -->
|
|
35
|
-
<span class="absolute text-gray-400 -translate-y-1/2 pointer-events-none right-3 top-1/2">
|
|
36
|
-
<ChevronDown class="w-5 h-5" v-if="!isOpen" />
|
|
37
|
-
<ChevronUp class="w-5 h-5" v-else />
|
|
38
|
-
</span>
|
|
39
|
-
</div>
|
|
17
|
+
<button
|
|
18
|
+
v-if="clearable && isSelectionSet && !disabled"
|
|
19
|
+
type="button"
|
|
20
|
+
class="absolute text-gray-400 -translate-y-1/2 right-8 top-1/2 hover:text-red-500"
|
|
21
|
+
aria-label="Clear selection"
|
|
22
|
+
@pointerdown.stop.prevent="clearSelection"
|
|
23
|
+
@click.stop.prevent="clearSelection"
|
|
24
|
+
>
|
|
25
|
+
<X class="w-5 h-5 mr-2" />
|
|
26
|
+
</button>
|
|
40
27
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
ref="dropdown"
|
|
46
|
-
:id="dropdownId"
|
|
47
|
-
class="absolute z-[9999] mt-1 overflow-auto bg-white border border-gray-200 rounded-lg shadow-lg"
|
|
48
|
-
:class="dropdownHeight"
|
|
49
|
-
:style="dropdownPositionStyle"
|
|
50
|
-
role="listbox"
|
|
51
|
-
:aria-label="label || 'Select options'"
|
|
52
|
-
>
|
|
53
|
-
<!-- Search -->
|
|
54
|
-
<div class="sticky top-0 z-10 pt-2 pb-2 mx-2 bg-white">
|
|
55
|
-
<input
|
|
56
|
-
ref="searchInput"
|
|
57
|
-
type="text"
|
|
58
|
-
v-model="searchQuery"
|
|
59
|
-
placeholder="Αναζήτηση..."
|
|
60
|
-
class="w-full px-3 py-2 border border-gray-200 rounded-md focus:outline-none"
|
|
61
|
-
role="searchbox"
|
|
62
|
-
aria-label="Search options"
|
|
63
|
-
@keydown.esc.stop.prevent="closeDropdown"
|
|
64
|
-
/>
|
|
28
|
+
<span class="absolute text-gray-400 -translate-y-1/2 pointer-events-none right-3 top-1/2">
|
|
29
|
+
<ChevronDown class="w-5 h-5" v-if="!open" />
|
|
30
|
+
<ChevronUp class="w-5 h-5" v-else />
|
|
31
|
+
</span>
|
|
65
32
|
</div>
|
|
33
|
+
</SelectTrigger>
|
|
66
34
|
|
|
67
|
-
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
35
|
+
<SelectPortal>
|
|
36
|
+
<SelectContent
|
|
37
|
+
:id="dropdownId"
|
|
38
|
+
position="popper"
|
|
39
|
+
:side-offset="6"
|
|
40
|
+
class="z-[9999] overflow-hidden bg-white border border-gray-200 rounded-lg shadow-[0_20px_45px_-15px_rgba(15,23,42,0.35),0_8px_18px_-10px_rgba(15,23,42,0.25)] ring-1 ring-slate-900/10"
|
|
41
|
+
>
|
|
42
|
+
<div v-if="searchable" data-select-search-header class="sticky top-0 z-10 pt-2 pb-2 mx-2 bg-white">
|
|
43
|
+
<input
|
|
44
|
+
ref="searchInput"
|
|
45
|
+
type="text"
|
|
46
|
+
v-model="searchQuery"
|
|
47
|
+
placeholder="Αναζήτηση..."
|
|
48
|
+
class="w-full px-3 py-2 border border-gray-200 rounded-md focus:outline-none"
|
|
49
|
+
role="searchbox"
|
|
50
|
+
aria-label="Search options"
|
|
51
|
+
@keydown.stop="handleSearchKeydown"
|
|
52
|
+
/>
|
|
80
53
|
</div>
|
|
81
|
-
</div>
|
|
82
54
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
55
|
+
<div class="relative">
|
|
56
|
+
<SelectViewport
|
|
57
|
+
class="p-2 overflow-y-auto"
|
|
58
|
+
:class="canScrollAny ? 'pt-8 pb-8' : ''"
|
|
59
|
+
:style="{ maxHeight: viewportMaxHeight }"
|
|
60
|
+
@scroll="updateScrollState"
|
|
61
|
+
>
|
|
62
|
+
<div class="flex flex-col gap-0">
|
|
63
|
+
<SelectItem
|
|
64
|
+
v-for="(option, index) in filteredOptions"
|
|
65
|
+
:key="`${option[optionValue]}-${index}`"
|
|
66
|
+
:value="option[optionValue]"
|
|
67
|
+
class="px-3 py-2 rounded cursor-pointer outline-none text-slate-700 hover:bg-primary/20 hover:text-primary data-[highlighted]:bg-primary data-[highlighted]:text-white data-[state=checked]:text-primary data-[state=checked]:data-[highlighted]:text-white"
|
|
68
|
+
>
|
|
69
|
+
<div class="flex items-center justify-between gap-2">
|
|
70
|
+
<SelectItemText class="truncate">{{ option[optionLabel] }}</SelectItemText>
|
|
71
|
+
<span class="inline-flex items-center justify-center w-4 h-4 shrink-0">
|
|
72
|
+
<SelectItemIndicator>
|
|
73
|
+
<Check class="w-4 h-4" />
|
|
74
|
+
</SelectItemIndicator>
|
|
75
|
+
</span>
|
|
76
|
+
</div>
|
|
77
|
+
</SelectItem>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div v-if="filteredOptions.length === 0" class="px-3 py-2 text-gray-400">Δεν βρέθηκαν επιλογές</div>
|
|
81
|
+
</SelectViewport>
|
|
82
|
+
|
|
83
|
+
<SelectScrollUpButton class="absolute top-0 left-0 right-0 z-10 flex items-center justify-center h-6 text-gray-500 bg-white/95">
|
|
84
|
+
<ChevronUp class="w-4 h-4" />
|
|
85
|
+
</SelectScrollUpButton>
|
|
86
|
+
|
|
87
|
+
<SelectScrollDownButton class="absolute bottom-0 left-0 right-0 z-10 flex items-center justify-center h-6 text-gray-500 bg-white/95">
|
|
88
|
+
<ChevronDown class="w-4 h-4" />
|
|
89
|
+
</SelectScrollDownButton>
|
|
90
|
+
</div>
|
|
91
|
+
</SelectContent>
|
|
92
|
+
</SelectPortal>
|
|
93
|
+
</SelectRoot>
|
|
86
94
|
|
|
87
|
-
<!-- Error -->
|
|
88
95
|
<div class="mt-1 text-red-500" v-if="hasError && error !== true && error !== ''">
|
|
89
96
|
{{ error }}
|
|
90
97
|
</div>
|
|
91
98
|
|
|
92
|
-
<!-- Info -->
|
|
93
99
|
<div class="mt-1 text-gray-500" v-if="info">
|
|
94
100
|
{{ info }}
|
|
95
101
|
</div>
|
|
@@ -97,163 +103,168 @@
|
|
|
97
103
|
</template>
|
|
98
104
|
|
|
99
105
|
<script setup>
|
|
100
|
-
import {
|
|
101
|
-
|
|
106
|
+
import { computed, nextTick, ref, watch } from "vue";
|
|
107
|
+
import {
|
|
108
|
+
SelectContent,
|
|
109
|
+
SelectItem,
|
|
110
|
+
SelectItemIndicator,
|
|
111
|
+
SelectItemText,
|
|
112
|
+
SelectPortal,
|
|
113
|
+
SelectRoot,
|
|
114
|
+
SelectScrollDownButton,
|
|
115
|
+
SelectScrollUpButton,
|
|
116
|
+
SelectTrigger,
|
|
117
|
+
SelectValue,
|
|
118
|
+
SelectViewport,
|
|
119
|
+
} from "reka-ui";
|
|
120
|
+
import { X, ChevronDown, ChevronUp, Check } from "lucide-vue-next";
|
|
102
121
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
122
|
+
const props = defineProps({
|
|
123
|
+
options: { type: Array, required: true },
|
|
124
|
+
modelValue: [String, Number],
|
|
125
|
+
optionValue: { type: String, default: "value" },
|
|
126
|
+
optionLabel: { type: String, default: "label" },
|
|
127
|
+
label: String,
|
|
128
|
+
info: String,
|
|
129
|
+
error: [String, Boolean],
|
|
130
|
+
disabled: { type: Boolean, default: false },
|
|
131
|
+
placeholder: { type: String, default: "Επιλέξτε μία τιμή" },
|
|
132
|
+
id: { type: String, default: null },
|
|
133
|
+
clearable: { type: Boolean, default: true },
|
|
134
|
+
searchable: { type: Boolean, default: true },
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const emit = defineEmits(["update:modelValue"]);
|
|
138
|
+
|
|
139
|
+
const open = ref(false);
|
|
140
|
+
const searchQuery = ref("");
|
|
141
|
+
const searchInput = ref(null);
|
|
142
|
+
const canScrollAny = ref(false);
|
|
143
|
+
const generatedId = `select-${Math.random().toString(36).substr(2, 9)}`;
|
|
144
|
+
const viewportMaxHeight = "400px";
|
|
145
|
+
const defaultStyle = "w-full px-3 py-2 border rounded-lg transition shadow-sm focus:outline-none text-gray-700 focus:ring-2 focus:ring-primary/20 focus:border-primary bg-white placeholder-gray-400";
|
|
146
|
+
const errorStyle = "border-red-500 focus:ring focus:ring-red-300";
|
|
147
|
+
const disabledStyle = "!bg-gray-100 !text-gray-400 !cursor-not-allowed";
|
|
148
|
+
|
|
149
|
+
const computedId = computed(() => (props.id != null && props.id !== "" ? props.id : generatedId));
|
|
150
|
+
const dropdownId = computed(() => `${computedId.value}-listbox`);
|
|
151
|
+
|
|
152
|
+
const hasError = computed(() => props.error != null && props.error !== false);
|
|
153
|
+
const isSelectionSet = computed(() => props.modelValue !== null && props.modelValue !== undefined && props.modelValue !== "");
|
|
154
|
+
|
|
155
|
+
const internalValue = computed({
|
|
156
|
+
get() {
|
|
157
|
+
return props.modelValue === null ? undefined : props.modelValue;
|
|
130
158
|
},
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return this.id != null && this.id !== "" ? this.id : this.generatedId;
|
|
134
|
-
},
|
|
135
|
-
dropdownId() {
|
|
136
|
-
return `${this.computedId}-listbox`;
|
|
137
|
-
},
|
|
138
|
-
selectedValue() {
|
|
139
|
-
return this.modelValue;
|
|
140
|
-
},
|
|
141
|
-
isSelectionSet() {
|
|
142
|
-
return this.selectedValue !== null && this.selectedValue !== undefined && this.selectedValue !== "";
|
|
143
|
-
},
|
|
144
|
-
selectedLabel() {
|
|
145
|
-
const match = this.options.find((o) => o[this.optionValue] === this.modelValue);
|
|
146
|
-
return match ? match[this.optionLabel] : "";
|
|
147
|
-
},
|
|
148
|
-
filteredOptions() {
|
|
149
|
-
const q = this.searchQuery.trim().toLowerCase();
|
|
150
|
-
return this.options.filter((o) => String(o[this.optionLabel] ?? "").toLowerCase().includes(q));
|
|
151
|
-
},
|
|
152
|
-
hasError() {
|
|
153
|
-
return this.error != null && this.error !== false;
|
|
154
|
-
},
|
|
159
|
+
set(val) {
|
|
160
|
+
emit("update:modelValue", val ?? null);
|
|
155
161
|
},
|
|
156
|
-
|
|
157
|
-
toggleDropdown() {
|
|
158
|
-
if (this.disabled) return;
|
|
159
|
-
this.isOpen = !this.isOpen;
|
|
160
|
-
if (this.isOpen) {
|
|
161
|
-
this.$nextTick(() => {
|
|
162
|
-
this.getDropdownPosition();
|
|
163
|
-
this.$refs.searchInput?.focus();
|
|
164
|
-
});
|
|
165
|
-
} else {
|
|
166
|
-
this.searchQuery = "";
|
|
167
|
-
}
|
|
168
|
-
},
|
|
169
|
-
closeDropdown() {
|
|
170
|
-
this.isOpen = false;
|
|
171
|
-
this.searchQuery = "";
|
|
172
|
-
},
|
|
173
|
-
selectOption(option) {
|
|
174
|
-
this.$emit("update:modelValue", option[this.optionValue]);
|
|
175
|
-
this.closeDropdown();
|
|
176
|
-
},
|
|
177
|
-
clearSelection() {
|
|
178
|
-
this.$emit("update:modelValue", null);
|
|
179
|
-
this.closeDropdown();
|
|
180
|
-
},
|
|
181
|
-
handleTriggerKeydown(event) {
|
|
182
|
-
if (this.disabled) return;
|
|
183
|
-
if (event.key === "Enter" || event.key === " ") {
|
|
184
|
-
event.preventDefault();
|
|
185
|
-
this.toggleDropdown();
|
|
186
|
-
} else if (event.key === "ArrowDown" && !this.isOpen) {
|
|
187
|
-
event.preventDefault();
|
|
188
|
-
this.toggleDropdown();
|
|
189
|
-
} else if (event.key === "Escape" && this.isOpen) {
|
|
190
|
-
event.preventDefault();
|
|
191
|
-
this.closeDropdown();
|
|
192
|
-
}
|
|
193
|
-
},
|
|
194
|
-
closeOnClickOutside(e) {
|
|
195
|
-
const clickedOutside = !this.$el.contains(e.target) && !(this.$refs.dropdown && this.$refs.dropdown.contains(e.target));
|
|
162
|
+
});
|
|
196
163
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
this.getDropdownPosition();
|
|
204
|
-
},
|
|
164
|
+
const filteredOptions = computed(() => {
|
|
165
|
+
if (!props.searchable) return props.options;
|
|
166
|
+
const q = searchQuery.value.trim().toLowerCase();
|
|
167
|
+
if (!q) return props.options;
|
|
168
|
+
return props.options.filter((o) => String(o?.[props.optionLabel] ?? "").toLowerCase().includes(q));
|
|
169
|
+
});
|
|
205
170
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
171
|
+
watch(open, (val) => {
|
|
172
|
+
if (val) {
|
|
173
|
+
nextTick(() => {
|
|
174
|
+
if (props.searchable) searchInput.value?.focus();
|
|
175
|
+
updateScrollState();
|
|
176
|
+
});
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
searchQuery.value = "";
|
|
180
|
+
canScrollAny.value = false;
|
|
181
|
+
});
|
|
210
182
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const viewportWidth = window.innerWidth;
|
|
216
|
-
const pad = 8;
|
|
183
|
+
watch(filteredOptions, () => {
|
|
184
|
+
if (!open.value) return;
|
|
185
|
+
nextTick(() => updateScrollState());
|
|
186
|
+
});
|
|
217
187
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const desiredHeight = Math.min(contentHeight, maxViewportHeight);
|
|
188
|
+
function clearSelection() {
|
|
189
|
+
emit("update:modelValue", null);
|
|
190
|
+
open.value = false;
|
|
191
|
+
}
|
|
223
192
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
193
|
+
function focusFirstOrLastOption(last = false) {
|
|
194
|
+
const container = document.getElementById(dropdownId.value);
|
|
195
|
+
if (!container) return;
|
|
196
|
+
const nodes = Array.from(container.querySelectorAll('[role="option"]:not([data-disabled])'));
|
|
197
|
+
if (!nodes.length) return;
|
|
198
|
+
const target = last ? nodes[nodes.length - 1] : nodes[0];
|
|
199
|
+
target.focus();
|
|
200
|
+
}
|
|
227
201
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
202
|
+
function moveOptionFocus(step) {
|
|
203
|
+
const container = document.getElementById(dropdownId.value);
|
|
204
|
+
if (!container) return;
|
|
205
|
+
const nodes = Array.from(container.querySelectorAll('[role="option"]:not([data-disabled])'));
|
|
206
|
+
if (!nodes.length) return;
|
|
207
|
+
const activeIndex = nodes.findIndex((node) => node === document.activeElement);
|
|
208
|
+
const baseIndex = activeIndex >= 0 ? activeIndex : step > 0 ? -1 : 0;
|
|
209
|
+
const nextIndex = Math.min(Math.max(baseIndex + step, 0), nodes.length - 1);
|
|
210
|
+
nodes[nextIndex].focus();
|
|
211
|
+
}
|
|
232
212
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
213
|
+
function handleSearchKeydown(event) {
|
|
214
|
+
if (event.key === "Escape") {
|
|
215
|
+
event.preventDefault();
|
|
216
|
+
event.stopPropagation();
|
|
217
|
+
open.value = false;
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
237
220
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
258
|
-
|
|
221
|
+
if (event.key === "ArrowDown") {
|
|
222
|
+
event.preventDefault();
|
|
223
|
+
event.stopPropagation();
|
|
224
|
+
moveOptionFocus(1);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (event.key === "ArrowUp") {
|
|
229
|
+
event.preventDefault();
|
|
230
|
+
event.stopPropagation();
|
|
231
|
+
moveOptionFocus(-1);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (event.key === "Home") {
|
|
236
|
+
event.preventDefault();
|
|
237
|
+
event.stopPropagation();
|
|
238
|
+
focusFirstOrLastOption(false);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (event.key === "End") {
|
|
243
|
+
event.preventDefault();
|
|
244
|
+
event.stopPropagation();
|
|
245
|
+
focusFirstOrLastOption(true);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (event.key === "Enter") {
|
|
250
|
+
const container = document.getElementById(dropdownId.value);
|
|
251
|
+
if (!container) return;
|
|
252
|
+
const active = document.activeElement;
|
|
253
|
+
if (active && container.contains(active) && active.getAttribute("role") === "option") {
|
|
254
|
+
event.preventDefault();
|
|
255
|
+
event.stopPropagation();
|
|
256
|
+
active.click();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function updateScrollState() {
|
|
262
|
+
const container = document.getElementById(dropdownId.value);
|
|
263
|
+
const viewport = container?.querySelector("[data-reka-select-viewport]");
|
|
264
|
+
if (!viewport) {
|
|
265
|
+
canScrollAny.value = false;
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
canScrollAny.value = viewport.scrollHeight > viewport.clientHeight + 1;
|
|
269
|
+
}
|
|
259
270
|
</script>
|
package/src/ui/kToggle.vue
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div :class="labelStyle === 'inline' ? 'flex items-center' : ''">
|
|
3
3
|
<!-- Block-style label -->
|
|
4
|
-
<div v-if="showLabel && labelStyle !== 'inline'" class="block mb-1 text-sm font-bold text-primary/90" for="toggle">
|
|
4
|
+
<div v-if="showLabel && labelStyle !== 'inline'" :class="['block mb-1 text-sm font-bold', disabled ? 'text-slate-500' : 'text-primary/90']" for="toggle">
|
|
5
5
|
{{ label }}
|
|
6
6
|
</div>
|
|
7
7
|
|
|
@@ -13,15 +13,27 @@
|
|
|
13
13
|
:aria-disabled="disabled.toString()"
|
|
14
14
|
:disabled="disabled"
|
|
15
15
|
:class="[
|
|
16
|
-
'w-16 h-8 flex items-center rounded-full p-1 transition duration-300',
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
'w-16 h-8 flex items-center rounded-full p-1 transition duration-300 border',
|
|
17
|
+
disabled
|
|
18
|
+
? 'bg-slate-200 border-slate-300 cursor-not-allowed'
|
|
19
|
+
: modelValue
|
|
20
|
+
? 'bg-primary border-primary shadow-sm shadow-primary/30'
|
|
21
|
+
: 'bg-gray-400 border-gray-400',
|
|
19
22
|
]">
|
|
20
|
-
<div
|
|
23
|
+
<div
|
|
24
|
+
:class="[
|
|
25
|
+
'w-6 h-6 rounded-full shadow-md transform transition duration-300',
|
|
26
|
+
disabled ? 'bg-slate-100' : modelValue ? 'bg-white ring-2 ring-primary/20' : 'bg-white',
|
|
27
|
+
modelValue ? 'translate-x-8' : 'translate-x-0',
|
|
28
|
+
]"></div>
|
|
21
29
|
</button>
|
|
22
30
|
|
|
23
31
|
<!-- Inline-style label -->
|
|
24
|
-
<div
|
|
32
|
+
<div
|
|
33
|
+
v-if="showLabel && labelStyle === 'inline'"
|
|
34
|
+
:class="['ml-2 text-sm font-bold', disabled ? 'text-slate-500 cursor-not-allowed' : 'text-primary/90 cursor-pointer']"
|
|
35
|
+
:for="computedId"
|
|
36
|
+
@click="toggle">
|
|
25
37
|
{{ label }}
|
|
26
38
|
</div>
|
|
27
39
|
</div>
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
export const K_INPUT_THEME = {
|
|
2
2
|
label: "text-primary/90",
|
|
3
|
+
labelDisabled: "text-slate-500",
|
|
3
4
|
labelError: "text-rose-800",
|
|
4
5
|
baseInput:
|
|
5
6
|
"w-full px-3 py-2 border rounded-lg transition shadow-sm focus:outline-none text-slate-900 bg-white placeholder-gray-400 focus:ring-2 focus:ring-primary/25 focus:border-primary",
|
|
6
7
|
withRightAdornment: "pr-10",
|
|
7
8
|
withPasswordToggle: "pr-14",
|
|
8
|
-
disabled: "bg-slate-
|
|
9
|
+
disabled: "!bg-slate-50 !text-slate-400 !border-slate-200 !shadow-none cursor-not-allowed placeholder-slate-400",
|
|
9
10
|
errorInput: "border-rose-500 bg-rose-50/40 focus:border-rose-600 focus:ring-rose-500/20",
|
|
10
11
|
infoText: "text-sm text-slate-600",
|
|
11
12
|
errorText: "text-sm text-rose-700",
|
|
12
13
|
trailingIcon: "absolute w-4 h-4 text-primary/70 -translate-y-1/2 pointer-events-none right-3 top-1/2",
|
|
14
|
+
trailingIconDisabled: "text-slate-400",
|
|
13
15
|
passwordToggle: "absolute inset-y-0 right-0 flex items-center pr-3 text-slate-700",
|
|
14
16
|
passwordToggleButton:
|
|
15
17
|
"text-xs font-medium select-none rounded px-1 py-0.5 hover:bg-primary/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary",
|
|
18
|
+
passwordToggleButtonDisabled: "text-slate-400 cursor-not-allowed hover:bg-transparent",
|
|
16
19
|
};
|
package/tailwind-preset.js
CHANGED
|
@@ -8,7 +8,7 @@ const config = {
|
|
|
8
8
|
header: '#256D96',
|
|
9
9
|
background: '#FEFCF6',
|
|
10
10
|
accent: '#8C1F1F',
|
|
11
|
-
primary: '#
|
|
11
|
+
primary: '#0369A1',
|
|
12
12
|
secondary: '#f5f7fa',
|
|
13
13
|
success: '#228B22',
|
|
14
14
|
danger: '#A32626',
|
|
@@ -50,4 +50,4 @@ const config = {
|
|
|
50
50
|
plugins: [],
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
export default config
|
|
53
|
+
export default config
|