forked from yvv/decision
- 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>
259 lines
6.3 KiB
Vue
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>
|