refactor(omg): touch targets, shared Input, ConfirmDialog, Tooltip

- Replace raw h-7 w-7 action buttons with Button size="icon" (40px)
- Replace raw inline <input> elements with shared Input component
- Replace handleDelete/window.confirm guard with ConfirmDialog + pendingDeleteId ref
- Wrap action buttons in Tooltip with descriptive content
- Upgrade empty state to EmptyState component
- Add focus-visible rings on interactive elements

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vadym Samoilenko 2026-05-13 11:16:06 +01:00
parent fd49ad9865
commit a605ba44eb

View file

@ -6,7 +6,11 @@ import Input from '@/components/ui/Input.vue'
import Textarea from '@/components/ui/Textarea.vue'
import Button from '@/components/ui/Button.vue'
import Spinner from '@/components/ui/Spinner.vue'
import ConfirmDialog from '@/components/ui/ConfirmDialog.vue'
import Tooltip from '@/components/ui/Tooltip.vue'
import EmptyState from '@/components/ui/EmptyState.vue'
import { toast } from 'vue-sonner'
import { Pencil, Trash2, Plus, FileText } from 'lucide-vue-next'
import type { OmgEntry } from '@/types'
const entries = ref<OmgEntry[]>([])
@ -22,6 +26,10 @@ const form = ref({ name: '', client: '', job_number: '', notes: '' })
const inlineEdit = ref<{ id: string; field: 'name' | 'client' | 'job_number' } | null>(null)
const inlineValue = ref('')
// Confirm delete state
const showDeleteConfirm = ref(false)
const pendingDeleteId = ref<string | null>(null)
onMounted(loadEntries)
async function loadEntries() {
@ -82,10 +90,18 @@ async function handleSave() {
}
}
async function handleDelete(entry: OmgEntry) {
function requestDelete(entry: OmgEntry) {
pendingDeleteId.value = entry.id
showDeleteConfirm.value = true
}
async function confirmDelete() {
if (!pendingDeleteId.value) return
const id = pendingDeleteId.value
pendingDeleteId.value = null
try {
await omgApi.remove(entry.id)
entries.value = entries.value.filter(e => e.id !== entry.id)
await omgApi.remove(id)
entries.value = entries.value.filter(e => e.id !== id)
toast.success('Entry deleted')
} catch {
toast.error('Failed to delete entry')
@ -138,14 +154,19 @@ function cancelInline() {
<Spinner size="lg" class="text-primary" />
</div>
<div v-else-if="entries.length === 0" class="text-center text-muted-foreground py-16">
No entries yet. Click "Add entry" to create one.
</div>
<EmptyState
v-else-if="entries.length === 0"
title="No entries yet"
description='Click "Add entry" to create one.'
:icons="[FileText, Plus]"
action-label="Add entry"
@action="openNew"
/>
<!-- Table -->
<div v-else class="border border-border rounded-xl overflow-hidden">
<!-- Header -->
<div class="grid grid-cols-[1fr_1fr_140px_80px] gap-4 px-4 py-2.5 bg-muted/30 border-b border-border text-xs font-semibold text-muted-foreground uppercase tracking-wide">
<div class="grid grid-cols-[1fr_1fr_140px_96px] gap-4 px-4 py-2.5 bg-muted/30 border-b border-border text-xs font-semibold text-muted-foreground uppercase tracking-wide">
<span>Project name</span>
<span>Client</span>
<span>Job #</span>
@ -156,16 +177,16 @@ function cancelInline() {
<div
v-for="entry in entries"
:key="entry.id"
class="grid grid-cols-[1fr_1fr_140px_80px] gap-4 px-4 py-3 border-b border-border last:border-0 items-center hover:bg-muted/10 transition-colors"
class="grid grid-cols-[1fr_1fr_140px_96px] gap-4 px-4 py-3 border-b border-border last:border-0 items-center hover:bg-muted/10 transition-colors"
>
<!-- Name (inline edit on dblclick) -->
<div>
<input
<Input
v-if="inlineEdit?.id === entry.id && inlineEdit.field === 'name'"
:value="inlineValue"
class="w-full text-sm bg-transparent border-b border-primary outline-none py-0.5"
:model-value="inlineValue"
class="h-7 border-0 border-b border-primary rounded-none bg-transparent px-0 focus-visible:ring-0 text-sm"
autofocus
@input="inlineValue = ($event.target as HTMLInputElement).value"
@update:model-value="inlineValue = $event"
@blur="commitInline(entry)"
@keydown.enter="commitInline(entry)"
@keydown.escape="cancelInline"
@ -180,12 +201,12 @@ function cancelInline() {
<!-- Client -->
<div>
<input
<Input
v-if="inlineEdit?.id === entry.id && inlineEdit.field === 'client'"
:value="inlineValue"
class="w-full text-sm bg-transparent border-b border-primary outline-none py-0.5"
:model-value="inlineValue"
class="h-7 border-0 border-b border-primary rounded-none bg-transparent px-0 focus-visible:ring-0 text-sm"
autofocus
@input="inlineValue = ($event.target as HTMLInputElement).value"
@update:model-value="inlineValue = $event"
@blur="commitInline(entry)"
@keydown.enter="commitInline(entry)"
@keydown.escape="cancelInline"
@ -201,12 +222,12 @@ function cancelInline() {
<!-- Job # -->
<div>
<input
<Input
v-if="inlineEdit?.id === entry.id && inlineEdit.field === 'job_number'"
:value="inlineValue"
class="w-full text-sm bg-transparent border-b border-primary outline-none py-0.5"
:model-value="inlineValue"
class="h-7 border-0 border-b border-primary rounded-none bg-transparent px-0 focus-visible:ring-0 text-sm"
autofocus
@input="inlineValue = ($event.target as HTMLInputElement).value"
@update:model-value="inlineValue = $event"
@blur="commitInline(entry)"
@keydown.enter="commitInline(entry)"
@keydown.escape="cancelInline"
@ -221,25 +242,27 @@ function cancelInline() {
</div>
<!-- Actions -->
<div class="flex items-center justify-end gap-1.5">
<button
class="h-7 w-7 rounded flex items-center justify-center text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors"
title="Edit"
@click="openEdit(entry)"
>
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
<button
class="h-7 w-7 rounded flex items-center justify-center text-muted-foreground hover:text-red-500 hover:bg-red-50 transition-colors"
title="Delete"
@click="handleDelete(entry)"
>
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
<div class="flex items-center justify-end gap-1">
<Tooltip content="Edit entry">
<Button
variant="ghost"
size="icon"
class="text-muted-foreground hover:text-foreground"
@click="openEdit(entry)"
>
<Pencil class="h-4 w-4" />
</Button>
</Tooltip>
<Tooltip content="Delete entry">
<Button
variant="ghost"
size="icon"
class="text-muted-foreground hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-950/30"
@click="requestDelete(entry)"
>
<Trash2 class="h-4 w-4" />
</Button>
</Tooltip>
</div>
</div>
</div>
@ -279,5 +302,13 @@ function cancelInline() {
</Button>
</template>
</Dialog>
<!-- Delete confirmation -->
<ConfirmDialog
v-model:open="showDeleteConfirm"
title="Delete entry"
description="This action cannot be undone."
@confirm="confirmDelete"
/>
</div>
</template>