edvoyui-component-library-test-flight 0.0.245 → 0.0.246

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.
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div>
3
- <label class="flex flex-row items-center scale-100 translate-x-0 translate-y-0 eui-checkbox">
3
+ <label class="flex flex-row items-center scale-100 translate-x-0 translate-y-0 eui-checkbox" :class="{'select-none pointer-events-none opacity-50':disabled}">
4
4
  <input
5
5
  v-model="isChecked"
6
6
  type="checkbox"
@@ -79,46 +79,55 @@ watch(props, (newValue) => {
79
79
  <style lang="scss">
80
80
  .eui-checkbox .cbx-icon {
81
81
  transition: all 0.2s ease;
82
- @apply select-none cursor-pointer p-px rounded-md overflow-hidden inline-block scale-110;
82
+ @apply select-none cursor-pointer p-px rounded-md overflow-hidden inline-block scale-100;
83
83
  }
84
84
 
85
85
  .eui-checkbox .cbx-icon span {
86
86
  transform: translate3d(0, 0, 0);
87
87
  transition: all 0.2s ease;
88
- @apply float-left align-middle relative size-5 border-[1.5px] border-gray-400 rounded scale-100;
88
+ @apply float-left align-middle relative size-5 -outline-offset-1 outline outline-1 outline-gray-400 rounded scale-100;
89
89
  }
90
90
 
91
91
  .eui-checkbox .cbx-icon span > svg {
92
- top: 3.5px;
93
- left: 2.5px;
92
+ top: 5px;
93
+ left: 4px;
94
94
  stroke-width: 1.75;
95
95
  stroke-linecap: round;
96
96
  stroke-linejoin: round;
97
97
  stroke-dasharray: 18px;
98
98
  stroke-dashoffset: 18px;
99
- transition: all 0.3s ease;
100
- transition-delay: 0.1s;
99
+ transition: stroke-dashoffset 0.16s ease-out;
101
100
  transform: translate3d(0, 0, 0);
102
101
  @apply absolute fill-none stroke-white;
103
102
  }
104
103
 
105
- .eui-checkbox .input-checkbox:checked + .cbx-icon > span {
106
- animation: anim 0.4s ease;
107
- @apply bg-purple-700 border-purple-700;
104
+ .eui-checkbox .input-checkbox:checked:not(:indeterminate) + .cbx-icon > span {
105
+ animation: anim 0.18s ease-out;
106
+ @apply bg-violet-700 outline-violet-700;
108
107
  }
109
108
 
110
109
  .eui-checkbox .input-checkbox:indeterminate + .cbx-icon > span {
111
- @apply bg-purple-700 border-purple-700;
110
+ @apply bg-violet-700 outline-violet-700;
111
+ }
112
+
113
+ .eui-checkbox .input-checkbox:indeterminate + .cbx-icon span > svg {
114
+ stroke-dashoffset: 18px;
115
+ transition: none;
112
116
  }
113
117
 
114
118
  .eui-checkbox .input-checkbox:indeterminate + .cbx-icon > span::after {
115
119
  content: '';
120
+ animation: indeterminate-in 0.14s ease-out both;
116
121
  @apply absolute left-1/2 top-1/2 h-0.5 w-2.5 -translate-x-1/2 -translate-y-1/2 rounded bg-white;
117
122
  }
118
123
 
119
- .eui-checkbox .input-checkbox:checked + .cbx-icon span > svg {
124
+ .eui-checkbox .input-checkbox:checked:not(:indeterminate) + .cbx-icon span > svg {
120
125
  stroke-dashoffset: 0;
121
- transition: rotate(8deg);
126
+ }
127
+
128
+ @keyframes indeterminate-in {
129
+ from { opacity: 0; scale: 0.5 1; }
130
+ to { opacity: 1; scale: 1 1; }
122
131
  }
123
132
 
124
133
  @keyframes anim {
@@ -15,6 +15,8 @@ interface Props {
15
15
  searchable?: boolean;
16
16
  backendPagination?: boolean;
17
17
  hideNoData?: boolean;
18
+ /** Show a loading state while options are being fetched. */
19
+ loading?: boolean;
18
20
  disabled?: boolean;
19
21
  tag?: string;
20
22
  tagColor?: "None" | "Success" | "Error";
@@ -26,12 +28,13 @@ interface Props {
26
28
  ignoreDeselect?: boolean;
27
29
  popperClasses?: string;
28
30
  showCount?: boolean;
29
- /** Include an All row for every item in `items` (independent of search). */
31
+ /** Show an All row when search has results; it selects every item in `items`. Enabled by default in multiple mode. */
30
32
  showSelectAll?: boolean;
31
33
  selectAllLabel?: string;
32
34
  /** Maximum selected tags shown before the More/Less control; 0 shows all. */
33
35
  maxVisibleTags?: number;
34
36
  tagSize?: "sm" | "md" | "lg";
37
+ dropdownWidth?: string;
35
38
  }
36
39
  export type SelectItemKey = string | (string | number)[] | ((item: object, fallback?: any) => any);
37
40
  declare function __VLS_template(): {
@@ -182,6 +185,7 @@ declare const __VLS_component: import('../../../node_modules/vue').DefineCompone
182
185
  onCreate?: ((value: string) => any) | undefined;
183
186
  "onOn-deselect"?: ((value: any) => any) | undefined;
184
187
  }>, {
188
+ loading: boolean;
185
189
  disabled: boolean;
186
190
  name: string;
187
191
  items: any[];
@@ -209,6 +213,7 @@ declare const __VLS_component: import('../../../node_modules/vue').DefineCompone
209
213
  selectAllLabel: string;
210
214
  maxVisibleTags: number;
211
215
  tagSize: "sm" | "md" | "lg";
216
+ dropdownWidth: string;
212
217
  }, {}, {}, {}, string, import('../../../node_modules/vue').ComponentProvideOptions, false, {
213
218
  componentRef: HTMLDivElement;
214
219
  searchInputRef: import('../../../node_modules/vue').CreateComponentPublicInstanceWithMixins<Readonly<import('../../../node_modules/vue').ExtractPropTypes<{
@@ -86,10 +86,10 @@
86
86
  <template #content>
87
87
  <div
88
88
  v-if="showDropDown"
89
- class="w-full p-4 overflow-y-auto text-sm bg-white border-2 border-gray-100 border-solid shadow-lg rounded-3xl max-h-72 scrollbar--hide"
89
+ :class="['w-full p-4 overflow-y-auto text-sm bg-white border-2 border-gray-100 border-solid shadow-lg rounded-3xl max-h-72 scrollbar--hide min-w-28', dropdownWidth]"
90
90
  >
91
91
  <h3
92
- class="sticky z-10 p-2 mb-2 -mx-px -mt-3 font-semibold text-gray-500 bg-white text-sm/4 -top-4"
92
+ class="sticky z-10 p-2 pb-0 mb-2 -mx-px -mt-3 text-xs font-bold tracking-wide text-gray-500 uppercase bg-white -top-4"
93
93
  >
94
94
  Search Results
95
95
  </h3>
@@ -102,10 +102,10 @@
102
102
  class="space-y-0.5"
103
103
  >
104
104
  <li
105
- v-if="selectType === 'multiple' && showSelectAll && items.length"
105
+ v-if="selectType === 'multiple' && showSelectAll && !loading && filteredItems.length"
106
106
  role="option"
107
107
  :aria-selected="allItemsSelected"
108
- class="mb-2 border-b border-gray-100 pb-2"
108
+ class="pb-0.5 border-b border-gray-100"
109
109
  @click.stop
110
110
  >
111
111
  <EUICheckbox
@@ -113,9 +113,9 @@
113
113
  :indeterminate="someItemsSelected && !allItemsSelected"
114
114
  :disabled="disabled"
115
115
  :class="[
116
- 'relative flex items-center text-sm transition-colors duration-75 w-full multiple-select rounded-md font-medium [&_label]:w-full [&_label]:p-2.5',
116
+ 'relative flex items-center text-sm transition-colors duration-75 w-full multiple-select rounded-md font-medium [&_label]:w-full [&_label]:h-9 [&_label]:px-2.5',
117
117
  allItemsSelected || someItemsSelected
118
- ? 'text-gray-800 hover:bg-purple-50 hover:text-purple-600'
118
+ ? 'text-gray-800 hover:bg-violet-50 hover:text-violet-600'
119
119
  : 'text-gray-600 hover:bg-gray-50 hover:text-gray-800',
120
120
  ]"
121
121
  @toggle="handleSelectAll"
@@ -125,14 +125,24 @@
125
125
  </template>
126
126
  </EUICheckbox>
127
127
  </li>
128
- <li v-if="filteredItems.length === 0 && !hideNoData">
128
+ <li v-if="loading" role="status" aria-live="polite" class="px-2 py-4 text-sm text-gray-500">
129
+ <span class="inline-flex items-center gap-2">
130
+ <span class="inline-block border-2 border-gray-200 rounded-full size-4 border-t-violet-600 animate-spin" aria-hidden="true"></span>
131
+ Loading options...
132
+ </span>
133
+ </li>
134
+ <li v-else-if="filteredItems.length === 0 && !hideNoData">
129
135
  <slot name="no-items">
130
136
  <div
131
- v-if="internalSearch.length > 0"
132
- class="px-2 py-4 mb-6 text-sm font-medium text-center text-gray-500 min-h-32"
137
+ class="px-2 py-4 text-sm font-medium text-center text-gray-500"
133
138
  >
134
- No results found for "<strong>{{ internalSearch }}</strong
135
- >"
139
+ <template v-if="internalSearch.trim()">
140
+ No results found for "<strong>{{ internalSearch }}</strong>"
141
+ </template>
142
+ <template v-else-if="backendPagination">
143
+ Type in the search box to see results
144
+ </template>
145
+ <template v-else>No options available</template>
136
146
  </div>
137
147
  </slot>
138
148
  </li>
@@ -160,7 +170,7 @@
160
170
  <div
161
171
  :class="[
162
172
  selected(item)
163
- ? 'bg-purple-50 text-purple-600'
173
+ ? 'bg-violet-50 text-violet-600'
164
174
  : 'text-gray-600 hover:bg-gray-50 hover:text-gray-800',
165
175
  'relative py-2 px-4 flex items-center capitalize text-sm transition-colors duration-75 rounded',
166
176
  ]"
@@ -194,9 +204,9 @@
194
204
  name="checkbox"
195
205
  :class="[
196
206
  selected(item)
197
- ? 'text-gray-800 hover:bg-purple-50 hover:text-purple-600'
207
+ ? 'text-gray-800 hover:bg-violet-50 hover:text-violet-600'
198
208
  : 'text-gray-600 hover:bg-gray-50 hover:text-gray-800',
199
- 'relative flex items-center capitalize text-sm transition-colors duration-75 w-full multiple-select rounded-md font-medium [&_label]:w-full [&_label]:p-2.5',
209
+ 'relative flex items-center capitalize text-sm transition-colors duration-75 w-full multiple-select rounded-md font-medium [&_label]:w-full [&_label]:h-9 [&_label]:px-2.5',
200
210
  ]"
201
211
  @toggle="
202
212
  (checked) => handleCheckboxChange(item, checked)
@@ -225,10 +235,10 @@
225
235
  class="my-2 space-y-2"
226
236
  >
227
237
  <div
228
- class="px-0.5 gap-x-3 gap-y-2 items-center flex-wrap"
238
+ class="px-0.5 gap-2 items-center flex-wrap"
229
239
  :class="[
230
240
  effectiveTagLimit ? 'flex w-full' : 'inline-flex',
231
- tagsExpanded && effectiveTagLimit ? 'max-h-32 overflow-y-auto' : '',
241
+ tagsExpanded && effectiveTagLimit ? 'max-h-32 overflow-y-auto scrollbar--thin' : '',
232
242
  ]"
233
243
  >
234
244
  <slot
@@ -261,7 +271,7 @@
261
271
  <button
262
272
  v-if="canExpandTags && tagsExpanded"
263
273
  type="button"
264
- class="text-xs font-medium text-violet-600 hover:text-violet-700"
274
+ class="text-xs font-medium text-gray-600 hover:text-gray-700"
265
275
  @click="tagsExpanded = false"
266
276
  >
267
277
  Show less
@@ -327,6 +337,8 @@ interface Props {
327
337
  searchable?: boolean;
328
338
  backendPagination?: boolean;
329
339
  hideNoData?: boolean;
340
+ /** Show a loading state while options are being fetched. */
341
+ loading?: boolean;
330
342
  disabled?: boolean;
331
343
  tag?: string;
332
344
  tagColor?: "None" | "Success" | "Error";
@@ -338,12 +350,13 @@ interface Props {
338
350
  ignoreDeselect?: boolean;
339
351
  popperClasses?: string;
340
352
  showCount?: boolean;
341
- /** Include an All row for every item in `items` (independent of search). */
353
+ /** Show an All row when search has results; it selects every item in `items`. Enabled by default in multiple mode. */
342
354
  showSelectAll?: boolean;
343
355
  selectAllLabel?: string;
344
356
  /** Maximum selected tags shown before the More/Less control; 0 shows all. */
345
357
  maxVisibleTags?: number;
346
358
  tagSize?: "sm" | "md" | "lg";
359
+ dropdownWidth?: string,
347
360
  }
348
361
  const props = withDefaults(defineProps<Props>(), {
349
362
  items: () => [],
@@ -357,6 +370,7 @@ const props = withDefaults(defineProps<Props>(), {
357
370
  searchable: true,
358
371
  backendPagination: false,
359
372
  hideNoData: false,
373
+ loading: false,
360
374
  disabled: false,
361
375
  tag: "",
362
376
  tagColor: "None",
@@ -369,10 +383,11 @@ const props = withDefaults(defineProps<Props>(), {
369
383
  ignoreDeselect: false,
370
384
  popperClasses: "",
371
385
  showCount: false,
372
- showSelectAll: false,
386
+ showSelectAll: true,
373
387
  selectAllLabel: "All",
374
388
  maxVisibleTags: 0,
375
389
  tagSize: "md",
390
+ dropdownWidth: ''
376
391
  });
377
392
 
378
393
 
@@ -395,8 +410,8 @@ const showDropDown = ref(false);
395
410
  const options = ref<HTMLElement>();
396
411
  const searchInputRef = ref<any>();
397
412
  const componentRef = ref<HTMLElement>();
398
- const isOpening = ref(false);
399
413
  const tagsExpanded = ref(false);
414
+ let focusTimer: ReturnType<typeof setTimeout> | undefined;
400
415
 
401
416
  // Computed properties
402
417
  const selectedValues = computed({
@@ -644,26 +659,28 @@ const handleSelect = (item: any) => {
644
659
  };
645
660
 
646
661
  const handleOpen = () => {
647
- if (props.disabled) return;
648
- isOpening.value = true;
662
+ if (props.disabled || showDropDown.value) return;
649
663
 
650
664
  // Only clear search for single select, preserve it for multiple select
651
665
  if (props.selectType === "single") {
652
666
  internalSearch.value = "";
653
667
  }
654
668
  showDropDown.value = true;
669
+ window.addEventListener("scroll", handleScroll, true);
655
670
  emit("open");
656
671
 
657
672
  nextTick(() => {
658
- setTimeout(() => {
673
+ if (!showDropDown.value) return;
674
+ focusTimer = setTimeout(() => {
675
+ focusTimer = undefined;
676
+ if (!showDropDown.value) return;
659
677
  internalSearch.value = "";
660
- isOpening.value = false;
661
678
  if (searchInputRef.value?.searchInput) {
662
- searchInputRef.value.searchInput.focus();
679
+ searchInputRef.value.searchInput.focus({ preventScroll: true });
663
680
  } else if (searchInputRef.value?.$el) {
664
681
  const inputElement = searchInputRef.value.$el.querySelector("input");
665
682
  if (inputElement) {
666
- inputElement.focus();
683
+ inputElement.focus({ preventScroll: true });
667
684
  }
668
685
  }
669
686
  }, 50);
@@ -671,6 +688,11 @@ const handleOpen = () => {
671
688
  };
672
689
 
673
690
  const handleClose = () => {
691
+ window.removeEventListener("scroll", handleScroll, true);
692
+ if (focusTimer) {
693
+ clearTimeout(focusTimer);
694
+ focusTimer = undefined;
695
+ }
674
696
  if (showDropDown.value) {
675
697
  emit("blur");
676
698
  emit("close");
@@ -689,30 +711,25 @@ const handleSearchFocus = () => {
689
711
 
690
712
  // Document click handler for outside clicks
691
713
  const handleDocumentClick = (event: MouseEvent) => {
692
- // Don't close if we're currently opening
693
- if (isOpening.value) return;
694
-
695
- const target = event.target as Node;
696
-
697
- // Check if click is outside the component and popper
698
- if (showDropDown.value && componentRef.value) {
699
- // Check if click is outside the main component
700
- const isOutsideComponent = !componentRef.value.contains(target);
701
-
702
- // Check if click is outside any popper content
703
- const popperElements = document.querySelectorAll(
704
- '[data-popper-placement], .popper-defaultstyle, [role="tooltip"]'
705
- );
706
- const isOutsidePopper = Array.from(popperElements).every(
707
- (popper) => !popper.contains(target)
708
- );
709
-
710
- if (isOutsideComponent && isOutsidePopper) {
711
- handleClose();
712
- }
714
+ // The trigger can be replaced during this click. composedPath retains its
715
+ // original ancestors, so an inside click stays inside after that DOM update.
716
+ if (
717
+ showDropDown.value &&
718
+ componentRef.value &&
719
+ !event.composedPath().includes(componentRef.value)
720
+ ) {
721
+ handleClose();
713
722
  }
714
723
  };
715
724
 
725
+ const handleScroll = (event: Event) => {
726
+ if (!showDropDown.value) return;
727
+ const target = event.target;
728
+ // Keep the options usable; scrolling any ancestor or the page closes them.
729
+ if (target instanceof Node && componentRef.value?.contains(target)) return;
730
+ handleClose();
731
+ };
732
+
716
733
  // Lifecycle hooks
717
734
  onMounted(() => {
718
735
  document.addEventListener("click", handleDocumentClick);
@@ -720,6 +737,8 @@ onMounted(() => {
720
737
 
721
738
  onUnmounted(() => {
722
739
  document.removeEventListener("click", handleDocumentClick);
740
+ window.removeEventListener("scroll", handleScroll, true);
741
+ if (focusTimer) clearTimeout(focusTimer);
723
742
  });
724
743
 
725
744
  // Watchers