picocrank 1.3.1 → 1.5.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/package.json +1 -1
- package/router.js +11 -0
- package/vue/components/Calendar.vue +632 -0
- package/vue/components/Header.vue +5 -3
- package/vue/components/Sidebar.vue +19 -1
- package/vue/views/App.vue +4 -0
- package/vue/views/CalendarExample.vue +328 -0
package/package.json
CHANGED
package/router.js
CHANGED
|
@@ -4,6 +4,7 @@ import { HomeIcon } from '@hugeicons/core-free-icons';
|
|
|
4
4
|
import { TableIcon } from '@hugeicons/core-free-icons';
|
|
5
5
|
import { ViewIcon } from '@hugeicons/core-free-icons';
|
|
6
6
|
import { SecurityValidationIcon } from '@hugeicons/core-free-icons';
|
|
7
|
+
import { CalendarIcon } from '@hugeicons/core-free-icons';
|
|
7
8
|
|
|
8
9
|
const routes = [
|
|
9
10
|
{
|
|
@@ -43,6 +44,16 @@ const routes = [
|
|
|
43
44
|
title: 'Admin',
|
|
44
45
|
icon: SecurityValidationIcon,
|
|
45
46
|
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'CalendarExample',
|
|
50
|
+
path: '/calendar-example',
|
|
51
|
+
component: () => import('./vue/views/CalendarExample.vue'),
|
|
52
|
+
props: true,
|
|
53
|
+
meta: {
|
|
54
|
+
title: 'Calendar Example',
|
|
55
|
+
icon: CalendarIcon,
|
|
56
|
+
}
|
|
46
57
|
}
|
|
47
58
|
];
|
|
48
59
|
|
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
export interface CalendarEvent {
|
|
5
|
+
id: string | number
|
|
6
|
+
title: string
|
|
7
|
+
startDate?: Date | string | null
|
|
8
|
+
endDate?: Date | string | null
|
|
9
|
+
date?: Date | string | null
|
|
10
|
+
[key: string]: any
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface CalendarProps {
|
|
14
|
+
events: CalendarEvent[]
|
|
15
|
+
monthNames?: string[]
|
|
16
|
+
dayNames?: string[]
|
|
17
|
+
loading?: boolean
|
|
18
|
+
error?: string | null
|
|
19
|
+
// Formatter functions
|
|
20
|
+
getEventDate?: (event: CalendarEvent) => Date | null
|
|
21
|
+
getEventDateRange?: (event: CalendarEvent) => { start: Date | null; end: Date | null }
|
|
22
|
+
formatEventTime?: (event: CalendarEvent, date: Date) => string
|
|
23
|
+
// Customization
|
|
24
|
+
showNavigation?: boolean
|
|
25
|
+
currentMonth?: number
|
|
26
|
+
currentYear?: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const props = withDefaults(defineProps<CalendarProps>(), {
|
|
30
|
+
section: false,
|
|
31
|
+
monthNames: () => [
|
|
32
|
+
'January', 'February', 'March', 'April', 'May', 'June',
|
|
33
|
+
'July', 'August', 'September', 'October', 'November', 'December'
|
|
34
|
+
],
|
|
35
|
+
dayNames: () => ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
|
|
36
|
+
loading: false,
|
|
37
|
+
error: null,
|
|
38
|
+
showNavigation: true,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const emit = defineEmits<{
|
|
42
|
+
'event-click': [event: CalendarEvent]
|
|
43
|
+
'date-click': [date: Date]
|
|
44
|
+
'month-change': [month: number, year: number]
|
|
45
|
+
'event-context-menu': [event: CalendarEvent, mouseEvent: MouseEvent]
|
|
46
|
+
}>()
|
|
47
|
+
|
|
48
|
+
// Internal calendar state
|
|
49
|
+
const internalCurrentDate = ref(new Date())
|
|
50
|
+
const currentDate = computed(() => {
|
|
51
|
+
if (props.currentMonth !== undefined && props.currentYear !== undefined) {
|
|
52
|
+
return new Date(props.currentYear, props.currentMonth, 1)
|
|
53
|
+
}
|
|
54
|
+
return internalCurrentDate.value
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const currentMonth = computed(() => currentDate.value.getMonth())
|
|
58
|
+
const currentYear = computed(() => currentDate.value.getFullYear())
|
|
59
|
+
|
|
60
|
+
// Helper function to check if a time is midnight (00:00)
|
|
61
|
+
function isMidnight(dateValue: any): boolean {
|
|
62
|
+
if (!dateValue) return false
|
|
63
|
+
const date = new Date(dateValue)
|
|
64
|
+
return date.getHours() === 0 && date.getMinutes() === 0
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Helper function to format time, returning "No time" if midnight
|
|
68
|
+
function formatTimeOrNoTime(dateValue: any): string {
|
|
69
|
+
if (!dateValue) return 'No time'
|
|
70
|
+
if (isMidnight(dateValue)) return 'No time'
|
|
71
|
+
const date = new Date(dateValue)
|
|
72
|
+
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Get event date range
|
|
76
|
+
function getEventDateRange(event: CalendarEvent): { start: Date | null; end: Date | null } {
|
|
77
|
+
if (props.getEventDateRange) {
|
|
78
|
+
return props.getEventDateRange(event)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Default implementation
|
|
82
|
+
let start: Date | null = null
|
|
83
|
+
let end: Date | null = null
|
|
84
|
+
|
|
85
|
+
if (event.startDate) {
|
|
86
|
+
start = new Date(event.startDate)
|
|
87
|
+
} else if (event.date) {
|
|
88
|
+
start = new Date(event.date)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (event.endDate) {
|
|
92
|
+
end = new Date(event.endDate)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { start, end }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Get events for a specific date
|
|
99
|
+
function getEventsForDate(date: Date): CalendarEvent[] {
|
|
100
|
+
const targetDate = new Date(date)
|
|
101
|
+
targetDate.setHours(0, 0, 0, 0)
|
|
102
|
+
|
|
103
|
+
return props.events.filter(event => {
|
|
104
|
+
const { start, end } = getEventDateRange(event)
|
|
105
|
+
|
|
106
|
+
if (!start) return false
|
|
107
|
+
|
|
108
|
+
const startDateOnly = new Date(start)
|
|
109
|
+
startDateOnly.setHours(0, 0, 0, 0)
|
|
110
|
+
|
|
111
|
+
if (end) {
|
|
112
|
+
const endDateOnly = new Date(end)
|
|
113
|
+
endDateOnly.setHours(0, 0, 0, 0)
|
|
114
|
+
return targetDate >= startDateOnly && targetDate <= endDateOnly
|
|
115
|
+
} else {
|
|
116
|
+
return targetDate.getTime() === startDateOnly.getTime()
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check if an event is a multi-day event
|
|
122
|
+
function isMultiDayEvent(event: CalendarEvent): boolean {
|
|
123
|
+
const { start, end } = getEventDateRange(event)
|
|
124
|
+
|
|
125
|
+
if (!start || !end) return false
|
|
126
|
+
|
|
127
|
+
const startDate = new Date(start)
|
|
128
|
+
const endDate = new Date(end)
|
|
129
|
+
|
|
130
|
+
startDate.setHours(0, 0, 0, 0)
|
|
131
|
+
endDate.setHours(0, 0, 0, 0)
|
|
132
|
+
|
|
133
|
+
return startDate.getTime() !== endDate.getTime()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Get the position of an event within a multi-day range for a specific date
|
|
137
|
+
function getMultiDayPosition(event: CalendarEvent, date: Date): 'start' | 'middle' | 'end' | 'single' {
|
|
138
|
+
const { start, end } = getEventDateRange(event)
|
|
139
|
+
|
|
140
|
+
if (!start || !end) return 'single'
|
|
141
|
+
|
|
142
|
+
const startDate = new Date(start)
|
|
143
|
+
const endDate = new Date(end)
|
|
144
|
+
const targetDate = new Date(date)
|
|
145
|
+
|
|
146
|
+
startDate.setHours(0, 0, 0, 0)
|
|
147
|
+
endDate.setHours(0, 0, 0, 0)
|
|
148
|
+
targetDate.setHours(0, 0, 0, 0)
|
|
149
|
+
|
|
150
|
+
if (startDate.getTime() === endDate.getTime()) return 'single'
|
|
151
|
+
if (targetDate.getTime() === startDate.getTime()) return 'start'
|
|
152
|
+
if (targetDate.getTime() === endDate.getTime()) return 'end'
|
|
153
|
+
if (targetDate > startDate && targetDate < endDate) return 'middle'
|
|
154
|
+
|
|
155
|
+
return 'single'
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Format event time based on position in multi-day event
|
|
159
|
+
function formatEventTimeDefault(event: CalendarEvent, date: Date): string {
|
|
160
|
+
if (props.formatEventTime) {
|
|
161
|
+
return props.formatEventTime(event, date)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const { start, end } = getEventDateRange(event)
|
|
165
|
+
const position = getMultiDayPosition(event, date)
|
|
166
|
+
|
|
167
|
+
if (!start || !end) return 'No time'
|
|
168
|
+
|
|
169
|
+
if (position === 'start') {
|
|
170
|
+
return formatTimeOrNoTime(start)
|
|
171
|
+
} else if (position === 'end') {
|
|
172
|
+
return formatTimeOrNoTime(end)
|
|
173
|
+
} else if (position === 'middle') {
|
|
174
|
+
return 'All day'
|
|
175
|
+
} else if (position === 'single') {
|
|
176
|
+
return formatTimeOrNoTime(start)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return 'All day'
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Calendar generation
|
|
183
|
+
const calendarDays = computed(() => {
|
|
184
|
+
const year = currentYear.value
|
|
185
|
+
const month = currentMonth.value
|
|
186
|
+
|
|
187
|
+
const firstDay = new Date(year, month, 1)
|
|
188
|
+
const lastDay = new Date(year, month + 1, 0)
|
|
189
|
+
const daysInMonth = lastDay.getDate()
|
|
190
|
+
|
|
191
|
+
// Get starting day of week (Monday as first day)
|
|
192
|
+
const startDay = (firstDay.getDay() + 6) % 7
|
|
193
|
+
|
|
194
|
+
const days = []
|
|
195
|
+
|
|
196
|
+
// Add days from previous month to fill the first row
|
|
197
|
+
const prevMonth = month === 0 ? 11 : month - 1
|
|
198
|
+
const prevYear = month === 0 ? year - 1 : year
|
|
199
|
+
const prevMonthLastDay = new Date(prevYear, prevMonth + 1, 0).getDate()
|
|
200
|
+
|
|
201
|
+
for (let i = 0; i < startDay; i++) {
|
|
202
|
+
const day = prevMonthLastDay - startDay + i + 1
|
|
203
|
+
const date = new Date(prevYear, prevMonth, day)
|
|
204
|
+
days.push({
|
|
205
|
+
date,
|
|
206
|
+
events: getEventsForDate(date)
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Add days of the month
|
|
211
|
+
for (let day = 1; day <= daysInMonth; day++) {
|
|
212
|
+
const date = new Date(year, month, day)
|
|
213
|
+
days.push({
|
|
214
|
+
date,
|
|
215
|
+
events: getEventsForDate(date)
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Add days from next month to complete the last row
|
|
220
|
+
const totalCells = 42
|
|
221
|
+
const remainingCells = totalCells - days.length
|
|
222
|
+
|
|
223
|
+
for (let day = 1; day <= remainingCells; day++) {
|
|
224
|
+
const date = new Date(year, month + 1, day)
|
|
225
|
+
days.push({
|
|
226
|
+
date,
|
|
227
|
+
events: getEventsForDate(date)
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return days
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
// Navigation functions
|
|
235
|
+
function previousMonth() {
|
|
236
|
+
if (props.currentMonth !== undefined && props.currentYear !== undefined) {
|
|
237
|
+
const newMonth = currentMonth.value === 0 ? 11 : currentMonth.value - 1
|
|
238
|
+
const newYear = currentMonth.value === 0 ? currentYear.value - 1 : currentYear.value
|
|
239
|
+
emit('month-change', newMonth, newYear)
|
|
240
|
+
} else {
|
|
241
|
+
internalCurrentDate.value = new Date(currentYear.value, currentMonth.value - 1, 1)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function nextMonth() {
|
|
246
|
+
if (props.currentMonth !== undefined && props.currentYear !== undefined) {
|
|
247
|
+
const newMonth = currentMonth.value === 11 ? 0 : currentMonth.value + 1
|
|
248
|
+
const newYear = currentMonth.value === 11 ? currentYear.value + 1 : currentYear.value
|
|
249
|
+
emit('month-change', newMonth, newYear)
|
|
250
|
+
} else {
|
|
251
|
+
internalCurrentDate.value = new Date(currentYear.value, currentMonth.value + 1, 1)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function goToToday() {
|
|
256
|
+
if (props.currentMonth !== undefined && props.currentYear !== undefined) {
|
|
257
|
+
const today = new Date()
|
|
258
|
+
emit('month-change', today.getMonth(), today.getFullYear())
|
|
259
|
+
} else {
|
|
260
|
+
internalCurrentDate.value = new Date()
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Event handlers
|
|
265
|
+
function handleEventClick(event: CalendarEvent) {
|
|
266
|
+
emit('event-click', event)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function handleDateClick(date: Date) {
|
|
270
|
+
emit('date-click', date)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function handleContextMenu(event: CalendarEvent, mouseEvent: MouseEvent) {
|
|
274
|
+
mouseEvent.preventDefault()
|
|
275
|
+
mouseEvent.stopPropagation()
|
|
276
|
+
emit('event-context-menu', event, mouseEvent)
|
|
277
|
+
}
|
|
278
|
+
</script>
|
|
279
|
+
|
|
280
|
+
<template>
|
|
281
|
+
<div class="calendar-wrapper">
|
|
282
|
+
<div v-if="showNavigation" class="calendar-header-nav">
|
|
283
|
+
<h2 class="calendar-title">{{ monthNames[currentMonth] }} {{ currentYear }}</h2>
|
|
284
|
+
<div class="calendar-nav-buttons">
|
|
285
|
+
<slot name="nav-buttons">
|
|
286
|
+
<button @click="previousMonth" class="button neutral">‹</button>
|
|
287
|
+
<button @click="goToToday" class="button neutral">Today</button>
|
|
288
|
+
<button @click="nextMonth" class="button neutral">›</button>
|
|
289
|
+
</slot>
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
<div v-if="error" class="calendar-error">{{ error }}</div>
|
|
294
|
+
<div v-else-if="loading" class="calendar-loading">Loading…</div>
|
|
295
|
+
<div v-else class="calendar-container">
|
|
296
|
+
<div class="calendar-grid">
|
|
297
|
+
<!-- Day headers -->
|
|
298
|
+
<div v-for="day in dayNames" :key="day" class="day-header">{{ day }}</div>
|
|
299
|
+
|
|
300
|
+
<!-- Calendar days -->
|
|
301
|
+
<div
|
|
302
|
+
v-for="(day, index) in calendarDays"
|
|
303
|
+
:key="index"
|
|
304
|
+
class="calendar-day"
|
|
305
|
+
:class="{
|
|
306
|
+
'today': day && day.date.toDateString() === new Date().toDateString(),
|
|
307
|
+
'weekend': day && (day.date.getDay() === 0 || day.date.getDay() === 6),
|
|
308
|
+
'prev-month': day && day.date.getMonth() !== currentMonth && day.date.getMonth() !== (currentMonth + 1) % 12,
|
|
309
|
+
'next-month': day && day.date.getMonth() !== currentMonth && day.date.getMonth() === (currentMonth + 1) % 12
|
|
310
|
+
}"
|
|
311
|
+
>
|
|
312
|
+
<div v-if="day" class="day-content" @click="handleDateClick(day.date)">
|
|
313
|
+
<div class="day-number clickable">
|
|
314
|
+
{{ day.date.getDate() }}
|
|
315
|
+
</div>
|
|
316
|
+
<div class="day-events">
|
|
317
|
+
<div
|
|
318
|
+
v-for="event in day.events.sort((a, b) => {
|
|
319
|
+
const aMultiDay = isMultiDayEvent(a);
|
|
320
|
+
const bMultiDay = isMultiDayEvent(b);
|
|
321
|
+
if (aMultiDay && !bMultiDay) return -1;
|
|
322
|
+
if (!aMultiDay && bMultiDay) return 1;
|
|
323
|
+
return 0;
|
|
324
|
+
}).slice(0, 3)"
|
|
325
|
+
:key="event.id"
|
|
326
|
+
class="calendar-event"
|
|
327
|
+
:class="{
|
|
328
|
+
'multi-day': isMultiDayEvent(event),
|
|
329
|
+
'multi-day-start': isMultiDayEvent(event) && getMultiDayPosition(event, day.date) === 'start',
|
|
330
|
+
'multi-day-middle': isMultiDayEvent(event) && getMultiDayPosition(event, day.date) === 'middle',
|
|
331
|
+
'multi-day-end': isMultiDayEvent(event) && getMultiDayPosition(event, day.date) === 'end'
|
|
332
|
+
}"
|
|
333
|
+
@click.stop="handleEventClick(event)"
|
|
334
|
+
@contextmenu.stop="handleContextMenu(event, $event)"
|
|
335
|
+
>
|
|
336
|
+
<slot name="event" :event="event" :date="day.date" :position="getMultiDayPosition(event, day.date)">
|
|
337
|
+
<div class="event-content">
|
|
338
|
+
<div class="event-title">
|
|
339
|
+
{{ event.title }}
|
|
340
|
+
<span v-if="isMultiDayEvent(event)" class="multi-day-indicator">
|
|
341
|
+
{{ getMultiDayPosition(event, day.date) === 'start' ? '▶' :
|
|
342
|
+
getMultiDayPosition(event, day.date) === 'end' ? '◀' :
|
|
343
|
+
getMultiDayPosition(event, day.date) === 'middle' ? '▬' : '' }}
|
|
344
|
+
</span>
|
|
345
|
+
</div>
|
|
346
|
+
<div class="event-time">
|
|
347
|
+
{{ formatEventTimeDefault(event, day.date) }}
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
</slot>
|
|
351
|
+
</div>
|
|
352
|
+
<div v-if="day.events.length > 3" class="more-events">
|
|
353
|
+
+{{ day.events.length - 3 }} more
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
</template>
|
|
362
|
+
|
|
363
|
+
<style scoped>
|
|
364
|
+
.calendar-wrapper {
|
|
365
|
+
width: 100%;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.calendar-header-nav {
|
|
369
|
+
display: flex;
|
|
370
|
+
justify-content: space-between;
|
|
371
|
+
align-items: center;
|
|
372
|
+
margin-bottom: 1rem;
|
|
373
|
+
padding: 1rem;
|
|
374
|
+
background: #f8f9fa;
|
|
375
|
+
border-radius: 8px;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.calendar-title {
|
|
379
|
+
margin: 0;
|
|
380
|
+
font-size: 1.5rem;
|
|
381
|
+
font-weight: 600;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.calendar-nav-buttons {
|
|
385
|
+
display: flex;
|
|
386
|
+
align-items: center;
|
|
387
|
+
gap: 0.5rem;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.calendar-error {
|
|
391
|
+
color: #b00020;
|
|
392
|
+
padding: 1rem;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.calendar-loading {
|
|
396
|
+
padding: 1rem;
|
|
397
|
+
text-align: center;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.calendar-container {
|
|
401
|
+
background: white;
|
|
402
|
+
border: 1px solid #e0e0e0;
|
|
403
|
+
overflow: hidden;
|
|
404
|
+
border-radius: 8px;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.calendar-grid {
|
|
408
|
+
display: grid;
|
|
409
|
+
grid-template-columns: repeat(7, minmax(0, 1fr));
|
|
410
|
+
min-height: 400px;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.day-header {
|
|
414
|
+
padding: 1rem;
|
|
415
|
+
text-align: center;
|
|
416
|
+
font-weight: 600;
|
|
417
|
+
color: #666;
|
|
418
|
+
border-right: 1px solid #e0e0e0;
|
|
419
|
+
border-bottom: 1px solid #e0e0e0;
|
|
420
|
+
background: #f8f9fa;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.day-header:last-child {
|
|
424
|
+
border-right: none;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.calendar-day {
|
|
428
|
+
border-right: 1px solid #e0e0e0;
|
|
429
|
+
border-bottom: 1px solid #e0e0e0;
|
|
430
|
+
min-height: 120px;
|
|
431
|
+
position: relative;
|
|
432
|
+
transition: background-color 0.2s ease;
|
|
433
|
+
cursor: pointer;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.calendar-day:hover {
|
|
437
|
+
background-color: #f0f8ff;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.calendar-day:nth-child(7n) {
|
|
441
|
+
border-right: none;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.calendar-day.weekend {
|
|
445
|
+
background: #f8f9fa;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.calendar-day.weekend:hover {
|
|
449
|
+
background: #e9ecef;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.calendar-day.next-month,
|
|
453
|
+
.calendar-day.prev-month {
|
|
454
|
+
background: #f8f9fa;
|
|
455
|
+
opacity: 0.6;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.calendar-day.next-month:hover,
|
|
459
|
+
.calendar-day.prev-month:hover {
|
|
460
|
+
background: #e9ecef;
|
|
461
|
+
opacity: 0.8;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.calendar-day.today {
|
|
465
|
+
background: #f7f8d7;
|
|
466
|
+
font-weight: bold;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
.calendar-day.today:hover {
|
|
470
|
+
background: #bbdefb;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
.day-content {
|
|
475
|
+
height: 100%;
|
|
476
|
+
display: flex;
|
|
477
|
+
flex-direction: column;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
.day-number {
|
|
481
|
+
font-size: 0.9rem;
|
|
482
|
+
margin-bottom: 0.25rem;
|
|
483
|
+
color: #333;
|
|
484
|
+
text-decoration: none;
|
|
485
|
+
display: inline-block;
|
|
486
|
+
padding: 0.1rem;
|
|
487
|
+
border-radius: 4px;
|
|
488
|
+
transition: all 0.2s;
|
|
489
|
+
min-width: 1.5rem;
|
|
490
|
+
text-align: center;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.day-number.clickable {
|
|
494
|
+
cursor: pointer;
|
|
495
|
+
padding: 0.25rem;
|
|
496
|
+
border-radius: 4px;
|
|
497
|
+
transition: background-color 0.2s;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.day-content:hover .day-number.clickable {
|
|
501
|
+
color: #007bff;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.day-events {
|
|
505
|
+
flex: 1;
|
|
506
|
+
overflow: hidden;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.calendar-event {
|
|
510
|
+
background: #d6f1aa;
|
|
511
|
+
border: 1px solid #c4db96;
|
|
512
|
+
margin-bottom: 0.25rem;
|
|
513
|
+
cursor: pointer;
|
|
514
|
+
transition: all 0.2s;
|
|
515
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
516
|
+
padding: 0.1rem 0.1rem;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
.calendar-event:hover {
|
|
520
|
+
background: #b3de6e;
|
|
521
|
+
border-color: #c4db96;
|
|
522
|
+
transform: translateY(-1px);
|
|
523
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
.calendar-event.multi-day-start {
|
|
527
|
+
border-top-left-radius: 4px;
|
|
528
|
+
border-bottom-left-radius: 4px;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
.calendar-event.multi-day-middle {
|
|
532
|
+
border-radius: 0;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
.calendar-event.multi-day-end {
|
|
536
|
+
border-top-right-radius: 4px;
|
|
537
|
+
border-bottom-right-radius: 4px;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.event-content {
|
|
541
|
+
cursor: pointer;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.event-title {
|
|
545
|
+
font-weight: bold;
|
|
546
|
+
font-size: 0.85rem;
|
|
547
|
+
color: #333;
|
|
548
|
+
white-space: normal;
|
|
549
|
+
overflow: hidden;
|
|
550
|
+
text-overflow: ellipsis;
|
|
551
|
+
display: flex;
|
|
552
|
+
align-items: center;
|
|
553
|
+
gap: 0.25rem;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
.multi-day-indicator {
|
|
557
|
+
font-size: 0.7rem;
|
|
558
|
+
color: #007bff;
|
|
559
|
+
font-weight: bold;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
.event-time {
|
|
563
|
+
font-size: 0.75rem;
|
|
564
|
+
color: #666;
|
|
565
|
+
margin-top: 0.125rem;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
.more-events {
|
|
569
|
+
font-size: 0.75rem;
|
|
570
|
+
color: #666;
|
|
571
|
+
text-align: center;
|
|
572
|
+
padding: 0.25rem;
|
|
573
|
+
background: #f8f9fa;
|
|
574
|
+
border-radius: 4px;
|
|
575
|
+
margin-top: 0.25rem;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/* Responsive design */
|
|
579
|
+
@media (max-width: 768px) {
|
|
580
|
+
.calendar-header-nav {
|
|
581
|
+
flex-wrap: wrap;
|
|
582
|
+
gap: 0.5rem;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
.calendar-title {
|
|
586
|
+
font-size: 1.2rem;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
.calendar-day {
|
|
590
|
+
min-height: 80px;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.day-number {
|
|
594
|
+
font-size: 1rem;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
.event-title {
|
|
598
|
+
font-size: 0.8rem;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
.event-time {
|
|
602
|
+
font-size: 0.7rem;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
@media (max-width: 480px) {
|
|
607
|
+
.calendar-grid {
|
|
608
|
+
min-height: 300px;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
.calendar-day {
|
|
612
|
+
min-height: 60px;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
.day-header {
|
|
616
|
+
padding: 0.5rem 0.25rem;
|
|
617
|
+
font-size: 0.8rem;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
@media (min-width: 768px) {
|
|
622
|
+
.day-content {
|
|
623
|
+
padding: 0.45rem;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
.calendar-event {
|
|
627
|
+
border-radius: 4px;
|
|
628
|
+
padding: 0.25rem 0.5rem;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
</style>
|
|
632
|
+
|
|
@@ -17,9 +17,11 @@
|
|
|
17
17
|
|
|
18
18
|
<div class = "fg1"></div>
|
|
19
19
|
|
|
20
|
-
<
|
|
21
|
-
<
|
|
22
|
-
|
|
20
|
+
<slot name = "user-info">
|
|
21
|
+
<div class = "user-info">
|
|
22
|
+
<span v-if="username">{{ username }}</span>
|
|
23
|
+
</div>
|
|
24
|
+
</slot>
|
|
23
25
|
</header>
|
|
24
26
|
</template>
|
|
25
27
|
|
|
@@ -18,7 +18,12 @@
|
|
|
18
18
|
<li v-for="link in navigationLinks" :key="link.name" :title="link.title">
|
|
19
19
|
<!-- Render separator if link is a separator -->
|
|
20
20
|
<div v-if="link.type === 'separator'" class="separator"></div>
|
|
21
|
-
|
|
21
|
+
<div v-else-if="link.type === 'callback'">
|
|
22
|
+
<a href="#" @click.prevent="link.callback()">
|
|
23
|
+
<HugeiconsIcon :icon="link.icon" />
|
|
24
|
+
<span>{{ link.title }}</span>
|
|
25
|
+
</a>
|
|
26
|
+
</div>
|
|
22
27
|
<router-link v-else :to="link.path" :class="{ active: isActive(link.path) }">
|
|
23
28
|
<HugeiconsIcon :icon="link.icon" />
|
|
24
29
|
<span>{{ link.title }}</span>
|
|
@@ -68,6 +73,18 @@ function addNavigationLink(link) {
|
|
|
68
73
|
}
|
|
69
74
|
}
|
|
70
75
|
|
|
76
|
+
function addCallback(title, callback, options = {}) {
|
|
77
|
+
const callbackLink = {
|
|
78
|
+
name: options.name || title.toLowerCase().replace(/\s+/g, '-'),
|
|
79
|
+
type: 'callback',
|
|
80
|
+
icon: options.icon || PinIcon,
|
|
81
|
+
callback: callback || (() => {}),
|
|
82
|
+
title: title
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
addNavigationLink(callbackLink)
|
|
86
|
+
}
|
|
87
|
+
|
|
71
88
|
function addSeparator(id) {
|
|
72
89
|
const separator = {
|
|
73
90
|
name: id || `nav-separator-${Date.now()}`,
|
|
@@ -131,6 +148,7 @@ defineExpose({
|
|
|
131
148
|
isActive,
|
|
132
149
|
addNavigationLink,
|
|
133
150
|
addRouterLink,
|
|
151
|
+
addCallback,
|
|
134
152
|
addSeparator,
|
|
135
153
|
removeNavigationLink,
|
|
136
154
|
clearNavigationLinks,
|
package/vue/views/App.vue
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
<script setup>
|
|
29
29
|
import { ref, onMounted } from 'vue';
|
|
30
30
|
import { useRouter } from 'vue-router';
|
|
31
|
+
import { Pin02Icon } from '@hugeicons/core-free-icons'
|
|
31
32
|
|
|
32
33
|
import QuickSearch from './../components/QuickSearch.vue'
|
|
33
34
|
|
|
@@ -44,9 +45,12 @@
|
|
|
44
45
|
onMounted(() => {
|
|
45
46
|
sidebar.value.addRouterLink('Welcome')
|
|
46
47
|
sidebar.value.addRouterLink('TableExample')
|
|
48
|
+
sidebar.value.addRouterLink('CalendarExample')
|
|
47
49
|
sidebar.value.addSeparator('separator-1');
|
|
48
50
|
sidebar.value.addRouterLink('ViewItem', { id: 1 })
|
|
49
51
|
sidebar.value.addSeparator('separator-2');
|
|
52
|
+
sidebar.value.addCallback('Hello World', helloWorld, { icon: Pin02Icon })
|
|
53
|
+
sidebar.value.addSeparator('separator-3');
|
|
50
54
|
sidebar.value.addRouterLink('Admin')
|
|
51
55
|
sidebar.value.open();
|
|
52
56
|
sidebar.value.stick();
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Section
|
|
3
|
+
title="Example Calendar"
|
|
4
|
+
padding
|
|
5
|
+
>
|
|
6
|
+
<template #toolbar>
|
|
7
|
+
<button @click="goToToday">Today</button>
|
|
8
|
+
<button @click="previousMonth">‹</button>
|
|
9
|
+
<div class="date-picker-container">
|
|
10
|
+
<input
|
|
11
|
+
id="goto-date"
|
|
12
|
+
type="month"
|
|
13
|
+
v-model="selectedDate"
|
|
14
|
+
@change="goToSelectedDate"
|
|
15
|
+
class="date-picker-input"
|
|
16
|
+
/>
|
|
17
|
+
</div>
|
|
18
|
+
<button @click="nextMonth">›</button>
|
|
19
|
+
</template>
|
|
20
|
+
<Calendar
|
|
21
|
+
:section="true"
|
|
22
|
+
:events="events"
|
|
23
|
+
:loading="loading"
|
|
24
|
+
:error="error"
|
|
25
|
+
:show-navigation="false"
|
|
26
|
+
:current-month="currentMonthIndex"
|
|
27
|
+
:current-year="currentYear"
|
|
28
|
+
@event-click="handleEventClick"
|
|
29
|
+
@date-click="handleDateClick"
|
|
30
|
+
@month-change="handleMonthChange"
|
|
31
|
+
/>
|
|
32
|
+
</Section>
|
|
33
|
+
</template>
|
|
34
|
+
|
|
35
|
+
<script setup>
|
|
36
|
+
import { ref, onMounted } from 'vue'
|
|
37
|
+
import Calendar from '../components/Calendar.vue'
|
|
38
|
+
import Section from '../components/Section.vue'
|
|
39
|
+
|
|
40
|
+
const events = ref([])
|
|
41
|
+
const loading = ref(false)
|
|
42
|
+
const error = ref(null)
|
|
43
|
+
const currentMonth = ref('?')
|
|
44
|
+
const currentMonthIndex = ref(new Date().getMonth())
|
|
45
|
+
const currentYear = ref(new Date().getFullYear())
|
|
46
|
+
|
|
47
|
+
// Date picker state - using native HTML5 month input
|
|
48
|
+
const selectedDate = ref('')
|
|
49
|
+
|
|
50
|
+
// Generate dummy events for the current month
|
|
51
|
+
function generateDummyEvents() {
|
|
52
|
+
const now = new Date()
|
|
53
|
+
const currentMonth = now.getMonth()
|
|
54
|
+
const currentYear = now.getFullYear()
|
|
55
|
+
|
|
56
|
+
// Get the number of days in the current month
|
|
57
|
+
const daysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate()
|
|
58
|
+
|
|
59
|
+
const dummyEvents = []
|
|
60
|
+
|
|
61
|
+
// Add some recurring events
|
|
62
|
+
for (let day = 1; day <= daysInMonth; day++) {
|
|
63
|
+
const date = new Date(currentYear, currentMonth, day)
|
|
64
|
+
const dayOfWeek = date.getDay()
|
|
65
|
+
|
|
66
|
+
// Add weekend events (Saturday and Sunday)
|
|
67
|
+
if (dayOfWeek === 0 || dayOfWeek === 6) {
|
|
68
|
+
dummyEvents.push({
|
|
69
|
+
id: `weekend-${day}`,
|
|
70
|
+
title: 'Weekend Activity',
|
|
71
|
+
date: new Date(currentYear, currentMonth, day, 10, 0),
|
|
72
|
+
description: 'Relaxing weekend activity'
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Add some random events
|
|
77
|
+
if (Math.random() > 0.7) {
|
|
78
|
+
const eventTypes = [
|
|
79
|
+
{ title: 'Team Meeting', time: [9, 0], duration: 1 },
|
|
80
|
+
{ title: 'Client Call', time: [14, 30], duration: 0.5 },
|
|
81
|
+
{ title: 'Project Review', time: [16, 0], duration: 2 },
|
|
82
|
+
{ title: 'Lunch Break', time: [12, 0], duration: 1 },
|
|
83
|
+
{ title: 'Code Review', time: [10, 0], duration: 1.5 }
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
const eventType = eventTypes[Math.floor(Math.random() * eventTypes.length)]
|
|
87
|
+
const [hours, minutes] = eventType.time
|
|
88
|
+
|
|
89
|
+
dummyEvents.push({
|
|
90
|
+
id: `event-${day}-${Math.random().toString(36).substr(2, 9)}`,
|
|
91
|
+
title: eventType.title,
|
|
92
|
+
date: new Date(currentYear, currentMonth, day, hours, minutes),
|
|
93
|
+
description: `Scheduled ${eventType.title.toLowerCase()}`
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Add some multi-day events
|
|
99
|
+
dummyEvents.push({
|
|
100
|
+
id: 'conference-1',
|
|
101
|
+
title: 'Tech Conference',
|
|
102
|
+
startDate: new Date(currentYear, currentMonth, 5, 9, 0),
|
|
103
|
+
endDate: new Date(currentYear, currentMonth, 7, 17, 0),
|
|
104
|
+
description: 'Annual technology conference'
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
dummyEvents.push({
|
|
108
|
+
id: 'vacation-1',
|
|
109
|
+
title: 'Vacation',
|
|
110
|
+
startDate: new Date(currentYear, currentMonth, 15, 0, 0),
|
|
111
|
+
endDate: new Date(currentYear, currentMonth, 18, 23, 59),
|
|
112
|
+
description: 'Family vacation time'
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// Add some all-day events
|
|
116
|
+
dummyEvents.push({
|
|
117
|
+
id: 'holiday-1',
|
|
118
|
+
title: 'Public Holiday',
|
|
119
|
+
date: new Date(currentYear, currentMonth, 1, 0, 0),
|
|
120
|
+
description: 'National holiday - office closed'
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
if (daysInMonth >= 25) {
|
|
124
|
+
dummyEvents.push({
|
|
125
|
+
id: 'deadline-1',
|
|
126
|
+
title: 'Project Deadline',
|
|
127
|
+
date: new Date(currentYear, currentMonth, 25, 0, 0),
|
|
128
|
+
description: 'Important project deadline'
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return dummyEvents
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function handleEventClick(event) {
|
|
136
|
+
console.log('Event clicked:', event)
|
|
137
|
+
alert(`Event: ${event.title}\nDescription: ${event.description || 'No description'}`)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function handleDateClick(date) {
|
|
141
|
+
console.log('Date clicked:', date)
|
|
142
|
+
alert(`Date selected: ${date.toLocaleDateString()}`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Helper function to update the date picker value
|
|
146
|
+
function updateDatePicker(monthIndex, year) {
|
|
147
|
+
const month = String(monthIndex + 1).padStart(2, '0')
|
|
148
|
+
selectedDate.value = `${year}-${month}`
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Navigation functions - using the same fast path as internal calendar navigation
|
|
152
|
+
function previousMonth() {
|
|
153
|
+
const newMonth = currentMonthIndex.value === 0 ? 11 : currentMonthIndex.value - 1
|
|
154
|
+
const newYear = currentMonthIndex.value === 0 ? currentYear.value - 1 : currentYear.value
|
|
155
|
+
|
|
156
|
+
// Update state directly (fast path)
|
|
157
|
+
currentMonthIndex.value = newMonth
|
|
158
|
+
currentYear.value = newYear
|
|
159
|
+
|
|
160
|
+
// Update title
|
|
161
|
+
const date = new Date(newYear, newMonth, 1)
|
|
162
|
+
currentMonth.value = date.toLocaleDateString('en-US', {
|
|
163
|
+
month: 'long',
|
|
164
|
+
year: 'numeric'
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
// Update date picker
|
|
168
|
+
updateDatePicker(newMonth, newYear)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function nextMonth() {
|
|
172
|
+
const newMonth = currentMonthIndex.value === 11 ? 0 : currentMonthIndex.value + 1
|
|
173
|
+
const newYear = currentMonthIndex.value === 11 ? currentYear.value + 1 : currentYear.value
|
|
174
|
+
|
|
175
|
+
// Update state directly (fast path)
|
|
176
|
+
currentMonthIndex.value = newMonth
|
|
177
|
+
currentYear.value = newYear
|
|
178
|
+
|
|
179
|
+
// Update title
|
|
180
|
+
const date = new Date(newYear, newMonth, 1)
|
|
181
|
+
currentMonth.value = date.toLocaleDateString('en-US', {
|
|
182
|
+
month: 'long',
|
|
183
|
+
year: 'numeric'
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
// Update date picker
|
|
187
|
+
updateDatePicker(newMonth, newYear)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function goToToday() {
|
|
191
|
+
const now = new Date()
|
|
192
|
+
const newMonth = now.getMonth()
|
|
193
|
+
const newYear = now.getFullYear()
|
|
194
|
+
|
|
195
|
+
// Update state directly (fast path)
|
|
196
|
+
currentMonthIndex.value = newMonth
|
|
197
|
+
currentYear.value = newYear
|
|
198
|
+
|
|
199
|
+
// Update title
|
|
200
|
+
currentMonth.value = now.toLocaleDateString('en-US', {
|
|
201
|
+
month: 'long',
|
|
202
|
+
year: 'numeric'
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
// Update date picker
|
|
206
|
+
updateDatePicker(newMonth, newYear)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function goToSelectedDate() {
|
|
210
|
+
if (!selectedDate.value) return
|
|
211
|
+
|
|
212
|
+
// Parse the YYYY-MM format from the native date picker
|
|
213
|
+
const [year, month] = selectedDate.value.split('-').map(Number)
|
|
214
|
+
const monthIndex = month - 1 // Convert to 0-based month index
|
|
215
|
+
|
|
216
|
+
// Update state directly (fast path)
|
|
217
|
+
currentMonthIndex.value = monthIndex
|
|
218
|
+
currentYear.value = year
|
|
219
|
+
|
|
220
|
+
// Update title
|
|
221
|
+
const date = new Date(year, monthIndex, 1)
|
|
222
|
+
currentMonth.value = date.toLocaleDateString('en-US', {
|
|
223
|
+
month: 'long',
|
|
224
|
+
year: 'numeric'
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function handleMonthChange(month, year) {
|
|
229
|
+
console.log('Month changed:', month, year)
|
|
230
|
+
// In a real app, you might fetch events for the new month
|
|
231
|
+
loading.value = true
|
|
232
|
+
|
|
233
|
+
// Update current month and year state
|
|
234
|
+
currentMonthIndex.value = month
|
|
235
|
+
currentYear.value = year
|
|
236
|
+
|
|
237
|
+
// Use native JavaScript to format month and year
|
|
238
|
+
const date = new Date(year, month, 1)
|
|
239
|
+
currentMonth.value = date.toLocaleDateString('en-US', {
|
|
240
|
+
month: 'long',
|
|
241
|
+
year: 'numeric'
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
// Update date picker to reflect the new month
|
|
245
|
+
updateDatePicker(month, year)
|
|
246
|
+
|
|
247
|
+
setTimeout(() => {
|
|
248
|
+
// Simulate loading delay
|
|
249
|
+
events.value = generateDummyEvents()
|
|
250
|
+
loading.value = false
|
|
251
|
+
}, 500)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
onMounted(() => {
|
|
255
|
+
// Generate initial events for current month
|
|
256
|
+
events.value = generateDummyEvents()
|
|
257
|
+
|
|
258
|
+
// Set initial month title using native JavaScript
|
|
259
|
+
const now = new Date()
|
|
260
|
+
currentMonthIndex.value = now.getMonth()
|
|
261
|
+
currentYear.value = now.getFullYear()
|
|
262
|
+
currentMonth.value = now.toLocaleDateString('en-US', {
|
|
263
|
+
month: 'long',
|
|
264
|
+
year: 'numeric'
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
// Initialize the native date picker with current month/year
|
|
268
|
+
const year = now.getFullYear()
|
|
269
|
+
const month = String(now.getMonth() + 1).padStart(2, '0')
|
|
270
|
+
selectedDate.value = `${year}-${month}`
|
|
271
|
+
})
|
|
272
|
+
</script>
|
|
273
|
+
|
|
274
|
+
<style scoped>
|
|
275
|
+
.date-picker-container {
|
|
276
|
+
display: flex;
|
|
277
|
+
align-items: center;
|
|
278
|
+
gap: 0.5rem;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.date-picker-label {
|
|
282
|
+
font-size: 0.875rem;
|
|
283
|
+
font-weight: 500;
|
|
284
|
+
color: #374151;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.date-picker-input {
|
|
288
|
+
padding: 0.5rem;
|
|
289
|
+
border: 1px solid #d1d5db;
|
|
290
|
+
border-radius: 4px;
|
|
291
|
+
font-size: 0.875rem;
|
|
292
|
+
background: white;
|
|
293
|
+
cursor: pointer;
|
|
294
|
+
transition: all 0.2s;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.date-picker-input:hover {
|
|
298
|
+
border-color: #9ca3af;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.date-picker-input:focus {
|
|
302
|
+
outline: none;
|
|
303
|
+
border-color: #3b82f6;
|
|
304
|
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/* Dark theme support */
|
|
308
|
+
@media (prefers-color-scheme: dark) {
|
|
309
|
+
.date-picker-label {
|
|
310
|
+
color: #d1d5db;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.date-picker-input {
|
|
314
|
+
background: #374151;
|
|
315
|
+
border-color: #4b5563;
|
|
316
|
+
color: #f9fafb;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.date-picker-input:hover {
|
|
320
|
+
border-color: #6b7280;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.date-picker-input:focus {
|
|
324
|
+
border-color: #60a5fa;
|
|
325
|
+
box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.1);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
</style>
|