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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ketekny-ui-kit",
3
3
  "type": "module",
4
- "version": "1.0.140",
4
+ "version": "1.0.142",
5
5
  "description": "A Vue 3 UI component library with Tailwind CSS styling",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -1,66 +1,116 @@
1
1
  <template>
2
- <div class="flex flex-wrap items-center gap-3">
3
- <div class="text-sm font-medium text-slate-500">
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
- <kButton
8
- label="First"
9
- icon="ChevronsLeft"
10
- variant="soft"
11
- :disabled="loading || currentPage <= 1"
12
- @click="emit('first')"
13
- />
14
-
15
- <kButton
16
- label="Previous"
17
- icon="ChevronLeft"
18
- variant="soft"
19
- :disabled="loading || currentPage <= 1"
20
- @click="emit('previous')"
21
- />
22
-
23
- <div class="flex items-center gap-1">
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="px-2 text-sm font-medium text-slate-400"
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
- <kButton
75
+ <button
33
76
  v-else
34
- :label="String(item.page)"
35
- variant="soft"
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
- <kButton
44
- label="Next"
45
- icon="ChevronRight"
46
- variant="soft"
47
- :disabled="loading || currentPage >= safeTotalPages"
48
- @click="emit('next')"
49
- />
50
-
51
- <kButton
52
- label="Last"
53
- icon="ChevronsRight"
54
- variant="soft"
55
- :disabled="loading || currentPage >= safeTotalPages"
56
- @click="emit('last')"
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 kButton from './kButton.vue'
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])
@@ -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-scroll"
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-8 pr-3 text-base cursor-pointer outline-none select-none text-slate-700 dark:text-slate-200
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="absolute left-2 flex items-center justify-center w-4 h-4 shrink-0">
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-3.5 h-3.5" />
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: stable;
435
+ scrollbar-gutter: auto;
422
436
  scrollbar-width: auto !important;
423
437
  scrollbar-color: #475569 #cbd5e1 !important;
424
438
  -ms-overflow-style: auto !important;