@bagelink/vue 1.12.31 → 1.12.36

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 (50) hide show
  1. package/dist/components/AddToCalendar.vue.d.ts.map +1 -1
  2. package/dist/components/FilterQuery.vue.d.ts +30 -0
  3. package/dist/components/FilterQuery.vue.d.ts.map +1 -0
  4. package/dist/components/Modal.vue.d.ts.map +1 -1
  5. package/dist/components/QueryFilter.vue.d.ts +30 -0
  6. package/dist/components/QueryFilter.vue.d.ts.map +1 -0
  7. package/dist/components/calendar/Index.vue.d.ts.map +1 -1
  8. package/dist/components/calendar/views/AgendaView.vue.d.ts.map +1 -1
  9. package/dist/components/calendar/views/MonthView.vue.d.ts.map +1 -1
  10. package/dist/components/calendar/views/WeekView.vue.d.ts.map +1 -1
  11. package/dist/components/form/inputs/ArrayInput.vue.d.ts.map +1 -1
  12. package/dist/components/form/inputs/RadioGroup.vue.d.ts.map +1 -1
  13. package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
  14. package/dist/components/index.d.ts.map +1 -1
  15. package/dist/components/layout/AppContent.vue.d.ts.map +1 -1
  16. package/dist/components/layout/AppSidebar.vue.d.ts.map +1 -1
  17. package/dist/components/layout/TabsNav.vue.d.ts.map +1 -1
  18. package/dist/dialog/Dialog.vue.d.ts.map +1 -1
  19. package/dist/form-flow/form-flow.d.ts.map +1 -1
  20. package/dist/i18n/index.d.ts.map +1 -1
  21. package/dist/index.cjs +91 -91
  22. package/dist/index.mjs +13416 -13060
  23. package/dist/style.css +1 -1
  24. package/dist/utils/BagelFormUtils.d.ts.map +1 -1
  25. package/dist/utils/calendar/dateUtils.d.ts.map +1 -1
  26. package/package.json +2 -2
  27. package/src/components/AddToCalendar.vue +12 -10
  28. package/src/components/{Filter.vue → FilterQuery.vue} +5 -5
  29. package/src/components/Modal.vue +3 -1
  30. package/src/components/calendar/Index.vue +39 -18
  31. package/src/components/calendar/views/AgendaView.vue +190 -213
  32. package/src/components/calendar/views/MonthView.vue +9 -2
  33. package/src/components/calendar/views/WeekView.vue +14 -6
  34. package/src/components/form/inputs/ArrayInput.vue +16 -3
  35. package/src/components/form/inputs/RadioGroup.vue +19 -28
  36. package/src/components/form/inputs/SelectInput.vue +31 -5
  37. package/src/components/index.ts +1 -1
  38. package/src/components/layout/AppContent.vue +16 -14
  39. package/src/components/layout/AppSidebar.vue +6 -5
  40. package/src/components/layout/TabsNav.vue +123 -31
  41. package/src/dialog/Dialog.vue +2 -0
  42. package/src/form-flow/form-flow.ts +2 -0
  43. package/src/i18n/locales/en.json +25 -0
  44. package/src/i18n/locales/es.json +19 -0
  45. package/src/i18n/locales/fr.json +19 -1
  46. package/src/i18n/locales/he.json +25 -0
  47. package/src/i18n/locales/it.json +19 -0
  48. package/src/i18n/locales/ru.json +19 -0
  49. package/src/utils/BagelFormUtils.ts +1 -0
  50. package/src/utils/calendar/dateUtils.ts +2 -2
@@ -1,13 +1,21 @@
1
1
  <script setup lang="ts">
2
2
  import type { SetupContext } from 'vue'
3
3
  import type { CalendarEvent } from '../CalendarTypes'
4
- import { formatDate } from '@bagelink/vue'
5
- import { ref, computed, onMounted, onUnmounted, useSlots } from 'vue'
4
+ import { formatDate, getI18n, useI18n, Icon, Card } from '@bagelink/vue'
5
+ import { ref, computed, watch, watchEffect, nextTick, onMounted, onUnmounted, useSlots } from 'vue'
6
6
 
7
7
  interface AgendaViewEvent extends CalendarEvent {
8
- dayLabel: string
8
+ durationMin: number
9
+ }
10
+
11
+ interface DayGroup {
12
+ dateKey: string
13
+ date: Date
14
+ dayNumber: string
15
+ monthShort: string
16
+ weekdayLong: string
9
17
  isToday: boolean
10
- height: number
18
+ events: AgendaViewEvent[]
11
19
  }
12
20
 
13
21
  const props = defineProps<{
@@ -22,267 +30,236 @@ const emit = defineEmits<{
22
30
  }>()
23
31
 
24
32
  const slots: SetupContext['slots'] = useSlots()
33
+ const { t } = useI18n()
25
34
 
26
- // UI references
27
35
  const containerRef = ref<HTMLElement | null>(null)
28
- const currentDate = ref<Date | null>(null)
29
36
 
30
- // Process events for agenda view
31
- const processedEvents = computed(() => {
32
- const events: AgendaViewEvent[] = []
37
+ function getLocale(): string {
38
+ try { return (getI18n().global.locale as { value: string }).value } catch { return 'en' }
39
+ }
40
+
41
+ function formatDuration(minutes: number): string {
42
+ if (minutes < 60) return `${minutes} min`
43
+ const h = Math.floor(minutes / 60)
44
+ const m = minutes % 60
45
+ return m ? `${h}h ${m}m` : `${h}h`
46
+ }
47
+
48
+ // Group events by day
49
+ const dayGroups = computed((): DayGroup[] => {
50
+ const locale = getLocale()
33
51
  const today = new Date().toDateString()
34
52
 
35
- // Sort events by start time
36
- const sortedEvents = [...props.events].sort(
53
+ const sorted = [...props.events].sort(
37
54
  (a, b) => new Date(a.start_time).getTime() - new Date(b.start_time).getTime()
38
55
  )
39
56
 
40
- // Group events by date
41
- const eventsByDate = sortedEvents.reduce((acc, event) => {
42
- const date = new Date(event.start_time).toDateString()
43
- if (!acc[date]) { acc[date] = [] }
44
- acc[date].push(event)
45
- return acc
46
- }, {} as { [key: string]: CalendarEvent[] })
47
-
48
- // Process each date's events
49
- Object.entries(eventsByDate).forEach(([dateStr, dateEvents]) => {
50
- const date = new Date(dateStr)
51
- const isToday = date.toDateString() === today
52
-
53
- dateEvents.forEach((event) => {
54
- const duration = (new Date(event.end_time).getTime() - new Date(event.start_time).getTime()) / (1000 * 60)
55
- // Limit height: 60px per 30 minutes, max 200px
56
- const height = Math.min(duration / 30 * 60, 200)
57
-
58
- events.push({
59
- ...event,
60
- dayLabel: date.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' }),
61
- isToday,
62
- height
57
+ const map = new Map<string, DayGroup>()
58
+
59
+ for (const event of sorted) {
60
+ const date = new Date(event.start_time)
61
+ const dateKey = date.toDateString()
62
+
63
+ if (!map.has(dateKey)) {
64
+ map.set(dateKey, {
65
+ dateKey,
66
+ date,
67
+ dayNumber: date.toLocaleDateString(locale, { day: '2-digit' }),
68
+ monthShort: date.toLocaleDateString(locale, { month: 'short' }),
69
+ weekdayLong: date.toLocaleDateString(locale, { weekday: 'long' }),
70
+ isToday: dateKey === today,
71
+ events: [],
63
72
  })
64
- })
65
- })
73
+ }
74
+
75
+ const durationMin = Math.round(
76
+ (new Date(event.end_time).getTime() - new Date(event.start_time).getTime()) / 60000
77
+ )
78
+ map.get(dateKey)!.events.push({ ...event, durationMin })
79
+ }
66
80
 
67
- return events
81
+ return Array.from(map.values())
68
82
  })
69
83
 
70
- // Handle scroll to determine visible date
84
+ const scrolledOnce = ref(false)
85
+
86
+ // Scroll tracking (informs parent of visible date for header display only)
71
87
  function handleScroll() {
72
88
  if (!containerRef.value) { return }
73
-
74
89
  const container = containerRef.value
75
- const eventElements = container.querySelectorAll('.event')
76
- let mostVisibleEvent: Element | null = null
77
- let maxVisibility = 0
78
-
79
- // Find the event that's most visible in the viewport
80
- eventElements.forEach((element) => {
81
- const rect = element.getBoundingClientRect()
82
- const containerRect = container.getBoundingClientRect()
83
-
84
- // Calculate how much of the element is visible
85
- const visibleTop = Math.max(rect.top, containerRect.top)
86
- const visibleBottom = Math.min(rect.bottom, containerRect.bottom)
87
- const visibleHeight = Math.max(0, visibleBottom - visibleTop)
88
- const visibility = visibleHeight / rect.height
89
-
90
- if (visibility > maxVisibility) {
91
- maxVisibility = visibility
92
- mostVisibleEvent = element
93
- }
90
+ const dayEls = container.querySelectorAll<HTMLElement>('.agenda-day-group')
91
+ const containerTop = container.getBoundingClientRect().top
92
+
93
+ let closest: HTMLElement | null = null
94
+ let minDist = Infinity
95
+
96
+ dayEls.forEach((el) => {
97
+ const dist = Math.abs(el.getBoundingClientRect().top - containerTop)
98
+ if (dist < minDist) { minDist = dist; closest = el }
94
99
  })
95
100
 
96
- // Update current date if a new date is most visible
97
- if (mostVisibleEvent) {
98
- const element = mostVisibleEvent as HTMLElement
99
- const dateStr = element.dataset.date || element.getAttribute('data-date')
100
- if (dateStr) {
101
- const date = new Date(dateStr)
102
- if (!currentDate.value || date.toDateString() !== currentDate.value.toDateString()) {
103
- currentDate.value = date
104
- emit('dateChange', date)
105
- }
106
- }
101
+ if (closest) {
102
+ const dateStr = (closest as HTMLElement).dataset.date
103
+ if (dateStr) { emit('dateChange', new Date(dateStr)) }
107
104
  }
108
105
  }
109
106
 
110
- // Update event handling to emit the openPopover event
111
107
  function handleEventSelection(event: CalendarEvent, domEvent?: MouseEvent) {
112
108
  if (!slots.eventContent) {
113
109
  emit('eventClick', event)
114
110
  return
115
111
  }
116
-
117
- // Calculate position from DOM event if available
118
112
  if (domEvent) {
119
113
  const rect = (domEvent.currentTarget as HTMLElement).getBoundingClientRect()
120
- const position = {
121
- top: rect.top + window.scrollY + (rect.height / 2),
122
- left: rect.left + window.scrollX + rect.width + 10
123
- }
124
- emit('openPopover', event, position)
114
+ emit('openPopover', event, {
115
+ top: rect.top + window.scrollY + rect.height / 2,
116
+ left: rect.left + window.scrollX + rect.width + 10,
117
+ })
125
118
  } else {
126
119
  emit('openPopover', event)
127
120
  }
128
121
  }
129
122
 
130
- // Lifecycle hooks
131
- onMounted(() => {
132
- if (containerRef.value) {
133
- containerRef.value.addEventListener('scroll', handleScroll)
123
+ function toMidnight(date: Date): number {
124
+ const d = new Date(date)
125
+ d.setHours(0, 0, 0, 0)
126
+ return d.getTime()
127
+ }
128
+
129
+ function scrollToDate(targetDate: Date, behavior: 'smooth' | 'instant' | 'auto' = 'instant', direction: 'forward' | 'backward' | 'nearest' = 'nearest') {
130
+ if (!containerRef.value) { return }
131
+ const dayEls = Array.from(containerRef.value.querySelectorAll<HTMLElement>('.agenda-day-group'))
132
+ if (!dayEls.length) { return }
133
+ // Normalize to midnight to avoid timestamp-within-day comparison issues
134
+ const targetDay = toMidnight(targetDate)
135
+ let closest: HTMLElement | null = null
136
+ let minDiff = Infinity
137
+
138
+ if (direction === 'forward') {
139
+ // First day on or after target date (midnight-normalized)
140
+ for (const el of dayEls) {
141
+ const dateStr = el.dataset.date
142
+ if (!dateStr) { continue }
143
+ const t = toMidnight(new Date(dateStr))
144
+ if (t >= targetDay && t - targetDay < minDiff) { minDiff = t - targetDay; closest = el }
145
+ }
146
+ } else if (direction === 'backward') {
147
+ // Last day on or before target date (midnight-normalized)
148
+ for (const el of dayEls) {
149
+ const dateStr = el.dataset.date
150
+ if (!dateStr) { continue }
151
+ const t = toMidnight(new Date(dateStr))
152
+ if (t <= targetDay && targetDay - t < minDiff) { minDiff = targetDay - t; closest = el }
153
+ }
154
+ }
155
+
156
+ // Fallback to nearest (also used for 'nearest' direction)
157
+ if (!closest) {
158
+ for (const el of dayEls) {
159
+ const dateStr = el.dataset.date
160
+ if (!dateStr) { continue }
161
+ const diff = Math.abs(toMidnight(new Date(dateStr)) - targetDay)
162
+ if (diff < minDiff) { minDiff = diff; closest = el }
163
+ }
134
164
  }
165
+
166
+ closest?.scrollIntoView({ block: 'start', behavior })
167
+ }
168
+
169
+ // Navigate to date when buttons are clicked (parent changes startDate)
170
+ watch(() => props.startDate, (newDate, oldDate) => {
171
+ const direction = !oldDate || toMidnight(newDate) >= toMidnight(oldDate) ? 'forward' : 'backward'
172
+ nextTick(() => scrollToDate(newDate, 'smooth', direction))
135
173
  })
136
174
 
137
- onUnmounted(() => {
138
- if (containerRef.value) {
139
- containerRef.value.removeEventListener('scroll', handleScroll)
140
- }
175
+ // Initial scroll to today — wait until data covers today before scrolling,
176
+ // to avoid scrolling to a wrong date during partial/incremental data load.
177
+ watchEffect(() => {
178
+ if (scrolledOnce.value || !dayGroups.value.length) { return }
179
+ const todayTime = new Date().getTime()
180
+ const hasTodayOrLater = dayGroups.value.some(g => g.date.getTime() >= todayTime - 86400000)
181
+ if (!hasTodayOrLater) { return }
182
+ scrolledOnce.value = true
183
+ nextTick(() => scrollToDate(new Date(), 'instant', 'nearest'))
141
184
  })
185
+
186
+ onMounted(() => { containerRef.value?.addEventListener('scroll', handleScroll) })
187
+ onUnmounted(() => { containerRef.value?.removeEventListener('scroll', handleScroll) })
142
188
  </script>
143
189
 
144
190
  <template>
145
- <div class="agenda-view">
146
- <div class="agenda-header">
147
- <div class="time-column">
148
- Time
191
+ <div ref="containerRef" class="display-flex column h-100p overflow gap-0">
192
+ <template v-if="dayGroups.length === 0">
193
+ <div class="flex h-100p justify-content-center column gap-1 opacity-3">
194
+ <Icon icon="calendar" size="4" weight="200" />
195
+ {{ t('calendar.agenda.noEvents') }}
196
+ </div>
197
+ </template>
198
+
199
+ <section v-for="group in dayGroups" :key="group.dateKey" class="mb-1 agenda-day-group" :data-date="group.date.toISOString()" :data-today="group.isToday || undefined">
200
+ <div class="sticky z-2 flex gap-05 p-1 m_p-05 mb-05" :class="{ 'bg-primary-light': group.isToday, 'bg-bg': !group.isToday }">
201
+ <div class="bg-white border line-height-12 radius-1 grid align-items-center px-05 py-025 txt-center">
202
+ <p class="txt18">{{ group.dayNumber }}</p>
203
+ <p class="txt12">{{ group.monthShort }}</p>
149
204
  </div>
150
- <div class="event-column">
151
- Event
205
+ <div>
206
+ <p class=" line-height-12">{{ group.weekdayLong }}</p>
207
+ <p class="txt12 opacity-6">
208
+ {{ t('calendar.agenda.eventsCount', group.events.length, { named: { n: group.events.length } }) }}
209
+ </p>
152
210
  </div>
211
+ <p v-if="group.isToday" class="ms-auto">{{ t('calendar.today') }}</p>
153
212
  </div>
154
- <div ref="containerRef" class="agenda-content">
155
- <div
156
- v-for="event in processedEvents"
157
- :key="event.id"
158
- class="event"
159
- :style="{
160
- // height: `${event.height}px`,
161
- backgroundColor: event.color || 'var(--bgl-primary)',
162
- }"
163
- :data-date="event.start_time"
164
- @click.stop="handleEventSelection(event, $event)"
165
- >
166
- <div class="event-content">
167
- <div class="event-time">
168
- {{ formatDate(event.start_time, { format: 'HH:mm' }) }}
169
- </div>
170
- <div class="event-details">
171
- <div class="event-title">
172
- {{ event.title }}
173
- </div>
174
- <div class="event-day" :class="{ today: event.isToday }">
175
- {{ event.dayLabel }}
176
- </div>
177
- </div>
178
- </div>
213
+
214
+ <!-- Event rows -->
215
+ <article v-for="event in group.events" :key="event.id" class="agenda-event-row grid gap-05 hover pe-1" @click.stop="handleEventSelection(event, $event)">
216
+ <div class="agenda-event-time txt-center pt-1 txt14 opacity-6 bold relative">
217
+ {{ formatDate(event.start_time, { format: 'HH:mm' }) }}
179
218
  </div>
180
- </div>
181
- </div>
219
+ <Card class="agenda-event-card overflow-hidden transition py-025 px-075 my-025" frame :style="{ '--event-color': event.color || 'var(--bgl-primary)' }">
220
+ <div class="flex space-between">
221
+ <h3 class="txt16 m-0 semi">{{ event.title }}</h3>
222
+ <p class="txt12 opacity-6">{{ formatDuration(event.durationMin) }}</p>
223
+ </div>
224
+ <p v-if="event.description" class="txt12 opacity-6">{{ event.description }}</p>
225
+ </Card>
226
+ </article>
227
+ </section>
228
+ </div>
182
229
  </template>
183
230
 
184
231
  <style scoped>
185
- .agenda-view {
186
- display: flex;
187
- flex-direction: column;
188
- height: 100%;
189
- overflow: hidden;
190
- }
191
-
192
- .agenda-header {
193
- display: flex;
194
- padding: 0.5rem;
195
- border-bottom: 1px solid var(--border-color);
196
- }
197
-
198
- .time-column {
199
- width: 100px;
200
- flex-shrink: 0;
201
- }
202
-
203
- .event-column {
204
- flex-grow: 1;
205
- }
206
-
207
- .agenda-content {
208
- flex-grow: 1;
209
- overflow-y: auto;
210
- padding: 1rem 0;
232
+ .agenda-event-row {
233
+ grid-template-columns: 72px 1fr;
234
+ align-items: stretch;
211
235
  }
212
236
 
213
- .event {
214
- margin-bottom: 0.5rem;
215
- border-radius: 4px;
216
- overflow: hidden;
217
- cursor: pointer;
218
- transition: all 0.2s ease;
237
+ .agenda-event-time::after {
238
+ content: '';
239
+ display: block;
240
+ width: 1px;
241
+ height: calc(100% - 2rem);
242
+ background: var(--border-color, #e6e8ec);
243
+ margin: 0.5rem auto 0;
219
244
  }
220
245
 
221
- .event:hover {
222
- /* transform: scale(1.01); */
223
- filter: brightness(90%);
246
+ /* .agenda-event-card {
247
+ border-inline-start: var(--event-color, var(--bgl-primary)) solid calc(var(--card-border-radius) + 0.25rem);
248
+ } */
249
+
250
+ .agenda-event-card h3::before {
251
+ content: '';
252
+ width: 10px;
253
+ height: 10px;
254
+ display: inline-block;
255
+ border-radius: 100%;
256
+ margin-inline-end: 0.35rem;
257
+ background: var(--event-color, var(--bgl-primary, #2563eb));
224
258
  }
225
259
 
226
- .event-content {
227
- display: flex;
228
- height: 100%;
229
- color: white;
260
+ @media (max-width: 910px) {
261
+ .agenda-event-row {
262
+ grid-template-columns: 50px 1fr;
230
263
  }
231
-
232
- .event-time {
233
- width: 100px;
234
- flex-shrink: 0;
235
- padding: 0.5rem;
236
- display: flex;
237
- align-items: center;
238
- font-size: 0.9rem;
239
- }
240
-
241
- .event-details {
242
- flex-grow: 1;
243
- padding: 0.5rem;
244
- border-left: 1px solid rgba(255, 255, 255, 0.2);
245
- }
246
-
247
- .event-title {
248
- font-size: 1rem;
249
- margin-bottom: 0.25rem;
250
- }
251
-
252
- .event-day {
253
- font-size: 0.8rem;
254
- opacity: 0.8;
255
- }
256
-
257
- .event-day.today {
258
- color: var(--bgl-primary);
259
- opacity: 1;
260
- }
261
-
262
- @media (max-width: 768px) {
263
- .agenda-header {
264
- padding: 0.5rem;
265
- }
266
-
267
- .time-column {
268
- width: 80px;
269
- }
270
-
271
- .event-content {
272
- font-size: 0.9rem;
273
- }
274
-
275
- .event-time {
276
- width: 80px;
277
- font-size: 0.8rem;
278
- }
279
-
280
- .event-title {
281
- font-size: 0.9rem;
282
- }
283
-
284
- .event-day {
285
- font-size: 0.7rem;
286
- }
287
264
  }
288
265
  </style>
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import type { SetupContext } from 'vue'
3
3
  import type { CalendarEvent } from '../CalendarTypes'
4
- import { formatDate } from '@bagelink/vue'
4
+ import { formatDate, getI18n } from '@bagelink/vue'
5
5
  import { computed, useSlots } from 'vue'
6
6
 
7
7
  interface MonthViewEvent {
@@ -27,7 +27,14 @@ const slots: SetupContext['slots'] = useSlots()
27
27
  const isMobile = computed(() => window.innerWidth < 768)
28
28
 
29
29
  // Calendar data
30
- const weekDays = computed(() => ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'])
30
+ const weekDays = computed(() => {
31
+ let locale = 'en'
32
+ try { locale = (getI18n().global.locale as { value: string }).value } catch { /* fallback */ }
33
+ // Jan 7-13 2024 = Sun–Sat
34
+ return Array.from({ length: 7 }, (_, i) =>
35
+ new Date(2024, 0, 7 + i).toLocaleDateString(locale, { weekday: 'short' })
36
+ )
37
+ })
31
38
 
32
39
  // Calculate days for the current month view
33
40
  const currentMonth = computed(() => {
@@ -45,7 +45,7 @@ const timeRange = { start: 0, end: 24 }
45
45
 
46
46
  // Check if drag-to-create is enabled based on whether eventCreate handler is provided
47
47
  const isDragToCreateEnabled = computed(() => {
48
- return !!slots.eventCreate || emit.hasOwnProperty('eventCreate')
48
+ return !!slots.eventCreate || Object.prototype.hasOwnProperty.call(emit, 'eventCreate')
49
49
  })
50
50
 
51
51
  // Drag state
@@ -603,11 +603,6 @@ function scrollToTime(time: number) {
603
603
 
604
604
  // Lifecycle hooks
605
605
  onMounted(() => {
606
- console.log('[WeekView] mounted with props:', {
607
- availabilityMode: props.availabilityMode,
608
- availabilityEditable: props.availabilityEditable,
609
- availabilitySlotsCount: props.availabilitySlots?.length
610
- })
611
606
  updateCurrentTimeIndicator()
612
607
  currentTimeInterval.value = setInterval(updateCurrentTimeIndicator, 60000)
613
608
 
@@ -669,6 +664,13 @@ onUnmounted(() => {
669
664
  class="day-column top bottom border-start relative"
670
665
  @mousedown="props.availabilityEditable ? handleAvailMouseDown($event, index) : handleMouseDown($event, day)"
671
666
  >
667
+ <!-- Hour grid lines -->
668
+ <div
669
+ v-for="slot in timeSlots" :key="`line-${slot.time}`"
670
+ class="hour-line absolute start end pointer-events-none"
671
+ :style="{ top: `${slot.hour * slotHeight}px` }"
672
+ />
673
+
672
674
  <!-- Availability painted blocks (behind events) -->
673
675
  <template v-if="props.availabilityMode">
674
676
  <div
@@ -794,6 +796,12 @@ onUnmounted(() => {
794
796
  /* Adjust dot position */
795
797
  }
796
798
 
799
+ .hour-line {
800
+ border-top: 1px solid var(--bgl-gray-80);
801
+ z-index: 0;
802
+ height: 0;
803
+ }
804
+
797
805
  /* This ensures all grid content aligns properly */
798
806
  .overflow {
799
807
  position: relative;
@@ -4,7 +4,7 @@ import type { Ref, WritableComputedRef } from 'vue'
4
4
  import type { DraggableEvent } from '../../draggable/useDraggable'
5
5
 
6
6
  import { Btn, resolveI18n, useI18n } from '@bagelink/vue'
7
- import { computed, ref, watch } from 'vue'
7
+ import { computed, ref, watch, useSlots } from 'vue'
8
8
  import Draggable from '../../draggable/Draggable.vue'
9
9
 
10
10
  const props = defineProps<{
@@ -18,6 +18,7 @@ const props = defineProps<{
18
18
  deleteTooltip?: string
19
19
  itemLabel?: string
20
20
  collapsible?: boolean
21
+ simple?: boolean
21
22
  }>()
22
23
 
23
24
  const emit = defineEmits(['update:modelValue'])
@@ -25,6 +26,7 @@ defineSlots<{
25
26
  default: (props: { item: WritableComputedRef<T>, index: number }) => unknown
26
27
  }>()
27
28
 
29
+ const slots = useSlots()
28
30
  const { $t } = useI18n()
29
31
 
30
32
  const items = ref(Array.isArray(props.modelValue) ? [...props.modelValue] : []) as Ref<T[]>
@@ -116,6 +118,14 @@ function onUpdate(value: T, i: number) {
116
118
  updateModel()
117
119
  }
118
120
 
121
+ const isSimple = computed(() => {
122
+ if (props.simple) return true
123
+ if (!slots.default) return false
124
+ const dummyItem = computed({ get: () => undefined as T, set: () => { } })
125
+ const vnodes = slots.default({ item: dummyItem, index: 0 })
126
+ return vnodes.filter(vn => typeof vn.type !== 'symbol').length === 1
127
+ })
128
+
119
129
  // Create a writable computed ref for each item
120
130
  function getItemRef(i: number) {
121
131
  return computed({
@@ -136,9 +146,9 @@ function getItemRef(i: number) {
136
146
  {{ helpText }}
137
147
  </p>
138
148
  <template v-if="!collapsible">
139
- <div v-for="(_, i) in items" :key="i" class="array-input-row grid align-items-center pb-1">
149
+ <div v-for="(_, i) in items" :key="i" class="array-input-row grid align-items-center" :class="{ 'simpleArray': isSimple, 'pb-1': !isSimple }">
140
150
  <slot :item="getItemRef(i)" :index="i" />
141
- <div v-if="allowDelete" class="ms-075 my-1 border-start h-100p ps-025">
151
+ <div v-if="allowDelete" class=" h-100p" :class="{ 'border-start ps-025 ms-075': !simple }">
142
152
  <Btn
143
153
  v-tooltip="resolveI18n(deleteTooltip || 'Delete')" flat icon="delete"
144
154
  @click="deleteItem(i)"
@@ -227,4 +237,7 @@ function getItemRef(i: number) {
227
237
  .grid-array-line button:active {
228
238
  filter: brightness(80%);
229
239
  }
240
+ .simpleArray .label-text {
241
+ display: none;
242
+ }
230
243
  </style>
@@ -103,38 +103,29 @@ function handleChange() {
103
103
  <p v-if="label" class="group-label">
104
104
  {{ resolveI18n(label) }} <span v-if="required">*</span>
105
105
  </p>
106
- <label
107
- v-for="(opt, index) in visibleOptions" :key="opt.id || `${name}-${index}`"
108
- class="border rounded flex active-list-item hover" :for="opt.id || `${name}-${index}`"
109
- :class="{ 'p-05 gap-025': thin, 'py-1 gap-075': !thin, 'ps-05': !hideRadio, 'bg-gray-light': !bgColor && !flat, 'align-items-start': align === 'start' || align === 'top', 'align-items-center': align === 'center', 'align-items-end': align === 'end' || align === 'bottom', invertedActive }"
110
- :style="{ backgroundColor: bgColor, borderColor }"
111
- >
112
- <input
113
- :id="opt.id || `${name}-${index}`" v-model="selectedOption" :disabled class="radio-input-list"
114
- type="radio" :name :value="opt.value" :required="required" :class="{
106
+ <div class="radio-group-wrap">
107
+ <label v-for="(opt, index) in visibleOptions" :key="opt.id || `${name}-${index}`" class="border rounded flex active-list-item hover mb-05" :for="opt.id || `${name}-${index}`"
108
+ :class="{ 'p-05 gap-025': thin, 'py-1 gap-075': !thin, 'ps-05': !hideRadio, 'bg-gray-light': !bgColor && !flat, 'align-items-start': align === 'start' || align === 'top', 'align-items-center': align === 'center', 'align-items-end': align === 'end' || align === 'bottom', invertedActive }"
109
+ :style="{ backgroundColor: bgColor, borderColor }">
110
+ <input :id="opt.id || `${name}-${index}`" v-model="selectedOption" :disabled class="radio-input-list" type="radio" :name :value="opt.value" :required="required" :class="{
115
111
  'mt-025': align === 'start' || align === 'top',
116
112
  'mb-025': align === 'end' || align === 'bottom',
117
113
  'hideRadio': hideRadio,
118
- }" @focus="handleFocus" @blur="handleBlur" @change="handleChange"
119
- >
120
- <div
121
- class="flex w-100 gap-1 flex-wrap m_gap-05 m_gap-row-025"
122
- :class="{ 'txt-center justify-content-center': textAlign === 'center', 'txt-end justify-content-end': textAlign === 'right', 'txt-start justify-content-start': textAlign === 'left' }"
123
- :style="{ color: textColor }"
124
- >
125
- <img
126
- v-if="opt.imgSrc" class="py-025 radius-05 m_w40 object-fit" height="40" width="40"
127
- :src="opt.imgSrc" :alt="opt.imgAlt"
128
- >
129
- <Icon v-else-if="opt.icon" :icon="opt.icon" :size="1" />
130
- <div class="">
131
- <div v-if="opt.label" class="m-0 m_txt-14 line-height-14" v-html="opt.label" />
132
- <p v-if="opt.subLabel" class="txt-gray txt-12 m-0 pt-025">{{ opt.subLabel }}</p>
114
+ }" @focus="handleFocus" @blur="handleBlur" @change="handleChange">
115
+ <div class="flex w-100 gap-1 flex-wrap m_gap-05 m_gap-row-025"
116
+ :class="{ 'txt-center justify-content-center': textAlign === 'center', 'txt-end justify-content-end': textAlign === 'right', 'txt-start justify-content-start': textAlign === 'left' }"
117
+ :style="{ color: textColor }">
118
+ <img v-if="opt.imgSrc" class="py-025 radius-05 m_w40 object-fit" height="40" width="40" :src="opt.imgSrc" :alt="opt.imgAlt">
119
+ <Icon v-else-if="opt.icon" :icon="opt.icon" :size="1" />
120
+ <div class="">
121
+ <div v-if="opt.label" class="m-0 m_txt-14 line-height-14" v-html="opt.label" />
122
+ <p v-if="opt.subLabel" class="txt-gray txt-12 m-0 pt-025">{{ opt.subLabel }}</p>
123
+ </div>
124
+ <slot name="radioItem" v-bind="opt" />
133
125
  </div>
134
- <slot name="radioItem" v-bind="opt" />
135
- </div>
136
- <Btn v-if="deletable" class="ms-auto" flat thin icon="delete" @click="$emit('delete', opt)" />
137
- </label>
126
+ <Btn v-if="deletable" class="ms-auto" flat thin icon="delete" @click="$emit('delete', opt)" />
127
+ </label>
128
+ </div>
138
129
  </div>
139
130
  </template>
140
131