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
This commit is contained in:
@@ -4,7 +4,7 @@ import { useFocusTrap } from '../utils/focusTrap'
|
||||
import { useAnnouncer } from '../composables/useAnnouncer'
|
||||
import { useEntryTemplatesStore, type EntryTemplate } from '../stores/entryTemplates'
|
||||
import { useProjectsStore } from '../stores/projects'
|
||||
import { X, FileText } from 'lucide-vue-next'
|
||||
import { X, FileText, Pencil, Trash2 } from 'lucide-vue-next'
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
@@ -68,10 +68,46 @@ function onKeydown(e: KeyboardEvent) {
|
||||
function selectTemplate(tpl: EntryTemplate) {
|
||||
emit('select', tpl)
|
||||
}
|
||||
|
||||
const editingId = ref<number | null>(null)
|
||||
const editForm = ref({ name: '', project_id: 0, duration: 0 })
|
||||
const confirmDeleteId = ref<number | null>(null)
|
||||
|
||||
function startEdit(tpl: EntryTemplate) {
|
||||
editingId.value = tpl.id!
|
||||
editForm.value = { name: tpl.name, project_id: tpl.project_id, duration: tpl.duration || 0 }
|
||||
confirmDeleteId.value = null
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
editingId.value = null
|
||||
}
|
||||
|
||||
async function saveEdit(tpl: EntryTemplate) {
|
||||
await templatesStore.updateTemplate({
|
||||
...tpl,
|
||||
name: editForm.value.name,
|
||||
project_id: editForm.value.project_id,
|
||||
duration: editForm.value.duration,
|
||||
})
|
||||
editingId.value = null
|
||||
announce('Template updated')
|
||||
}
|
||||
|
||||
function confirmDelete(id: number) {
|
||||
confirmDeleteId.value = id
|
||||
editingId.value = null
|
||||
}
|
||||
|
||||
async function executeDelete(id: number) {
|
||||
await templatesStore.deleteTemplate(id)
|
||||
confirmDeleteId.value = null
|
||||
announce('Template deleted')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Teleport to="#app">
|
||||
<Transition name="modal">
|
||||
<div
|
||||
v-if="show"
|
||||
@@ -94,29 +130,102 @@ function selectTemplate(tpl: EntryTemplate) {
|
||||
@click="$emit('cancel')"
|
||||
class="p-1 text-text-tertiary hover:text-text-primary transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
|
||||
aria-label="Close"
|
||||
v-tooltip="'Close'"
|
||||
>
|
||||
<X class="w-4 h-4" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="templatesStore.templates.length > 0" class="space-y-1 max-h-64 overflow-y-auto" role="listbox" aria-label="Entry templates">
|
||||
<button
|
||||
<div
|
||||
v-for="(tpl, i) in templatesStore.templates"
|
||||
:key="tpl.id"
|
||||
@click="selectTemplate(tpl)"
|
||||
role="option"
|
||||
:aria-selected="i === activeIndex"
|
||||
:class="[
|
||||
'w-full text-left px-3 py-2 rounded-lg transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent',
|
||||
i === activeIndex ? 'bg-accent-muted' : 'hover:bg-bg-elevated'
|
||||
]"
|
||||
>
|
||||
<p class="text-[0.8125rem] text-text-primary">{{ tpl.name }}</p>
|
||||
<p class="text-[0.6875rem] text-text-tertiary">
|
||||
{{ getProjectName(tpl.project_id) }}
|
||||
<span v-if="tpl.duration"> - {{ formatDuration(tpl.duration) }}</span>
|
||||
</p>
|
||||
</button>
|
||||
<!-- Delete confirmation -->
|
||||
<div v-if="confirmDeleteId === tpl.id" class="px-3 py-2 rounded-lg bg-status-error/10 border border-status-error/20">
|
||||
<p class="text-[0.8125rem] text-text-primary mb-2">Delete this template?</p>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@click="executeDelete(tpl.id!)"
|
||||
class="px-3 py-1.5 text-[0.75rem] font-medium bg-status-error text-white rounded-lg hover:bg-status-error/90 transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<button
|
||||
@click="confirmDeleteId = null"
|
||||
class="px-3 py-1.5 text-[0.75rem] font-medium text-text-secondary hover:text-text-primary transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit mode -->
|
||||
<div v-else-if="editingId === tpl.id" class="px-3 py-2 rounded-lg bg-bg-elevated space-y-2">
|
||||
<input
|
||||
v-model="editForm.name"
|
||||
class="w-full px-2 py-1.5 text-[0.8125rem] bg-bg-base border border-border-subtle rounded-lg text-text-primary focus:outline-2 focus:outline-accent"
|
||||
placeholder="Template name"
|
||||
/>
|
||||
<select
|
||||
v-model="editForm.project_id"
|
||||
class="w-full px-2 py-1.5 text-[0.8125rem] bg-bg-base border border-border-subtle rounded-lg text-text-primary focus:outline-2 focus:outline-accent"
|
||||
>
|
||||
<option v-for="p in projectsStore.activeProjects" :key="p.id" :value="p.id">{{ p.name }}</option>
|
||||
</select>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@click="saveEdit(tpl)"
|
||||
class="px-3 py-1.5 text-[0.75rem] font-medium bg-accent text-white rounded-lg hover:bg-accent/90 transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
@click="cancelEdit"
|
||||
class="px-3 py-1.5 text-[0.75rem] font-medium text-text-secondary hover:text-text-primary transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Normal display -->
|
||||
<div v-else class="flex items-center group">
|
||||
<button
|
||||
@click="selectTemplate(tpl)"
|
||||
role="option"
|
||||
:aria-selected="i === activeIndex"
|
||||
:class="[
|
||||
'flex-1 text-left px-3 py-2 rounded-lg transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent',
|
||||
i === activeIndex ? 'bg-accent-muted' : 'hover:bg-bg-elevated'
|
||||
]"
|
||||
>
|
||||
<p class="text-[0.8125rem] text-text-primary">{{ tpl.name }}</p>
|
||||
<p class="text-[0.6875rem] text-text-tertiary">
|
||||
{{ getProjectName(tpl.project_id) }}
|
||||
<span v-if="tpl.duration"> - {{ formatDuration(tpl.duration) }}</span>
|
||||
</p>
|
||||
</button>
|
||||
<div class="flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity shrink-0 pr-1">
|
||||
<button
|
||||
@click.stop="startEdit(tpl)"
|
||||
class="p-1.5 text-text-tertiary hover:text-text-primary transition-colors rounded-lg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
|
||||
aria-label="Edit template"
|
||||
v-tooltip="'Edit'"
|
||||
>
|
||||
<Pencil class="w-3.5 h-3.5" aria-hidden="true" :stroke-width="1.5" />
|
||||
</button>
|
||||
<button
|
||||
@click.stop="confirmDelete(tpl.id!)"
|
||||
class="p-1.5 text-text-tertiary hover:text-status-error transition-colors rounded-lg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
|
||||
aria-label="Delete template"
|
||||
v-tooltip="'Delete'"
|
||||
>
|
||||
<Trash2 class="w-3.5 h-3.5" aria-hidden="true" :stroke-width="1.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="py-8 text-center">
|
||||
|
||||
Reference in New Issue
Block a user