@bagelink/vue 1.4.107 → 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 (69) hide show
  1. package/dist/components/Btn.vue.d.ts +8 -0
  2. package/dist/components/Btn.vue.d.ts.map +1 -1
  3. package/dist/components/ListItem.vue.d.ts +6 -1
  4. package/dist/components/ListItem.vue.d.ts.map +1 -1
  5. package/dist/components/analytics/BarChart.vue.d.ts +39 -0
  6. package/dist/components/analytics/BarChart.vue.d.ts.map +1 -0
  7. package/dist/components/analytics/KpiCard.vue.d.ts +24 -0
  8. package/dist/components/analytics/KpiCard.vue.d.ts.map +1 -0
  9. package/dist/components/analytics/LineChart.vue.d.ts +26 -0
  10. package/dist/components/analytics/LineChart.vue.d.ts.map +1 -0
  11. package/dist/components/analytics/PieChart.vue.d.ts +24 -0
  12. package/dist/components/analytics/PieChart.vue.d.ts.map +1 -0
  13. package/dist/components/analytics/index.d.ts +5 -0
  14. package/dist/components/analytics/index.d.ts.map +1 -0
  15. package/dist/components/form/BagelForm.vue.d.ts.map +1 -1
  16. package/dist/components/form/inputs/DatePicker.vue.d.ts +1 -0
  17. package/dist/components/form/inputs/DatePicker.vue.d.ts.map +1 -1
  18. package/dist/components/form/inputs/RadioGroup.vue.d.ts +6 -10
  19. package/dist/components/form/inputs/RadioGroup.vue.d.ts.map +1 -1
  20. package/dist/components/form/inputs/SelectInput.vue.d.ts +2 -2
  21. package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
  22. package/dist/components/layout/AppContent.vue.d.ts +34 -0
  23. package/dist/components/layout/AppContent.vue.d.ts.map +1 -0
  24. package/dist/components/layout/AppLayout.vue.d.ts +27 -0
  25. package/dist/components/layout/AppLayout.vue.d.ts.map +1 -0
  26. package/dist/components/layout/AppSidebar.vue.d.ts +44 -0
  27. package/dist/components/layout/AppSidebar.vue.d.ts.map +1 -0
  28. package/dist/components/layout/index.d.ts +3 -0
  29. package/dist/components/layout/index.d.ts.map +1 -1
  30. package/dist/composables/useFormField.d.ts.map +1 -1
  31. package/dist/composables/useSchemaField.d.ts.map +1 -1
  32. package/dist/index.cjs +19 -19
  33. package/dist/index.mjs +10 -10
  34. package/dist/style.css +1 -1
  35. package/dist/types/BagelForm.d.ts +2 -2
  36. package/dist/types/BagelForm.d.ts.map +1 -1
  37. package/dist/utils/BagelFormUtils.d.ts +1 -2
  38. package/dist/utils/BagelFormUtils.d.ts.map +1 -1
  39. package/dist/utils/calendar/dateUtils.d.ts +21 -0
  40. package/dist/utils/calendar/dateUtils.d.ts.map +1 -1
  41. package/dist/utils/elementUtils.d.ts +5 -0
  42. package/dist/utils/elementUtils.d.ts.map +1 -1
  43. package/dist/utils/useSearch.d.ts.map +1 -1
  44. package/package.json +1 -1
  45. package/src/components/Btn.vue +53 -0
  46. package/src/components/ListItem.vue +32 -24
  47. package/src/components/analytics/BarChart.vue +153 -0
  48. package/src/components/analytics/KpiCard.vue +84 -0
  49. package/src/components/analytics/LineChart.vue +267 -0
  50. package/src/components/analytics/PieChart.vue +196 -0
  51. package/src/components/analytics/index.ts +4 -0
  52. package/src/components/form/BagelForm.vue +24 -0
  53. package/src/components/form/inputs/DatePicker.vue +3 -2
  54. package/src/components/form/inputs/RadioGroup.vue +60 -35
  55. package/src/components/form/inputs/SelectInput.vue +94 -101
  56. package/src/components/form/inputs/Upload/upload.css +135 -138
  57. package/src/components/layout/AppContent.vue +105 -0
  58. package/src/components/layout/AppLayout.vue +124 -0
  59. package/src/components/layout/AppSidebar.vue +271 -0
  60. package/src/components/layout/index.ts +5 -0
  61. package/src/composables/useFormField.ts +6 -0
  62. package/src/composables/useSchemaField.ts +31 -3
  63. package/src/styles/inputs.css +9 -0
  64. package/src/styles/theme.css +2 -2
  65. package/src/types/BagelForm.ts +3 -2
  66. package/src/utils/BagelFormUtils.ts +1 -3
  67. package/src/utils/calendar/dateUtils.ts +71 -17
  68. package/src/utils/elementUtils.ts +22 -0
  69. package/src/utils/useSearch.ts +14 -7
@@ -1,12 +1,24 @@
1
1
  <script lang="ts" setup>
2
2
  import type { IconType, Option } from '@bagelink/vue'
3
3
  import type { AlignedPlacement } from '../../Dropdown.vue'
4
- import { Btn, Card, Skeleton, Dropdown, Icon, TextInput, useSearch } from '@bagelink/vue'
4
+ import { Btn, Card, Skeleton, Dropdown, Icon, TextInput, useSearch } from '@bagelink/vue'
5
5
  import { onMounted, watch } from 'vue'
6
6
  import 'floating-vue/style.css'
7
7
 
8
+ type OptionsSource = Option[] | ((query: string) => Promise<Option[]>)
9
+ const props = withDefaults(defineProps<PropTypes>(), {
10
+ placeholder: 'Select',
11
+ placement: 'bottom-start',
12
+ })
13
+
14
+ const emit = defineEmits(['update:modelValue'])
15
+
16
+ const isAsyncSource = (src: OptionsSource): src is (q: string) => Promise<Option[]> => typeof src === 'function'
17
+
18
+ type Primitive = string | number | boolean
19
+
8
20
  interface PropTypes {
9
- options: Option[]
21
+ options: OptionsSource
10
22
  placeholder?: string
11
23
  disabled?: boolean
12
24
  modelValue?: Option
@@ -21,16 +33,8 @@ interface PropTypes {
21
33
  clearable?: boolean
22
34
  placement?: AlignedPlacement
23
35
  searchPlaceholder?: string
24
- onSearch?: (search: string) => Promise<Option[]>
25
36
  }
26
37
 
27
- const props = withDefaults(defineProps<PropTypes>(), {
28
- placeholder: 'Select',
29
- placement: 'bottom-start',
30
- })
31
-
32
- const emit = defineEmits(['update:modelValue']) // Add 'search' event
33
-
34
38
  const searchInput = $ref<HTMLElement | undefined>()
35
39
 
36
40
  let selectedItems = $ref<Option[]>([])
@@ -55,7 +59,12 @@ const selectedLabel = $computed((): string => {
55
59
  })
56
60
  const searchPlaceholder = $computed(() => props.searchPlaceholder ?? selectedLabel ?? 'Search')
57
61
 
58
- const { results, isLoading } = useSearch<Option>({ searchTerm: () => searchTerm, serverSearch: props.onSearch, items: () => props.options })
62
+ const { results, isLoading } = useSearch<Option>({
63
+ searchTerm: () => searchTerm,
64
+ serverSearch: isAsyncSource(props.options) ? props.options : undefined,
65
+ items: () => (Array.isArray(props.options) ? props.options : []),
66
+ minChars: isAsyncSource(props.options) ? 0 : 2,
67
+ })
59
68
 
60
69
  let highlightedIndex = $ref(-1)
61
70
 
@@ -81,20 +90,35 @@ function navigate(direction: 'up' | 'down') {
81
90
 
82
91
  const isSelected = (option: Option) => selectedItems.find(item => getValue(option) === getValue(item)) !== undefined
83
92
 
93
+ function scrollToSelectedItem() {
94
+ if (!selectOptions || selectedItemCount === 0) return
95
+
96
+ // Find the first selected item in the results
97
+ const selectedIndex = results.value.findIndex(option => isSelected(option))
98
+ if (selectedIndex === -1) return
99
+
100
+ // Get the selected option element
101
+ const selectedElement = selectOptions.children[selectedIndex] as HTMLElement
102
+ if (!selectedElement) return
103
+
104
+ // Scroll the selected item into view
105
+ selectedElement.scrollIntoView({
106
+ block: 'center',
107
+ behavior: 'instant'
108
+ })
109
+ }
110
+
84
111
  function getLabel(option: Option) {
85
- if (!option) return ''
86
- if (typeof option === 'string') return option
87
- if (typeof option === 'number') return `${option}`
112
+ if (option == null) return ''
113
+ if (typeof option === 'object') return option.label ?? String((option as any).value ?? '')
88
114
  if (typeof option === 'boolean') return option ? 'Yes' : 'No'
89
- return option.label
115
+ return String(option)
90
116
  }
91
117
 
92
- function getValue(option?: Option) {
93
- if (!option) return
94
- if (typeof option === 'string') return option
95
- if (typeof option === 'number') return option
96
- if (typeof option === 'boolean') return option ? 'Yes' : 'No'
97
- return option.value
118
+ function getValue(option?: Option): Primitive | undefined {
119
+ if (option == null) return
120
+ if (typeof option === 'object') return option.value as Primitive
121
+ return option as Primitive
98
122
  }
99
123
 
100
124
  function focusInput() {
@@ -109,14 +133,16 @@ function select(option: Option) {
109
133
  const existingIndex = selectedItems.findIndex(
110
134
  item => getValue(item) === getValue(option),
111
135
  )
112
- if (existingIndex > -1) { selectedItems.splice(existingIndex, 1)
136
+ if (existingIndex > -1) {
137
+ selectedItems.splice(existingIndex, 1)
113
138
  }
114
139
  else if (props.multiselect) {
115
140
  const current = [...selectedItems]
116
141
  current.push(option)
117
142
 
118
143
  selectedItems = current
119
- } else { selectedItems.splice(0, selectedItemCount, option)
144
+ } else {
145
+ selectedItems.splice(0, selectedItemCount, option)
120
146
  }
121
147
 
122
148
  if (!props.multiselect) open = false
@@ -135,17 +161,18 @@ function emitUpdate() {
135
161
 
136
162
  function compareArrays(arr1: Option[], arr2: Option[]) {
137
163
  if (arr1.length !== arr2.length) return false
138
- const arr1Values = [...arr1].map(a => getValue(a)).filter(Boolean)
139
- const arr2Values = [...arr2].map(a => getValue(a)).filter(Boolean)
140
- const isSame = arr1Values.every((item: Option) => arr2Values.includes(item))
141
- return isSame
164
+ const s1 = arr1.map(getValue).filter(Boolean).map(String).sort()
165
+ const s2 = arr2.map(getValue).filter(Boolean).map(String).sort()
166
+ return s1.every((v, i) => v === s2[i])
142
167
  }
143
168
 
144
169
  watch(
145
170
  () => props.modelValue,
146
171
  (newVal: Option | Option[]) => {
147
172
  if (!props.multiselect) {
148
- const newOption = props.options.find(o => getValue(o) === newVal) ?? newVal
173
+ const newOption = Array.isArray(props.options)
174
+ ? (props.options.find(o => getValue(o) === newVal) ?? newVal)
175
+ : newVal
149
176
  if (newOption && !isSelected(newOption)) selectedItems = [newOption]
150
177
  } else {
151
178
  const newData = [newVal].flat()
@@ -161,15 +188,19 @@ watch(
161
188
  watch(() => open, (value) => {
162
189
  if (value) {
163
190
  highlightedIndex = -1
191
+ // Scroll to selected item when dropdown opens
192
+ setTimeout(() => { scrollToSelectedItem() }, 10)
164
193
  }
165
194
  })
166
195
 
167
196
  watch(
168
197
  () => props.options,
169
198
  () => {
199
+ const opts = props.options
200
+ if (!Array.isArray(opts)) return
170
201
  selectedItems.forEach((option, i) => {
171
- const exists = props.options.find(
172
- o => getValue(o) === getValue(option),
202
+ const exists = opts.find(
203
+ (o: Option) => getValue(o) === getValue(option),
173
204
  )
174
205
  if (exists === undefined) selectedItems.splice(i, 1)
175
206
  else selectedItems.splice(i, 1, exists)
@@ -182,9 +213,10 @@ watch(
182
213
  )
183
214
 
184
215
  onMounted(() => {
185
- if (props.defaultValue !== undefined) {
186
- const defaultOption = props.options.find(
187
- o => getValue(o) === getValue(props.defaultValue),
216
+ const opts = props.options
217
+ if (props.defaultValue !== undefined && Array.isArray(opts)) {
218
+ const defaultOption = opts.find(
219
+ (o: Option) => getValue(o) === getValue(props.defaultValue),
188
220
  )
189
221
 
190
222
  if (defaultOption === undefined) return
@@ -196,11 +228,7 @@ onMounted(() => {
196
228
 
197
229
  <template>
198
230
  <Dropdown
199
- ref="dropdown"
200
- v-model:shown="open"
201
- :placement="placement"
202
- class="bagel-input selectinput"
203
- no-auto-focus
231
+ ref="dropdown" v-model:shown="open" :placement="placement" class="bagel-input selectinput" no-auto-focus
204
232
  @click.stop
205
233
  >
206
234
  <template #trigger>
@@ -208,24 +236,13 @@ onMounted(() => {
208
236
  {{ label }} <span v-if="required && label">*</span>
209
237
  <div class="flex gap-05">
210
238
  <TextInput
211
- v-if="searchable && open"
212
- ref="searchInput"
213
- v-model="searchTerm"
214
- class="mb-0"
215
- :placeholder="searchPlaceholder"
216
- icon="search"
217
- @input="selected = false"
218
- @click="focusInput"
219
- @keydown.down.prevent="navigate('down')"
220
- @keydown.up.prevent="navigate('up')"
239
+ v-if="searchable && open" ref="searchInput" v-model="searchTerm" class="mb-0"
240
+ :placeholder="searchPlaceholder" icon="search" @input="selected = false" @click="focusInput"
241
+ @keydown.down.prevent="navigate('down')" @keydown.up.prevent="navigate('up')"
221
242
  />
222
243
  <button
223
- v-else
224
- :disabled="disabled"
225
- type="button"
226
- class="selectinput-btn"
227
- :class="{ isEmpty: selectedItemCount === 0 }"
228
- @keydown.down.prevent="navigate('down')"
244
+ v-else :disabled="disabled" type="button" class="selectinput-btn"
245
+ :class="{ isEmpty: selectedItemCount === 0 }" @keydown.down.prevent="navigate('down')"
229
246
  @keydown.up.prevent="navigate('up')"
230
247
  >
231
248
  <Icon v-if="icon" :icon="icon" />
@@ -234,61 +251,31 @@ onMounted(() => {
234
251
  </p>
235
252
 
236
253
  <div v-if="clearable && selectedItemCount > 0" class="ms-auto ps-05 me-05">
237
- <Btn
238
- flat
239
- thin
240
- icon="clear"
241
- class="color-gray"
242
- @click="selectedItems = []; emitUpdate()"
243
- />
254
+ <Btn flat thin icon="clear" class="color-gray" @click="selectedItems = []; emitUpdate()" />
244
255
  </div>
245
- <Icon
246
- v-if="!disabled"
247
- thin
248
- v-bind="{ icon: open ? 'unfold_less' : 'unfold_more' }"
249
- />
256
+ <Icon v-if="!disabled" thin v-bind="{ icon: open ? 'unfold_less' : 'unfold_more' }" />
250
257
  </button>
251
258
  <input
252
- v-if="required"
253
- tabindex="-1"
254
- style="width: 0; height: 0; position: absolute; opacity: 0; z-index: -1"
255
- :value="selectedItems"
259
+ v-if="required" tabindex="-1"
260
+ style="width: 0; height: 0; position: absolute; opacity: 0; z-index: -1" :value="selectedItems"
256
261
  required
257
262
  >
258
263
  </div>
259
264
  </label>
260
265
  </template>
261
266
  <Skeleton v-if="isLoading" :count="6" width="200px" height="25px" class="mx-1 my-1" />
262
- <Card
263
- class="p-05"
264
- :style="{ width: fullWidth ? '100%' : 'auto' }"
265
- @click.stop
266
- >
267
+ <Card class="p-05" :style="{ width: fullWidth ? '100%' : 'auto' }" @click.stop>
267
268
  <div ref="selectOptions" class="selectinput-options" :class="{ multiselect }">
268
269
  <div
269
- v-for="(option, i) in results"
270
- :key="`${option}${i}`"
271
- class="selectinput-option hover gap-1 align-items-center"
272
- :class="{ selected: isSelected(option) }"
273
- role="option"
274
- tabindex="0"
275
- :aria-selected="isSelected(option)"
276
- @click="select(option)"
277
- @keydown.enter="select(option)"
278
- @keydown.down.prevent="navigate('down')"
270
+ v-for="(option, i) in results" :key="`${String(getValue(option))}-${i}`"
271
+ class="selectinput-option hover gap-1 align-items-center" :class="{ selected: isSelected(option) }"
272
+ role="option" tabindex="0" :aria-selected="isSelected(option)" @click="select(option)"
273
+ @keydown.enter="select(option)" @keydown.down.prevent="navigate('down')"
279
274
  @keydown.up.prevent="navigate('up')"
280
275
  >
281
276
  <template v-if="multiselect">
282
- <Icon
283
- v-if="isSelected(option)" :size="1.1"
284
- icon="select_check_box"
285
- />
286
- <Icon
287
- v-if="!isSelected(option)"
288
- class="opacity-3"
289
- icon="check_box_outline_blank"
290
- :size="1.1"
291
- />
277
+ <Icon v-if="isSelected(option)" :size="1.1" icon="select_check_box" />
278
+ <Icon v-if="!isSelected(option)" class="opacity-3" icon="check_box_outline_blank" :size="1.1" />
292
279
  </template>
293
280
  <span class="block">
294
281
  {{ getLabel(option) }}
@@ -311,15 +298,17 @@ onMounted(() => {
311
298
  border-radius: 5px;
312
299
  transition: all 0.2s;
313
300
  display: grid;
314
- grid-template-columns:1fr;
301
+ grid-template-columns: 1fr;
315
302
  justify-content: space-between;
316
303
  width: 100%;
317
304
  font-size: var(--input-font-size);
318
305
  margin-block: 0.15rem;
319
306
  }
320
- .selectinput-option .bgl_icon-font{
307
+
308
+ .selectinput-option .bgl_icon-font {
321
309
  line-height: normal;
322
310
  }
311
+
323
312
  .selectinput-options.multiselect .selectinput-option {
324
313
  grid-template-columns: 10px 1fr;
325
314
 
@@ -330,14 +319,17 @@ onMounted(() => {
330
319
  overflow-y: auto;
331
320
  }
332
321
 
333
- .selectinput-option:hover, .highlight {
322
+ .selectinput-option:hover,
323
+ .highlight {
334
324
  background: var(--bgl-gray-tint);
335
325
  }
326
+
336
327
  .isEmpty p {
337
328
  opacity: 0.3;
338
329
  }
339
- .selected{
340
- /* background: var(--bgl-primary-tint); */
330
+
331
+ .selected {
332
+ color: var(--bgl-primary);
341
333
  background: var(--bgl-selected);
342
334
  }
343
335
  </style>
@@ -361,7 +353,8 @@ onMounted(() => {
361
353
  font-family: inherit;
362
354
  font-size: var(--input-font-size);
363
355
  }
364
- .selectinput-btn p{
356
+
357
+ .selectinput-btn p {
365
358
  white-space: nowrap;
366
359
  overflow: hidden;
367
360
  text-overflow: ellipsis;