@bagelink/vue 1.4.109 → 1.4.111

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 (66) hide show
  1. package/dist/components/ListItem.vue.d.ts +6 -1
  2. package/dist/components/ListItem.vue.d.ts.map +1 -1
  3. package/dist/components/analytics/BarChart.vue.d.ts +39 -0
  4. package/dist/components/analytics/BarChart.vue.d.ts.map +1 -0
  5. package/dist/components/analytics/KpiCard.vue.d.ts +24 -0
  6. package/dist/components/analytics/KpiCard.vue.d.ts.map +1 -0
  7. package/dist/components/analytics/LineChart.vue.d.ts +26 -0
  8. package/dist/components/analytics/LineChart.vue.d.ts.map +1 -0
  9. package/dist/components/analytics/PieChart.vue.d.ts +24 -0
  10. package/dist/components/analytics/PieChart.vue.d.ts.map +1 -0
  11. package/dist/components/analytics/index.d.ts +5 -0
  12. package/dist/components/analytics/index.d.ts.map +1 -0
  13. package/dist/components/form/BagelForm.vue.d.ts.map +1 -1
  14. package/dist/components/form/inputs/DatePicker.vue.d.ts +1 -0
  15. package/dist/components/form/inputs/DatePicker.vue.d.ts.map +1 -1
  16. package/dist/components/form/inputs/RadioGroup.vue.d.ts +6 -10
  17. package/dist/components/form/inputs/RadioGroup.vue.d.ts.map +1 -1
  18. package/dist/components/form/inputs/SelectInput.vue.d.ts +2 -2
  19. package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
  20. package/dist/components/layout/AppContent.vue.d.ts +34 -0
  21. package/dist/components/layout/AppContent.vue.d.ts.map +1 -0
  22. package/dist/components/layout/AppLayout.vue.d.ts +27 -0
  23. package/dist/components/layout/AppLayout.vue.d.ts.map +1 -0
  24. package/dist/components/layout/AppSidebar.vue.d.ts +44 -0
  25. package/dist/components/layout/AppSidebar.vue.d.ts.map +1 -0
  26. package/dist/components/layout/index.d.ts +3 -0
  27. package/dist/components/layout/index.d.ts.map +1 -1
  28. package/dist/composables/useFormField.d.ts.map +1 -1
  29. package/dist/composables/useSchemaField.d.ts.map +1 -1
  30. package/dist/index.cjs +19 -19
  31. package/dist/index.mjs +10 -10
  32. package/dist/style.css +1 -1
  33. package/dist/types/BagelForm.d.ts +2 -2
  34. package/dist/types/BagelForm.d.ts.map +1 -1
  35. package/dist/utils/BagelFormUtils.d.ts +1 -2
  36. package/dist/utils/BagelFormUtils.d.ts.map +1 -1
  37. package/dist/utils/calendar/dateUtils.d.ts +21 -0
  38. package/dist/utils/calendar/dateUtils.d.ts.map +1 -1
  39. package/dist/utils/elementUtils.d.ts +5 -0
  40. package/dist/utils/elementUtils.d.ts.map +1 -1
  41. package/dist/utils/useSearch.d.ts.map +1 -1
  42. package/package.json +1 -1
  43. package/src/components/ListItem.vue +32 -24
  44. package/src/components/analytics/BarChart.vue +153 -0
  45. package/src/components/analytics/KpiCard.vue +84 -0
  46. package/src/components/analytics/LineChart.vue +267 -0
  47. package/src/components/analytics/PieChart.vue +196 -0
  48. package/src/components/analytics/index.ts +4 -0
  49. package/src/components/form/BagelForm.vue +24 -0
  50. package/src/components/form/inputs/DatePicker.vue +3 -2
  51. package/src/components/form/inputs/RadioGroup.vue +60 -35
  52. package/src/components/form/inputs/SelectInput.vue +94 -101
  53. package/src/components/form/inputs/Upload/upload.css +135 -138
  54. package/src/components/layout/AppContent.vue +105 -0
  55. package/src/components/layout/AppLayout.vue +124 -0
  56. package/src/components/layout/AppSidebar.vue +271 -0
  57. package/src/components/layout/index.ts +5 -0
  58. package/src/composables/useFormField.ts +6 -0
  59. package/src/composables/useSchemaField.ts +31 -3
  60. package/src/styles/inputs.css +9 -0
  61. package/src/styles/theme.css +2 -2
  62. package/src/types/BagelForm.ts +3 -2
  63. package/src/utils/BagelFormUtils.ts +1 -3
  64. package/src/utils/calendar/dateUtils.ts +71 -17
  65. package/src/utils/elementUtils.ts +22 -0
  66. package/src/utils/useSearch.ts +14 -7
@@ -0,0 +1,271 @@
1
+ <script lang="ts" setup>
2
+ import { inject, computed, ref, watch } from 'vue'
3
+ import { useRoute } from 'vue-router'
4
+ import { Btn, Icon } from '@bagelink/vue'
5
+ import type { NavLink } from '@bagelink/vue'
6
+
7
+ // Extended interface for footer links that can have actions
8
+ interface FooterLink extends NavLink {
9
+ action?: () => void
10
+ }
11
+
12
+ interface Props {
13
+ navLinks: NavLink[]
14
+ footerLinks?: FooterLink[]
15
+ logo?: string
16
+ logoAlt?: string
17
+ card?: boolean
18
+ bgColor?: string
19
+ textColor?: string
20
+ activeColor?: string
21
+ logoHeight?: string
22
+ name?: string
23
+ frame?: boolean
24
+ }
25
+
26
+ const props = withDefaults(defineProps<Props>(), {
27
+ card: true,
28
+ bgColor: 'var(--bgl-white)',
29
+ textColor: 'var(--bgl-black)',
30
+ activeColor: 'var(--bgl-black)',
31
+ logoAlt: 'Logo',
32
+ logoHeight: '2rem',
33
+ name: 'App Name',
34
+ footerLinks: () => [],
35
+ })
36
+
37
+ const route = useRoute()
38
+ const isTransitioning = ref(false)
39
+
40
+ // Inject menu state from parent
41
+ const menuState = inject('menuState') as {
42
+ isOpen: { value: boolean }
43
+ isMobile: { value: boolean }
44
+ closeOnMobile: () => void
45
+ sidebarWidth: string
46
+ sidebarCollapsedWidth: string
47
+ }
48
+
49
+ // Watch for changes in menu state to handle transitioning
50
+ watch(
51
+ () => menuState.isOpen.value,
52
+ () => {
53
+ if (!menuState.isMobile.value) {
54
+ isTransitioning.value = true
55
+ // Reset after transition completes
56
+ setTimeout(() => {
57
+ isTransitioning.value = false
58
+ }, 300) // Match the CSS transition duration
59
+ }
60
+ }
61
+ )
62
+
63
+ // Computed styles
64
+ const sidebarStyles = computed(() => {
65
+ let width = '280px'
66
+
67
+ if (!menuState.isMobile.value) {
68
+ const collapsedWidth = props.card ? '82px' : '76px'
69
+ width = menuState.isOpen.value ? menuState.sidebarWidth : collapsedWidth
70
+ }
71
+
72
+ return {
73
+ width,
74
+ }
75
+ })
76
+
77
+ function logout() {
78
+ console.log('Logging out...')
79
+ // Add your logout logic here
80
+ }
81
+ </script>
82
+
83
+ <template>
84
+ <aside
85
+ class="app-sidebar transition-400 fixed inset h-100vh z-99"
86
+ :class="{
87
+ 'sidebar-mobile-open': menuState.isMobile.value && menuState.isOpen.value,
88
+ 'sidebar-mobile-closed':
89
+ menuState.isMobile.value && !menuState.isOpen.value,
90
+ transitioning: isTransitioning,
91
+ 'p-05': props.card,
92
+ 'sidebar-collapsed': !menuState.isMobile.value && !menuState.isOpen.value,
93
+ }"
94
+ :style="sidebarStyles"
95
+ >
96
+ <div
97
+ :style="{
98
+ backgroundColor: props.bgColor,
99
+ color: props.textColor,
100
+ ...(props.card && { borderRadius: 'var(--card-border-radius)' }),
101
+ }"
102
+ :class="{
103
+ card: props.card,
104
+ 'ps-05': !menuState.isOpen.value,
105
+ 'scrollbar-gutter-both': menuState.isOpen.value,
106
+ aside_frame: props.frame,
107
+ }"
108
+ class="overflow-hidden flex column flex-stretch gap-1 w100p pt-1 pb-05 h-100p"
109
+ >
110
+ <!-- Logo/Brand -->
111
+ <router-link
112
+ to="/"
113
+ class="decoration-none flex px-05"
114
+ :class="{
115
+ 'gap-025': menuState.isOpen.value,
116
+ 'gap-0': !menuState.isOpen.value,
117
+ }"
118
+ >
119
+ <img
120
+ v-if="props.logo"
121
+ :src="props.logo"
122
+ :alt="props.logoAlt"
123
+ class="contain"
124
+ :style="{ height: props.logoHeight }"
125
+ />
126
+ <span class="nav-text">
127
+ {{ props.name }}
128
+ </span>
129
+ </router-link>
130
+
131
+ <!-- Navigation Links -->
132
+ <nav class="sidebar-nav flex column flex-stretch gap-025 align-items-start scrollbar-gutter-stable">
133
+ <Btn v-for="link in props.navLinks" :key="link.to" :title="!menuState.isOpen.value && !menuState.isMobile.value
134
+ ? link.label
135
+ : ''
136
+ " fullWidth alignTxt="start" class="flex-shrink-0 px-1" :class="{ 'nav-btn-active': route.path === link.to }" :style="{
137
+ backgroundColor:
138
+ route.path === link.to ? props.activeColor : props.bgColor,
139
+ color: route.path === link.to ? 'white' : props.textColor,
140
+ }" :to="link.to || '/'">
141
+ <Icon :name="link.icon" size="1.2" />
142
+ <span class="nav-text">
143
+ {{ link.label }}
144
+ </span>
145
+ </Btn>
146
+ </nav>
147
+ <!-- Footer -->
148
+ <div class="sidebar-footer flex column flex-stretch gap-025 align-items-start mt-auto scrollbar-gutter-stable">
149
+ <!-- Footer Links -->
150
+ <Btn v-for="link in props.footerLinks" :key="link.to || link.label" :title="!menuState.isOpen.value && !menuState.isMobile.value
151
+ ? link.label
152
+ : ''
153
+ " alignTxt="start" fullWidth flat :icon="link.icon" class="flex-shrink-0 px-1" :to="link.to" @click="link.action">
154
+ <span class="nav-text">
155
+ {{ link.label }}
156
+ </span>
157
+ </Btn>
158
+
159
+ <!-- Default Logout Button if no footer links provided -->
160
+ <Btn v-if="props.footerLinks.length === 0" :title="!menuState.isOpen.value && !menuState.isMobile.value ? 'Logout' : ''
161
+ " alignTxt="start" fullWidth flat icon="logout" class="flex-shrink-0 px-1" @click="logout">
162
+ <span class="nav-text"> Logout </span>
163
+ </Btn>
164
+
165
+ <!-- Custom Footer Content Slot -->
166
+ <slot name="footer" />
167
+ </div>
168
+ </div>
169
+ </aside>
170
+ </template>
171
+
172
+ <style>
173
+ .aside_frame {
174
+ outline: 1px solid var(--border-color);
175
+ }
176
+
177
+ .card .aside_frame {
178
+ outline-offset: -1px;
179
+ }
180
+
181
+ .app-sidebar {
182
+ transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
183
+ --input-font-size: 0.875rem;
184
+ /* 14px */
185
+ }
186
+
187
+ /* הסתרת סרגל גלילה רק בזמן טרנזישן */
188
+ .sidebar-nav {
189
+ overflow-y: auto;
190
+ flex: 1;
191
+ }
192
+
193
+ /* הסתרת גלילה בזמן שינוי גודל */
194
+ .app-sidebar.transitioning .sidebar-nav {
195
+ scrollbar-width: none;
196
+ /* Firefox */
197
+ -ms-overflow-style: none;
198
+ /* IE and Edge */
199
+ }
200
+
201
+ .app-sidebar.transitioning .sidebar-nav::-webkit-scrollbar {
202
+ display: none;
203
+ /* Chrome, Safari and Opera */
204
+ }
205
+
206
+ /* אנימציות איטות לטקסט */
207
+ .nav-text {
208
+ transition:
209
+ opacity 0.3s ease,
210
+ transform 0.3s ease;
211
+ white-space: nowrap;
212
+ overflow: hidden;
213
+ opacity: 1;
214
+ transform: translateX(0);
215
+ max-width: 200px;
216
+ font-weight: 300;
217
+ }
218
+
219
+ .app-sidebar.sidebar-collapsed .bgl_btn_fullWidth {
220
+ width: fit-content !important;
221
+ }
222
+
223
+ .app-sidebar.sidebar-collapsed .bgl_btn-flex {
224
+ gap: 0 !important;
225
+ }
226
+
227
+ /* אנימציות נכנסות ויוצאות */
228
+ @media (min-width: 911px) {
229
+ .app-sidebar {
230
+ transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
231
+ }
232
+
233
+ /* כשהמניו סגור - הטקסט נעלם תחילה */
234
+ .app-sidebar.sidebar-collapsed .nav-text {
235
+ opacity: 0;
236
+ transform: translateX(-15px);
237
+ max-width: 0;
238
+ transition:
239
+ opacity 0.15s ease,
240
+ transform 0.15s ease,
241
+ max-width 0.15s ease;
242
+ }
243
+
244
+ /* כשהמניו פתוח - הטקסט מופיע אחרי שהרוחב משתנה */
245
+ .app-sidebar:not(.sidebar-collapsed) .nav-text {
246
+ opacity: 1;
247
+ transform: translateX(0);
248
+ max-width: 200px;
249
+ transition:
250
+ opacity 0.3s ease 0.15s,
251
+ transform 0.3s ease 0.15s,
252
+ max-width 0.3s ease 0.15s;
253
+ }
254
+ }
255
+
256
+ /* במובייל הטקסט תמיד מופיע */
257
+ @media (max-width: 910px) {
258
+ .app-sidebar {
259
+ transform: translateX(-100%);
260
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
261
+ }
262
+
263
+ .sidebar-mobile-open {
264
+ transform: translateX(0);
265
+ }
266
+
267
+ .sidebar-mobile-closed {
268
+ transform: translateX(-100%);
269
+ }
270
+ }
271
+ </style>
@@ -6,3 +6,8 @@ export { default as TabbedLayout } from './TabbedLayout.vue'
6
6
  export { default as Tabs } from './Tabs.vue'
7
7
  export { default as TabsBody } from './TabsBody.vue'
8
8
  export { default as TabsNav } from './TabsNav.vue'
9
+ export { default as AppContent } from './AppContent.vue'
10
+ export { default as AppLayout } from './AppLayout.vue'
11
+ export { default as AppSidebar } from './AppSidebar.vue'
12
+
13
+
@@ -14,7 +14,13 @@ export function useFormField<T>(props: {
14
14
  get: () => {
15
15
  if (!props.fieldID || !formState) return props.modelValue ?? props.field.defaultValue ?? ''
16
16
  const value = formState.getFieldData(props.fieldID)
17
+ // For nested form containers, default to empty object
17
18
  if (props.field.$el === 'form' && !value) return {}
19
+ // If no value is set yet and a defaultValue exists, write it into form state once
20
+ if ((value === undefined || value === null || value === '') && props.field.defaultValue !== undefined) {
21
+ formState.updateField(props.fieldID, props.field.defaultValue)
22
+ return props.field.defaultValue
23
+ }
18
24
  return value ?? ''
19
25
  },
20
26
  set: (val: any) => {
@@ -226,9 +226,37 @@ export function useSchemaField<T extends { [key: string]: any }, SP extends Path
226
226
 
227
227
  // Add options if they exist in the field
228
228
  if (field.options) {
229
- props.options = typeof field.options === 'function'
230
- ? field.options(currentValue, rowData)
231
- : field.options
229
+ if (Array.isArray(field.options)) {
230
+ props.options = field.options
231
+ } else if (typeof field.options === 'function') {
232
+ const fn = field.options as any
233
+ // Component-aware mapping
234
+ if (Component === SelectInput) {
235
+ props.options = (query: string) => {
236
+ const row = getFormData?.()
237
+ const val = currentValue
238
+ const out = fn(val, row)
239
+ if (Array.isArray(out)) return out
240
+ if (typeof out === 'function') return out(query)
241
+ if (out && typeof out.then === 'function') return out
242
+ return fn(query, val, row)
243
+ }
244
+ } else {
245
+ // Non-search components (e.g., RadioGroup): return a zero-arg async loader using latest row
246
+ props.options = async () => {
247
+ const row = getFormData?.()
248
+ const val = currentValue
249
+ const out = fn(val, row)
250
+ if (Array.isArray(out)) return out
251
+ if (typeof out === 'function') return await out('')
252
+ if (out && typeof out.then === 'function') return await out
253
+ if (fn.length >= 3) return await fn('', val, row)
254
+ return []
255
+ }
256
+ }
257
+ } else {
258
+ props.options = field.options
259
+ }
232
260
  }
233
261
 
234
262
  // Handle dynamic props and attrs
@@ -65,6 +65,15 @@ select {
65
65
  width: 100%;
66
66
  }
67
67
 
68
+ .bagel-input.frame input,
69
+ .bagel-input.frame textarea,
70
+ .bagel-input.frame select,
71
+ .custom-select.frame .input {
72
+ outline: 1px solid var(--border-color);
73
+ outline-offset: -1px;
74
+ }
75
+
76
+
68
77
  .bagel-input input::-webkit-input-placeholder,
69
78
  .bagel-input textarea::-webkit-input-placeholder,
70
79
  .bagel-input select::-webkit-input-placeholder,
@@ -44,7 +44,7 @@
44
44
  /* TYPE */
45
45
  :root {
46
46
  --bgl-font: 'Lexend', 'Ploni', sans-serif;
47
- --input-font-size: 16px;
47
+ --input-font-size: 14px;
48
48
  }
49
49
 
50
50
  /* DIMENSIONS */
@@ -55,7 +55,7 @@
55
55
  --pill-font-size: 12px;
56
56
  --input-border-radius: 7px;
57
57
  --card-border-radius: 12px;
58
- --btn-border-radius: 10px;
58
+ --btn-border-radius: 6px;
59
59
  --btn-padding: 30px;
60
60
  --btn-height: 40px;
61
61
  --pill-border-radius: 8px;
@@ -1,4 +1,4 @@
1
- import type { ArrayAttrs, FieldArray, SelectInput, TextInput } from '@bagelink/vue'
1
+ import type { ArrayAttrs, FieldArray, SelectInput, TextInput, Option } from '@bagelink/vue'
2
2
  import type { Paths, Get, IterableElement, OmitIndexSignature } from 'type-fest'
3
3
  import type { ToString } from 'type-fest/source/internal'
4
4
  import type { LiteralStringUnion } from 'type-fest/source/literal-union'
@@ -34,7 +34,8 @@ export type BagelFieldOptions<T, P extends Path<T>> =
34
34
  | boolean
35
35
  | { [key: string]: any }
36
36
  )[]
37
- | ((val?: FieldVal<T, P>, rowData?: T) => void)
37
+ | ((val?: FieldVal<T, P>, rowData?: T) => Option[] | ((query: string) => Promise<Option[]>))
38
+ | ((query: string, val?: FieldVal<T, P>, rowData?: T) => Promise<Option[]>)
38
39
 
39
40
  export type VIfType<T, P extends Path<T>> =
40
41
  | string
@@ -33,7 +33,6 @@ export interface SlctInputOptions<T, K extends Path<T>> extends InputOptions<T,
33
33
  searchable?: boolean
34
34
  multiselect?: boolean
35
35
  clearable?: boolean
36
- onSearch?: (search: string) => any
37
36
  }
38
37
 
39
38
  export interface NumFieldOptions<T, K extends Path<T>> extends InputOptions<T, K> {
@@ -128,7 +127,7 @@ export function selectField<
128
127
  >(
129
128
  id?: P,
130
129
  label?: string,
131
- options?: Option[] | (() => Option[]),
130
+ options?: Option[] | ((query: string) => Promise<Option[]>) | ((val?: any, row?: T) => Option[] | ((query: string) => Promise<Option[]>)) | ((query: string, val?: any, row?: T) => Promise<Option[]>),
132
131
  config?: SlctInputOptions<T, P>,
133
132
  ): SelectBagelField<T, P, PO> {
134
133
  return {
@@ -148,7 +147,6 @@ export function selectField<
148
147
  disabled: config?.disabled,
149
148
  searchable: config?.searchable,
150
149
  multiselect: config?.multiselect,
151
- onSearch: config?.onSearch,
152
150
  clearable: config?.clearable,
153
151
  autocomplete: config?.autocomplete,
154
152
  },
@@ -9,31 +9,53 @@ interface TimeDeltaOptions {
9
9
  Week?: number
10
10
  Month?: number
11
11
  Year?: number
12
+ days?: number
13
+ hours?: number
14
+ minutes?: number
15
+ seconds?: number
16
+ weeks?: number
17
+ months?: number
18
+ years?: number
19
+ day?: number
20
+ hour?: number
21
+ minute?: number
22
+ second?: number
23
+ week?: number
24
+ month?: number
25
+ year?: number
12
26
  }
13
27
 
14
28
  export function timeDelta(date: string | Date, options: TimeDeltaOptions) {
29
+ console.log('date', date)
15
30
  date = new Date(date)
16
- const { Day, Hour, Minute, Second, Week, Month, Year } = options
17
- if (Day) {
18
- date.setDate(date.getDate() + Day)
31
+ console.log('date', date)
32
+ const day = options.Day || options.day || options.days || 0
33
+ const hour = options.Hour || options.hour || options.hours || 0
34
+ const minute = options.Minute || options.minute || options.minutes || 0
35
+ const second = options.Second || options.second || options.seconds || 0
36
+ const week = options.Week || options.week || options.weeks || 0
37
+ const month = options.Month || options.month || options.months || 0
38
+ const year = options.Year || options.year || options.years || 0
39
+ if (day) {
40
+ date.setDate(date.getDate() + day)
19
41
  }
20
- if (Hour) {
21
- date.setHours(date.getHours() + Hour)
42
+ if (hour) {
43
+ date.setHours(date.getHours() + hour)
22
44
  }
23
- if (Minute) {
24
- date.setMinutes(date.getMinutes() + Minute)
45
+ if (minute) {
46
+ date.setMinutes(date.getMinutes() + minute)
25
47
  }
26
- if (Second) {
27
- date.setSeconds(date.getSeconds() + Second)
48
+ if (second) {
49
+ date.setSeconds(date.getSeconds() + second)
28
50
  }
29
- if (Week) {
30
- date.setDate(date.getDate() + Week * 7)
51
+ if (week) {
52
+ date.setDate(date.getDate() + week * 7)
31
53
  }
32
- if (Month) {
33
- date.setMonth(date.getMonth() + Month)
54
+ if (month) {
55
+ date.setMonth(date.getMonth() + month)
34
56
  }
35
- if (Year) {
36
- date.setFullYear(date.getFullYear() + Year)
57
+ if (year) {
58
+ date.setFullYear(date.getFullYear() + year)
37
59
  }
38
60
  return date
39
61
  }
@@ -247,20 +269,52 @@ export interface FormatDateOptions extends Partial<Pick<Intl.DateTimeFormatOptio
247
269
  locale?: Intl.LocalesArgument
248
270
  tz?: string
249
271
  }
272
+
250
273
  /**
251
274
  * Formats a date based on the provided format string, locale, and timezone.
252
275
  * @param date The date to format (string or Date object).
276
+ * @param format The format string (default is 'DD.MM.YY').
277
+ * @returns Formatted date string.
278
+ */
279
+ export function formatDate(date?: DateLike, format?: DateTimeAcceptedFormats): string
280
+
281
+ /**
282
+ * Formats a date based on the provided options.
283
+ * @param date The date to format (string or Date object).
253
284
  * @param opts Options for formatting the date.
254
285
  * @param opts.fmt The format string (default is 'DD.MM.YY').
255
286
  * @param opts.locale The locale to use for formatting (default is browser's locale).
256
287
  * @param opts.tz The timezone to use for formatting (default is local timezone).
257
288
  * @returns Formatted date string.
258
289
  */
290
+ export function formatDate(date?: DateLike, opts?: FormatDateOptions): string
291
+
292
+ /**
293
+ * Formats a date based on the provided format string, locale, and timezone.
294
+ * @param date The date to format (string or Date object).
295
+ * @param formatOrOpts The format string or options object.
296
+ * @returns Formatted date string.
297
+ */
259
298
  export function formatDate(
260
299
  date?: DateLike,
261
- opts: FormatDateOptions = {},
300
+ formatOrOpts?: DateTimeAcceptedFormats | FormatDateOptions,
262
301
  ): string {
263
- let { fmt: format, locale, tz: timeZone, ...rest } = opts
302
+ let format: DateTimeAcceptedFormats | undefined
303
+ let locale: Intl.LocalesArgument | undefined
304
+ let timeZone: string | undefined
305
+ let rest: Partial<Pick<Intl.DateTimeFormatOptions, 'hour12'>> = {}
306
+
307
+ // Handle both overloads
308
+ if (typeof formatOrOpts === 'string') {
309
+ // First overload: format string directly
310
+ format = formatOrOpts
311
+ } else if (formatOrOpts && typeof formatOrOpts === 'object') {
312
+ // Second overload: options object
313
+ format = formatOrOpts.fmt
314
+ locale = formatOrOpts.locale
315
+ timeZone = formatOrOpts.tz
316
+ rest = formatOrOpts
317
+ }
264
318
 
265
319
  if (!date) return ''
266
320
  format = format || 'DD.MM.YY'
@@ -22,6 +22,14 @@ export interface BtnElementOptions<
22
22
  target?: '_blank' | '_self' | '_parent' | '_top'
23
23
  }
24
24
 
25
+ export interface PillElementOptions<
26
+ T = any,
27
+ PO extends PathsOptions = DefaultPathsOptions
28
+ > extends Partial<BaseElementField<T, PO>> {
29
+ color?: string
30
+ icon?: IconType
31
+ }
32
+
25
33
  export interface TxtElementOptions<
26
34
  T = any,
27
35
  PO extends PathsOptions = DefaultPathsOptions
@@ -388,6 +396,20 @@ export function img<
388
396
  }
389
397
  }
390
398
 
399
+ export function pill<
400
+ T = any,
401
+ PO extends PathsOptions = DefaultPathsOptions
402
+ >(options: PillElementOptions<T, PO>): BaseElementField<T, PO> {
403
+ return {
404
+ $el: 'pill',
405
+ id: options?.id,
406
+ class: options?.class,
407
+ vIf: options?.vIf,
408
+ style: options?.style,
409
+ attrs: options?.attrs || {},
410
+ }
411
+ }
412
+
391
413
  export function dropdown<
392
414
  T = any,
393
415
  PO extends PathsOptions = DefaultPathsOptions
@@ -372,8 +372,10 @@ export function useSearch<T>(
372
372
  clearTimeout(debounceTimeout)
373
373
  }
374
374
 
375
- // Skip search if term is too short
376
- if (!newTerm || typeof newTerm !== 'string' || newTerm.length < minChars) {
375
+ const term = typeof newTerm === 'string' ? newTerm : ''
376
+
377
+ // Allow initial/default fetch when minChars === 0 (including empty term)
378
+ if (term.length < minChars && minChars > 0) {
377
379
  serverResults.value = []
378
380
  return
379
381
  }
@@ -382,7 +384,7 @@ export function useSearch<T>(
382
384
  debounceTimeout = window.setTimeout(async () => {
383
385
  try {
384
386
  isLoading.value = true
385
- serverResults.value = await serverSearch(newTerm)
387
+ serverResults.value = await serverSearch(term)
386
388
  } catch (error) {
387
389
  console.error('Server search error:', error)
388
390
  serverResults.value = []
@@ -390,7 +392,8 @@ export function useSearch<T>(
390
392
  isLoading.value = false
391
393
  }
392
394
  }, debounceMs)
393
- }
395
+ },
396
+ { immediate: true }
394
397
  )
395
398
  }
396
399
 
@@ -401,9 +404,13 @@ export function useSearch<T>(
401
404
  const getFilteredResults = (): T[] => {
402
405
  const term = getSearchTermValue()
403
406
 
404
- // If using server-side search and has search term, return server results
405
- if (serverSearch && term && typeof term === 'string' && term.length >= minChars) {
406
- return serverResults.value as T[]
407
+ // If using server-side search
408
+ if (serverSearch) {
409
+ if (term && typeof term === 'string' && term.length >= minChars) {
410
+ return serverResults.value as T[]
411
+ }
412
+ // If minChars is 0, allow default server results when empty
413
+ if (minChars === 0) return serverResults.value as T[]
407
414
  }
408
415
 
409
416
  // Otherwise use client-side filtering