picocrank 1.25.2 → 1.27.0

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 (51) hide show
  1. package/eslint.config.js +75 -0
  2. package/package.json +17 -4
  3. package/router.js +74 -0
  4. package/styles/dark-theme.css +20 -11
  5. package/styles/picocrank-extensions.css +123 -30
  6. package/supplemental-themes/catppuccin-latte-frappe/theme.css +10 -24
  7. package/supplemental-themes/dracula-alucard/theme.css +10 -24
  8. package/supplemental-themes/gruvbox-dark-light/theme.css +10 -24
  9. package/supplemental-themes/waffles/theme.css +2 -16
  10. package/vite.config.js +5 -0
  11. package/vue/components/Breadcrumbs.vue +192 -45
  12. package/vue/components/Calendar.vue +162 -145
  13. package/vue/components/DangerZone.vue +186 -0
  14. package/vue/components/FormField.spec.js +350 -0
  15. package/vue/components/FormField.vue +236 -32
  16. package/vue/components/FormLayout.spec.js +56 -0
  17. package/vue/components/Header.vue +22 -6
  18. package/vue/components/Login.vue +152 -41
  19. package/vue/components/NavigationGrid.vue +26 -180
  20. package/vue/components/NotificationBlock.vue +120 -0
  21. package/vue/components/NotificationPopup.vue +44 -41
  22. package/vue/components/Pagination.vue +5 -4
  23. package/vue/components/ReadOnlyTextArea.vue +1 -1
  24. package/vue/components/SidebarNavLink.vue +3 -3
  25. package/vue/components/Table.vue +107 -79
  26. package/vue/components/TableColumnOptionsPopover.vue +9 -0
  27. package/vue/components/Tabs.spec.js +101 -0
  28. package/vue/components/Tabs.vue +455 -81
  29. package/vue/components/TopBar.vue +2 -2
  30. package/vue/components/form-validation.spec.js +126 -0
  31. package/vue/composables/tableColumnPriorities.js +33 -0
  32. package/vue/composables/tableFilters.js +1 -1
  33. package/vue/data/navSectionHubs.js +196 -0
  34. package/vue/data/routeBreadcrumbs.js +90 -0
  35. package/vue/examples/Admin.vue +12 -12
  36. package/vue/examples/App.vue +27 -36
  37. package/vue/examples/CalendarExample.vue +32 -22
  38. package/vue/examples/ExampleHeaderThemeSwitcher.vue +1 -1
  39. package/vue/examples/FormExample.vue +76 -48
  40. package/vue/examples/Login.vue +5 -3
  41. package/vue/examples/LoginExample.vue +99 -15
  42. package/vue/examples/NavSectionHub.vue +36 -0
  43. package/vue/examples/PageStructure.vue +15 -12
  44. package/vue/examples/StatusExample.vue +248 -129
  45. package/vue/examples/TableExample.vue +72 -71
  46. package/vue/examples/TabsExample.vue +236 -257
  47. package/vue/examples/ThemePreview.vue +21 -23
  48. package/vue/examples/ViewItem.vue +32 -5
  49. package/vue/examples/Welcome.vue +3 -40
  50. package/vue/test/formTestUtils.js +279 -0
  51. package/vue/test/setup.js +5 -0
@@ -1,82 +1,79 @@
1
- <script setup lang="ts">
1
+ <script setup>
2
2
  import { ref, computed, watch, onUnmounted } from 'vue'
3
3
  import { HugeiconsIcon } from '@hugeicons/vue'
4
4
 
5
- export interface CalendarEvent {
6
- id: string | number
7
- title: string
8
- startDate?: Date | string | null
9
- endDate?: Date | string | null
10
- date?: Date | string | null
11
- /** Optional Hugeicons icon (from @hugeicons/core-free-icons). */
12
- icon?: unknown
13
- /** Optional CSS color overriding the default event background. */
14
- color?: string
15
- [key: string]: any
16
- }
17
-
18
- /** Payload for `event-move-request`: call `respond(true)` after a successful commit, or `respond(false)` to decline (e.g. failed server update). */
19
- export interface CalendarEventMoveRequest {
20
- event: CalendarEvent
21
- /** Calendar cell where the drag started (for multi-day events, the visible segment day). */
22
- sourceDate: Date
23
- /** Calendar cell where the event was dropped. */
24
- targetDate: Date
25
- /** Invoke exactly once with the outcome. If omitted until timeout, the move is treated as rejected. */
26
- respond: (accepted: boolean) => void
27
- }
28
-
29
- export interface CalendarProps {
30
- events: CalendarEvent[]
31
- monthNames?: string[]
32
- dayNames?: string[]
33
- loading?: boolean
34
- error?: string | null
35
- /** When true, events can be dragged to another day. */
36
- eventDragEnabled?: boolean
37
- /**
38
- * Max time (ms) to wait for `respond()` on `event-move-request`.
39
- * If `respond` is not called in time, the move is rejected and a warning is logged.
40
- * Set to `0` to disable the timeout (not recommended if listeners may omit `respond`).
41
- */
42
- eventMoveResponseTimeoutMs?: number
43
- // Formatter functions
44
- getEventDate?: (event: CalendarEvent) => Date | null
45
- getEventDateRange?: (event: CalendarEvent) => { start: Date | null; end: Date | null }
46
- formatEventTime?: (event: CalendarEvent, date: Date) => string
47
- // Customization
48
- showNavigation?: boolean
49
- /** When true, day titles show ordinal + full month name (e.g. "21st July") instead of the day number alone. */
50
- shortMonthSuffix?: boolean
51
- currentMonth?: number
52
- currentYear?: number
53
- }
54
-
55
- const props = withDefaults(defineProps<CalendarProps>(), {
56
- section: false,
57
- monthNames: () => [
58
- 'January', 'February', 'March', 'April', 'May', 'June',
59
- 'July', 'August', 'September', 'October', 'November', 'December'
60
- ],
61
- dayNames: () => ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
62
- loading: false,
63
- error: null,
64
- eventDragEnabled: true,
65
- eventMoveResponseTimeoutMs: 15000,
66
- showNavigation: true,
67
- shortMonthSuffix: false,
5
+ const props = defineProps({
6
+ events: {
7
+ type: Array,
8
+ required: true,
9
+ },
10
+ monthNames: {
11
+ type: Array,
12
+ default: () => [
13
+ 'January', 'February', 'March', 'April', 'May', 'June',
14
+ 'July', 'August', 'September', 'October', 'November', 'December',
15
+ ],
16
+ },
17
+ dayNames: {
18
+ type: Array,
19
+ default: () => ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
20
+ },
21
+ loading: {
22
+ type: Boolean,
23
+ default: false,
24
+ },
25
+ error: {
26
+ type: String,
27
+ default: null,
28
+ },
29
+ eventDragEnabled: {
30
+ type: Boolean,
31
+ default: true,
32
+ },
33
+ eventMoveResponseTimeoutMs: {
34
+ type: Number,
35
+ default: 15000,
36
+ },
37
+ getEventDate: {
38
+ type: Function,
39
+ default: null,
40
+ },
41
+ getEventDateRange: {
42
+ type: Function,
43
+ default: null,
44
+ },
45
+ formatEventTime: {
46
+ type: Function,
47
+ default: null,
48
+ },
49
+ showNavigation: {
50
+ type: Boolean,
51
+ default: true,
52
+ },
53
+ shortMonthSuffix: {
54
+ type: Boolean,
55
+ default: false,
56
+ },
57
+ currentMonth: {
58
+ type: Number,
59
+ default: undefined,
60
+ },
61
+ currentYear: {
62
+ type: Number,
63
+ default: undefined,
64
+ },
68
65
  })
69
66
 
70
- const emit = defineEmits<{
71
- 'event-click': [event: CalendarEvent]
72
- 'date-click': [date: Date]
73
- 'date-range-select': [start: Date, end: Date]
74
- 'month-change': [month: number, year: number]
75
- 'event-context-menu': [event: CalendarEvent, mouseEvent: MouseEvent]
76
- 'event-move-request': [payload: CalendarEventMoveRequest]
77
- 'event-moved': [payload: { event: CalendarEvent; sourceDate: Date; targetDate: Date }]
78
- 'event-move-rejected': [payload: { event: CalendarEvent; sourceDate: Date; targetDate: Date }]
79
- }>()
67
+ const emit = defineEmits([
68
+ 'event-click',
69
+ 'date-click',
70
+ 'date-range-select',
71
+ 'month-change',
72
+ 'event-context-menu',
73
+ 'event-move-request',
74
+ 'event-moved',
75
+ 'event-move-rejected',
76
+ ])
80
77
 
81
78
  // Internal calendar state
82
79
  const internalCurrentDate = ref(new Date())
@@ -87,11 +84,11 @@ const currentDate = computed(() => {
87
84
  return internalCurrentDate.value
88
85
  })
89
86
 
90
- const currentMonth = computed(() => currentDate.value.getMonth())
91
- const currentYear = computed(() => currentDate.value.getFullYear())
87
+ const viewMonth = computed(() => currentDate.value.getMonth())
88
+ const viewYear = computed(() => currentDate.value.getFullYear())
92
89
 
93
90
  // Compute the first visible day (Monday) for the current month view
94
- function getStartOfGrid(date: Date): Date {
91
+ function getStartOfGrid(date) {
95
92
  const firstOfMonth = new Date(date.getFullYear(), date.getMonth(), 1)
96
93
  const dayOfWeek = (firstOfMonth.getDay() + 6) % 7 // Monday=0
97
94
  const start = new Date(firstOfMonth)
@@ -101,19 +98,19 @@ function getStartOfGrid(date: Date): Date {
101
98
  }
102
99
 
103
100
  // Anchor date for the 6x7 grid; we will shift this by weeks without re-rendering the grid structure
104
- const gridStartDate = ref<Date>(getStartOfGrid(currentDate.value))
101
+ const gridStartDate = ref(getStartOfGrid(currentDate.value))
105
102
  // Track if we're actively scrolling to prevent watcher from resetting grid anchor
106
103
  const isScrolling = ref(false)
107
104
 
108
105
  // Helper function to check if a time is midnight (00:00)
109
- function isMidnight(dateValue: any): boolean {
106
+ function isMidnight(dateValue) {
110
107
  if (!dateValue) return false
111
108
  const date = new Date(dateValue)
112
109
  return date.getHours() === 0 && date.getMinutes() === 0
113
110
  }
114
111
 
115
112
  // Helper function to format time, returning "No time" if midnight
116
- function formatTimeOrNoTime(dateValue: any): string {
113
+ function formatTimeOrNoTime(dateValue) {
117
114
  if (!dateValue) return 'No time'
118
115
  if (isMidnight(dateValue)) return 'No time'
119
116
  const date = new Date(dateValue)
@@ -121,7 +118,7 @@ function formatTimeOrNoTime(dateValue: any): string {
121
118
  }
122
119
 
123
120
  // Determine if a date is in the past (before today)
124
- function isPastDay(date: Date): boolean {
121
+ function isPastDay(date) {
125
122
  const day = new Date(date)
126
123
  day.setHours(0, 0, 0, 0)
127
124
  const today = new Date()
@@ -130,14 +127,14 @@ function isPastDay(date: Date): boolean {
130
127
  }
131
128
 
132
129
  // Get event date range
133
- function getEventDateRange(event: CalendarEvent): { start: Date | null; end: Date | null } {
130
+ function resolveEventDateRange(event) {
134
131
  if (props.getEventDateRange) {
135
132
  return props.getEventDateRange(event)
136
133
  }
137
134
 
138
135
  // Default implementation
139
- let start: Date | null = null
140
- let end: Date | null = null
136
+ let start = null
137
+ let end = null
141
138
 
142
139
  if (event.startDate) {
143
140
  start = new Date(event.startDate)
@@ -153,12 +150,12 @@ function getEventDateRange(event: CalendarEvent): { start: Date | null; end: Dat
153
150
  }
154
151
 
155
152
  // Get events for a specific date
156
- function getEventsForDate(date: Date): CalendarEvent[] {
153
+ function getEventsForDate(date) {
157
154
  const targetDate = new Date(date)
158
155
  targetDate.setHours(0, 0, 0, 0)
159
156
 
160
157
  return props.events.filter(event => {
161
- const { start, end } = getEventDateRange(event)
158
+ const { start, end } = resolveEventDateRange(event)
162
159
 
163
160
  if (!start) return false
164
161
 
@@ -176,8 +173,8 @@ function getEventsForDate(date: Date): CalendarEvent[] {
176
173
  }
177
174
 
178
175
  // Check if an event is a multi-day event
179
- function isMultiDayEvent(event: CalendarEvent): boolean {
180
- const { start, end } = getEventDateRange(event)
176
+ function isMultiDayEvent(event) {
177
+ const { start, end } = resolveEventDateRange(event)
181
178
 
182
179
  if (!start || !end) return false
183
180
 
@@ -191,8 +188,8 @@ function isMultiDayEvent(event: CalendarEvent): boolean {
191
188
  }
192
189
 
193
190
  // Get the position of an event within a multi-day range for a specific date
194
- function getMultiDayPosition(event: CalendarEvent, date: Date): 'start' | 'middle' | 'end' | 'single' {
195
- const { start, end } = getEventDateRange(event)
191
+ function getMultiDayPosition(event, date) {
192
+ const { start, end } = resolveEventDateRange(event)
196
193
 
197
194
  if (!start || !end) return 'single'
198
195
 
@@ -213,12 +210,12 @@ function getMultiDayPosition(event: CalendarEvent, date: Date): 'start' | 'middl
213
210
  }
214
211
 
215
212
  // Format event time based on position in multi-day event
216
- function formatEventTimeDefault(event: CalendarEvent, date: Date): string {
213
+ function formatEventTimeDefault(event, date) {
217
214
  if (props.formatEventTime) {
218
215
  return props.formatEventTime(event, date)
219
216
  }
220
217
 
221
- const { start, end } = getEventDateRange(event)
218
+ const { start, end } = resolveEventDateRange(event)
222
219
  const position = getMultiDayPosition(event, date)
223
220
 
224
221
  if (!start || !end) return 'No time'
@@ -236,13 +233,19 @@ function formatEventTimeDefault(event: CalendarEvent, date: Date): string {
236
233
  return 'All day'
237
234
  }
238
235
 
239
- function getEventStyle(event: CalendarEvent): Record<string, string> | undefined {
240
- if (!event.color) return undefined
241
- return { '--event-color': event.color }
236
+ function getEventStyle(event) {
237
+ const style = {}
238
+ if (event.color) {
239
+ style['--event-color'] = event.color
240
+ }
241
+ if (event.foregroundColor) {
242
+ style['--event-foreground-color'] = event.foregroundColor
243
+ }
244
+ return Object.keys(style).length > 0 ? style : undefined
242
245
  }
243
246
 
244
247
  // Get ordinal suffix for a number (1st, 2nd, 3rd, 4th, etc.)
245
- function getOrdinalSuffix(day: number): string {
248
+ function getOrdinalSuffix(day) {
246
249
  const j = day % 10
247
250
  const k = day % 100
248
251
  if (j === 1 && k !== 11) {
@@ -259,7 +262,7 @@ function getOrdinalSuffix(day: number): string {
259
262
 
260
263
  // Calendar generation from persistent grid start
261
264
  const calendarDays = computed(() => {
262
- const days = [] as { date: Date; events: CalendarEvent[] }[]
265
+ const days = []
263
266
  const start = gridStartDate.value
264
267
  for (let i = 0; i < 42; i++) {
265
268
  const date = new Date(start)
@@ -275,25 +278,25 @@ const calendarDays = computed(() => {
275
278
  // Navigation functions
276
279
  function previousMonth() {
277
280
  if (props.currentMonth !== undefined && props.currentYear !== undefined) {
278
- const newMonth = currentMonth.value === 0 ? 11 : currentMonth.value - 1
279
- const newYear = currentMonth.value === 0 ? currentYear.value - 1 : currentYear.value
281
+ const newMonth = viewMonth.value === 0 ? 11 : viewMonth.value - 1
282
+ const newYear = viewMonth.value === 0 ? viewYear.value - 1 : viewYear.value
280
283
  emit('month-change', newMonth, newYear)
281
284
  // Optimistically update grid anchor for smooth transition
282
285
  gridStartDate.value = getStartOfGrid(new Date(newYear, newMonth, 1))
283
286
  } else {
284
- internalCurrentDate.value = new Date(currentYear.value, currentMonth.value - 1, 1)
287
+ internalCurrentDate.value = new Date(viewYear.value, viewMonth.value - 1, 1)
285
288
  gridStartDate.value = getStartOfGrid(internalCurrentDate.value)
286
289
  }
287
290
  }
288
291
 
289
292
  function nextMonth() {
290
293
  if (props.currentMonth !== undefined && props.currentYear !== undefined) {
291
- const newMonth = currentMonth.value === 11 ? 0 : currentMonth.value + 1
292
- const newYear = currentMonth.value === 11 ? currentYear.value + 1 : currentYear.value
294
+ const newMonth = viewMonth.value === 11 ? 0 : viewMonth.value + 1
295
+ const newYear = viewMonth.value === 11 ? viewYear.value + 1 : viewYear.value
293
296
  emit('month-change', newMonth, newYear)
294
297
  gridStartDate.value = getStartOfGrid(new Date(newYear, newMonth, 1))
295
298
  } else {
296
- internalCurrentDate.value = new Date(currentYear.value, currentMonth.value + 1, 1)
299
+ internalCurrentDate.value = new Date(viewYear.value, viewMonth.value + 1, 1)
297
300
  gridStartDate.value = getStartOfGrid(internalCurrentDate.value)
298
301
  }
299
302
  }
@@ -312,20 +315,20 @@ function goToToday() {
312
315
  // Week navigation (mouse wheel)
313
316
  const lastWheelAt = ref(0)
314
317
  const wheelThrottleMs = 180
315
- function getVisibleMonthYear(start: Date): { month: number; year: number } {
318
+ function getVisibleMonthYear(start) {
316
319
  const mid = new Date(start)
317
320
  mid.setDate(start.getDate() + 21) // middle of 6x7 grid
318
321
  return { month: mid.getMonth(), year: mid.getFullYear() }
319
322
  }
320
323
 
321
- function adjustByWeeks(weeks: number): void {
324
+ function adjustByWeeks(weeks) {
322
325
  const newStart = new Date(gridStartDate.value)
323
326
  newStart.setDate(newStart.getDate() + weeks * 7)
324
327
  gridStartDate.value = newStart
325
328
 
326
329
  if (props.currentMonth !== undefined && props.currentYear !== undefined) {
327
330
  const { month, year } = getVisibleMonthYear(newStart)
328
- if (month !== currentMonth.value || year !== currentYear.value) {
331
+ if (month !== viewMonth.value || year !== viewYear.value) {
329
332
  emit('month-change', month, year)
330
333
  // Set flag to prevent watcher from resetting our scroll position
331
334
  isScrolling.value = true
@@ -339,7 +342,7 @@ function adjustByWeeks(weeks: number): void {
339
342
  }
340
343
  }
341
344
 
342
- function handleWheel(event: WheelEvent): void {
345
+ function handleWheel(event) {
343
346
  // Prevent page scrolling while using calendar scroll
344
347
  event.preventDefault()
345
348
  const now = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now()
@@ -350,11 +353,11 @@ function handleWheel(event: WheelEvent): void {
350
353
  }
351
354
 
352
355
  // Event handlers
353
- function handleEventClick(event: CalendarEvent) {
356
+ function handleEventClick(event) {
354
357
  emit('event-click', event)
355
358
  }
356
359
 
357
- function handleContextMenu(event: CalendarEvent, mouseEvent: MouseEvent) {
360
+ function handleContextMenu(event, mouseEvent) {
358
361
  mouseEvent.preventDefault()
359
362
  mouseEvent.stopPropagation()
360
363
  emit('event-context-menu', event, mouseEvent)
@@ -362,34 +365,34 @@ function handleContextMenu(event: CalendarEvent, mouseEvent: MouseEvent) {
362
365
 
363
366
  // --- Date range selection (click / drag) ---
364
367
 
365
- function startOfDay(d: Date): Date {
368
+ function startOfDay(d) {
366
369
  const x = new Date(d)
367
370
  x.setHours(0, 0, 0, 0)
368
371
  return x
369
372
  }
370
373
 
371
- function toDateKey(d: Date): string {
374
+ function toDateKey(d) {
372
375
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
373
376
  }
374
377
 
375
- function dateFromKey(key: string): Date {
378
+ function dateFromKey(key) {
376
379
  const [y, m, day] = key.split('-').map(Number)
377
380
  return startOfDay(new Date(y, m - 1, day))
378
381
  }
379
382
 
380
383
  const isDraggingRange = ref(false)
381
- const rangeDragAnchor = ref<Date | null>(null)
382
- const rangeDragEnd = ref<Date | null>(null)
383
- const selectedRangeStart = ref<Date | null>(null)
384
- const selectedRangeEnd = ref<Date | null>(null)
384
+ const rangeDragAnchor = ref(null)
385
+ const rangeDragEnd = ref(null)
386
+ const selectedRangeStart = ref(null)
387
+ const selectedRangeEnd = ref(null)
385
388
 
386
- function isEventTarget(ev: Event): boolean {
389
+ function isEventTarget(ev) {
387
390
  const t = ev.target
388
391
  if (!(t instanceof Node)) return false
389
- return Boolean((t as HTMLElement).closest?.('.calendar-event'))
392
+ return Boolean(t.closest?.('.calendar-event'))
390
393
  }
391
394
 
392
- function getRangeBounds(): { lo: Date; hi: Date } | null {
395
+ function getRangeBounds() {
393
396
  const a = rangeDragAnchor.value
394
397
  const b = rangeDragEnd.value
395
398
  if (!a || !b) return null
@@ -399,15 +402,17 @@ function getRangeBounds(): { lo: Date; hi: Date } | null {
399
402
  return { lo: startOfDay(b), hi: startOfDay(a) }
400
403
  }
401
404
 
402
- function rangeHighlightForDate(date: Date): 'none' | 'single' | 'start' | 'end' | 'middle' {
405
+ function rangeHighlightForDate(date) {
403
406
  const bounds = isDraggingRange.value
404
407
  ? getRangeBounds()
405
408
  : selectedRangeStart.value && selectedRangeEnd.value
406
409
  ? (() => {
407
- const sa = startOfDay(selectedRangeStart.value!).getTime()
408
- const sb = startOfDay(selectedRangeEnd.value!).getTime()
409
- const lo = sa <= sb ? startOfDay(selectedRangeStart.value!) : startOfDay(selectedRangeEnd.value!)
410
- const hi = sa <= sb ? startOfDay(selectedRangeEnd.value!) : startOfDay(selectedRangeStart.value!)
410
+ const rangeStart = selectedRangeStart.value
411
+ const rangeEnd = selectedRangeEnd.value
412
+ const sa = startOfDay(rangeStart).getTime()
413
+ const sb = startOfDay(rangeEnd).getTime()
414
+ const lo = sa <= sb ? startOfDay(rangeStart) : startOfDay(rangeEnd)
415
+ const hi = sa <= sb ? startOfDay(rangeEnd) : startOfDay(rangeStart)
411
416
  return { lo, hi }
412
417
  })()
413
418
  : null
@@ -420,18 +425,18 @@ function rangeHighlightForDate(date: Date): 'none' | 'single' | 'start' | 'end'
420
425
  return 'middle'
421
426
  }
422
427
 
423
- function onWindowTouchMove(ev: TouchEvent): void {
428
+ function onWindowTouchMove(ev) {
424
429
  if (!isDraggingRange.value || ev.touches.length === 0) return
425
430
  const { clientX, clientY } = ev.touches[0]
426
431
  const el = document.elementFromPoint(clientX, clientY)
427
432
  if (!el) return
428
- const cell = (el as HTMLElement).closest?.('[data-calendar-date]')
433
+ const cell = el.closest?.('[data-calendar-date]')
429
434
  const key = cell?.dataset?.calendarDate
430
435
  if (!key) return
431
436
  rangeDragEnd.value = dateFromKey(key)
432
437
  }
433
438
 
434
- function detachRangePointerListeners(): void {
439
+ function detachRangePointerListeners() {
435
440
  window.removeEventListener('mouseup', endRangeSelectionFromPointer)
436
441
  window.removeEventListener('blur', endRangeSelectionFromPointer)
437
442
  window.removeEventListener('touchmove', onWindowTouchMove)
@@ -439,7 +444,7 @@ function detachRangePointerListeners(): void {
439
444
  window.removeEventListener('touchcancel', endRangeSelectionFromPointer)
440
445
  }
441
446
 
442
- function endRangeSelectionFromPointer(): void {
447
+ function endRangeSelectionFromPointer() {
443
448
  if (!isDraggingRange.value) return
444
449
  const bounds = getRangeBounds()
445
450
  isDraggingRange.value = false
@@ -458,7 +463,7 @@ function endRangeSelectionFromPointer(): void {
458
463
  }
459
464
  }
460
465
 
461
- function onDayPointerDown(date: Date, ev: MouseEvent | TouchEvent): void {
466
+ function onDayPointerDown(date, ev) {
462
467
  if (isEventTarget(ev)) return
463
468
  if (ev instanceof MouseEvent && ev.button !== 0) return
464
469
 
@@ -476,7 +481,7 @@ function onDayPointerDown(date: Date, ev: MouseEvent | TouchEvent): void {
476
481
  }
477
482
  }
478
483
 
479
- function onDayPointerEnter(date: Date): void {
484
+ function onDayPointerEnter(date) {
480
485
  if (!isDraggingRange.value) return
481
486
  rangeDragEnd.value = startOfDay(date)
482
487
  }
@@ -489,17 +494,17 @@ onUnmounted(() => {
489
494
 
490
495
  const CALENDAR_EVENT_DRAG_MIME = 'application/x-picocrank-calendar-event-id'
491
496
 
492
- const draggingEventId = ref<string | number | null>(null)
493
- const draggingSourceDate = ref<Date | null>(null)
494
- const dragOverTargetDate = ref<Date | null>(null)
497
+ const draggingEventId = ref(null)
498
+ const draggingSourceDate = ref(null)
499
+ const dragOverTargetDate = ref(null)
495
500
  const eventMoveLocked = ref(false)
496
501
 
497
- function sameCalendarDay(a: Date | null, b: Date | null): boolean {
502
+ function sameCalendarDay(a, b) {
498
503
  if (!a || !b) return false
499
504
  return startOfDay(a).getTime() === startOfDay(b).getTime()
500
505
  }
501
506
 
502
- function onEventDragStart(event: CalendarEvent, sourceCellDate: Date, dragEvent: DragEvent): void {
507
+ function onEventDragStart(event, sourceCellDate, dragEvent) {
503
508
  if (!props.eventDragEnabled) {
504
509
  dragEvent.preventDefault()
505
510
  return
@@ -513,13 +518,13 @@ function onEventDragStart(event: CalendarEvent, sourceCellDate: Date, dragEvent:
513
518
  }
514
519
  }
515
520
 
516
- function onEventDragEnd(): void {
521
+ function onEventDragEnd() {
517
522
  draggingEventId.value = null
518
523
  draggingSourceDate.value = null
519
524
  dragOverTargetDate.value = null
520
525
  }
521
526
 
522
- function onDayDragOver(dayDate: Date, ev: DragEvent): void {
527
+ function onDayDragOver(dayDate, ev) {
523
528
  if (!props.eventDragEnabled || draggingEventId.value === null) return
524
529
  ev.preventDefault()
525
530
  if (ev.dataTransfer) {
@@ -528,7 +533,7 @@ function onDayDragOver(dayDate: Date, ev: DragEvent): void {
528
533
  dragOverTargetDate.value = startOfDay(dayDate)
529
534
  }
530
535
 
531
- function onDayDrop(dayDate: Date, ev: DragEvent): void {
536
+ function onDayDrop(dayDate, ev) {
532
537
  ev.preventDefault()
533
538
  dragOverTargetDate.value = null
534
539
  if (!props.eventDragEnabled) return
@@ -552,9 +557,9 @@ function onDayDrop(dayDate: Date, ev: DragEvent): void {
552
557
  const targetClone = new Date(targetDate)
553
558
 
554
559
  let settled = false
555
- let watchdog: ReturnType<typeof setTimeout> | undefined
560
+ let watchdog
556
561
 
557
- function respond(accepted: boolean): void {
562
+ function respond(accepted) {
558
563
  if (settled) return
559
564
  settled = true
560
565
  if (watchdog !== undefined) {
@@ -589,9 +594,9 @@ function onDayDrop(dayDate: Date, ev: DragEvent): void {
589
594
  }
590
595
 
591
596
  // Sync grid start when controlled month/year props change (but not during scrolling)
592
- watch([currentMonth, currentYear], () => {
597
+ watch([viewMonth, viewYear], () => {
593
598
  if (isScrolling.value) return
594
- const anchor = new Date(currentYear.value, currentMonth.value, 1)
599
+ const anchor = new Date(viewYear.value, viewMonth.value, 1)
595
600
  gridStartDate.value = getStartOfGrid(anchor)
596
601
  })
597
602
 
@@ -600,7 +605,7 @@ watch([currentMonth, currentYear], () => {
600
605
  <template>
601
606
  <div class="calendar-wrapper">
602
607
  <div v-if="showNavigation" class="calendar-header-nav">
603
- <h2 class="calendar-title">{{ monthNames[currentMonth] }} {{ currentYear }}</h2>
608
+ <h2 class="calendar-title">{{ monthNames[viewMonth] }} {{ viewYear }}</h2>
604
609
  <div class="calendar-nav-buttons">
605
610
  <slot name="nav-buttons">
606
611
  <button @click="previousMonth" class="button neutral">‹</button>
@@ -638,8 +643,8 @@ watch([currentMonth, currentYear], () => {
638
643
  :class="{
639
644
  'today': day.date.toDateString() === new Date().toDateString(),
640
645
  'weekend': day.date.getDay() === 0 || day.date.getDay() === 6,
641
- 'prev-month': day.date.getMonth() !== currentMonth && day.date.getMonth() !== (currentMonth + 1) % 12,
642
- 'next-month': day.date.getMonth() !== currentMonth && day.date.getMonth() === (currentMonth + 1) % 12,
646
+ 'prev-month': day.date.getMonth() !== viewMonth && day.date.getMonth() !== (viewMonth + 1) % 12,
647
+ 'next-month': day.date.getMonth() !== viewMonth && day.date.getMonth() === (viewMonth + 1) % 12,
643
648
  'past': isPastDay(day.date),
644
649
  'range-single': rangeHighlightForDate(day.date) === 'single',
645
650
  'range-start': rangeHighlightForDate(day.date) === 'start',
@@ -980,6 +985,18 @@ watch([currentMonth, currentYear], () => {
980
985
  filter: brightness(0.92);
981
986
  }
982
987
 
988
+ .calendar-event.has-custom-color .event-title {
989
+ color: var(--event-foreground-color, var(--calendar-event-title-fg));
990
+ }
991
+
992
+ .calendar-event.has-custom-color .event-time {
993
+ color: var(--event-foreground-color, var(--calendar-muted-fg));
994
+ }
995
+
996
+ .calendar-event.has-custom-color .multi-day-indicator {
997
+ color: var(--event-foreground-color, var(--calendar-accent-fg));
998
+ }
999
+
983
1000
  .event-icon {
984
1001
  flex-shrink: 0;
985
1002
  }