Files
decision/frontend/app/components/onboarding/DataCollectivesCard.vue
T
YvvandClaude Fable 5 3301b764f7 v2 : correctifs QA — overlays posés, accueil jamais orphelin, finitions mobiles
- overlays Nuxt UI (UModal/USlideover) : géométrie posée en CSS
  (assets/css/overlays.css) — les utilitaires tailwind attendus par @nuxt/ui
  ne sont pas générés (UnoCSS ne scanne pas node_modules) : drawer mobile et
  modales de confirmation se rendaient hors écran
- a11y : title/description sur les 3 overlays (DialogTitle/Description
  rendus, warnings reka-ui éteints), fermeture du drawer ancrée et testée
- suppression du collectif actif : bascule sur un collectif restant —
  jamais d'accueil orphelin (test store ajouté, 343 vitest verts)
- observatoire : la route « transmis » entre dans les barres (base 100 %)
- overflows mobiles à zéro : formules KaTeX (défilement), table vigueur
  (défilement < 768px), pill cible (retour à la ligne), inputs decider et
  mandat (border-box)
- vérifié navigateur : drawer, modale, 14 routes sans débordement,
  zéro erreur console

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 20:11:54 +02:00

259 lines
6.3 KiB
Vue

<script setup lang="ts">
/**
* /donnees §1 — les collectifs de cette machine : export JSON (<slug>-<date>.json),
* suppression de la copie locale (confirmation), collectif actif marqué, lignée.
*/
import { useCollectiveStore } from '~/stores/collective'
import type { CollectiveIndexEntry } from '~/stores/collective'
import { loadState, toBundle } from '~/data/persistence'
import { LINEAGE_PREFIX } from '~/lexicon'
const store = useCollectiveStore()
const confirmEntry = ref<CollectiveIndexEntry | null>(null)
const confirmOpen = computed({
get: () => confirmEntry.value !== null,
set: (open: boolean) => {
if (!open) confirmEntry.value = null
},
})
const activeLineage = computed(() => store.current?.collective.lineage ?? null)
async function exportEntry(entry: CollectiveIndexEntry) {
const now = new Date().toISOString()
let json: string | null = null
if (entry.id === store.activeId) {
json = store.exportJson()
} else {
const state = await loadState(entry.id)
if (state) json = JSON.stringify(toBundle(state, now), null, 2)
}
if (!json) return
const blob = new Blob([json], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${entry.slug}-${now.slice(0, 10)}.json`
a.click()
URL.revokeObjectURL(url)
}
async function removeConfirmed() {
if (!confirmEntry.value) return
await store.removeCollective(confirmEntry.value.id)
confirmEntry.value = null
}
</script>
<template>
<!-- ld-v2 -->
<section class="ld-card data-card">
<h2 class="data-card__title">
<UIcon name="i-lucide-database" /> Collectifs sur cette machine
</h2>
<div v-if="!store.index.length" class="data-card__empty">
<p>Aucun collectif ici pour l'instant.</p>
<NuxtLink to="/creer" class="ld-btn ld-btn--ghost">
<UIcon name="i-lucide-sparkles" /> Créer un collectif
</NuxtLink>
</div>
<ul v-else class="col-list">
<li v-for="entry in store.index" :key="entry.id" class="col-row">
<span class="col-row__dot" :style="{ background: entry.color }" />
<span class="col-row__main">
<span class="col-row__name">
{{ entry.name }}
<span v-if="entry.id === store.activeId" class="status-pill status-adopted">actif</span>
</span>
<span class="col-row__slug">{{ entry.slug }}</span>
<span
v-if="entry.id === store.activeId && activeLineage"
class="col-row__lineage"
>
<UIcon name="i-lucide-sprout" /> {{ LINEAGE_PREFIX }} {{ activeLineage.sourceSlug }}
</span>
</span>
<span class="col-row__actions">
<button type="button" class="ld-btn ld-btn--ghost" @click="exportEntry(entry)">
<UIcon name="i-lucide-download" /> Exporter
</button>
<button
type="button"
class="ld-btn ld-btn--quiet col-row__delete"
@click="confirmEntry = entry"
>
<UIcon name="i-lucide-trash-2" /> Supprimer
</button>
</span>
</li>
</ul>
<UModal
v-model:open="confirmOpen"
:title="confirmEntry ? `Supprimer « ${confirmEntry.name} » ?` : 'Supprimer'"
description="Ce geste supprime uniquement la copie locale, sur cette machine."
>
<template #content>
<div class="confirm">
<h3 class="confirm__title">Supprimer « {{ confirmEntry?.name }} » ?</h3>
<p class="confirm__text">
Ce geste supprime uniquement la copie locale, sur cette machine.
Un fichier exporté ou une copie ailleurs n'est pas touché.
</p>
<div class="confirm__actions">
<button type="button" class="ld-btn ld-btn--ghost" @click="confirmEntry = null">
Garder
</button>
<button type="button" class="ld-btn confirm__danger" @click="removeConfirmed">
<UIcon name="i-lucide-trash-2" /> Supprimer la copie locale
</button>
</div>
</div>
</template>
</UModal>
</section>
</template>
<style scoped>
.data-card {
display: flex;
flex-direction: column;
gap: 1rem;
padding: clamp(1rem, 3vw, 1.5rem);
}
.data-card__title {
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0;
font-size: 1.0625rem;
font-weight: 800;
color: var(--mood-text);
}
.data-card__empty {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.75rem;
color: var(--mood-text-muted);
font-size: 0.9375rem;
}
.data-card__empty p { margin: 0; }
.col-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin: 0;
padding: 0;
list-style: none;
}
.col-row {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.7rem 0.85rem;
border-radius: var(--r-input);
background: var(--mood-bg);
}
.col-row__dot {
width: 0.9rem;
height: 0.9rem;
flex-shrink: 0;
border-radius: 50%;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.35);
}
.col-row__main {
display: flex;
flex-direction: column;
gap: 0.15rem;
min-width: 0;
flex: 1;
}
.col-row__name {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
font-weight: 700;
color: var(--mood-text);
}
.col-row__slug {
font-size: 0.8125rem;
color: var(--mood-text-muted);
overflow: hidden;
text-overflow: ellipsis;
}
.col-row__lineage {
display: inline-flex;
align-items: center;
gap: 0.3rem;
font-size: 0.8125rem;
font-style: italic;
color: var(--mood-tertiary);
}
.col-row__actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 0.4rem;
}
.col-row__delete:hover { color: var(--mood-error); }
.confirm {
display: flex;
flex-direction: column;
gap: 0.875rem;
padding: clamp(1.1rem, 3vw, 1.5rem);
}
.confirm__title {
margin: 0;
font-size: 1.0625rem;
font-weight: 800;
color: var(--mood-text);
}
.confirm__text {
margin: 0;
font-size: 0.9375rem;
line-height: 1.5;
color: var(--mood-text-muted);
}
.confirm__actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 0.5rem;
}
.confirm__danger {
background: var(--mood-error);
color: #ffffff;
}
@media (max-width: 480px) {
.col-row {
flex-wrap: wrap;
}
.col-row__actions {
width: 100%;
justify-content: flex-start;
}
}
</style>