feat: refonte UX du dashboard (coquille, tri/filtre/pagination, ⌘K, toasts)

Refonte front (packages/web) : coquille applicative partagée (sidebar desktop +
barre d'onglets mobile + PageHeader, langue/push/logout centralisés) pilotée par
route.meta.layout ; design system (BaseButton/BaseBadge/SegmentedControl/EmptyState/
SkeletonRow, anneau de focus, icônes @lucide/vue) ; tri/filtre/recherche/pagination
côté client via useListControls (+ synchro URL) sur sessions/worktrees/groupes ;
section « À traiter », palette de commandes ⌘K, toasts. Aucune modif serveur/protocole/DB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 11:47:36 +02:00
parent f015a105f9
commit b80e68e539
41 changed files with 2416 additions and 448 deletions

View File

@@ -1,20 +1,20 @@
<template>
<section class="flex flex-col gap-2 rounded-lg border border-zinc-800 bg-zinc-900/50 p-3">
<header class="flex items-center gap-2">
<section class="card flex flex-col gap-2">
<header class="flex flex-wrap items-center gap-2">
<h2 class="font-semibold text-zinc-100">{{ repo.label }}</h2>
<span v-if="!repo.valid" class="badge bg-red-950 text-red-400">{{ t('repos.invalid') }}</span>
<BaseBadge v-if="!repo.valid" tone="red">{{ t('repos.invalid') }}</BaseBadge>
<span class="truncate font-mono text-xs text-zinc-500" :title="repo.path">{{ repo.path }}</span>
<div class="ml-auto flex items-center gap-2">
<button class="btn text-xs" @click="creating = !creating">{{ t('worktrees.new') }}</button>
<button class="btn text-xs" :disabled="busy" @click="onPrune">{{ t('worktrees.prune') }}</button>
<button class="btn-danger text-xs" @click="onRemove">{{ t('repos.remove') }}</button>
<BaseButton size="sm" :icon="Plus" @click="creating = !creating">{{ t('worktrees.new') }}</BaseButton>
<BaseButton size="sm" :icon="Scissors" :loading="busy" @click="onPrune">{{ t('worktrees.prune') }}</BaseButton>
<BaseButton variant="danger" size="sm" icon-only :icon="Trash2" :aria-label="t('repos.remove')" @click="onRemove" />
</div>
</header>
<form v-if="creating" class="flex flex-wrap items-end gap-2 rounded border border-zinc-800 bg-zinc-950/40 p-2" @submit.prevent="onCreate">
<form v-if="creating" class="card-inset flex flex-wrap items-end gap-2" @submit.prevent="onCreate">
<label class="flex flex-1 flex-col gap-1 text-xs text-zinc-400">
{{ t('worktrees.branch') }}
<input v-model="branch" class="input font-mono" :placeholder="repo.defaultBranch ? `feature/…` : 'feature/…'" required />
<input v-model="branch" class="input font-mono" placeholder="feature/…" required />
</label>
<label class="flex flex-col gap-1 text-xs text-zinc-400">
{{ t('worktrees.start') }}
@@ -25,14 +25,15 @@
</select>
</label>
<label class="flex items-center gap-1 text-xs text-zinc-400"><input v-model="newBranch" type="checkbox" /> {{ t('worktrees.newBranch') }}</label>
<button type="submit" class="btn-primary text-xs" :disabled="busy || branch.trim() === ''">
{{ busy ? t('worktrees.creating') : t('worktrees.create') }}
</button>
<BaseButton type="submit" variant="primary" size="sm" :loading="busy" :disabled="branch.trim() === ''">
{{ t('worktrees.create') }}
</BaseButton>
</form>
<p v-if="error" class="text-sm text-red-400">{{ error }}</p>
<div class="flex flex-col gap-1.5">
<WorktreeCard v-for="w in worktrees" :key="w.path" :worktree="w" />
<p v-if="worktrees.length === 0" class="px-1 text-xs text-zinc-600">{{ t('worktrees.noSession') }}</p>
</div>
</section>
</template>
@@ -40,15 +41,23 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { Plus, Scissors, Trash2 } from '@lucide/vue';
import type { RepoSummary } from '@arboretum/shared';
import { useWorktreesStore } from '../stores/worktrees';
import { useWorktreeView } from '../composables/useWorktreeView';
import { useToastsStore } from '../stores/toasts';
import WorktreeCard from './WorktreeCard.vue';
import BaseButton from './ui/BaseButton.vue';
import BaseBadge from './ui/BaseBadge.vue';
const props = defineProps<{ repo: RepoSummary }>();
const { t } = useI18n();
const store = useWorktreesStore();
const view = useWorktreeView();
const toasts = useToastsStore();
const worktrees = computed(() => store.worktreesForRepo(props.repo.id));
// applique le tri/filtre/recherche partagé (toolbar du dashboard) aux worktrees de ce repo.
const worktrees = computed(() => view.apply(store.worktreesForRepo(props.repo.id)));
const creating = ref(false);
const branch = ref('');
const newBranch = ref(true);
@@ -63,6 +72,7 @@ async function onCreate(): Promise<void> {
await store.createWorktree(props.repo.id, { branch: branch.value.trim(), newBranch: newBranch.value, startSession: startSession.value });
branch.value = '';
creating.value = false;
toasts.success(t('toast.worktreeCreated'));
} catch (err) {
error.value = err instanceof Error ? err.message : String(err);
} finally {
@@ -75,19 +85,20 @@ async function onPrune(): Promise<void> {
error.value = null;
try {
await store.prune(props.repo.id);
toasts.success(t('toast.pruned'));
} catch (err) {
error.value = err instanceof Error ? err.message : String(err);
toasts.error(err);
} finally {
busy.value = false;
}
}
async function onRemove(): Promise<void> {
error.value = null;
try {
await store.removeRepo(props.repo.id);
toasts.success(t('toast.repoRemoved'));
} catch (err) {
error.value = err instanceof Error ? err.message : String(err);
toasts.error(err);
}
}
</script>