diff --git a/frontend/src/components/Calendar/CalendarEventPanel.vue b/frontend/src/components/Calendar/CalendarEventPanel.vue
index f11a6b81..22091b6f 100644
--- a/frontend/src/components/Calendar/CalendarEventPanel.vue
+++ b/frontend/src/components/Calendar/CalendarEventPanel.vue
@@ -177,6 +177,7 @@
:debounce="500"
:placeholder="__('Event title')"
@change="sync"
+ @keyup.enter="saveEvent"
/>
@@ -372,7 +373,7 @@ import {
CalendarActiveEvent as activeEvent,
createDocumentResource,
} from 'frappe-ui'
-import { ref, computed, watch, h } from 'vue'
+import { ref, computed, watch, h, onMounted, onBeforeUnmount } from 'vue'
import { useRouter } from 'vue-router'
const props = defineProps({
@@ -664,5 +665,61 @@ function updateEvent(_e) {
Object.assign(_event.value, _e)
}
+// Keyboard shortcuts
+function isTypingEvent(e) {
+ const el = e.target
+ if (!el) return false
+ const tag = el.tagName
+ const editable = el.isContentEditable
+ return (
+ editable ||
+ tag === 'INPUT' ||
+ tag === 'TEXTAREA' ||
+ tag === 'SELECT' ||
+ (el.closest && el.closest('[contenteditable="true"]'))
+ )
+}
+
+function keydownHandler(e) {
+ if (!show.value) return
+
+ // Esc always closes the panel
+ if (e.key === 'Escape') {
+ e.preventDefault()
+ close()
+ return
+ }
+
+ if (!['details', 'edit'].includes(props.mode)) return
+ if (isTypingEvent(e)) return
+
+ // Delete (no modifier) -> delete event
+ if (e.key === 'Delete' || e.key === 'Backspace') {
+ // Avoid capturing Backspace if it would navigate away when no focus
+ e.preventDefault()
+ deleteEvent()
+ return
+ }
+
+ // Cmd/Ctrl + D -> duplicate event
+ if (
+ (e.metaKey || e.ctrlKey) &&
+ !e.shiftKey &&
+ !e.altKey &&
+ e.key.toLowerCase() === 'd'
+ ) {
+ e.preventDefault()
+ duplicateEvent()
+ }
+}
+
+onMounted(() => {
+ window.addEventListener('keydown', keydownHandler)
+})
+
+onBeforeUnmount(() => {
+ window.removeEventListener('keydown', keydownHandler)
+})
+
defineExpose({ updateEvent })
diff --git a/frontend/src/pages/Calendar.vue b/frontend/src/pages/Calendar.vue
index d89ea754..db3fd608 100644
--- a/frontend/src/pages/Calendar.vue
+++ b/frontend/src/pages/Calendar.vue
@@ -113,6 +113,7 @@
v-model="showEventPanel"
v-model:event="event"
:mode="mode"
+ @new="newEvent"
@save="saveEvent"
@edit="editDetails"
@delete="deleteEvent"
@@ -139,7 +140,7 @@ import {
CalendarActiveEvent as activeEvent,
call,
} from 'frappe-ui'
-import { onMounted, ref, computed } from 'vue'
+import { onMounted, onBeforeUnmount, ref, computed } from 'vue'
const { user } = sessionStore()
const { $dialog } = globalStore()
@@ -189,7 +190,7 @@ const event = ref({})
const mode = ref('')
const isCreateDisabled = computed(() =>
- ['edit', 'new-event', 'duplicate-event'].includes(mode.value),
+ ['edit', 'new', 'duplicate'].includes(mode.value),
)
// Temp event helpers
@@ -325,6 +326,47 @@ onMounted(() => {
showEventPanel.value = false
})
+// Global shortcut: Cmd/Ctrl + E -> new event (when not already creating/editing)
+function isTypingEvent(e) {
+ const el = e.target
+ if (!el) return false
+ const tag = el.tagName
+ const editable = el.isContentEditable
+ return (
+ editable ||
+ tag === 'INPUT' ||
+ tag === 'TEXTAREA' ||
+ tag === 'SELECT' ||
+ (el.closest && el.closest('[contenteditable="true"]'))
+ )
+}
+
+function calendarKeydown(e) {
+ if (isTypingEvent(e)) return
+ if (
+ (e.metaKey || e.ctrlKey) &&
+ !e.shiftKey &&
+ !e.altKey &&
+ e.key.toLowerCase() === 'e'
+ ) {
+ if (isCreateDisabled.value) return
+ e.preventDefault()
+ newEvent({
+ date: dayjs().format('YYYY-MM-DD'),
+ time: dayjs().format('HH:mm'),
+ isFullDay: false,
+ })
+ }
+}
+
+onMounted(() => {
+ window.addEventListener('keydown', calendarKeydown)
+})
+
+onBeforeUnmount(() => {
+ window.removeEventListener('keydown', calendarKeydown)
+})
+
function showDetails(e, reloadEvent = false) {
openEvent(e, 'details', reloadEvent)
}