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:
parent
fd49ad9865
commit
a605ba44eb
1 changed files with 70 additions and 39 deletions
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue