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.
- package/eslint.config.js +75 -0
- package/package.json +17 -4
- package/router.js +74 -0
- package/styles/dark-theme.css +20 -11
- package/styles/picocrank-extensions.css +123 -30
- package/supplemental-themes/catppuccin-latte-frappe/theme.css +10 -24
- package/supplemental-themes/dracula-alucard/theme.css +10 -24
- package/supplemental-themes/gruvbox-dark-light/theme.css +10 -24
- package/supplemental-themes/waffles/theme.css +2 -16
- package/vite.config.js +5 -0
- package/vue/components/Breadcrumbs.vue +192 -45
- package/vue/components/Calendar.vue +162 -145
- package/vue/components/DangerZone.vue +186 -0
- package/vue/components/FormField.spec.js +350 -0
- package/vue/components/FormField.vue +236 -32
- package/vue/components/FormLayout.spec.js +56 -0
- package/vue/components/Header.vue +22 -6
- package/vue/components/Login.vue +152 -41
- package/vue/components/NavigationGrid.vue +26 -180
- package/vue/components/NotificationBlock.vue +120 -0
- package/vue/components/NotificationPopup.vue +44 -41
- package/vue/components/Pagination.vue +5 -4
- package/vue/components/ReadOnlyTextArea.vue +1 -1
- package/vue/components/SidebarNavLink.vue +3 -3
- package/vue/components/Table.vue +107 -79
- package/vue/components/TableColumnOptionsPopover.vue +9 -0
- package/vue/components/Tabs.spec.js +101 -0
- package/vue/components/Tabs.vue +455 -81
- package/vue/components/TopBar.vue +2 -2
- package/vue/components/form-validation.spec.js +126 -0
- package/vue/composables/tableColumnPriorities.js +33 -0
- package/vue/composables/tableFilters.js +1 -1
- package/vue/data/navSectionHubs.js +196 -0
- package/vue/data/routeBreadcrumbs.js +90 -0
- package/vue/examples/Admin.vue +12 -12
- package/vue/examples/App.vue +27 -36
- package/vue/examples/CalendarExample.vue +32 -22
- package/vue/examples/ExampleHeaderThemeSwitcher.vue +1 -1
- package/vue/examples/FormExample.vue +76 -48
- package/vue/examples/Login.vue +5 -3
- package/vue/examples/LoginExample.vue +99 -15
- package/vue/examples/NavSectionHub.vue +36 -0
- package/vue/examples/PageStructure.vue +15 -12
- package/vue/examples/StatusExample.vue +248 -129
- package/vue/examples/TableExample.vue +72 -71
- package/vue/examples/TabsExample.vue +236 -257
- package/vue/examples/ThemePreview.vue +21 -23
- package/vue/examples/ViewItem.vue +32 -5
- package/vue/examples/Welcome.vue +3 -40
- package/vue/test/formTestUtils.js +279 -0
- package/vue/test/setup.js +5 -0
|
@@ -1,82 +1,79 @@
|
|
|
1
|
-
<script setup
|
|
1
|
+
<script setup>
|
|
2
2
|
import { ref, computed, watch, onUnmounted } from 'vue'
|
|
3
3
|
import { HugeiconsIcon } from '@hugeicons/vue'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
91
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
140
|
-
let end
|
|
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
|
|
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 } =
|
|
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
|
|
180
|
-
const { start, end } =
|
|
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
|
|
195
|
-
const { start, end } =
|
|
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
|
|
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 } =
|
|
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
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
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 = []
|
|
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 =
|
|
279
|
-
const newYear =
|
|
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(
|
|
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 =
|
|
292
|
-
const newYear =
|
|
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(
|
|
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
|
|
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
|
|
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 !==
|
|
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
|
|
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
|
|
356
|
+
function handleEventClick(event) {
|
|
354
357
|
emit('event-click', event)
|
|
355
358
|
}
|
|
356
359
|
|
|
357
|
-
function handleContextMenu(event
|
|
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
|
|
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
|
|
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
|
|
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
|
|
382
|
-
const rangeDragEnd = ref
|
|
383
|
-
const selectedRangeStart = ref
|
|
384
|
-
const selectedRangeEnd = ref
|
|
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
|
|
389
|
+
function isEventTarget(ev) {
|
|
387
390
|
const t = ev.target
|
|
388
391
|
if (!(t instanceof Node)) return false
|
|
389
|
-
return Boolean(
|
|
392
|
+
return Boolean(t.closest?.('.calendar-event'))
|
|
390
393
|
}
|
|
391
394
|
|
|
392
|
-
function getRangeBounds()
|
|
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
|
|
405
|
+
function rangeHighlightForDate(date) {
|
|
403
406
|
const bounds = isDraggingRange.value
|
|
404
407
|
? getRangeBounds()
|
|
405
408
|
: selectedRangeStart.value && selectedRangeEnd.value
|
|
406
409
|
? (() => {
|
|
407
|
-
const
|
|
408
|
-
const
|
|
409
|
-
const
|
|
410
|
-
const
|
|
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
|
|
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 =
|
|
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()
|
|
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()
|
|
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
|
|
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
|
|
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
|
|
493
|
-
const draggingSourceDate = ref
|
|
494
|
-
const dragOverTargetDate = ref
|
|
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
|
|
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
|
|
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()
|
|
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
|
|
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
|
|
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
|
|
560
|
+
let watchdog
|
|
556
561
|
|
|
557
|
-
function respond(accepted
|
|
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([
|
|
597
|
+
watch([viewMonth, viewYear], () => {
|
|
593
598
|
if (isScrolling.value) return
|
|
594
|
-
const anchor = new Date(
|
|
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[
|
|
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() !==
|
|
642
|
-
'next-month': day.date.getMonth() !==
|
|
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
|
}
|