refactor(project-detail): EmptyState, Skeleton loading, spacing
- Replace "No data" / "No sessions" / "No tools" terse text with EmptyState components (FileText, Wrench, CalendarDays icons) - Add Skeleton placeholders for header, chart, and grid during loading - Replace raw <input> elements in edit mode with shared Input component - Replace raw save/cancel buttons in edit mode with Button component - Upgrade summarize button touch target from h-6/w-6 to h-8/w-8 - Add focus-visible rings on interactive buttons and links - Use space-y-8 between top-level sections, space-y-2 for label+input pairs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b118166693
commit
5cc38b3aae
1 changed files with 57 additions and 45 deletions
|
|
@ -7,9 +7,14 @@ import CardHeader from '@/components/ui/CardHeader.vue'
|
|||
import CardTitle from '@/components/ui/CardTitle.vue'
|
||||
import CardContent from '@/components/ui/CardContent.vue'
|
||||
import Spinner from '@/components/ui/Spinner.vue'
|
||||
import Skeleton from '@/components/ui/Skeleton.vue'
|
||||
import Input from '@/components/ui/Input.vue'
|
||||
import Button from '@/components/ui/Button.vue'
|
||||
import EmptyState from '@/components/ui/EmptyState.vue'
|
||||
import { formatDuration, formatDateTime } from '@/lib/utils'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { FileText, Wrench, CalendarDays } from 'lucide-vue-next'
|
||||
import type { ProjectDetail } from '@/types'
|
||||
|
||||
const route = useRoute()
|
||||
|
|
@ -110,13 +115,19 @@ async function summarizeSession(sessionId: string) {
|
|||
|
||||
<template>
|
||||
<div class="p-6">
|
||||
<div v-if="loading" class="flex items-center justify-center h-40">
|
||||
<Spinner size="lg" class="text-primary" />
|
||||
<!-- Loading skeleton -->
|
||||
<div v-if="loading" class="space-y-8">
|
||||
<Skeleton class="h-16 w-full rounded-xl" />
|
||||
<Skeleton class="h-64 w-full rounded-xl" />
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Skeleton class="h-48 w-full rounded-xl" />
|
||||
<Skeleton class="h-48 w-full rounded-xl" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-else-if="data">
|
||||
<!-- Header -->
|
||||
<div class="mb-6">
|
||||
<div class="mb-8">
|
||||
<div class="flex items-start justify-between gap-4 flex-wrap">
|
||||
<div class="flex-1 min-w-0">
|
||||
<!-- View mode -->
|
||||
|
|
@ -126,13 +137,13 @@ async function summarizeSession(sessionId: string) {
|
|||
<span v-if="dateParam" class="text-sm text-primary font-medium">{{ dateParam }}</span>
|
||||
<button
|
||||
v-if="dateParam"
|
||||
class="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
class="text-xs text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded"
|
||||
@click="router.push({ name: 'project-detail', params: { id: projectId } })"
|
||||
>
|
||||
← All time
|
||||
</button>
|
||||
<button
|
||||
class="p-1 rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/40 transition-colors"
|
||||
class="p-1 rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/40 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
title="Edit project"
|
||||
@click="openEdit"
|
||||
>
|
||||
|
|
@ -162,7 +173,7 @@ async function summarizeSession(sessionId: string) {
|
|||
v-if="data.project.repo_url"
|
||||
:href="data.project.repo_url"
|
||||
target="_blank"
|
||||
class="text-xs text-primary hover:underline"
|
||||
class="text-xs text-primary hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded"
|
||||
>
|
||||
Repository →
|
||||
</a>
|
||||
|
|
@ -172,51 +183,34 @@ async function summarizeSession(sessionId: string) {
|
|||
<!-- Edit mode -->
|
||||
<div v-else class="space-y-3">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="text-xs font-medium text-muted-foreground mb-1 block">Project name</label>
|
||||
<input
|
||||
v-model="editForm.display_name"
|
||||
class="w-full px-3 py-1.5 text-sm rounded-lg border border-border bg-background focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary"
|
||||
/>
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-medium text-muted-foreground block">Project name</label>
|
||||
<Input v-model="editForm.display_name" class="h-8 text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-medium text-muted-foreground mb-1 block">Client</label>
|
||||
<input
|
||||
v-model="editForm.client"
|
||||
class="w-full px-3 py-1.5 text-sm rounded-lg border border-border bg-background focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary"
|
||||
/>
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-medium text-muted-foreground block">Client</label>
|
||||
<Input v-model="editForm.client" class="h-8 text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-medium text-muted-foreground mb-1 block">Job number</label>
|
||||
<input
|
||||
v-model="editForm.job_number"
|
||||
placeholder="e.g. JOB-12345"
|
||||
class="w-full px-3 py-1.5 text-sm rounded-lg border border-border bg-background focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary"
|
||||
/>
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-medium text-muted-foreground block">Job number</label>
|
||||
<Input v-model="editForm.job_number" placeholder="e.g. JOB-12345" class="h-8 text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-medium text-muted-foreground mb-1 block">Repo URL</label>
|
||||
<input
|
||||
v-model="editForm.repo_url"
|
||||
placeholder="https://github.com/..."
|
||||
class="w-full px-3 py-1.5 text-sm rounded-lg border border-border bg-background focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary"
|
||||
/>
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-medium text-muted-foreground block">Repo URL</label>
|
||||
<Input v-model="editForm.repo_url" placeholder="https://github.com/..." class="h-8 text-sm" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
:disabled="saving"
|
||||
class="px-3 py-1.5 text-xs font-semibold rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 transition-colors"
|
||||
@click="saveEdit"
|
||||
>
|
||||
<Button size="sm" :loading="saving" @click="saveEdit">
|
||||
{{ saving ? 'Saving…' : 'Save' }}
|
||||
</button>
|
||||
<button
|
||||
class="px-3 py-1.5 text-xs font-medium rounded-lg text-muted-foreground hover:bg-muted/40 transition-colors"
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="editMode = false"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -255,7 +249,13 @@ async function summarizeSession(sessionId: string) {
|
|||
<CardTitle class="text-sm">Top Files</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div v-if="!data.top_files.length" class="text-sm text-muted-foreground">No data</div>
|
||||
<EmptyState
|
||||
v-if="!data.top_files.length"
|
||||
size="sm"
|
||||
title="No file data"
|
||||
description="File activity will appear after sessions are processed."
|
||||
:icons="[FileText]"
|
||||
/>
|
||||
<div v-else class="space-y-1.5">
|
||||
<div
|
||||
v-for="file in data.top_files.slice(0, 10)"
|
||||
|
|
@ -277,7 +277,13 @@ async function summarizeSession(sessionId: string) {
|
|||
<CardTitle class="text-sm">Tool Usage</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div v-if="!data.top_tools.length" class="text-sm text-muted-foreground">No data</div>
|
||||
<EmptyState
|
||||
v-if="!data.top_tools.length"
|
||||
size="sm"
|
||||
title="No tool data"
|
||||
description="Tool usage stats will appear after sessions are processed."
|
||||
:icons="[Wrench]"
|
||||
/>
|
||||
<div v-else class="space-y-2">
|
||||
<div
|
||||
v-for="tool in data.top_tools.slice(0, 8)"
|
||||
|
|
@ -333,7 +339,13 @@ async function summarizeSession(sessionId: string) {
|
|||
<CardTitle class="text-sm">{{ dateParam ? `Sessions — ${dateParam}` : 'Recent Sessions' }}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div v-if="!data.sessions.length" class="text-sm text-muted-foreground">No sessions</div>
|
||||
<EmptyState
|
||||
v-if="!data.sessions.length"
|
||||
size="sm"
|
||||
title="No sessions"
|
||||
description="Sessions will appear here once Claude Code starts sending data."
|
||||
:icons="[CalendarDays]"
|
||||
/>
|
||||
<div v-else class="space-y-2">
|
||||
<div
|
||||
v-for="session in data.sessions"
|
||||
|
|
@ -358,7 +370,7 @@ async function summarizeSession(sessionId: string) {
|
|||
<p class="text-xs text-muted-foreground">{{ session.commits.length }} commits</p>
|
||||
</div>
|
||||
<button
|
||||
class="flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:text-primary transition-colors"
|
||||
class="flex h-8 w-8 items-center justify-center rounded text-muted-foreground hover:text-primary transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': summarizingSession === session.id }"
|
||||
title="Generate AI summary"
|
||||
@click="summarizeSession(session.id)"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue