ketekny-ui-kit 1.0.139 → 1.0.141
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 +56 -5
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
|
@@ -17,13 +17,14 @@
|
|
|
17
17
|
:placeholder="placeholder"
|
|
18
18
|
class="block pr-10 truncate text-gray-700 data-[placeholder]:text-gray-400 dark:text-slate-100 dark:data-[placeholder]:text-slate-500"
|
|
19
19
|
/>
|
|
20
|
-
<div v-else class="flex
|
|
20
|
+
<div v-else ref="selectedContainer" class="flex items-center gap-0.5 pr-10 overflow-hidden whitespace-nowrap">
|
|
21
21
|
<span v-if="selectedOptions.length === 0" class="text-gray-400 dark:text-slate-500">
|
|
22
22
|
{{ placeholder }}
|
|
23
23
|
</span>
|
|
24
24
|
<kChip
|
|
25
|
-
v-for="option in
|
|
25
|
+
v-for="option in visibleSelectedOptions"
|
|
26
26
|
:key="option[optionValue]"
|
|
27
|
+
class="shrink-0"
|
|
27
28
|
size="small"
|
|
28
29
|
>
|
|
29
30
|
<span>{{ option[optionLabel] }}</span>
|
|
@@ -38,6 +39,9 @@
|
|
|
38
39
|
<X class="w-3 h-3" />
|
|
39
40
|
</button>
|
|
40
41
|
</kChip>
|
|
42
|
+
<span v-if="hiddenSelectedCount > 0" class="text-xs text-gray-500 shrink-0 dark:text-slate-400">
|
|
43
|
+
+{{ hiddenSelectedCount }}...
|
|
44
|
+
</span>
|
|
41
45
|
</div>
|
|
42
46
|
|
|
43
47
|
<button
|
|
@@ -82,7 +86,7 @@
|
|
|
82
86
|
|
|
83
87
|
<SelectViewport
|
|
84
88
|
ref="viewportRef"
|
|
85
|
-
class="select-viewport p-1 overflow-y-
|
|
89
|
+
class="select-viewport p-1 overflow-y-auto"
|
|
86
90
|
:style="{ maxHeight: resolvedDropdownHeight }"
|
|
87
91
|
@keydown="handleViewportKeydown"
|
|
88
92
|
>
|
|
@@ -112,6 +116,14 @@
|
|
|
112
116
|
</SelectPortal>
|
|
113
117
|
</SelectRoot>
|
|
114
118
|
|
|
119
|
+
<div ref="chipMeasureContainer" class="fixed invisible flex items-center gap-0.5 pointer-events-none" aria-hidden="true">
|
|
120
|
+
<kChip v-for="option in selectedOptions" :key="option[optionValue]" size="small">
|
|
121
|
+
<span>{{ option[optionLabel] }}</span>
|
|
122
|
+
<X class="w-3 h-3 ml-1" />
|
|
123
|
+
</kChip>
|
|
124
|
+
<span ref="overflowLabelMeasure" class="text-xs">+{{ selectedOptions.length }}...</span>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
115
127
|
<div class="mt-1 text-sm text-red-500 dark:text-rose-400" v-if="hasError && error !== true && error !== ''">
|
|
116
128
|
{{ error }}
|
|
117
129
|
</div>
|
|
@@ -123,7 +135,7 @@
|
|
|
123
135
|
</template>
|
|
124
136
|
|
|
125
137
|
<script setup>
|
|
126
|
-
import { computed, nextTick, ref, watch } from "vue";
|
|
138
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
|
127
139
|
import {
|
|
128
140
|
SelectContent,
|
|
129
141
|
SelectItem,
|
|
@@ -161,6 +173,11 @@ const open = ref(false);
|
|
|
161
173
|
const searchQuery = ref("");
|
|
162
174
|
const searchInput = ref(null);
|
|
163
175
|
const viewportRef = ref(null);
|
|
176
|
+
const selectedContainer = ref(null);
|
|
177
|
+
const chipMeasureContainer = ref(null);
|
|
178
|
+
const overflowLabelMeasure = ref(null);
|
|
179
|
+
const visibleSelectedCount = ref(0);
|
|
180
|
+
let selectedContainerObserver;
|
|
164
181
|
const generatedId = `select-${Math.random().toString(36).substr(2, 9)}`;
|
|
165
182
|
const defaultStyle = "w-full px-3 py-2 border rounded-lg transition shadow-sm focus-visible:outline-none text-gray-700 focus-visible:ring-2 focus-visible:ring-primary/20 focus-visible:border-primary bg-white placeholder-gray-400 dark:bg-slate-800 dark:text-slate-100 dark:border-slate-600 dark:placeholder-slate-500";
|
|
166
183
|
const errorStyle = "border-red-500 focus-visible:ring focus-visible:ring-red-300 dark:border-rose-500 dark:focus-visible:ring-rose-500/20";
|
|
@@ -213,6 +230,40 @@ const selectedOptions = computed(() => {
|
|
|
213
230
|
.map((value) => props.options.find((option) => option?.[props.optionValue] === value))
|
|
214
231
|
.filter(Boolean);
|
|
215
232
|
});
|
|
233
|
+
const visibleSelectedOptions = computed(() => selectedOptions.value.slice(0, visibleSelectedCount.value));
|
|
234
|
+
const hiddenSelectedCount = computed(() => selectedOptions.value.length - visibleSelectedOptions.value.length);
|
|
235
|
+
|
|
236
|
+
function updateVisibleSelectedCount() {
|
|
237
|
+
if (!props.multiple || !selectedContainer.value || !chipMeasureContainer.value) return;
|
|
238
|
+
|
|
239
|
+
const chipElements = Array.from(chipMeasureContainer.value.children).slice(0, selectedOptions.value.length);
|
|
240
|
+
const availableWidth = selectedContainer.value.clientWidth;
|
|
241
|
+
const overflowLabelWidth = overflowLabelMeasure.value?.offsetWidth ?? 0;
|
|
242
|
+
const gap = 2;
|
|
243
|
+
let usedWidth = 0;
|
|
244
|
+
let count = 0;
|
|
245
|
+
|
|
246
|
+
for (const chip of chipElements) {
|
|
247
|
+
const nextWidth = usedWidth + (count > 0 ? gap : 0) + chip.offsetWidth;
|
|
248
|
+
const remainingAfterNext = selectedOptions.value.length - count - 1;
|
|
249
|
+
const reservedWidth = remainingAfterNext > 0 ? gap + overflowLabelWidth : 0;
|
|
250
|
+
if (nextWidth + reservedWidth > availableWidth) break;
|
|
251
|
+
usedWidth = nextWidth;
|
|
252
|
+
count += 1;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
visibleSelectedCount.value = count;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
onMounted(() => {
|
|
259
|
+
selectedContainerObserver = new ResizeObserver(updateVisibleSelectedCount);
|
|
260
|
+
if (selectedContainer.value) selectedContainerObserver.observe(selectedContainer.value);
|
|
261
|
+
nextTick(updateVisibleSelectedCount);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
onBeforeUnmount(() => selectedContainerObserver?.disconnect());
|
|
265
|
+
|
|
266
|
+
watch(selectedOptions, () => nextTick(updateVisibleSelectedCount), { deep: true });
|
|
216
267
|
|
|
217
268
|
const internalValue = computed({
|
|
218
269
|
get() {
|
|
@@ -367,7 +418,7 @@ function handleViewportKeydown(event) {
|
|
|
367
418
|
}
|
|
368
419
|
|
|
369
420
|
:deep([data-reka-select-viewport]) {
|
|
370
|
-
scrollbar-gutter:
|
|
421
|
+
scrollbar-gutter: auto;
|
|
371
422
|
scrollbar-width: auto !important;
|
|
372
423
|
scrollbar-color: #475569 #cbd5e1 !important;
|
|
373
424
|
-ms-overflow-style: auto !important;
|