forked from yvv/decision
- Aujourd'hui (Fil 13 sections + capture sticky) + Le chemin (tunnel 2 gestes, Q0 inline, 3 chips, dérogation asymétrique, alternatives réglage/consignation) - Registre + fiche décision (timeline, périmètre premier/second lieu auditable, affluence non-ignorable, S'instruire condensé, éléments + cartographie de clôture, épreuve du réel, Remettre en question, PV A4, gravure) - Salle de vote 5 modalités (consentement, nuancé+histogramme, binaire hérité avec jauge inertielle, Réglage collectif complet — faisceau, médiane basse, Pour moi, Explorer, cristallisation-geste —, élection à départage humain) - Textes (Pacte en clair, document vivant, diff, vue projetée, Atelier des formules porté du v1 sur le moteur unique) + Mandats (faits comptés, feux de la rampe, wizard 3 étapes) + Observatoire (consigner→observer→protocoliser) - Onboarding 7 gabarits + Données locales (export/import, attributs, atelier) - voting→framing gardé (Reformuler d'un réglage figé non cristallisé) - Pages v1 supprimées (login, documents, mandates, protocols, sanctuary, tools, decisions/new) — 342 tests verts, build zéro erreur Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
255 lines
6.1 KiB
Vue
255 lines
6.1 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">
|
|
<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>
|