ketekny-ui-kit 1.0.139 → 1.0.140

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/ui/kSelect.vue +54 -3
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ketekny-ui-kit",
3
3
  "type": "module",
4
- "version": "1.0.139",
4
+ "version": "1.0.140",
5
5
  "description": "A Vue 3 UI component library with Tailwind CSS styling",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -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 flex-wrap items-center gap-1 pr-10">
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 selectedOptions"
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
@@ -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() {