@bagelink/vue 1.4.71 → 1.4.77

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": "@bagelink/vue",
3
3
  "type": "module",
4
- "version": "1.4.71",
4
+ "version": "1.4.77",
5
5
  "description": "Bagel core sdk packages",
6
6
  "author": {
7
7
  "name": "Neveh Allon",
@@ -2,7 +2,7 @@
2
2
  import type { IconType, ThemeType } from '@bagelink/vue'
3
3
  import type { SetupContext } from 'vue'
4
4
  import { Icon, Loading } from '@bagelink/vue'
5
- import { useSlots } from 'vue'
5
+ import { useSlots, ref, onMounted, onUnmounted } from 'vue'
6
6
  import { RouterLink } from 'vue-router'
7
7
 
8
8
  const props = withDefaults(
@@ -10,6 +10,8 @@ const props = withDefaults(
10
10
  disabled?: boolean
11
11
  icon?: IconType
12
12
  iconEnd?: IconType
13
+ iconSize?: number | string
14
+ iconMobileSize?: number | string
13
15
  color?: ThemeType
14
16
  theme?: ThemeType
15
17
  flat?: boolean
@@ -41,6 +43,26 @@ const props = withDefaults(
41
43
  },
42
44
  )
43
45
 
46
+ const isMobileScreen = ref(false)
47
+
48
+ const checkMobile = () => {
49
+ isMobileScreen.value = window.innerWidth <= 910
50
+ }
51
+
52
+ onMounted(() => {
53
+ checkMobile()
54
+ window.addEventListener('resize', checkMobile)
55
+ })
56
+
57
+ onUnmounted(() => {
58
+ window.removeEventListener('resize', checkMobile)
59
+ })
60
+
61
+ const iconSizeComputed = $computed(() => {
62
+ if (props.iconSize !== undefined) return props.iconSize
63
+ return isMobileScreen.value ? 1 : 1.2
64
+ })
65
+
44
66
  const isComponent = $computed(() => {
45
67
  if (props.disabled) return props.is
46
68
  if (props.to) return RouterLink
@@ -64,34 +86,25 @@ const slots: SetupContext['slots'] = useSlots()
64
86
  </script>
65
87
 
66
88
  <template>
67
- <component
68
- :is="isComponent"
69
- v-ripple="ripple" v-bind="bind" :disabled="disabled"
70
- class="bgl_btn"
71
- :class="{
72
- 'bgl_btn-icon': icon && !slots.default && !value,
73
- thin,
74
- round,
75
- 'bgl_btn_flat': flat,
76
- 'bgl_btn-border': border || outline,
77
- [`bgl_btn-${color}`]: color,
78
- [`bgl_btn-${theme}`]: theme,
79
- }"
80
- :tabindex="disabled ? -1 : 0"
81
- @click.stop="onClick"
82
- @keydown.enter="onClick"
83
- @keydown.space="onClick"
84
- >
85
- <Loading v-if="loading" class="h-100p" size="15" />
86
- <div v-else class="bgl_btn-flex">
87
- <Icon v-if="icon" :icon="icon" class="transition-400" />
88
- <slot />
89
- <template v-if="!slots.default && value">
90
- {{ value }}
91
- </template>
92
- <Icon v-if="iconEnd" :icon="iconEnd" class="transition-400" />
93
- </div>
94
- </component>
89
+ <component :is="isComponent" v-ripple="ripple" v-bind="bind" :disabled="disabled" class="bgl_btn" :class="{
90
+ 'bgl_btn-icon': icon && !slots.default && !value,
91
+ thin,
92
+ round,
93
+ 'bgl_btn_flat': flat,
94
+ 'bgl_btn-border': border || outline,
95
+ [`bgl_btn-${color}`]: color,
96
+ [`bgl_btn-${theme}`]: theme,
97
+ }" :tabindex="disabled ? -1 : 0" @click.stop="onClick" @keydown.enter="onClick" @keydown.space="onClick">
98
+ <Loading v-if="loading" class="h-100p" size="15" />
99
+ <div v-else class="bgl_btn-flex">
100
+ <Icon v-if="icon" :icon="icon" class="transition-400" :size="iconSizeComputed" :mobile-size="iconMobileSize" />
101
+ <slot />
102
+ <template v-if="!slots.default && value">
103
+ {{ value }}
104
+ </template>
105
+ <Icon v-if="iconEnd" :icon="iconEnd" class="transition-400" :size="iconSizeComputed" :mobile-size="iconMobileSize" />
106
+ </div>
107
+ </component>
95
108
  </template>
96
109
 
97
110
  <style scoped>
@@ -24,6 +24,12 @@ const {
24
24
  noAutoFocus,
25
25
  autoHide = true,
26
26
  triggers = ['click'],
27
+ showTriggers,
28
+ hideTriggers,
29
+ popperTriggers = [],
30
+ popperShowTriggers = [],
31
+ popperHideTriggers = [],
32
+ delay,
27
33
 
28
34
  } = defineProps<{
29
35
  value?: string
@@ -42,6 +48,12 @@ const {
42
48
  noAutoFocus?: boolean
43
49
  autoHide?: boolean
44
50
  triggers?: TriggerEvent[]
51
+ showTriggers?: TriggerEvent[]
52
+ hideTriggers?: TriggerEvent[]
53
+ popperTriggers?: TriggerEvent[]
54
+ popperShowTriggers?: TriggerEvent[]
55
+ popperHideTriggers?: TriggerEvent[]
56
+ delay?: number | { show: number, hide: number }
45
57
  // theme
46
58
  // referenceNode
47
59
  // shown
@@ -95,45 +107,74 @@ const shouldDisablePositioning = $computed(() => {
95
107
  return disablePlacement && isMobile
96
108
  })
97
109
 
110
+ // Intelligent trigger configuration
111
+ const computedShowTriggers = $computed((): TriggerEvent[] => {
112
+ if (showTriggers !== undefined) return showTriggers
113
+ return triggers
114
+ })
115
+
116
+ const computedHideTriggers = $computed((): TriggerEvent[] => {
117
+ if (hideTriggers !== undefined) return hideTriggers
118
+ // For click-only dropdowns, only hide on click
119
+ if (triggers.length === 1 && triggers[0] === 'click') {
120
+ return ['click'] as TriggerEvent[]
121
+ }
122
+ // For hover dropdowns, add click to hide triggers for better UX
123
+ if (triggers.includes('hover')) {
124
+ return [...triggers, 'click'] as TriggerEvent[]
125
+ }
126
+ return triggers
127
+ })
128
+
129
+ const computedPopperTriggers = $computed((): TriggerEvent[] => {
130
+ if (popperTriggers.length > 0) return popperTriggers
131
+ // For hover dropdowns, enable hover on popper to keep it open
132
+ if (triggers.includes('hover')) {
133
+ return ['hover'] as TriggerEvent[]
134
+ }
135
+ return []
136
+ })
137
+
138
+ const computedPopperHideTriggers = $computed((): TriggerEvent[] => {
139
+ if (popperHideTriggers && popperHideTriggers.length > 0) return popperHideTriggers
140
+ // For hover dropdowns, make sure popper hides on mouseout
141
+ if (triggers.includes('hover')) {
142
+ return ['hover'] as TriggerEvent[]
143
+ }
144
+ return []
145
+ })
146
+
147
+ const computedAutoHide = $computed(() => {
148
+ // Both hover and click dropdowns should support autoHide for outside click behavior
149
+ return autoHide
150
+ })
151
+
152
+ const computedDelay = $computed((): number | { show: number, hide: number } | undefined => {
153
+ if (delay !== undefined) return delay
154
+ // For hover dropdowns, add a hide delay
155
+ if (triggers.includes('hover')) {
156
+ return { show: 0, hide: 50 } // 50ms delay before hiding
157
+ }
158
+ // For click dropdowns, no delay
159
+ return 0
160
+ })
161
+
98
162
  defineExpose({ show, hide, shown })
99
163
  </script>
100
164
 
101
165
  <template>
102
166
  <DDown
103
- ref="ddownRef"
104
- v-model:shown="shown"
105
- :disabled
106
- :noAutoFocus
107
- :positioning-disabled="shouldDisablePositioning"
108
- :placement
109
- :autoHide
110
- :triggers
111
- @hide="emit('hide')"
112
- @show="emit('show')"
167
+ ref="ddownRef" v-model:shown="shown" :disabled :noAutoFocus :positioning-disabled="shouldDisablePositioning"
168
+ :placement :autoHide="computedAutoHide" :delay="computedDelay" :triggers :showTriggers="computedShowTriggers"
169
+ :hideTriggers="computedHideTriggers" :popperTriggers="computedPopperTriggers" :popperShowTriggers
170
+ :popperHideTriggers="computedPopperHideTriggers" @hide="emit('hide')" @show="emit('show')"
113
171
  >
114
172
  <div ref="triggerRef" />
115
- <slot
116
- name="trigger"
117
- :show
118
- :hide
119
- >
120
- <Btn
121
- :class="triggerClass"
122
- :iconEnd
123
- :icon
124
- :value
125
- :thin
126
- :flat
127
- :outline
128
- :round
129
- :color
130
- />
173
+ <slot name="trigger" :show :hide :shown>
174
+ <Btn :class="triggerClass" :iconEnd :icon :value :thin :flat :outline :round :color />
131
175
  </slot>
132
- <template #popper="{ hide, show }">
133
- <slot
134
- :hide
135
- :show
136
- />
176
+ <template #popper="{ hide: hidePopper, show: showPopper, shown: popperShown }">
177
+ <slot :hide="hidePopper" :show="showPopper" :shown="popperShown" />
137
178
  </template>
138
179
  </DDown>
139
180
  </template>
@@ -1,11 +1,13 @@
1
1
  <script setup lang="ts">
2
2
  import type { IconType } from '@bagelink/vue'
3
3
  import { FONT_AWESOME_ICONS, MATERIAL_ICONS, FONT_AWESOME_BRANDS_ICONS } from './constants'
4
+ import { ref, onMounted, onUnmounted } from 'vue'
4
5
 
5
6
  const props = withDefaults(defineProps<{
6
7
  icon?: IconType
7
8
  name?: IconType
8
9
  size?: number | string
10
+ mobileSize?: number | string
9
11
  color?: string
10
12
  round?: boolean
11
13
  weight?: number | string
@@ -17,6 +19,28 @@ const props = withDefaults(defineProps<{
17
19
 
18
20
  const iconRender = $computed(() => props.icon || props.name) as IconType
19
21
 
22
+ const isMobile = ref(false)
23
+
24
+ const checkMobile = () => {
25
+ isMobile.value = window.innerWidth <= 910
26
+ }
27
+
28
+ onMounted(() => {
29
+ checkMobile()
30
+ window.addEventListener('resize', checkMobile)
31
+ })
32
+
33
+ onUnmounted(() => {
34
+ window.removeEventListener('resize', checkMobile)
35
+ })
36
+
37
+ const computedSize = $computed(() => {
38
+ if (isMobile.value && props.mobileSize !== undefined) {
39
+ return props.mobileSize
40
+ }
41
+ return props.size
42
+ })
43
+
20
44
  const iconRenderType = $computed(() => {
21
45
  if (props.fontAwesome && (FONT_AWESOME_ICONS.includes(iconRender) || FONT_AWESOME_BRANDS_ICONS.includes(iconRender))) return 'font-awesome'
22
46
  if (MATERIAL_ICONS.includes(iconRender)) return 'material'
@@ -32,11 +56,12 @@ const isFaBrand = $computed(() => FONT_AWESOME_BRANDS_ICONS.includes(iconRender)
32
56
  v-if="iconRenderType === 'material'"
33
57
  class="bgl_icon-font notranslate"
34
58
  :class="{ 'round flex aspect-ratio-1 justify-content-center': round }"
35
- :style="{ 'fontSize': `${size}rem`,
59
+ :style="{
60
+ 'fontSize': `${computedSize}rem`,
36
61
  color,
37
62
  'font-variation-settings': `'FILL' ${fill ? 1 : 0}, 'wght' ${weight || 400}`,
38
- 'width': round ? `calc(${size}rem * 2)` : 'auto',
39
- 'height': round ? `calc(${size}rem * 2)` : 'auto',
63
+ 'width': round ? `calc(${computedSize}rem * 2)` : 'auto',
64
+ 'height': round ? `calc(${computedSize}rem * 2)` : 'auto',
40
65
  }"
41
66
  translate="no"
42
67
  >
@@ -53,7 +78,7 @@ const isFaBrand = $computed(() => FONT_AWESOME_BRANDS_ICONS.includes(iconRender)
53
78
  'far': !fill && !isFaBrand,
54
79
  },
55
80
  ]"
56
- :style="{ 'fontSize': `${size}rem`, color, 'font-variation-settings': `'wght' ${weight || 400}` }"
81
+ :style="{ 'fontSize': `${computedSize}rem`, color, 'font-variation-settings': `'wght' ${weight || 400}` }"
57
82
  translate="no"
58
83
  />
59
84
  </template>
@@ -93,6 +93,7 @@ onUnmounted(() => {
93
93
  flat
94
94
  icon="close"
95
95
  thin
96
+ icon-mobile-size="1.4"
96
97
  @click="closeModal"
97
98
  />
98
99
  <Title v-if="title" class="modal-title" tag="h3" :label="title" />
@@ -106,6 +107,7 @@ onUnmounted(() => {
106
107
  thin
107
108
  round
108
109
  color="white"
110
+ icon-mobile-size="1.4"
109
111
  @click="closeModal"
110
112
  />
111
113
  </div>
@@ -134,6 +136,7 @@ onUnmounted(() => {
134
136
  text-align: center;
135
137
  font-weight: 600;
136
138
  font-size: 20px;
139
+ margin-inline-end: 2rem;
137
140
  margin-top: 0.5rem;
138
141
  margin-bottom: 0 !important;
139
142
  width: 100%;
@@ -351,9 +351,17 @@ function buildSliderFrame(): void {
351
351
 
352
352
  const originalCount = innerElements.value.length
353
353
  const widthItem = selectorWidth.value / perPage.value
354
+
355
+ // Calculate how many times to repeat items to fill big screens
356
+ const minItemsNeeded = Math.max(perPage.value * 3, 20) // Ensure we have enough content for big screens
357
+ const repetitions = Math.max(1, Math.ceil(minItemsNeeded / originalCount))
358
+ const totalMainItems = originalCount * repetitions
359
+
360
+ // Ultra-aggressive buffer size for really big screens
361
+ const bufferSize = Math.max(perPage.value * 8, 20) // Even more buffer for huge screens
354
362
  const itemsToBuild = config.value.loop
355
- ? originalCount + (2 * perPage.value)
356
- : originalCount
363
+ ? totalMainItems + (2 * bufferSize)
364
+ : totalMainItems
357
365
 
358
366
  // Create a clean slate for the slider
359
367
  carouselRef.value.innerHTML = ''
@@ -380,7 +388,7 @@ function buildSliderFrame(): void {
380
388
 
381
389
  // Add clones before original items (for loop mode)
382
390
  if (config.value.loop && innerElements.value.length > 0) {
383
- for (let i = originalCount - perPage.value; i < originalCount; i++) {
391
+ for (let i = originalCount - bufferSize; i < originalCount; i++) {
384
392
  if (i >= 0 && i < originalCount) {
385
393
  const original = innerElements.value[i]
386
394
  const clone = original.cloneNode(true) as Element
@@ -396,16 +404,24 @@ function buildSliderFrame(): void {
396
404
  }
397
405
  }
398
406
 
399
- // Add main items (already clones of the slot content)
400
- for (let i = 0; i < originalCount; i++) {
401
- const element = innerElements.value[i]
402
- const wrapped = buildSliderFrameItem(element.cloneNode(true) as Element)
403
- docFragment.appendChild(wrapped)
407
+ // Add main items repeated multiple times to fill big screens
408
+ for (let rep = 0; rep < repetitions; rep++) {
409
+ for (let i = 0; i < originalCount; i++) {
410
+ const element = innerElements.value[i]
411
+ const clone = element.cloneNode(true) as Element
412
+
413
+ if (clone instanceof HTMLElement) {
414
+ clone.setAttribute('data-repetition', rep.toString())
415
+ }
416
+
417
+ const wrapped = buildSliderFrameItem(clone)
418
+ docFragment.appendChild(wrapped)
419
+ }
404
420
  }
405
421
 
406
422
  // Add clones after original items (for loop mode)
407
423
  if (config.value.loop && innerElements.value.length > 0) {
408
- for (let i = 0; i < perPage.value; i++) {
424
+ for (let i = 0; i < bufferSize; i++) {
409
425
  if (i >= 0 && i < originalCount) {
410
426
  const original = innerElements.value[i]
411
427
  const clone = original.cloneNode(true) as Element
@@ -505,9 +521,17 @@ function buildSliderFrameItem(elm: Element): HTMLElement {
505
521
  elementContainer.style.float = config.value.rtl ? 'right' : 'left'
506
522
  elementContainer.style.padding = config.value.slideGap ? `${config.value.slideGap / 2}rem` : '0'
507
523
 
524
+ // Calculate total items including repetitions for big screens
525
+ const originalCount = innerElements.value.length
526
+ const minItemsNeeded = Math.max(perPage.value * 3, 20)
527
+ const repetitions = Math.max(1, Math.ceil(minItemsNeeded / originalCount))
528
+ const totalMainItems = originalCount * repetitions
529
+
530
+ // Ultra-aggressive buffer size - must match buildSliderFrame buffer size
531
+ const itemBufferSize = Math.max(perPage.value * 8, 20) // Even more buffer for huge screens
508
532
  const percentage = config.value.loop
509
- ? 100 / (innerElements.value.length + (2 * perPage.value))
510
- : 100 / innerElements.value.length
533
+ ? 100 / (totalMainItems + (2 * itemBufferSize))
534
+ : 100 / totalMainItems
511
535
 
512
536
  elementContainer.style.width = `${percentage}%`
513
537
  elementContainer.appendChild(elm)
@@ -519,8 +543,10 @@ function slideToCurrent(enableTransitionFlag?: boolean): void {
519
543
  // Ensure component is still mounted
520
544
  if (!isMounted.value || !innerElements.value.length || !sliderFrame.value) return
521
545
 
546
+ // Ultra-aggressive buffer size - must match buildSliderFrame buffer size
547
+ const slideBufferSize = Math.max(perPage.value * 8, 20) // Even more buffer for huge screens
522
548
  const currentSlideValue = config.value.loop
523
- ? currentSlide.value + perPage.value
549
+ ? currentSlide.value + slideBufferSize
524
550
  : currentSlide.value
525
551
 
526
552
  const offset = (config.value.rtl ? 1 : -1) * currentSlideValue * (selectorWidth.value / perPage.value)
@@ -834,7 +860,7 @@ function touchmoveHandler(e: TouchEvent): void {
834
860
  : currentOffset - dragOffset
835
861
 
836
862
  sliderFrame.value.style[transformProperty.value as any]
837
- = `translate3d(${(config.value.rtl ? 1 : -1) * offset}px, 0, 0)`
863
+ = `translate3d(${(config.value.rtl ? 1 : -1) * offset}px, 0, 0)`
838
864
  }
839
865
  }
840
866
  }
@@ -972,7 +998,7 @@ function mousemoveHandler(e: MouseEvent): void {
972
998
  : currentOffset - dragOffset
973
999
 
974
1000
  sliderFrame.value.style[transformProperty.value as any]
975
- = `translate3d(${(config.value.rtl ? 1 : -1) * offset}px, 0, 0)`
1001
+ = `translate3d(${(config.value.rtl ? 1 : -1) * offset}px, 0, 0)`
976
1002
  }
977
1003
  }
978
1004
  }
@@ -1373,16 +1399,11 @@ defineExpose({
1373
1399
  <div ref="carouselRef" class="carousel-container">
1374
1400
  <!-- We'll populate this with cloned content programmatically -->
1375
1401
  </div>
1376
-
1377
1402
  <!-- Dots navigation (Vue-managed) -->
1378
1403
  <div v-if="props.dots && totalDots > 1" class="carousel-dots">
1379
1404
  <button
1380
- v-for="i in totalDots"
1381
- :key="i"
1382
- type="button"
1383
- class="carousel-dot" :class="[{ active: (i - 1) === currentSlide }]"
1384
- :aria-label="`Go to slide ${i}`"
1385
- @click="goTo(i - 1)"
1405
+ v-for="i in totalDots" :key="i" type="button" class="carousel-dot"
1406
+ :class="[{ active: (i - 1) === currentSlide }]" :aria-label="`Go to slide ${i}`" @click="goTo(i - 1)"
1386
1407
  />
1387
1408
  </div>
1388
1409
  </div>
@@ -1390,35 +1411,35 @@ defineExpose({
1390
1411
 
1391
1412
  <style scoped>
1392
1413
  .carousel-wrapper {
1393
- position: relative;
1394
- width: 100%;
1414
+ position: relative;
1415
+ width: 100%;
1395
1416
  }
1396
1417
 
1397
1418
  .carousel-container {
1398
- margin: 0 auto;
1399
- overflow: hidden;
1419
+ margin: 0 auto;
1420
+ overflow: hidden;
1400
1421
  }
1401
1422
 
1402
1423
  .carousel-dots {
1403
- display: flex;
1404
- justify-content: center;
1405
- gap: 8px;
1406
- margin-top: 16px;
1424
+ display: flex;
1425
+ justify-content: center;
1426
+ gap: 8px;
1427
+ margin-top: 16px;
1407
1428
  }
1408
1429
 
1409
1430
  .carousel-dot {
1410
- width: 12px;
1411
- height: 12px;
1412
- border-radius: 50px;
1413
- background-color: var(--bgl-gray-light);
1414
- border: none;
1415
- padding: 0;
1416
- cursor: pointer;
1417
- transition: all 0.3s ease-in-out;
1431
+ width: 12px;
1432
+ height: 12px;
1433
+ border-radius: 50px;
1434
+ background-color: var(--bgl-gray-light);
1435
+ border: none;
1436
+ padding: 0;
1437
+ cursor: pointer;
1438
+ transition: all 0.3s ease-in-out;
1418
1439
  }
1419
1440
 
1420
1441
  .carousel-dot.active {
1421
- background-color: var(--bgl-primary);
1422
- width: 26px;
1442
+ background-color: var(--bgl-primary);
1443
+ width: 26px;
1423
1444
  }
1424
1445
  </style>
@@ -1,7 +1,8 @@
1
- import type { BglFormSchemaT } from '@bagelink/vue'
1
+ import type { BglFormSchemaT, Field } from '@bagelink/vue'
2
+ import type { DefaultPathsOptions } from 'type-fest/source/paths'
2
3
  import type { MaybeRefOrGetter } from 'vue'
3
4
  import type { SortDirectionsT } from '../../types/TableSchema'
4
- import { useBglSchema, isDate, keyToLabel } from '@bagelink/vue'
5
+ import { useBglSchema, isDate, keyToLabel, formatDate } from '@bagelink/vue'
5
6
  import { computed, ref, watch, toValue } from 'vue'
6
7
 
7
8
  const NON_DIGIT_REGEX = /[^\d.-]/g
@@ -30,6 +31,13 @@ interface TransformedDataBase {
30
31
 
31
32
  type TransformedData<TransDataT> = TransDataT & TransformedDataBase
32
33
 
34
+ function autoTransform<T>(field: Field<T, DefaultPathsOptions>): Field<T, DefaultPathsOptions> {
35
+ if ((field.id === 'created_at' || field.id === 'updated_at') && !field.transform) {
36
+ field.transform = (val?: any) => val ? formatDate(val) : val
37
+ }
38
+ return field
39
+ }
40
+
33
41
  export function useTableData<T extends { [key: string]: any }>(options: UseTableDataOptions<T>) {
34
42
  // Sorting state
35
43
  const sortField = ref('')
@@ -55,7 +63,7 @@ export function useTableData<T extends { [key: string]: any }>(options: UseTable
55
63
 
56
64
  // If we have a valid schema with fields, filter out fields without an ID
57
65
  if (Array.isArray(schema) && schema.length > 0) {
58
- resolvedSchema.value = schema.filter(field => field && field.id)
66
+ resolvedSchema.value = schema.filter(field => field && field.id).map(autoTransform)
59
67
  } else if (Array.isArray(dataValue) && dataValue.length > 0) {
60
68
  // If no schema is provided or it's empty, generate a default schema from the data
61
69
  const firstItem = dataValue[0]
@@ -62,9 +62,13 @@ function useFormatting() {
62
62
  if (parts.length === 3) {
63
63
  const [day, month, year] = parts.map(p => Number.parseInt(p, 10))
64
64
  if (!Number.isNaN(day) && !Number.isNaN(month) && !Number.isNaN(year)) {
65
- const date = new Date(year, month - 1, day)
66
- if (date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day) {
67
- return date
65
+ const parsedDate = new Date(year, month - 1, day)
66
+ if (
67
+ parsedDate.getFullYear() === year
68
+ && parsedDate.getMonth() === month - 1
69
+ && parsedDate.getDate() === day
70
+ ) {
71
+ return parsedDate
68
72
  }
69
73
  }
70
74
  }
@@ -72,14 +76,27 @@ function useFormatting() {
72
76
  return null
73
77
  }
74
78
 
79
+ const normalizeDate = (date: Date): string => {
80
+ if (props.enableTime) {
81
+ // Keep the time when time is enabled
82
+ return date.toISOString()
83
+ } else {
84
+ // Normalize to midnight to emit exact date without time for server compatibility
85
+ const normalizedDate = new Date(date)
86
+ normalizedDate.setHours(0, 0, 0, 0)
87
+ return normalizedDate.toISOString().split('T')[0]
88
+ }
89
+ }
90
+
75
91
  return {
76
92
  formatDisplayDate,
77
- parseUserInput
93
+ parseUserInput,
94
+ normalizeDate
78
95
  }
79
96
  }
80
97
 
81
98
  // Initialize formatting
82
- const { formatDisplayDate, parseUserInput } = useFormatting()
99
+ const { formatDisplayDate, parseUserInput, normalizeDate } = useFormatting()
83
100
 
84
101
  // Input handling composable
85
102
  function useInputHandling() {
@@ -91,7 +108,7 @@ function useInputHandling() {
91
108
 
92
109
  const date = parseUserInput(input.value)
93
110
  if (date) {
94
- selectedDate.value = date.toISOString()
111
+ selectedDate.value = normalizeDate(date)
95
112
  }
96
113
  }
97
114
 
@@ -112,7 +129,7 @@ function useInputHandling() {
112
129
  } else if (event.key === 'Enter' && inputValue.value) {
113
130
  const date = parseUserInput(inputValue.value)
114
131
  if (date) {
115
- selectedDate.value = date.toISOString()
132
+ selectedDate.value = normalizeDate(date)
116
133
  isOpen.value = false
117
134
  }
118
135
  }
@@ -153,43 +170,23 @@ onMounted(() => {
153
170
  {{ label }}
154
171
  <span v-if="required" class="required">*</span>
155
172
  </label>
156
- <Dropdown
157
- :shown="isOpen"
158
- placement="bottom-start"
159
- :autoHide="false"
160
- :triggers="['click']"
161
- >
173
+ <Dropdown :shown="isOpen" placement="bottom-start" :autoHide="false" :triggers="['click']">
162
174
  <template #trigger>
163
175
  <div ref="datePickerRef" class="date-picker-container" @mousedown.stop @click.stop>
164
176
  <TextInput
165
- :modelValue="formatDisplayDate(selectedDate)"
166
- iconStart="calendar"
167
- :min="formatDisplayDate(min)"
168
- :max="formatDisplayDate(max)"
169
- :required="required"
170
- :disabled="!editMode"
171
- class="date-input m-0"
172
- :class="{
177
+ :modelValue="formatDisplayDate(selectedDate)" iconStart="calendar"
178
+ :min="formatDisplayDate(min)" :max="formatDisplayDate(max)" :required="required"
179
+ :disabled="!editMode" class="date-input m-0" :class="{
173
180
  'txt-center': center,
174
- }"
175
- :readonly="false"
176
- @input="handleInput"
177
- @focus="handleFocus"
178
- @click="handleClick"
179
- @keydown="handleKeydown"
181
+ }" :readonly="false" @input="handleInput" @focus="handleFocus" @click="handleClick" @keydown="handleKeydown"
180
182
  />
181
183
  </div>
182
184
  </template>
183
185
 
184
186
  <div ref="calendarRef" @click.stop>
185
187
  <DatePicker
186
- v-model="selectedDate"
187
- :min="min"
188
- :max="max"
189
- :mode="mode"
190
- :firstDayOfWeek="firstDayOfWeek"
191
- :locale="locale"
192
- :enableTime="enableTime"
188
+ v-model="selectedDate" :min="min" :max="max" :mode="mode" :firstDayOfWeek="firstDayOfWeek"
189
+ :locale="locale" :enableTime="enableTime"
193
190
  />
194
191
  </div>
195
192
  </Dropdown>