picocrank 1.29.0 → 1.30.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "picocrank",
3
- "version": "1.29.0",
3
+ "version": "1.30.0",
4
4
  "type": "module",
5
5
  "description": "A set of zero dependency Vue components built on the Femtocrank global CSS theme.",
6
6
  "main": "vite.config.js",
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { ref, computed, watch, onUnmounted } from 'vue'
2
+ import { ref, computed, watch, onUnmounted, nextTick } from 'vue'
3
3
  import { HugeiconsIcon } from '@hugeicons/vue'
4
4
 
5
5
  const props = defineProps({
@@ -62,6 +62,15 @@ const props = defineProps({
62
62
  type: Number,
63
63
  default: undefined,
64
64
  },
65
+ viewMode: {
66
+ type: String,
67
+ default: 'month',
68
+ validator: (value) => ['month', 'week', 'day'].includes(value),
69
+ },
70
+ focusDate: {
71
+ type: [Date, String, Number],
72
+ default: null,
73
+ },
65
74
  })
66
75
 
67
76
  const emit = defineEmits([
@@ -73,6 +82,9 @@ const emit = defineEmits([
73
82
  'event-move-request',
74
83
  'event-moved',
75
84
  'event-move-rejected',
85
+ 'update:viewMode',
86
+ 'update:focusDate',
87
+ 'view-change',
76
88
  ])
77
89
 
78
90
  // Internal calendar state
@@ -87,20 +99,91 @@ const currentDate = computed(() => {
87
99
  const viewMonth = computed(() => currentDate.value.getMonth())
88
100
  const viewYear = computed(() => currentDate.value.getFullYear())
89
101
 
102
+ // Monday-start week containing the given date
103
+ function getWeekStart(date) {
104
+ const d = new Date(date)
105
+ const dayOfWeek = (d.getDay() + 6) % 7 // Monday=0
106
+ d.setDate(d.getDate() - dayOfWeek)
107
+ d.setHours(0, 0, 0, 0)
108
+ return d
109
+ }
110
+
90
111
  // Compute the first visible day (Monday) for the current month view
91
112
  function getStartOfGrid(date) {
92
- const firstOfMonth = new Date(date.getFullYear(), date.getMonth(), 1)
93
- const dayOfWeek = (firstOfMonth.getDay() + 6) % 7 // Monday=0
94
- const start = new Date(firstOfMonth)
95
- start.setDate(firstOfMonth.getDate() - dayOfWeek)
96
- start.setHours(0, 0, 0, 0)
97
- return start
113
+ return getWeekStart(new Date(date.getFullYear(), date.getMonth(), 1))
98
114
  }
99
115
 
100
- // Anchor date for the 6x7 grid; we will shift this by weeks without re-rendering the grid structure
116
+ function startOfDay(d) {
117
+ const x = new Date(d)
118
+ x.setHours(0, 0, 0, 0)
119
+ return x
120
+ }
121
+
122
+ function parseFocusDate(value) {
123
+ if (value == null) {
124
+ return startOfDay(new Date())
125
+ }
126
+
127
+ const parsed = value instanceof Date ? new Date(value) : new Date(value)
128
+ if (Number.isNaN(parsed.getTime())) {
129
+ return startOfDay(new Date())
130
+ }
131
+
132
+ return startOfDay(parsed)
133
+ }
134
+
135
+ function normalizeViewAnchor(mode, focusDateValue) {
136
+ const focus = parseFocusDate(focusDateValue)
137
+
138
+ if (mode === 'week') {
139
+ return getWeekStart(focus)
140
+ }
141
+ if (mode === 'day') {
142
+ return focus
143
+ }
144
+
145
+ return getStartOfGrid(currentDate.value)
146
+ }
147
+
148
+ // Anchor date for the grid; we will shift this by weeks without re-rendering the grid structure
101
149
  const gridStartDate = ref(getStartOfGrid(currentDate.value))
102
150
  // Track if we're actively scrolling to prevent watcher from resetting grid anchor
103
151
  const isScrolling = ref(false)
152
+ const internalViewMode = ref(props.viewMode)
153
+ const isCompactView = computed(() => internalViewMode.value !== 'month')
154
+
155
+ const headerTitle = computed(() => {
156
+ if (internalViewMode.value === 'month') {
157
+ return `${props.monthNames[viewMonth.value]} ${viewYear.value}`
158
+ }
159
+
160
+ if (internalViewMode.value === 'day') {
161
+ const day = gridStartDate.value
162
+ return day.toLocaleDateString(undefined, {
163
+ weekday: 'long',
164
+ month: 'long',
165
+ day: 'numeric',
166
+ year: 'numeric',
167
+ })
168
+ }
169
+
170
+ const start = gridStartDate.value
171
+ const end = new Date(start)
172
+ end.setDate(start.getDate() + 6)
173
+
174
+ const startMonth = props.monthNames[start.getMonth()]
175
+ const endMonth = props.monthNames[end.getMonth()]
176
+ const startYear = start.getFullYear()
177
+ const endYear = end.getFullYear()
178
+
179
+ if (startYear === endYear && start.getMonth() === end.getMonth()) {
180
+ return `${startMonth} ${start.getDate()} – ${end.getDate()}, ${startYear}`
181
+ }
182
+ if (startYear === endYear) {
183
+ return `${startMonth} ${start.getDate()} – ${endMonth} ${end.getDate()}, ${startYear}`
184
+ }
185
+ return `${startMonth} ${start.getDate()}, ${startYear} – ${endMonth} ${end.getDate()}, ${endYear}`
186
+ })
104
187
 
105
188
  // Helper function to check if a time is midnight (00:00)
106
189
  function isMidnight(dateValue) {
@@ -260,11 +343,33 @@ function getOrdinalSuffix(day) {
260
343
  return 'th'
261
344
  }
262
345
 
346
+ function calendarDayCount(mode = internalViewMode.value) {
347
+ if (mode === 'week') return 7
348
+ if (mode === 'day') return 1
349
+ return 42
350
+ }
351
+
352
+ function sortedEventsForDay(day) {
353
+ return [...day.events].sort((a, b) => {
354
+ const aMultiDay = isMultiDayEvent(a)
355
+ const bMultiDay = isMultiDayEvent(b)
356
+ if (aMultiDay && !bMultiDay) return -1
357
+ if (!aMultiDay && bMultiDay) return 1
358
+ return 0
359
+ })
360
+ }
361
+
362
+ function visibleEventsForDay(day) {
363
+ const sorted = sortedEventsForDay(day)
364
+ return internalViewMode.value === 'day' ? sorted : sorted.slice(0, 3)
365
+ }
366
+
263
367
  // Calendar generation from persistent grid start
264
368
  const calendarDays = computed(() => {
265
369
  const days = []
266
370
  const start = gridStartDate.value
267
- for (let i = 0; i < 42; i++) {
371
+ const dayCount = calendarDayCount()
372
+ for (let i = 0; i < dayCount; i++) {
268
373
  const date = new Date(start)
269
374
  date.setDate(start.getDate() + i)
270
375
  days.push({
@@ -275,8 +380,68 @@ const calendarDays = computed(() => {
275
380
  return days
276
381
  })
277
382
 
383
+ function commitViewChange(mode, focusDateValue, { emitUpdates = true } = {}) {
384
+ const gridStart = normalizeViewAnchor(mode, focusDateValue)
385
+ internalViewMode.value = mode
386
+ gridStartDate.value = gridStart
387
+
388
+ if (mode !== 'month') {
389
+ syncMonthFromGridStart()
390
+ }
391
+
392
+ if (emitUpdates) {
393
+ emit('update:viewMode', mode)
394
+ emit('update:focusDate', mode === 'month' ? null : gridStart)
395
+ emit('view-change', { mode, focusDate: mode === 'month' ? null : gridStart })
396
+ }
397
+ }
398
+
399
+ function applyExternalView(mode, focusDateValue) {
400
+ const gridStart = normalizeViewAnchor(mode, focusDateValue)
401
+ if (
402
+ mode === internalViewMode.value
403
+ && toDateKey(gridStartDate.value) === toDateKey(gridStart)
404
+ ) {
405
+ return
406
+ }
407
+
408
+ internalViewMode.value = mode
409
+ gridStartDate.value = gridStart
410
+ if (mode !== 'month') {
411
+ syncMonthFromGridStart()
412
+ }
413
+ }
414
+
415
+ watch(
416
+ () => [props.viewMode, props.focusDate],
417
+ ([mode, focusDateValue]) => {
418
+ applyExternalView(mode, focusDateValue)
419
+ },
420
+ { immediate: true },
421
+ )
422
+
423
+ function syncMonthFromGridStart() {
424
+ const { month, year } = getVisibleMonthYear(gridStartDate.value, internalViewMode.value)
425
+ if (props.currentMonth !== undefined && props.currentYear !== undefined) {
426
+ if (month !== viewMonth.value || year !== viewYear.value) {
427
+ emit('month-change', month, year)
428
+ }
429
+ } else {
430
+ internalCurrentDate.value = new Date(year, month, 1)
431
+ }
432
+ }
433
+
278
434
  // Navigation functions
279
435
  function previousMonth() {
436
+ if (internalViewMode.value === 'week') {
437
+ adjustByWeeks(-1)
438
+ return
439
+ }
440
+ if (internalViewMode.value === 'day') {
441
+ adjustByDays(-1)
442
+ return
443
+ }
444
+
280
445
  if (props.currentMonth !== undefined && props.currentYear !== undefined) {
281
446
  const newMonth = viewMonth.value === 0 ? 11 : viewMonth.value - 1
282
447
  const newYear = viewMonth.value === 0 ? viewYear.value - 1 : viewYear.value
@@ -290,6 +455,15 @@ function previousMonth() {
290
455
  }
291
456
 
292
457
  function nextMonth() {
458
+ if (internalViewMode.value === 'week') {
459
+ adjustByWeeks(1)
460
+ return
461
+ }
462
+ if (internalViewMode.value === 'day') {
463
+ adjustByDays(1)
464
+ return
465
+ }
466
+
293
467
  if (props.currentMonth !== undefined && props.currentYear !== undefined) {
294
468
  const newMonth = viewMonth.value === 11 ? 0 : viewMonth.value + 1
295
469
  const newYear = viewMonth.value === 11 ? viewYear.value + 1 : viewYear.value
@@ -302,8 +476,17 @@ function nextMonth() {
302
476
  }
303
477
 
304
478
  function goToToday() {
479
+ const today = new Date()
480
+ if (internalViewMode.value === 'week') {
481
+ commitViewChange('week', today)
482
+ return
483
+ }
484
+ if (internalViewMode.value === 'day') {
485
+ commitViewChange('day', today)
486
+ return
487
+ }
488
+
305
489
  if (props.currentMonth !== undefined && props.currentYear !== undefined) {
306
- const today = new Date()
307
490
  emit('month-change', today.getMonth(), today.getFullYear())
308
491
  gridStartDate.value = getStartOfGrid(today)
309
492
  } else {
@@ -315,33 +498,49 @@ function goToToday() {
315
498
  // Week navigation (mouse wheel)
316
499
  const lastWheelAt = ref(0)
317
500
  const wheelThrottleMs = 180
318
- function getVisibleMonthYear(start) {
501
+ function getVisibleMonthYear(start, mode = internalViewMode.value) {
502
+ if (mode === 'day') {
503
+ return { month: start.getMonth(), year: start.getFullYear() }
504
+ }
505
+
319
506
  const mid = new Date(start)
320
- mid.setDate(start.getDate() + 21) // middle of 6x7 grid
507
+ mid.setDate(start.getDate() + (mode === 'week' ? 3 : 21))
321
508
  return { month: mid.getMonth(), year: mid.getFullYear() }
322
509
  }
323
510
 
324
- function adjustByWeeks(weeks) {
325
- const newStart = new Date(gridStartDate.value)
326
- newStart.setDate(newStart.getDate() + weeks * 7)
327
- gridStartDate.value = newStart
328
-
511
+ function syncNavigationMonth(newStart) {
329
512
  if (props.currentMonth !== undefined && props.currentYear !== undefined) {
330
513
  const { month, year } = getVisibleMonthYear(newStart)
331
514
  if (month !== viewMonth.value || year !== viewYear.value) {
332
515
  emit('month-change', month, year)
333
- // Set flag to prevent watcher from resetting our scroll position
334
516
  isScrolling.value = true
335
- // Clear the flag after a short delay to allow future prop-driven updates
336
517
  setTimeout(() => { isScrolling.value = false }, 100)
337
518
  }
338
519
  } else {
339
- // Keep internal anchor and current date roughly aligned (set to first of visible month)
340
520
  const { month, year } = getVisibleMonthYear(newStart)
341
521
  internalCurrentDate.value = new Date(year, month, 1)
342
522
  }
343
523
  }
344
524
 
525
+ function adjustByWeeks(weeks) {
526
+ const newStart = new Date(gridStartDate.value)
527
+ newStart.setDate(newStart.getDate() + weeks * 7)
528
+
529
+ if (internalViewMode.value === 'month') {
530
+ gridStartDate.value = newStart
531
+ syncNavigationMonth(newStart)
532
+ return
533
+ }
534
+
535
+ commitViewChange(internalViewMode.value, newStart)
536
+ }
537
+
538
+ function adjustByDays(days) {
539
+ const newStart = new Date(gridStartDate.value)
540
+ newStart.setDate(newStart.getDate() + days)
541
+ commitViewChange('day', newStart)
542
+ }
543
+
345
544
  function handleWheel(event) {
346
545
  // Prevent page scrolling while using calendar scroll
347
546
  event.preventDefault()
@@ -349,6 +548,10 @@ function handleWheel(event) {
349
548
  if (now - lastWheelAt.value < wheelThrottleMs) return
350
549
  lastWheelAt.value = now
351
550
  const direction = event.deltaY > 0 ? 1 : -1
551
+ if (internalViewMode.value === 'day') {
552
+ adjustByDays(direction)
553
+ return
554
+ }
352
555
  adjustByWeeks(direction)
353
556
  }
354
557
 
@@ -363,14 +566,133 @@ function handleContextMenu(event, mouseEvent) {
363
566
  emit('event-context-menu', event, mouseEvent)
364
567
  }
365
568
 
366
- // --- Date range selection (click / drag) ---
569
+ // --- Day context menu ---
367
570
 
368
- function startOfDay(d) {
369
- const x = new Date(d)
370
- x.setHours(0, 0, 0, 0)
371
- return x
571
+ const DAY_CONTEXT_MENU_MARGIN_PX = 8
572
+ const dayContextMenuOpen = ref(false)
573
+ const dayContextMenuStyle = ref({ top: '0px', left: '0px' })
574
+ const dayContextMenuDate = ref(null)
575
+ const dayContextMenuRef = ref(null)
576
+
577
+ function positionDayContextMenu(clientX, clientY) {
578
+ const menu = dayContextMenuRef.value
579
+ if (!menu) return
580
+
581
+ const rect = menu.getBoundingClientRect()
582
+ const margin = DAY_CONTEXT_MENU_MARGIN_PX
583
+ let top = clientY
584
+ let left = clientX
585
+
586
+ if (left + rect.width > window.innerWidth - margin) {
587
+ left = window.innerWidth - rect.width - margin
588
+ }
589
+ if (top + rect.height > window.innerHeight - margin) {
590
+ top = window.innerHeight - rect.height - margin
591
+ }
592
+ if (left < margin) left = margin
593
+ if (top < margin) top = margin
594
+
595
+ dayContextMenuStyle.value = {
596
+ top: `${top}px`,
597
+ left: `${left}px`,
598
+ }
599
+ }
600
+
601
+ function closeDayContextMenu() {
602
+ dayContextMenuOpen.value = false
603
+ dayContextMenuDate.value = null
604
+ }
605
+
606
+ function onDocumentPointerDownForDayMenu(event) {
607
+ if (!dayContextMenuOpen.value) return
608
+ if (dayContextMenuRef.value?.contains(event.target)) return
609
+ closeDayContextMenu()
372
610
  }
373
611
 
612
+ function onDocumentKeyDownForDayMenu(event) {
613
+ if (!dayContextMenuOpen.value) return
614
+ if (event.key === 'Escape') {
615
+ closeDayContextMenu()
616
+ }
617
+ }
618
+
619
+ function attachDayContextMenuListeners() {
620
+ window.addEventListener('pointerdown', onDocumentPointerDownForDayMenu, true)
621
+ window.addEventListener('keydown', onDocumentKeyDownForDayMenu)
622
+ }
623
+
624
+ function detachDayContextMenuListeners() {
625
+ window.removeEventListener('pointerdown', onDocumentPointerDownForDayMenu, true)
626
+ window.removeEventListener('keydown', onDocumentKeyDownForDayMenu)
627
+ }
628
+
629
+ function onDayContextMenu(date, mouseEvent) {
630
+ if (isEventTarget(mouseEvent)) return
631
+
632
+ mouseEvent.preventDefault()
633
+ mouseEvent.stopPropagation()
634
+
635
+ dayContextMenuDate.value = startOfDay(date)
636
+ dayContextMenuOpen.value = true
637
+ dayContextMenuStyle.value = {
638
+ top: `${mouseEvent.clientY}px`,
639
+ left: `${mouseEvent.clientX}px`,
640
+ }
641
+
642
+ nextTick(() => {
643
+ positionDayContextMenu(mouseEvent.clientX, mouseEvent.clientY)
644
+ dayContextMenuRef.value?.querySelector('button')?.focus()
645
+ })
646
+ }
647
+
648
+ function showOnlyThisWeek() {
649
+ const date = dayContextMenuDate.value
650
+ if (!date) return
651
+
652
+ commitViewChange('week', date)
653
+ closeDayContextMenu()
654
+ }
655
+
656
+ function showOnlyThisDay() {
657
+ const date = dayContextMenuDate.value
658
+ if (!date) return
659
+
660
+ commitViewChange('day', date)
661
+ closeDayContextMenu()
662
+ }
663
+
664
+ function showFullMonth() {
665
+ commitViewChange('month', currentDate.value)
666
+ closeDayContextMenu()
667
+ }
668
+
669
+ function showWeekViewFromDay() {
670
+ commitViewChange('week', gridStartDate.value)
671
+ }
672
+
673
+ function setView({ mode, focusDate: nextFocusDate } = {}) {
674
+ const nextMode = mode ?? internalViewMode.value
675
+ const anchor = nextFocusDate ?? (nextMode === 'month' ? currentDate.value : gridStartDate.value)
676
+ commitViewChange(nextMode, anchor)
677
+ }
678
+
679
+ defineExpose({
680
+ setView,
681
+ showMonthView: () => commitViewChange('month', currentDate.value),
682
+ showWeekView: (date) => commitViewChange('week', date ?? gridStartDate.value),
683
+ showDayView: (date) => commitViewChange('day', date ?? gridStartDate.value),
684
+ })
685
+
686
+ watch(dayContextMenuOpen, (open) => {
687
+ if (open) {
688
+ attachDayContextMenuListeners()
689
+ } else {
690
+ detachDayContextMenuListeners()
691
+ }
692
+ })
693
+
694
+ // --- Date range selection (click / drag) ---
695
+
374
696
  function toDateKey(d) {
375
697
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
376
698
  }
@@ -488,6 +810,7 @@ function onDayPointerEnter(date) {
488
810
 
489
811
  onUnmounted(() => {
490
812
  detachRangePointerListeners()
813
+ detachDayContextMenuListeners()
491
814
  })
492
815
 
493
816
  // --- Calendar event drag and drop (HTML5) ---
@@ -595,7 +918,7 @@ function onDayDrop(dayDate, ev) {
595
918
 
596
919
  // Sync grid start when controlled month/year props change (but not during scrolling)
597
920
  watch([viewMonth, viewYear], () => {
598
- if (isScrolling.value) return
921
+ if (isScrolling.value || internalViewMode.value !== 'month') return
599
922
  const anchor = new Date(viewYear.value, viewMonth.value, 1)
600
923
  gridStartDate.value = getStartOfGrid(anchor)
601
924
  })
@@ -605,7 +928,7 @@ watch([viewMonth, viewYear], () => {
605
928
  <template>
606
929
  <div class="calendar-wrapper">
607
930
  <div v-if="showNavigation" class="calendar-header-nav">
608
- <h2 class="calendar-title">{{ monthNames[viewMonth] }} {{ viewYear }}</h2>
931
+ <h2 class="calendar-title">{{ headerTitle }}</h2>
609
932
  <div class="calendar-nav-buttons">
610
933
  <slot name="nav-buttons">
611
934
  <button @click="previousMonth" class="button neutral">‹</button>
@@ -616,18 +939,40 @@ watch([viewMonth, viewYear], () => {
616
939
  </div>
617
940
 
618
941
  <div class="calendar-container">
942
+ <div v-if="isCompactView" class="calendar-compact-view-bar">
943
+ <button
944
+ v-if="internalViewMode === 'day'"
945
+ type="button"
946
+ class="button neutral"
947
+ @click="showWeekViewFromDay"
948
+ >
949
+ ‹ Week view
950
+ </button>
951
+ <button type="button" class="button neutral" @click="showFullMonth">
952
+ ‹ Month view
953
+ </button>
954
+ </div>
619
955
  <div v-if="error" class="calendar-error">{{ error }}</div>
620
956
  <div v-if="loading" class="calendar-loading-overlay">Loading…</div>
621
957
  <div
622
958
  class="calendar-grid"
623
959
  :class="{
624
960
  'is-dragging-range': isDraggingRange,
625
- 'is-dragging-event': draggingEventId !== null
961
+ 'is-dragging-event': draggingEventId !== null,
962
+ 'week-only': internalViewMode === 'week',
963
+ 'day-only': internalViewMode === 'day'
626
964
  }"
627
965
  @wheel.prevent="handleWheel"
628
966
  >
629
967
  <!-- Day headers -->
630
- <div v-for="day in dayNames" :key="day" class="day-header">{{ day }}</div>
968
+ <div
969
+ v-for="day in dayNames"
970
+ v-show="internalViewMode !== 'day'"
971
+ :key="day"
972
+ class="day-header"
973
+ >
974
+ {{ day }}
975
+ </div>
631
976
 
632
977
  <!-- Calendar days -->
633
978
  <div
@@ -637,6 +982,7 @@ watch([viewMonth, viewYear], () => {
637
982
  :data-calendar-date="toDateKey(day.date)"
638
983
  @mousedown="onDayPointerDown(day.date, $event)"
639
984
  @touchstart.passive="onDayPointerDown(day.date, $event)"
985
+ @contextmenu="onDayContextMenu(day.date, $event)"
640
986
  @mouseenter="onDayPointerEnter(day.date)"
641
987
  @dragover="onDayDragOver(day.date, $event)"
642
988
  @drop="onDayDrop(day.date, $event)"
@@ -667,13 +1013,7 @@ watch([viewMonth, viewYear], () => {
667
1013
  </div>
668
1014
  <div class="day-events">
669
1015
  <div
670
- v-for="event in day.events.sort((a, b) => {
671
- const aMultiDay = isMultiDayEvent(a);
672
- const bMultiDay = isMultiDayEvent(b);
673
- if (aMultiDay && !bMultiDay) return -1;
674
- if (!aMultiDay && bMultiDay) return 1;
675
- return 0;
676
- }).slice(0, 3)"
1016
+ v-for="event in visibleEventsForDay(day)"
677
1017
  :key="event.id"
678
1018
  class="calendar-event"
679
1019
  :class="{
@@ -714,7 +1054,10 @@ watch([viewMonth, viewYear], () => {
714
1054
  </div>
715
1055
  </slot>
716
1056
  </div>
717
- <div v-if="day.events.length > 3" class="more-events">
1057
+ <div
1058
+ v-if="internalViewMode !== 'day' && day.events.length > 3"
1059
+ class="more-events"
1060
+ >
718
1061
  +{{ day.events.length - 3 }} more
719
1062
  </div>
720
1063
  </div>
@@ -722,6 +1065,46 @@ watch([viewMonth, viewYear], () => {
722
1065
  </div>
723
1066
  </div>
724
1067
  </div>
1068
+
1069
+ <Teleport to="body">
1070
+ <div
1071
+ v-if="dayContextMenuOpen"
1072
+ ref="dayContextMenuRef"
1073
+ class="calendar-day-context-menu"
1074
+ role="menu"
1075
+ :style="dayContextMenuStyle"
1076
+ @click.stop
1077
+ @contextmenu.prevent
1078
+ >
1079
+ <button
1080
+ v-if="internalViewMode !== 'day'"
1081
+ type="button"
1082
+ role="menuitem"
1083
+ class="calendar-day-context-menu-item"
1084
+ @click="showOnlyThisDay"
1085
+ >
1086
+ Show only this day
1087
+ </button>
1088
+ <button
1089
+ v-if="internalViewMode !== 'week'"
1090
+ type="button"
1091
+ role="menuitem"
1092
+ class="calendar-day-context-menu-item"
1093
+ @click="showOnlyThisWeek"
1094
+ >
1095
+ Show only this week
1096
+ </button>
1097
+ <button
1098
+ v-if="internalViewMode !== 'month'"
1099
+ type="button"
1100
+ role="menuitem"
1101
+ class="calendar-day-context-menu-item"
1102
+ @click="showFullMonth"
1103
+ >
1104
+ Show full month
1105
+ </button>
1106
+ </div>
1107
+ </Teleport>
725
1108
  </div>
726
1109
  </template>
727
1110
 
@@ -781,12 +1164,42 @@ watch([viewMonth, viewYear], () => {
781
1164
  position: relative;
782
1165
  }
783
1166
 
1167
+ .calendar-compact-view-bar {
1168
+ display: flex;
1169
+ align-items: center;
1170
+ gap: 0.5rem;
1171
+ padding: 0.5rem 0.75rem;
1172
+ border-bottom: 1px solid var(--calendar-border);
1173
+ background: var(--calendar-chrome-bg);
1174
+ }
1175
+
784
1176
  .calendar-grid {
785
1177
  display: grid;
786
1178
  grid-template-columns: repeat(7, minmax(0, 1fr));
787
1179
  min-height: 400px;
788
1180
  }
789
1181
 
1182
+ .calendar-grid.week-only {
1183
+ min-height: min(72vh, 760px);
1184
+ grid-template-rows: auto 1fr;
1185
+ }
1186
+
1187
+ .calendar-grid.week-only .calendar-day {
1188
+ height: auto;
1189
+ min-height: min(68vh, 720px);
1190
+ }
1191
+
1192
+ .calendar-grid.day-only {
1193
+ grid-template-columns: minmax(0, 1fr);
1194
+ min-height: min(72vh, 760px);
1195
+ }
1196
+
1197
+ .calendar-grid.day-only .calendar-day {
1198
+ height: auto;
1199
+ min-height: min(68vh, 720px);
1200
+ border-right: none;
1201
+ }
1202
+
790
1203
  .calendar-grid.is-dragging-range,
791
1204
  .calendar-grid.is-dragging-event {
792
1205
  user-select: none;
@@ -1068,6 +1481,12 @@ watch([viewMonth, viewYear], () => {
1068
1481
  height: 110px;
1069
1482
  }
1070
1483
 
1484
+ .calendar-grid.week-only .calendar-day,
1485
+ .calendar-grid.day-only .calendar-day {
1486
+ height: auto;
1487
+ min-height: min(52vh, 420px);
1488
+ }
1489
+
1071
1490
  .day-number {
1072
1491
  font-size: 1rem;
1073
1492
  }
@@ -1103,3 +1522,35 @@ watch([viewMonth, viewYear], () => {
1103
1522
  }
1104
1523
  }
1105
1524
  </style>
1525
+
1526
+ <style>
1527
+ .calendar-day-context-menu {
1528
+ position: fixed;
1529
+ z-index: 1000;
1530
+ min-width: 12rem;
1531
+ padding: 0.35rem;
1532
+ border: 1px solid var(--table-popover-border);
1533
+ border-radius: 8px;
1534
+ background: var(--table-popover-bg);
1535
+ box-shadow: var(--table-popover-shadow);
1536
+ }
1537
+
1538
+ .calendar-day-context-menu-item {
1539
+ display: block;
1540
+ width: 100%;
1541
+ padding: 0.5rem 0.75rem;
1542
+ border: none;
1543
+ border-radius: 4px;
1544
+ background: transparent;
1545
+ color: var(--table-popover-fg);
1546
+ font: inherit;
1547
+ text-align: left;
1548
+ cursor: pointer;
1549
+ }
1550
+
1551
+ .calendar-day-context-menu-item:hover,
1552
+ .calendar-day-context-menu-item:focus-visible {
1553
+ background: var(--table-popover-item-bg);
1554
+ outline: none;
1555
+ }
1556
+ </style>
@@ -1,10 +1,16 @@
1
1
  <template>
2
- <Section
3
- title="Example Calendar"
4
- padding
2
+ <Section
3
+ title="Example Calendar"
4
+ subtitle="Open with ?view=week&date=2026-09-08 or ?view=day&date=2026-09-13 to load a specific week or day."
5
+ :icon="Calendar01Icon"
6
+ :padding="sectionPadding"
5
7
  >
6
8
  <template #toolbar>
7
- <label class="short-month-suffix-toggle">
9
+ <label class="calendar-example-toggle">
10
+ <input type="checkbox" v-model="sectionPadding" />
11
+ Section padding
12
+ </label>
13
+ <label class="calendar-example-toggle">
8
14
  <input type="checkbox" v-model="shortMonthSuffix" />
9
15
  Short month suffix
10
16
  </label>
@@ -21,8 +27,7 @@
21
27
  </div>
22
28
  <button @click="nextMonth">›</button>
23
29
  </template>
24
- <Calendar
25
- :section="true"
30
+ <Calendar
26
31
  :events="events"
27
32
  :loading="loading"
28
33
  :error="error"
@@ -30,6 +35,8 @@
30
35
  :short-month-suffix="shortMonthSuffix"
31
36
  :current-month="currentMonthIndex"
32
37
  :current-year="currentYear"
38
+ v-model:view-mode="calendarViewMode"
39
+ v-model:focus-date="calendarFocusDate"
33
40
  @event-click="handleEventClick"
34
41
  @date-click="handleDateClick"
35
42
  @date-range-select="handleDateRangeSelect"
@@ -37,12 +44,14 @@
37
44
  @event-moved="handleEventMoved"
38
45
  @event-move-rejected="handleEventMoveRejected"
39
46
  @month-change="handleMonthChange"
47
+ @view-change="handleViewChange"
40
48
  />
41
49
  </Section>
42
50
  </template>
43
51
 
44
52
  <script setup>
45
53
  import { ref, onMounted } from 'vue'
54
+ import { useRoute, useRouter } from 'vue-router'
46
55
  import Calendar from '../components/Calendar.vue'
47
56
  import Section from '../components/Section.vue'
48
57
  import {
@@ -67,10 +76,38 @@ const EVENT_COLORS = {
67
76
  deadline: { color: '#fde8e8', foregroundColor: '#7f1d1d' },
68
77
  }
69
78
 
79
+ const route = useRoute()
80
+ const router = useRouter()
81
+
82
+ const VALID_VIEW_MODES = new Set(['month', 'week', 'day'])
83
+
84
+ function parseRouteViewMode() {
85
+ const view = route.query.view
86
+ return typeof view === 'string' && VALID_VIEW_MODES.has(view) ? view : 'month'
87
+ }
88
+
89
+ function parseRouteFocusDate() {
90
+ const date = route.query.date
91
+ if (typeof date !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(date)) {
92
+ return null
93
+ }
94
+
95
+ const parsed = new Date(`${date}T00:00:00`)
96
+ return Number.isNaN(parsed.getTime()) ? null : parsed
97
+ }
98
+
99
+ function formatFocusDateQuery(dateValue) {
100
+ const date = new Date(dateValue)
101
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
102
+ }
103
+
70
104
  const events = ref([])
71
105
  const loading = ref(false)
72
106
  const error = ref(null)
107
+ const sectionPadding = ref(true)
73
108
  const shortMonthSuffix = ref(false)
109
+ const calendarViewMode = ref(parseRouteViewMode())
110
+ const calendarFocusDate = ref(parseRouteFocusDate())
74
111
  const currentMonth = ref('?')
75
112
  const currentMonthIndex = ref(new Date().getMonth())
76
113
  const currentYear = ref(new Date().getFullYear())
@@ -368,6 +405,22 @@ function goToSelectedDate() {
368
405
  events.value = getEventsForSurroundingMonths(monthIndex, year)
369
406
  }
370
407
 
408
+ function handleViewChange({ mode, focusDate }) {
409
+ const query = { ...route.query }
410
+
411
+ if (mode === 'month') {
412
+ delete query.view
413
+ delete query.date
414
+ } else {
415
+ query.view = mode
416
+ if (focusDate) {
417
+ query.date = formatFocusDateQuery(focusDate)
418
+ }
419
+ }
420
+
421
+ router.replace({ query })
422
+ }
423
+
371
424
  function handleMonthChange(month, year) {
372
425
  console.log('Month changed:', month, year)
373
426
  // In a real app, you might fetch events for the new month
@@ -395,29 +448,24 @@ function handleMonthChange(month, year) {
395
448
  }
396
449
 
397
450
  onMounted(() => {
398
- // Generate initial events for current, previous, and next months
399
- const now = new Date()
400
- const initMonth = now.getMonth()
401
- const initYear = now.getFullYear()
451
+ const initialAnchor = calendarFocusDate.value ?? new Date()
452
+ const initMonth = initialAnchor.getMonth()
453
+ const initYear = initialAnchor.getFullYear()
454
+
402
455
  events.value = getEventsForSurroundingMonths(initMonth, initYear)
403
-
404
- // Set initial month title using native JavaScript
405
- currentMonthIndex.value = now.getMonth()
406
- currentYear.value = now.getFullYear()
407
- currentMonth.value = now.toLocaleDateString('en-US', {
408
- month: 'long',
409
- year: 'numeric'
456
+ currentMonthIndex.value = initMonth
457
+ currentYear.value = initYear
458
+ currentMonth.value = initialAnchor.toLocaleDateString('en-US', {
459
+ month: 'long',
460
+ year: 'numeric',
410
461
  })
411
-
412
- // Initialize the native date picker with current month/year
413
- const year = now.getFullYear()
414
- const month = String(now.getMonth() + 1).padStart(2, '0')
415
- selectedDate.value = `${year}-${month}`
462
+
463
+ updateDatePicker(initMonth, initYear)
416
464
  })
417
465
  </script>
418
466
 
419
467
  <style scoped>
420
- .short-month-suffix-toggle {
468
+ .calendar-example-toggle {
421
469
  display: inline-flex;
422
470
  align-items: center;
423
471
  gap: 0.35rem;