ketekny-ui-kit 1.0.141 → 1.0.143

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.141",
4
+ "version": "1.0.143",
5
5
  "description": "A Vue 3 UI component library with Tailwind CSS styling",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -158,6 +158,7 @@ export default {
158
158
  const favorites = []
159
159
  const collect = (items) => {
160
160
  for (const item of items) {
161
+ if (item?.isDivider) continue
161
162
  if (item?.children?.length) {
162
163
  collect(item.children)
163
164
  continue
@@ -329,7 +330,7 @@ export default {
329
330
  (routeFullPath && itemPath === routeFullPath) ||
330
331
  (routePath && normalizedItemPath === routePath)
331
332
 
332
- if (matchesPath) {
333
+ if (matchesPath && item.selectable !== false) {
333
334
  return {
334
335
  id: item.id,
335
336
  parentIds,
@@ -350,8 +351,9 @@ export default {
350
351
  return
351
352
  }
352
353
 
353
- if (!this.activeId && this.mainMenu[0]?.id) {
354
- this.activeId = this.mainMenu[0].id
354
+ if (!this.activeId) {
355
+ const firstSelectableItem = this.mainMenu.find((item) => !item.isDivider && item.selectable !== false)
356
+ if (firstSelectableItem?.id) this.activeId = firstSelectableItem.id
355
357
  }
356
358
  },
357
359
  toggleCollapsed() {
@@ -375,6 +377,7 @@ export default {
375
377
  this.openIds = new Set([...this.openIds, id])
376
378
  },
377
379
  select(itemOrId) {
380
+ if (typeof itemOrId !== 'string' && (itemOrId?.isDivider || itemOrId?.selectable === false)) return
378
381
  const id = typeof itemOrId === 'string' ? itemOrId : itemOrId?.id
379
382
  if (!id) return
380
383
  this.activeId = id
@@ -399,7 +402,8 @@ export default {
399
402
  filterTree(nodes, q) {
400
403
  const result = []
401
404
  for (const node of nodes) {
402
- const matches = node.label.toLowerCase().includes(q)
405
+ if (node.isDivider) continue
406
+ const matches = String(node.label ?? '').toLowerCase().includes(q)
403
407
  const matchedChildren = node.children ? this.filterTree(node.children, q) : []
404
408
  if (matches) result.push(node)
405
409
  else if (matchedChildren.length) result.push({ ...node, children: matchedChildren })
@@ -30,6 +30,9 @@ export default {
30
30
  hasChildren() {
31
31
  return Array.isArray(this.item.children) && this.item.children.length > 0
32
32
  },
33
+ isDivider() {
34
+ return this.item.isDivider === true
35
+ },
33
36
  isOpen() {
34
37
  return this.openIds.has(this.item.id)
35
38
  },
@@ -41,7 +44,7 @@ export default {
41
44
  return this.hasChildren && this.containsActive(this.item)
42
45
  },
43
46
  isActiveLeaf() {
44
- return !this.hasChildren && this.item.id === this.activeId
47
+ return !this.hasChildren && this.item.selectable !== false && this.item.id === this.activeId
45
48
  },
46
49
  isActive() {
47
50
  return this.isActiveLeaf || this.isActiveTrail
@@ -82,7 +85,7 @@ export default {
82
85
  },
83
86
  methods: {
84
87
  containsActive(node) {
85
- if (node.id === this.activeId) return true
88
+ if (node.selectable !== false && node.id === this.activeId) return true
86
89
  if (!node.children) return false
87
90
  return node.children.some((child) => this.containsActive(child))
88
91
  },
@@ -125,7 +128,18 @@ export default {
125
128
  </script>
126
129
 
127
130
  <template>
128
- <li :class="isCollapsedTop ? 'group/fly relative' : ''">
131
+ <li v-if="isDivider" role="separator" :aria-label="item.label || undefined" class="py-2">
132
+ <div v-if="isCollapsedTop" class="mx-2 border-t border-[var(--sidebar-border)]" />
133
+ <div v-else class="flex items-center gap-2 px-3">
134
+ <span class="h-px flex-1 bg-[var(--sidebar-border)]" />
135
+ <span v-if="item.label" class="shrink-0 text-xs font-medium uppercase tracking-wider text-[var(--sidebar-text-muted)]">
136
+ {{ item.label }}
137
+ </span>
138
+ <span v-if="item.label" class="h-px flex-1 bg-[var(--sidebar-border)]" />
139
+ </div>
140
+ </li>
141
+
142
+ <li v-else :class="isCollapsedTop ? 'group/fly relative' : ''">
129
143
  <button
130
144
  type="button"
131
145
  :style="isCollapsedTop ? undefined : indentStyle"
@@ -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"
@@ -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
  }