frappe-ui 0.1.192 → 0.1.193

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 (35) hide show
  1. package/package.json +2 -1
  2. package/src/components/Calendar/Calendar.story.md +3 -5
  3. package/src/components/Calendar/Calendar.story.vue +25 -14
  4. package/src/components/Calendar/Calendar.vue +232 -88
  5. package/src/components/Calendar/CalendarDaily.vue +80 -30
  6. package/src/components/Calendar/CalendarEvent.vue +204 -99
  7. package/src/components/Calendar/CalendarMonthly.vue +56 -25
  8. package/src/components/Calendar/CalendarTimeMarker.vue +12 -13
  9. package/src/components/Calendar/CalendarWeekly.vue +104 -88
  10. package/src/components/Calendar/DateMonthYearPicker.vue +271 -0
  11. package/src/components/Calendar/EventModalContent.vue +17 -6
  12. package/src/components/Calendar/Icon/DayIcon.vue +14 -0
  13. package/src/components/Calendar/Icon/MonthIcon.vue +14 -0
  14. package/src/components/Calendar/Icon/WeekIcon.vue +14 -0
  15. package/src/components/Calendar/NewEventModal.vue +24 -18
  16. package/src/components/Calendar/ShowMoreCalendarEvent.vue +7 -6
  17. package/src/components/Calendar/calendarUtils.js +135 -50
  18. package/src/components/Calendar/composables/useCalendarData.js +8 -6
  19. package/src/components/Calendar/composables/useEventModal.js +20 -6
  20. package/src/components/Calendar/index.ts +4 -0
  21. package/src/components/DatePicker/DatePicker.vue +3 -0
  22. package/src/components/TabButtons/TabButtons.vue +2 -2
  23. package/src/components/TextEditor/Menu.vue +3 -3
  24. package/src/components/TextEditor/TextEditor.vue +6 -1
  25. package/src/components/TextEditor/TextEditorFixedMenu.vue +1 -0
  26. package/src/components/TextEditor/commands.js +7 -0
  27. package/src/components/TextEditor/extensions/iframe/IframeNodeView.vue +305 -0
  28. package/src/components/TextEditor/extensions/iframe/InsertIframe.vue +199 -0
  29. package/src/components/TextEditor/extensions/iframe/iframe-extension.ts +352 -0
  30. package/src/components/TextEditor/extensions/iframe/index.ts +11 -0
  31. package/src/components/TextEditor/extensions/iframe/utils.ts +282 -0
  32. package/src/components/TextEditor/extensions/slash-commands/slash-commands-extension.ts +8 -0
  33. package/src/components/TextEditor/types.ts +1 -0
  34. package/src/components/TextInput/TextInput.vue +1 -0
  35. package/src/index.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frappe-ui",
3
- "version": "0.1.192",
3
+ "version": "0.1.193",
4
4
  "description": "A set of components and utilities for rapid UI development",
5
5
  "main": "./src/index.ts",
6
6
  "type": "module",
@@ -34,6 +34,7 @@
34
34
  "@headlessui/vue": "^1.7.14",
35
35
  "@popperjs/core": "^2.11.2",
36
36
  "@tailwindcss/forms": "^0.5.3",
37
+ "@tailwindcss/line-clamp": "^0.4.4",
37
38
  "@tailwindcss/typography": "^0.5.16",
38
39
  "@tiptap/core": "^2.26.1",
39
40
  "@tiptap/extension-code-block": "^2.26.1",
@@ -59,7 +59,6 @@ The object for this kind of event looks like:
59
59
  defaultMode: 'Month',
60
60
  isEditMode: false,
61
61
  eventIcons: {},
62
- redundantCellHeight: 50,
63
62
  hourHeight: 50,
64
63
  enableShortcuts: true,
65
64
  showIcon: true,
@@ -90,7 +89,6 @@ e.g.
90
89
  'Meeting': <MeetingIcon />,
91
90
  }
92
91
 
93
- - `redundantCellHeight`: The height of the cell to display full day events. This value is in Pixel, by default the value is `50px`.
94
92
  - `hourHeight`: The height of each cell below the full day events cell. This value is in pixel, by default the value is `50px`.
95
93
  - `enableShortcuts`: Boolean value which determines whether shortcuts will be enabled or not. By default the value is true i.e. shortcuts will be enabled, can be disabled by setting it to false, currently the calendar supports shortcuts like
96
94
 
@@ -141,7 +139,7 @@ If you wish to handle clicks on your own, the Calendar provides 3 functions to h
141
139
  :events="events"
142
140
  :onClick="(event) => console.log('onClick', event)"
143
141
  :onDblClick="(event) => console.log('onDblClick', event)"
144
- :onCellDblClick="(data) => console.log('onCellDblClick', data)"
142
+ :onCellClick="(data) => console.log('onCellClick', data)"
145
143
  />
146
144
 
147
145
  `Note: while using custom click events, the create, update & delete prop functions will not be triggered.`
@@ -166,7 +164,7 @@ If you wish to handle clicks on your own, the Calendar provides 3 functions to h
166
164
  - e: this key represent the MouseEvent.
167
165
  - calendarEvent: This key is an object, the object of calendarEvent is displayed above
168
166
 
169
- - `onCellDblClick`: The function is triggered when a cell is double clicked. In the callback function you receive an argument which is an object and it looks like this:
167
+ - `onCellClick`: The function is triggered when a cell is clicked. In the callback function you receive an argument which is an object and it looks like this:
170
168
 
171
169
  {
172
170
  e:MouseEvent,
@@ -176,7 +174,7 @@ If you wish to handle clicks on your own, the Calendar provides 3 functions to h
176
174
  }
177
175
 
178
176
  - e: this key represent the MouseEvent.
179
- - date: Date Object, which has the date of the cell which was double clicked.
177
+ - date: Date Object, which has the date of the cell which was clicked.
180
178
  - time: String, ranges from "00:00" to "23:00", where the cell was clicked in the grid that time value will be displayed over here. (Note, this will be empty in Month view)
181
179
  - view: String, shows the view in which the event was triggered.
182
180
 
@@ -54,7 +54,7 @@
54
54
  :events="events"
55
55
  :onClick="(event) => logEvent('onClick', event)"
56
56
  :onDblClick="(event) => logEvent('onDblClick', event)"
57
- :onCellDblClick="(data) => logEvent('onCellDblClick', data)"
57
+ :onCellClick="(data) => logEvent('onCellClick', data)"
58
58
  >
59
59
  </Calendar>
60
60
  </div>
@@ -72,7 +72,6 @@ const config = {
72
72
  isEditMode: true,
73
73
  eventIcons: {},
74
74
  allowCustomClickEvents: true,
75
- redundantCellHeight: 100,
76
75
  enableShortcuts: false,
77
76
  }
78
77
 
@@ -92,8 +91,10 @@ const events = ref([
92
91
  participant: 'Ryan Mathew',
93
92
  id: 'EDU-CSH-2024-00091',
94
93
  venue: 'CNF-ROOM-2024-00001',
95
- fromDate: currentMonthYear + '-02 16:30:00', //can be a date object
96
- toDate: currentMonthYear + '-02 17:30:00',
94
+ fromDate: currentMonthYear + '-02', //can be a date object
95
+ toDate: currentMonthYear + '-02',
96
+ fromTime: '16:30',
97
+ toTime: '17:30',
97
98
  color: 'violet',
98
99
  },
99
100
  {
@@ -101,8 +102,10 @@ const events = ref([
101
102
  participant: 'Ryan Mathew',
102
103
  id: 'EDU-CSH-2024-00092',
103
104
  venue: 'CNF-ROOM-2024-00002',
104
- fromDate: currentMonthYear + '-04 13:30:00',
105
- toDate: currentMonthYear + '-04 17:30:00',
105
+ fromDate: currentMonthYear + '-04',
106
+ toDate: currentMonthYear + '-04',
107
+ fromTime: '13:30',
108
+ toTime: '17:30',
106
109
  color: 'green',
107
110
  },
108
111
  {
@@ -110,8 +113,10 @@ const events = ref([
110
113
  participant: 'Sheldon',
111
114
  id: 'EDU-CSH-2024-00093',
112
115
  venue: 'CNF-ROOM-2024-00001',
113
- fromDate: currentMonthYear + '-16 10:30:00',
114
- toDate: currentMonthYear + '-16 11:30:00',
116
+ fromDate: currentMonthYear + '-16',
117
+ toDate: currentMonthYear + '-16',
118
+ fromTime: '10:30',
119
+ toTime: '11:30',
115
120
  color: 'blue',
116
121
  },
117
122
  {
@@ -119,8 +124,10 @@ const events = ref([
119
124
  participant: 'Ryan Mathew',
120
125
  id: 'EDU-CSH-2024-00094',
121
126
  venue: 'CNF-ROOM-2024-00001',
122
- fromDate: currentMonthYear + '-21 16:30:00',
123
- toDate: currentMonthYear + '-21 17:30:00',
127
+ fromDate: currentMonthYear + '-21',
128
+ toDate: currentMonthYear + '-21',
129
+ fromTime: '16:30',
130
+ toTime: '17:30',
124
131
  color: 'red',
125
132
  },
126
133
  {
@@ -128,8 +135,10 @@ const events = ref([
128
135
  participant: 'John',
129
136
  id: '#htrht41',
130
137
  venue: 'Google Meet',
131
- fromDate: currentMonthYear + '-11 00:00:00',
132
- toDate: currentMonthYear + '-11 23:59:59',
138
+ fromDate: currentMonthYear + '-11',
139
+ toDate: currentMonthYear + '-11',
140
+ fromTime: '00:00',
141
+ toTime: '02:00',
133
142
  color: 'amber',
134
143
  isFullDay: true,
135
144
  },
@@ -138,8 +147,10 @@ const events = ref([
138
147
  participant: 'Sheldon',
139
148
  id: '#htrht42',
140
149
  venue: 'Google Meet',
141
- fromDate: currentMonthYear + '-07 00:00:00',
142
- toDate: currentMonthYear + '-07 23:59:59',
150
+ fromDate: currentMonthYear + '-07',
151
+ toDate: currentMonthYear + '-07',
152
+ fromTime: '00:00',
153
+ toTime: '02:00',
143
154
  color: 'amber',
144
155
  isFullDay: true,
145
156
  },
@@ -4,36 +4,36 @@
4
4
  name="header"
5
5
  v-bind="{
6
6
  currentMonthYear,
7
+ currentYear,
8
+ currentMonth,
7
9
  enabledModes,
8
10
  activeView,
9
11
  decrement,
10
12
  increment,
11
13
  updateActiveView,
14
+ setCalendarDate,
15
+ onMonthYearChange,
16
+ selectedMonthDate,
12
17
  }"
13
18
  >
14
19
  <div class="mb-2 flex justify-between">
15
20
  <!-- left side -->
16
21
  <!-- Year, Month -->
17
- <span class="text-lg font-medium text-ink-gray-8">
18
- {{ currentMonthYear }}
19
- </span>
22
+ <div class="flex items-center">
23
+ <DateMonthYearPicker
24
+ :modelValue="selectedMonthDate"
25
+ :formatter="() => currentMonthYear"
26
+ @update:modelValue="(val) => onMonthYearChange(val)"
27
+ />
28
+ </div>
20
29
  <!-- right side -->
21
30
  <!-- actions buttons for calendar -->
22
31
  <div class="flex gap-x-1">
23
32
  <!-- Increment and Decrement Button-->
24
33
 
25
- <Button
26
- @click="decrement()"
27
- variant="ghost"
28
- class="h-4 w-4"
29
- icon="chevron-left"
30
- />
31
- <Button
32
- @click="increment()"
33
- variant="ghost"
34
- class="h-4 w-4"
35
- icon="chevron-right"
36
- />
34
+ <Button @click="decrement" variant="ghost" icon="chevron-left" />
35
+ <Button label="Today" @click="setCalendarDate()" variant="ghost" />
36
+ <Button @click="increment" variant="ghost" icon="chevron-right" />
37
37
 
38
38
  <!-- View change button, default is months or can be set via props! -->
39
39
  <TabButtons
@@ -83,15 +83,29 @@
83
83
  </div>
84
84
  </template>
85
85
  <script setup>
86
- import { computed, onMounted, onUnmounted, provide, ref } from 'vue'
86
+ import {
87
+ computed,
88
+ onMounted,
89
+ onUnmounted,
90
+ provide,
91
+ ref,
92
+ watch,
93
+ nextTick,
94
+ } from 'vue'
87
95
  import { Button } from '../Button'
88
96
  import { TabButtons } from '../TabButtons'
89
97
  import {
90
98
  getCalendarDates,
91
99
  monthList,
92
- parseDate,
100
+ handleSeconds,
101
+ formatMonthYear,
102
+ getWeekMonthParts,
93
103
  } from './calendarUtils'
94
- import { dayjs } from "../../utils/dayjs"
104
+ import { dayjs } from '../../utils/dayjs'
105
+ import DayIcon from './Icon/DayIcon.vue'
106
+ import WeekIcon from './Icon/WeekIcon.vue'
107
+ import MonthIcon from './Icon/MonthIcon.vue'
108
+ import DateMonthYearPicker from './DateMonthYearPicker.vue'
95
109
  import CalendarMonthly from './CalendarMonthly.vue'
96
110
  import CalendarWeekly from './CalendarWeekly.vue'
97
111
  import CalendarDaily from './CalendarDaily.vue'
@@ -115,7 +129,7 @@ const props = defineProps({
115
129
  type: Function,
116
130
  required: false,
117
131
  },
118
- onCellDblClick: {
132
+ onCellClick: {
119
133
  type: Function,
120
134
  required: false,
121
135
  },
@@ -129,19 +143,50 @@ const defaultConfig = {
129
143
  defaultMode: 'Month',
130
144
  isEditMode: false,
131
145
  eventIcons: {},
132
- redundantCellHeight: 50,
133
146
  hourHeight: 50,
134
147
  enableShortcuts: true,
135
148
  showIcon: true,
136
149
  timeFormat: '12h',
150
+ weekends: ['sunday'],
137
151
  }
138
152
 
139
153
  const overrideConfig = { ...defaultConfig, ...props.config }
140
154
  let activeView = ref(overrideConfig.defaultMode)
141
155
 
142
- function updateActiveView(value) {
143
- console.log(value)
156
+ function updateActiveView(value, d, isPreviousMonth, isNextMonth) {
144
157
  activeView.value = value
158
+ if (value == 'Day' && d) {
159
+ date.value = findIndexOfDate(d)
160
+ isPreviousMonth && decrementMonth()
161
+ isNextMonth && incrementMonth()
162
+ }
163
+ }
164
+
165
+ const selectedMonthDate = ref(dayjs().format('YYYY-MM-DD'))
166
+
167
+ function onMonthYearChange(val = '') {
168
+ const d = dayjs(val)
169
+ selectedMonthDate.value = d.format('YYYY-MM-DD')
170
+
171
+ setCalendarDate(selectedMonthDate.value)
172
+ }
173
+
174
+ function syncSelectedMonth(year, month) {
175
+ // Keep same day if possible; otherwise clamp to last day
176
+ if (typeof year === 'number' && typeof month === 'number') {
177
+ const currentDay = dayjs(selectedMonthDate.value).date()
178
+
179
+ let tentative = dayjs(
180
+ `${year}-${String(month + 1).padStart(2, '0')}-01`,
181
+ ).date(currentDay)
182
+
183
+ if (tentative.month() !== month) {
184
+ // overflowed into next month, use last day of target month
185
+ tentative = tentative.startOf('month').month(month).endOf('month')
186
+ }
187
+
188
+ selectedMonthDate.value = tentative.format('YYYY-MM-DD')
189
+ }
145
190
  }
146
191
 
147
192
  // shortcuts for changing the active view and navigating through the calendar
@@ -176,46 +221,67 @@ provide('config', overrideConfig)
176
221
  const parseEvents = computed(() => {
177
222
  return (
178
223
  props.events?.map((event) => {
179
- const { fromDate, toDate, ...rest } = event
180
- const date = parseDate(fromDate)
181
- const from_time = dayjs(fromDate).format("HH:mm:ss")
182
- const to_time = dayjs(toDate).format("HH:mm:ss")
183
- if (event.isFullDay) {
184
- return { ...rest, date }
224
+ const { fromDate, toDate, fromTime, toTime, ...rest } = event
225
+ const date = fromDate
226
+ const fromDateTime = fromDate + ' ' + fromTime
227
+ const toDateTime = toDate + ' ' + toTime
228
+
229
+ return {
230
+ ...rest,
231
+ date,
232
+ fromDateTime,
233
+ toDateTime,
234
+ fromDate,
235
+ toDate,
236
+ fromTime,
237
+ toTime,
185
238
  }
186
- return { ...rest, date, from_time, to_time }
187
239
  }) || []
188
240
  )
189
241
  })
190
242
  const events = ref(parseEvents.value)
191
243
 
244
+ watch(
245
+ () => props.events,
246
+ () => reloadEvents(),
247
+ { deep: true },
248
+ )
249
+
192
250
  function reloadEvents() {
193
251
  events.value = parseEvents.value
194
252
  }
195
253
 
254
+ events.value.forEach((event) => {
255
+ if (!event.fromTime || !event.toTime) return
256
+
257
+ event.fromTime = handleSeconds(event.fromTime)
258
+ event.toTime = handleSeconds(event.toTime)
259
+ })
260
+
196
261
  const { showEventModal, newEvent, openNewEventModal } = useEventModal()
197
262
 
198
263
  provide('calendarActions', {
199
264
  createNewEvent,
200
265
  updateEventState,
201
266
  deleteEvent,
202
- handleCellDblClick,
267
+ handleCellClick,
268
+ updateActiveView,
203
269
  props,
204
270
  })
205
271
 
206
272
  // CRUD actions on an event
207
273
  function createNewEvent(event) {
208
274
  events.value.push(event)
209
- event.fromDate = event.date + ' ' + event.from_time
210
- event.toDate = event.date + ' ' + event.to_time
275
+ event.fromDateTime = event.fromDate + ' ' + event.fromTime
276
+ event.toDateTime = event.toDate + ' ' + event.toTime
211
277
  emit('create', event)
212
278
  }
213
279
 
214
280
  function updateEventState(event) {
215
281
  const eventID = event.id
216
282
  let eventIndex = events.value.findIndex((e) => e.id === eventID)
217
- event.fromDate = event.date + ' ' + event.from_time
218
- event.toDate = event.date + ' ' + event.to_time
283
+ event.fromDateTime = event.fromDate + ' ' + event.fromTime
284
+ event.toDateTime = event.toDate + ' ' + event.toTime
219
285
  events.value[eventIndex] = event
220
286
  emit('update', event)
221
287
  }
@@ -228,21 +294,22 @@ function deleteEvent(eventID) {
228
294
  }
229
295
 
230
296
  function openModal(data) {
231
- const { e, view, date, time } = data
297
+ const { e, view, date, time, isFullDay } = data
232
298
  const config = overrideConfig.isEditMode
233
- openNewEventModal(e, view, date, config, time)
299
+ openNewEventModal(e, view, date, config, time, isFullDay)
234
300
  }
235
301
 
236
- function handleCellDblClick(e, date, time = '') {
302
+ function handleCellClick(e, date, time = '', isFullDay = false) {
237
303
  const data = {
238
304
  e,
239
305
  view: activeView.value,
240
306
  date,
241
307
  time,
308
+ isFullDay,
242
309
  }
243
310
 
244
- if (props.onCellDblClick) {
245
- props.onCellDblClick(data)
311
+ if (props.onCellClick) {
312
+ props.onCellClick(data)
246
313
  return
247
314
  }
248
315
  openModal(data)
@@ -250,12 +317,12 @@ function handleCellDblClick(e, date, time = '') {
250
317
 
251
318
  // Calendar View Options
252
319
  const actionOptions = [
253
- { label: 'Day', variant: 'solid' },
254
- { label: 'Week', variant: 'solid' },
255
- { label: 'Month', variant: 'solid' },
320
+ { label: 'Day', value: 'Day', iconLeft: DayIcon },
321
+ { label: 'Week', value: 'Week', iconLeft: WeekIcon },
322
+ { label: 'Month', value: 'Month', iconLeft: MonthIcon },
256
323
  ]
257
324
  let enabledModes = actionOptions.filter(
258
- (mode) => !overrideConfig.disableModes.includes(mode.label),
325
+ (mode) => !overrideConfig.disableModes.includes(mode.value),
259
326
  )
260
327
 
261
328
  let currentYear = ref(new Date().getFullYear())
@@ -302,6 +369,16 @@ function updateCurrentDate(d) {
302
369
  week.value = findCurrentWeek(d)
303
370
  }
304
371
 
372
+ function increment() {
373
+ incrementClickEvents[activeView.value]()
374
+ syncSelectedMonth(currentYear.value, currentMonth.value)
375
+ }
376
+
377
+ function decrement() {
378
+ decrementClickEvents[activeView.value]()
379
+ syncSelectedMonth(currentYear.value, currentMonth.value)
380
+ }
381
+
305
382
  const incrementClickEvents = {
306
383
  Month: incrementMonth,
307
384
  Week: incrementWeek,
@@ -316,71 +393,97 @@ const decrementClickEvents = {
316
393
 
317
394
  function incrementMonth() {
318
395
  currentMonth.value++
319
- date.value = findFirstDateOfMonth(currentMonth.value, currentYear.value)
320
- week.value = findCurrentWeek(currentMonthDates.value[date.value]) + 1
321
396
  if (currentMonth.value > 11) {
322
397
  currentMonth.value = 0
323
398
  currentYear.value++
324
399
  }
400
+ // After month changes, recompute month dates and reset to first in-month day
401
+ date.value = findFirstDateOfMonth(currentMonth.value, currentYear.value)
402
+ week.value = findCurrentWeek(currentMonthDates.value[date.value])
325
403
  }
326
404
 
327
405
  function decrementMonth() {
328
- currentMonth.value--
329
- date.value = findLastDateOfMonth(currentMonth.value, currentYear.value)
330
- week.value = findCurrentWeek(currentMonthDates.value[date.value])
331
- if (currentMonth.value < 0) {
406
+ if (currentMonth.value === 0) {
332
407
  currentMonth.value = 11
333
408
  currentYear.value--
409
+ } else {
410
+ currentMonth.value--
334
411
  }
335
- }
336
-
337
- function increment() {
338
- incrementClickEvents[activeView.value]()
339
- }
340
-
341
- function decrement() {
342
- decrementClickEvents[activeView.value]()
412
+ // After adjusting month/year, pick last in-month date and its week
413
+ date.value = findLastDateOfMonth(currentMonth.value, currentYear.value)
414
+ week.value = findCurrentWeek(currentMonthDates.value[date.value])
343
415
  }
344
416
 
345
417
  function incrementWeek() {
346
- week.value += 1
347
- if (week.value < datesInWeeks.value.length) {
348
- date.value = findIndexOfDate(datesInWeeks.value[week.value][0])
349
- }
350
- if (week.value > datesInWeeks.value.length - 1) {
351
- incrementMonth()
352
- }
353
- let nextMonthDates = filterCurrentWeekDates()
354
- if (nextMonthDates.length > 0) {
355
- incrementMonth()
356
- week.value = findCurrentWeek(nextMonthDates[0])
418
+ const nextWeek = week.value + 1 // target next week index
419
+
420
+ // Case 1: still within current grid
421
+ if (nextWeek < datesInWeeks.value.length) {
422
+ week.value = nextWeek
423
+ const weekDates = datesInWeeks.value[week.value]
424
+ const spansNextMonth = weekDates.some(
425
+ (d) => d.getMonth() !== currentMonth.value,
426
+ ) // overlap into next month
427
+ if (spansNextMonth) {
428
+ // cross boundary -> advance month
429
+ incrementMonth()
430
+ week.value = 0 // first week row of new month
431
+ const firstWeekDates = datesInWeeks.value[0]
432
+ const day = firstInMonth(firstWeekDates, currentMonth.value) // first in-month day
433
+ date.value = findIndexOfDate(day)
434
+ return
435
+ }
436
+ const day = firstInMonth(weekDates, currentMonth.value) // first in-month day in target week
437
+ date.value = findIndexOfDate(day)
438
+ return
357
439
  }
440
+
441
+ // Case 2: overflow -> next month first week
442
+ incrementMonth()
443
+ week.value = 0
444
+ const firstWeekDates = datesInWeeks.value[0]
445
+ const day = firstInMonth(firstWeekDates, currentMonth.value) // first valid in-month day
446
+ date.value = findIndexOfDate(day)
358
447
  }
359
448
 
360
449
  function decrementWeek() {
361
- week.value -= 1
362
- if (week.value < 0) {
363
- decrementMonth()
450
+ const prevWeek = week.value - 1 // target previous week index
451
+
452
+ // Case 1: still within current grid
453
+ if (prevWeek >= 0) {
454
+ week.value = prevWeek
455
+ const weekDates = datesInWeeks.value[week.value]
456
+ const spansPrevMonth = weekDates.some(
457
+ (d) => d.getMonth() !== currentMonth.value,
458
+ ) // overlap into previous month
459
+ if (spansPrevMonth) {
460
+ // cross boundary -> go to previous month
461
+ decrementMonth()
462
+ week.value = datesInWeeks.value.length - 1 // last week row of new month
463
+ const targetWeekDates = datesInWeeks.value[week.value]
464
+ const day = firstInMonth(targetWeekDates, currentMonth.value) // first day actually in that month
465
+ date.value = findIndexOfDate(day)
466
+ return
467
+ }
468
+ const day = firstInMonth(weekDates, currentMonth.value) // first in-month day in target week
469
+ date.value = findIndexOfDate(day)
364
470
  return
365
471
  }
366
472
 
367
- if (week.value > 0) {
368
- date.value = findIndexOfDate(datesInWeeks.value[week.value][0])
369
- }
370
-
371
- let previousMonthDates = filterCurrentWeekDates()
372
- if (previousMonthDates.length > 0) {
373
- decrementMonth()
374
- week.value = findCurrentWeek(previousMonthDates[0])
375
- }
376
- }
377
-
378
- function filterCurrentWeekDates() {
379
- let currentWeekDates = datesInWeeks.value[week.value]
380
- let differentMonthDates = currentWeekDates.filter(
473
+ // Case 2: underflow -> jump to previous month
474
+ decrementMonth()
475
+ let targetIndex = datesInWeeks.value.length - 1 // start at last row
476
+ const lastWeekDates = datesInWeeks.value[targetIndex]
477
+ const hasNextMonthDates = lastWeekDates.some(
381
478
  (d) => d.getMonth() !== currentMonth.value,
382
- )
383
- return differentMonthDates
479
+ ) // overlap into next month
480
+ if (hasNextMonthDates && targetIndex > 0) {
481
+ targetIndex = targetIndex - 1 // skip overlap row
482
+ }
483
+ week.value = targetIndex
484
+ const targetWeekDates = datesInWeeks.value[week.value]
485
+ const day = firstInMonth(targetWeekDates, currentMonth.value) // first valid in-month day
486
+ date.value = findIndexOfDate(day)
384
487
  }
385
488
 
386
489
  function incrementDay() {
@@ -403,6 +506,10 @@ function decrementDay() {
403
506
  }
404
507
  }
405
508
 
509
+ function firstInMonth(weekDates, month) {
510
+ return weekDates.find((d) => d.getMonth() === month) || weekDates[0]
511
+ }
512
+
406
513
  function findLastDateOfMonth(month, year) {
407
514
  let inputDate = new Date(year, month + 1, 0)
408
515
  let lastDateIndex = currentMonthDates.value.findIndex(
@@ -424,8 +531,26 @@ function findIndexOfDate(date) {
424
531
  (d) => new Date(d).toDateString() === new Date(date).toDateString(),
425
532
  )
426
533
  }
534
+
427
535
  const currentMonthYear = computed(() => {
428
- return monthList[currentMonth.value] + ', ' + currentYear.value
536
+ // Non-week views or empty week fallback
537
+ if (activeView.value !== 'Week')
538
+ return formatMonthYear(currentMonth.value, currentYear.value)
539
+
540
+ const weekDates = datesInWeeks.value[week.value] || []
541
+ if (!weekDates.length)
542
+ return formatMonthYear(currentMonth.value, currentYear.value)
543
+
544
+ const parts = getWeekMonthParts(weekDates)
545
+ if (parts.length === 1) return formatMonthYear(parts[0].month, parts[0].year)
546
+
547
+ const short = monthList.map((m) => m.slice(0, 3))
548
+ const first = parts[0]
549
+ const last = parts[parts.length - 1]
550
+
551
+ return first.year === last.year
552
+ ? `${short[first.month]} - ${short[last.month]} ${first.year}` // Same year span
553
+ : `${short[first.month]} ${first.year} - ${short[last.month]} ${last.year}` // Cross-year span
429
554
  })
430
555
 
431
556
  function isCurrentMonthDate(date) {
@@ -433,5 +558,24 @@ function isCurrentMonthDate(date) {
433
558
  return date.getMonth() === currentMonth.value
434
559
  }
435
560
 
561
+ function setCalendarDate(d) {
562
+ const dt = d ? new Date(d) : new Date()
563
+ if (dt.toString() === 'Invalid Date') return
564
+ currentYear.value = dt.getFullYear()
565
+ currentMonth.value = dt.getMonth()
566
+ currentDate.value = dt
567
+ // Wait for reactive recalculations of month dates
568
+ nextTick(() => {
569
+ week.value = findCurrentWeek(dt)
570
+ const idx = findIndexOfDate(dt)
571
+ if (idx >= 0) {
572
+ date.value = idx
573
+ } else {
574
+ // Fallback: first date of month
575
+ date.value = findFirstDateOfMonth(currentMonth.value, currentYear.value)
576
+ }
577
+ })
578
+ }
579
+
436
580
  defineExpose({ reloadEvents })
437
581
  </script>