ketekny-ui-kit 1.0.140 → 1.0.142
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 +1 -1
- package/src/ui/kPagination.vue +138 -43
- package/src/ui/kSelect.vue +21 -7
package/package.json
CHANGED
package/src/ui/kPagination.vue
CHANGED
|
@@ -1,66 +1,116 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="flex flex-wrap items-center gap-3">
|
|
3
|
-
<div class="
|
|
2
|
+
<div class="flex flex-wrap items-center" :class="size === 'small' ? 'gap-2' : 'gap-3'">
|
|
3
|
+
<div class="font-medium text-slate-500" :class="size === 'small' ? 'text-xs' : 'text-sm'">
|
|
4
4
|
{{ paginationLabel }}
|
|
5
5
|
</div>
|
|
6
6
|
|
|
7
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
7
|
+
<div v-if="showPageSize" class="w-36 shrink-0">
|
|
8
|
+
<kSelect
|
|
9
|
+
v-model="pageSizeModel"
|
|
10
|
+
:options="normalizedPageSizeOptions"
|
|
11
|
+
:clearable="false"
|
|
12
|
+
:searchable="false"
|
|
13
|
+
aria-label="Μέγεθος σελίδας"
|
|
14
|
+
/>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div v-if="compact" class="inline-flex items-center gap-3 text-sm text-slate-600 dark:text-slate-300">
|
|
18
|
+
<button
|
|
19
|
+
type="button"
|
|
20
|
+
class="inline-flex items-center justify-center w-8 h-8 rounded-md transition-colors hover:bg-slate-100 disabled:cursor-not-allowed disabled:text-slate-300 dark:hover:bg-slate-800 dark:disabled:text-slate-600"
|
|
21
|
+
:disabled="loading || currentPage <= 1"
|
|
22
|
+
aria-label="Προηγούμενη σελίδα"
|
|
23
|
+
@click="emit('previous')"
|
|
24
|
+
>
|
|
25
|
+
<ChevronLeft class="w-4 h-4" />
|
|
26
|
+
</button>
|
|
27
|
+
|
|
28
|
+
<span class="inline-flex items-center justify-center min-w-10 h-10 px-3 border border-slate-200 rounded-lg text-slate-700 bg-white dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200">
|
|
29
|
+
{{ currentPage }}
|
|
30
|
+
</span>
|
|
31
|
+
<span>από</span>
|
|
32
|
+
<span>{{ safeTotalPages }}</span>
|
|
33
|
+
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
class="inline-flex items-center justify-center w-8 h-8 rounded-md transition-colors hover:bg-slate-100 disabled:cursor-not-allowed disabled:text-slate-300 dark:hover:bg-slate-800 dark:disabled:text-slate-600"
|
|
37
|
+
:disabled="loading || currentPage >= safeTotalPages"
|
|
38
|
+
aria-label="Επόμενη σελίδα"
|
|
39
|
+
@click="emit('next')"
|
|
40
|
+
>
|
|
41
|
+
<ChevronRight class="w-4 h-4" />
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div v-else class="inline-flex overflow-hidden border border-slate-200 rounded-lg divide-x divide-slate-200 dark:border-slate-700 dark:divide-slate-700">
|
|
46
|
+
<button
|
|
47
|
+
type="button"
|
|
48
|
+
:class="controlClass"
|
|
49
|
+
:disabled="loading || currentPage <= 1"
|
|
50
|
+
@click="emit('first')"
|
|
51
|
+
>
|
|
52
|
+
<ChevronsLeft :class="iconClass" />
|
|
53
|
+
<span>Πρώτη</span>
|
|
54
|
+
</button>
|
|
55
|
+
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
:class="controlClass"
|
|
59
|
+
:disabled="loading || currentPage <= 1"
|
|
60
|
+
@click="emit('previous')"
|
|
61
|
+
>
|
|
62
|
+
<ChevronLeft :class="iconClass" />
|
|
63
|
+
<span>Προηγ.</span>
|
|
64
|
+
</button>
|
|
65
|
+
|
|
24
66
|
<template v-for="item in paginationItems" :key="item.key">
|
|
25
67
|
<span
|
|
26
68
|
v-if="item.type === 'ellipsis'"
|
|
27
|
-
class="
|
|
69
|
+
class="flex items-center justify-center font-medium text-slate-400 bg-white dark:bg-slate-900"
|
|
70
|
+
:class="segmentClass"
|
|
28
71
|
>
|
|
29
72
|
...
|
|
30
73
|
</span>
|
|
31
74
|
|
|
32
|
-
<
|
|
75
|
+
<button
|
|
33
76
|
v-else
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
:class="item.active ? 'ring-2 ring-cyan-200' : ''"
|
|
77
|
+
type="button"
|
|
78
|
+
:class="[pageClass, item.active ? activePageClass : 'hover:bg-slate-50 dark:hover:bg-slate-800']"
|
|
37
79
|
:disabled="loading || item.active"
|
|
38
80
|
@click="emit('page', item.page)"
|
|
39
|
-
|
|
81
|
+
:aria-current="item.active ? 'page' : undefined"
|
|
82
|
+
>
|
|
83
|
+
{{ item.page }}
|
|
84
|
+
</button>
|
|
40
85
|
</template>
|
|
41
|
-
</div>
|
|
42
86
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
:class="controlClass"
|
|
90
|
+
:disabled="loading || currentPage >= safeTotalPages"
|
|
91
|
+
@click="emit('next')"
|
|
92
|
+
>
|
|
93
|
+
<span>Επόμ.</span>
|
|
94
|
+
<ChevronRight :class="iconClass" />
|
|
95
|
+
</button>
|
|
96
|
+
|
|
97
|
+
<button
|
|
98
|
+
type="button"
|
|
99
|
+
:class="controlClass"
|
|
100
|
+
:disabled="loading || currentPage >= safeTotalPages"
|
|
101
|
+
@click="emit('last')"
|
|
102
|
+
>
|
|
103
|
+
<span>Τελευταία</span>
|
|
104
|
+
<ChevronsRight :class="iconClass" />
|
|
105
|
+
</button>
|
|
106
|
+
</div>
|
|
58
107
|
</div>
|
|
59
108
|
</template>
|
|
60
109
|
|
|
61
110
|
<script setup>
|
|
62
111
|
import { computed } from 'vue'
|
|
63
|
-
import
|
|
112
|
+
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from '@lucide/vue'
|
|
113
|
+
import kSelect from './kSelect.vue'
|
|
64
114
|
|
|
65
115
|
const props = defineProps({
|
|
66
116
|
paginationLabel: {
|
|
@@ -79,12 +129,57 @@ const props = defineProps({
|
|
|
79
129
|
type: Boolean,
|
|
80
130
|
default: false,
|
|
81
131
|
},
|
|
132
|
+
size: {
|
|
133
|
+
type: String,
|
|
134
|
+
default: 'default',
|
|
135
|
+
validator: (value) => ['default', 'small'].includes(value),
|
|
136
|
+
},
|
|
137
|
+
compact: {
|
|
138
|
+
type: Boolean,
|
|
139
|
+
default: false,
|
|
140
|
+
},
|
|
141
|
+
pageSize: {
|
|
142
|
+
type: Number,
|
|
143
|
+
default: 10,
|
|
144
|
+
},
|
|
145
|
+
pageSizeOptions: {
|
|
146
|
+
type: Array,
|
|
147
|
+
default: () => [10, 25, 50, 100],
|
|
148
|
+
},
|
|
149
|
+
showPageSize: {
|
|
150
|
+
type: Boolean,
|
|
151
|
+
default: true,
|
|
152
|
+
},
|
|
82
153
|
})
|
|
83
154
|
|
|
84
|
-
const emit = defineEmits(['first', 'previous', 'page', 'next', 'last'])
|
|
155
|
+
const emit = defineEmits(['first', 'previous', 'page', 'next', 'last', 'update:pageSize'])
|
|
85
156
|
|
|
86
157
|
const safeTotalPages = computed(() => Math.max(1, Math.trunc(props.totalPages || 0)))
|
|
87
158
|
const currentPage = computed(() => Math.min(Math.max(Math.trunc(props.page || 1), 1), safeTotalPages.value))
|
|
159
|
+
const segmentClass = computed(() => props.size === 'small' ? 'h-8 min-w-8 px-2 text-xs' : 'h-10 min-w-9 px-3 text-sm')
|
|
160
|
+
const controlClass = computed(() => [
|
|
161
|
+
segmentClass.value,
|
|
162
|
+
'inline-flex items-center justify-center gap-1 bg-white text-slate-700 transition-colors',
|
|
163
|
+
'hover:bg-slate-50 disabled:cursor-not-allowed disabled:text-slate-300 disabled:hover:bg-white',
|
|
164
|
+
'dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800 dark:disabled:text-slate-600 dark:disabled:hover:bg-slate-900',
|
|
165
|
+
])
|
|
166
|
+
const pageClass = computed(() => [
|
|
167
|
+
segmentClass.value,
|
|
168
|
+
'inline-flex items-center justify-center bg-white text-slate-700 transition-colors',
|
|
169
|
+
'disabled:cursor-default dark:bg-slate-900 dark:text-slate-200',
|
|
170
|
+
])
|
|
171
|
+
const activePageClass = 'bg-slate-100 text-slate-900 dark:bg-slate-700 dark:text-white'
|
|
172
|
+
const iconClass = computed(() => props.size === 'small' ? 'w-3.5 h-3.5' : 'w-4 h-4')
|
|
173
|
+
const normalizedPageSizeOptions = computed(() =>
|
|
174
|
+
props.pageSizeOptions.map((value) => ({
|
|
175
|
+
value: Number(value),
|
|
176
|
+
label: `${value} / σελίδα`,
|
|
177
|
+
}))
|
|
178
|
+
)
|
|
179
|
+
const pageSizeModel = computed({
|
|
180
|
+
get: () => props.pageSize,
|
|
181
|
+
set: (value) => emit('update:pageSize', Number(value)),
|
|
182
|
+
})
|
|
88
183
|
|
|
89
184
|
const paginationItems = computed(() => {
|
|
90
185
|
const pages = new Set([1, safeTotalPages.value, currentPage.value - 1, currentPage.value, currentPage.value + 1])
|
package/src/ui/kSelect.vue
CHANGED
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
ref="searchInput"
|
|
75
75
|
type="text"
|
|
76
76
|
v-model="searchQuery"
|
|
77
|
+
autofocus
|
|
77
78
|
placeholder="Αναζήτηση..."
|
|
78
79
|
class="w-full pl-9 pr-3 py-2 text-base border border-gray-200 rounded-lg focus-visible:outline-none focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-primary/20 transition-colors placeholder-gray-400 dark:bg-slate-700 dark:text-slate-100 dark:border-slate-600 dark:placeholder-slate-500"
|
|
79
80
|
role="searchbox"
|
|
@@ -86,7 +87,7 @@
|
|
|
86
87
|
|
|
87
88
|
<SelectViewport
|
|
88
89
|
ref="viewportRef"
|
|
89
|
-
class="select-viewport p-1 overflow-y-
|
|
90
|
+
class="select-viewport p-1 overflow-y-auto"
|
|
90
91
|
:style="{ maxHeight: resolvedDropdownHeight }"
|
|
91
92
|
@keydown="handleViewportKeydown"
|
|
92
93
|
>
|
|
@@ -94,17 +95,20 @@
|
|
|
94
95
|
v-for="(option, index) in filteredOptions"
|
|
95
96
|
:key="option[optionValue]"
|
|
96
97
|
:value="option[optionValue]"
|
|
97
|
-
class="select-item relative flex items-center gap-1.5 rounded-[4px] py-2.5 pl-
|
|
98
|
+
class="select-item relative flex items-center gap-1.5 rounded-[4px] py-2.5 pl-3 pr-8 text-base cursor-pointer outline-none select-none text-slate-700 dark:text-slate-200
|
|
98
99
|
data-[highlighted]:bg-primary data-[highlighted]:text-white
|
|
99
100
|
data-[state=checked]:bg-primary/5 data-[state=checked]:text-primary dark:data-[state=checked]:bg-slate-700 dark:data-[state=checked]:text-primary
|
|
100
101
|
data-[state=checked]:data-[highlighted]:bg-primary data-[state=checked]:data-[highlighted]:text-white"
|
|
101
102
|
>
|
|
102
|
-
<span class="
|
|
103
|
+
<span v-if="hasOptionIcons" class="flex items-center justify-center w-4 h-4 shrink-0">
|
|
104
|
+
<kIcon v-if="option[optionIcon]" :name="option[optionIcon]" class="w-4 h-4 !text-current" />
|
|
105
|
+
</span>
|
|
106
|
+
<SelectItemText class="truncate">{{ option[optionLabel] }}</SelectItemText>
|
|
107
|
+
<span class="absolute right-2 flex items-center justify-center w-5 h-5 shrink-0">
|
|
103
108
|
<SelectItemIndicator>
|
|
104
|
-
<Check class="w-
|
|
109
|
+
<Check class="w-4 h-4 stroke-[2.5]" />
|
|
105
110
|
</SelectItemIndicator>
|
|
106
111
|
</span>
|
|
107
|
-
<SelectItemText class="truncate">{{ option[optionLabel] }}</SelectItemText>
|
|
108
112
|
</SelectItem>
|
|
109
113
|
|
|
110
114
|
<div v-if="filteredOptions.length === 0" class="flex flex-col items-center gap-1.5 px-3 py-4 text-base text-gray-400 text-center dark:text-slate-500">
|
|
@@ -149,6 +153,7 @@ import {
|
|
|
149
153
|
} from "reka-ui";
|
|
150
154
|
import { X, ChevronDown, Check, Search, SearchX } from "@lucide/vue";
|
|
151
155
|
import kChip from "./kChip.vue";
|
|
156
|
+
import kIcon from "./kIcon.vue";
|
|
152
157
|
|
|
153
158
|
const props = defineProps({
|
|
154
159
|
options: { type: Array, required: true },
|
|
@@ -156,6 +161,7 @@ const props = defineProps({
|
|
|
156
161
|
multiple: { type: Boolean, default: false },
|
|
157
162
|
optionValue: { type: String, default: "value" },
|
|
158
163
|
optionLabel: { type: String, default: "label" },
|
|
164
|
+
optionIcon: { type: String, default: "icon" },
|
|
159
165
|
label: String,
|
|
160
166
|
info: String,
|
|
161
167
|
error: [String, Boolean],
|
|
@@ -224,6 +230,7 @@ const isSelectionSet = computed(() => {
|
|
|
224
230
|
return props.modelValue !== null && props.modelValue !== undefined && props.modelValue !== "";
|
|
225
231
|
});
|
|
226
232
|
const resolvedDropdownHeight = computed(() => normalizeDropdownHeight(props.dropdownHeight));
|
|
233
|
+
const hasOptionIcons = computed(() => props.options.some((option) => Boolean(option?.[props.optionIcon])));
|
|
227
234
|
const selectedOptions = computed(() => {
|
|
228
235
|
if (!props.multiple || !Array.isArray(props.modelValue)) return [];
|
|
229
236
|
return props.modelValue
|
|
@@ -294,10 +301,17 @@ const filteredOptions = computed(() => {
|
|
|
294
301
|
watch(open, (val) => {
|
|
295
302
|
if (val) {
|
|
296
303
|
nextTick(() => {
|
|
297
|
-
if (props.searchable) searchInput.value?.focus();
|
|
298
304
|
const el = viewportRef.value?.$el ?? viewportRef.value;
|
|
299
305
|
const checked = el?.querySelector('[data-state="checked"]');
|
|
300
306
|
checked?.scrollIntoView({ block: "nearest" });
|
|
307
|
+
|
|
308
|
+
if (props.searchable) {
|
|
309
|
+
requestAnimationFrame(() => {
|
|
310
|
+
requestAnimationFrame(() => {
|
|
311
|
+
if (open.value) searchInput.value?.focus({ preventScroll: true });
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
}
|
|
301
315
|
});
|
|
302
316
|
return;
|
|
303
317
|
}
|
|
@@ -418,7 +432,7 @@ function handleViewportKeydown(event) {
|
|
|
418
432
|
}
|
|
419
433
|
|
|
420
434
|
:deep([data-reka-select-viewport]) {
|
|
421
|
-
scrollbar-gutter:
|
|
435
|
+
scrollbar-gutter: auto;
|
|
422
436
|
scrollbar-width: auto !important;
|
|
423
437
|
scrollbar-color: #475569 #cbd5e1 !important;
|
|
424
438
|
-ms-overflow-style: auto !important;
|