@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.
- package/dist/components/AddToCalendar.vue.d.ts.map +1 -1
- package/dist/components/FilterQuery.vue.d.ts +30 -0
- package/dist/components/FilterQuery.vue.d.ts.map +1 -0
- package/dist/components/Modal.vue.d.ts.map +1 -1
- package/dist/components/QueryFilter.vue.d.ts +30 -0
- package/dist/components/QueryFilter.vue.d.ts.map +1 -0
- package/dist/components/calendar/Index.vue.d.ts.map +1 -1
- package/dist/components/calendar/views/AgendaView.vue.d.ts.map +1 -1
- package/dist/components/calendar/views/MonthView.vue.d.ts.map +1 -1
- package/dist/components/calendar/views/WeekView.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/ArrayInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RadioGroup.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/layout/AppContent.vue.d.ts.map +1 -1
- package/dist/components/layout/AppSidebar.vue.d.ts.map +1 -1
- package/dist/components/layout/TabsNav.vue.d.ts.map +1 -1
- package/dist/dialog/Dialog.vue.d.ts.map +1 -1
- package/dist/form-flow/form-flow.d.ts.map +1 -1
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/index.cjs +91 -91
- package/dist/index.mjs +13416 -13060
- package/dist/style.css +1 -1
- package/dist/utils/BagelFormUtils.d.ts.map +1 -1
- package/dist/utils/calendar/dateUtils.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/components/AddToCalendar.vue +12 -10
- package/src/components/{Filter.vue → FilterQuery.vue} +5 -5
- package/src/components/Modal.vue +3 -1
- package/src/components/calendar/Index.vue +39 -18
- package/src/components/calendar/views/AgendaView.vue +190 -213
- package/src/components/calendar/views/MonthView.vue +9 -2
- package/src/components/calendar/views/WeekView.vue +14 -6
- package/src/components/form/inputs/ArrayInput.vue +16 -3
- package/src/components/form/inputs/RadioGroup.vue +19 -28
- package/src/components/form/inputs/SelectInput.vue +31 -5
- package/src/components/index.ts +1 -1
- package/src/components/layout/AppContent.vue +16 -14
- package/src/components/layout/AppSidebar.vue +6 -5
- package/src/components/layout/TabsNav.vue +123 -31
- package/src/dialog/Dialog.vue +2 -0
- package/src/form-flow/form-flow.ts +2 -0
- package/src/i18n/locales/en.json +25 -0
- package/src/i18n/locales/es.json +19 -0
- package/src/i18n/locales/fr.json +19 -1
- package/src/i18n/locales/he.json +25 -0
- package/src/i18n/locales/it.json +19 -0
- package/src/i18n/locales/ru.json +19 -0
- package/src/utils/BagelFormUtils.ts +1 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
81
|
+
return Array.from(map.values())
|
|
68
82
|
})
|
|
69
83
|
|
|
70
|
-
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
121
|
-
top: rect.top + window.scrollY +
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
151
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
181
|
-
|
|
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-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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(() =>
|
|
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 ||
|
|
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="
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
<
|
|
135
|
-
</
|
|
136
|
-
|
|
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
|
|