Files
zeroclock/src/components/AppShortcutRecorder.vue
Your Name ee82abe63e feat: tooltips, two-column timer, font selector, tray behavior, icons, readme
- Custom tooltip directive (WCAG AAA) on every button in the app
- Two-column timer layout with sticky hero and recent entries sidebar
- Timer font selector with 16 monospace Google Fonts and live preview
- UI font selector with 15+ Google Fonts
- Close-to-tray and minimize-to-tray settings
- New app icons (no-glow variants), platform icon set
- Mini timer pop-out window
- Favorites strip with drag-reorder and inline actions
- Comprehensive README with feature documentation
- Remove tracked files that belong in gitignore
2026-02-21 01:15:57 +02:00

162 lines
4.3 KiB
Vue

<script setup lang="ts">
import { ref, computed, nextTick } from 'vue'
import { X } from 'lucide-vue-next'
interface Props {
modelValue: string
label?: string
}
const props = withDefaults(defineProps<Props>(), {
label: '',
})
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
const recording = ref(false)
const announcement = ref('')
const recorderRef = ref<HTMLDivElement | null>(null)
const isMac = navigator.platform.toUpperCase().includes('MAC')
const keyChips = computed(() => {
if (!props.modelValue) return []
return props.modelValue.split('+').map(k =>
k === 'CmdOrCtrl' ? (isMac ? 'Cmd' : 'Ctrl') : k
)
})
function startRecording() {
recording.value = true
announcement.value = 'Recording. Press your key combination.'
nextTick(() => {
recorderRef.value?.focus()
})
}
function cancelRecording() {
recording.value = false
announcement.value = ''
}
function onKeydown(e: KeyboardEvent) {
if (!recording.value) return
e.preventDefault()
e.stopPropagation()
if (e.key === 'Escape') {
cancelRecording()
return
}
// Ignore standalone modifier keys
const modifierKeys = ['Control', 'Shift', 'Alt', 'Meta']
if (modifierKeys.includes(e.key)) return
// Must have at least one modifier
const hasModifier = e.ctrlKey || e.metaKey || e.shiftKey || e.altKey
if (!hasModifier) return
// Build the shortcut string
const parts: string[] = []
if (e.ctrlKey || e.metaKey) {
parts.push('CmdOrCtrl')
}
if (e.shiftKey) {
parts.push('Shift')
}
if (e.altKey) {
parts.push('Alt')
}
// Normalize the key name
let key = e.key
if (key === ' ') {
key = 'Space'
} else if (key.length === 1) {
key = key.toUpperCase()
}
parts.push(key)
const combo = parts.join('+')
recording.value = false
emit('update:modelValue', combo)
announcement.value = `Shortcut set to ${combo}`
}
function clearShortcut() {
emit('update:modelValue', '')
announcement.value = 'Shortcut cleared'
}
</script>
<template>
<div role="group" :aria-label="label || 'Keyboard shortcut'" class="inline-flex items-center gap-2">
<!-- Key chips display -->
<div v-if="!recording && modelValue" class="flex items-center gap-1" aria-hidden="true">
<template v-for="(chip, index) in keyChips" :key="index">
<span
class="px-1.5 py-0.5 bg-bg-elevated border border-border-subtle rounded text-text-secondary font-mono text-[0.6875rem]"
>{{ chip }}</span>
<span v-if="index < keyChips.length - 1" class="text-text-tertiary text-[0.6875rem]">+</span>
</template>
</div>
<!-- Screen reader text -->
<span class="sr-only">Current shortcut: {{ modelValue || 'None' }}</span>
<!-- Recording capture area (focused div) -->
<div
v-if="recording"
ref="recorderRef"
tabindex="0"
role="application"
aria-label="Press your key combination"
@keydown="onKeydown"
@blur="cancelRecording"
class="flex items-center gap-2 px-3 py-1 border border-accent rounded-lg bg-bg-inset focus:outline-none focus:ring-1 focus:ring-accent"
>
<span class="text-[0.75rem] text-accent motion-safe:animate-pulse">Press keys...</span>
<button
type="button"
aria-label="Cancel recording"
@mousedown.prevent="cancelRecording"
class="text-[0.6875rem] text-text-tertiary hover:text-text-primary transition-colors"
>
Cancel
</button>
</div>
<!-- Record button -->
<button
v-if="!recording"
type="button"
aria-label="Record shortcut"
@click="startRecording"
class="px-2.5 py-1 text-[0.6875rem] font-medium border border-border-subtle text-text-secondary rounded-lg hover:bg-bg-elevated hover:text-text-primary transition-colors"
>
Record
</button>
<!-- Clear button -->
<button
v-if="!recording && modelValue"
type="button"
aria-label="Clear shortcut"
v-tooltip="'Clear shortcut'"
@click="clearShortcut"
class="w-5 h-5 flex items-center justify-center text-text-tertiary hover:text-text-primary transition-colors"
>
<X class="w-3.5 h-3.5" :stroke-width="2" aria-hidden="true" />
</button>
<!-- aria-live region for announcements -->
<div class="sr-only" aria-live="assertive" aria-atomic="true">{{ announcement }}</div>
</div>
</template>