forked from yvv/decision
- stores/collective.ts (état local-first, gabarits, import/export, seeds) + stores/decisions.ts (cycle de vie complet : capture→chemin→fenêtres→ sessions→cristallisation→épreuve du réel→révocation) - data/templates.ts : 7 gabarits (Page blanche observatoire-d'abord, 5 points de départ, Institution symétrique) - composables useFeed (13 sections du Fil en sélecteurs purs) + useSearch (index unique Cmd+K/Q0) - shell : layouts default/bare, app.vue mince, useMood v2 (Source/Margelle/ Nappe/Minuit), sceau 井 = logo, LdWorkspaceSelector avec finalité A1, LdAvatarStack (premier/second lieu), LdCountdown (suspension striée) - 338 tests vitest verts, npm run build zéro erreur Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
243 lines
5.7 KiB
Vue
243 lines
5.7 KiB
Vue
<script setup lang="ts">
|
|
/**
|
|
* Sélecteur de collectif — pastille couleur + nom + finalité (clause A1 du Pacte)
|
|
* en une ligne. Menu léger : collectifs locaux, « Créer un collectif », « Données locales ».
|
|
* Accès défensifs (?.) : le store peut ne pas être hydraté.
|
|
*/
|
|
import { useCollectiveStore } from '~/stores/collective'
|
|
|
|
const store = useCollectiveStore()
|
|
const route = useRoute()
|
|
|
|
const open = ref(false)
|
|
const rootEl = ref<HTMLElement | null>(null)
|
|
onClickOutside(rootEl, () => { open.value = false })
|
|
watch(() => route.path, () => { open.value = false })
|
|
|
|
const current = computed(() => store.current?.collective ?? null)
|
|
const index = computed(() => store.index ?? [])
|
|
|
|
/** Finalité du collectif = contenu de la version courante de la clause A1 du Pacte. */
|
|
const purpose = computed(() => {
|
|
const state = store.current
|
|
if (!state) return ''
|
|
const clause = state.clauses?.find(c => c.code === 'A1')
|
|
if (!clause?.currentVersionId) return ''
|
|
return state.versions?.find(v => v.id === clause.currentVersionId)?.content ?? ''
|
|
})
|
|
|
|
async function pick(id: string) {
|
|
open.value = false
|
|
if (id !== store.activeId) await store.switchTo?.(id)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<!-- ld-v2 -->
|
|
<div ref="rootEl" class="ws">
|
|
<!-- Aucun collectif : inviter à créer -->
|
|
<NuxtLink v-if="!current" to="/creer" class="ld-btn ld-btn--ghost ws__create">
|
|
<UIcon name="i-lucide-sparkles" />
|
|
<span>Créer</span>
|
|
</NuxtLink>
|
|
|
|
<template v-else>
|
|
<button
|
|
class="ws__trigger"
|
|
:aria-expanded="open"
|
|
aria-haspopup="menu"
|
|
@click="open = !open"
|
|
>
|
|
<span class="ws__dot" :style="{ background: current.color }" />
|
|
<span class="ws__text">
|
|
<span class="ws__name">{{ current.name }}</span>
|
|
<span v-if="purpose" class="ws__purpose">{{ purpose }}</span>
|
|
</span>
|
|
<UIcon
|
|
name="i-lucide-chevron-down"
|
|
class="ws__chevron"
|
|
:class="{ 'ws__chevron--open': open }"
|
|
/>
|
|
</button>
|
|
|
|
<Transition name="ws-pop">
|
|
<div v-if="open" class="ws__menu ld-card" role="menu">
|
|
<button
|
|
v-for="entry in index"
|
|
:key="entry.id"
|
|
class="ws__item"
|
|
:class="{ 'ws__item--active': entry.id === store.activeId }"
|
|
role="menuitem"
|
|
@click="pick(entry.id)"
|
|
>
|
|
<span class="ws__dot" :style="{ background: entry.color }" />
|
|
<span class="ws__item-name">{{ entry.name }}</span>
|
|
<UIcon
|
|
v-if="entry.id === store.activeId"
|
|
name="i-lucide-check"
|
|
class="ws__item-check"
|
|
/>
|
|
</button>
|
|
|
|
<div class="ws__sep" />
|
|
|
|
<NuxtLink to="/creer" class="ws__item" role="menuitem">
|
|
<UIcon name="i-lucide-sparkles" class="ws__item-icon" />
|
|
<span class="ws__item-name">Créer un collectif</span>
|
|
</NuxtLink>
|
|
<NuxtLink to="/donnees" class="ws__item" role="menuitem">
|
|
<UIcon name="i-lucide-hard-drive" class="ws__item-icon" />
|
|
<span class="ws__item-name">Données locales</span>
|
|
</NuxtLink>
|
|
</div>
|
|
</Transition>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.ws {
|
|
position: relative;
|
|
display: inline-flex;
|
|
max-width: 100%;
|
|
}
|
|
|
|
.ws__create {
|
|
font-size: 0.875rem;
|
|
padding: 0.4rem 1rem;
|
|
}
|
|
|
|
.ws__trigger {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.625rem;
|
|
max-width: 20rem;
|
|
padding: 0.375rem 0.75rem;
|
|
background: none;
|
|
border-radius: var(--r-input);
|
|
cursor: pointer;
|
|
text-align: left;
|
|
transition: background 0.12s ease;
|
|
}
|
|
.ws__trigger:hover {
|
|
background: var(--mood-accent-soft);
|
|
}
|
|
|
|
.ws__dot {
|
|
width: 0.75rem;
|
|
height: 0.75rem;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
box-shadow: 0 1px 3px var(--mood-shadow);
|
|
}
|
|
|
|
.ws__text {
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-width: 0;
|
|
line-height: 1.25;
|
|
}
|
|
|
|
.ws__name {
|
|
font-size: 0.9375rem;
|
|
font-weight: 700;
|
|
color: var(--mood-text);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.ws__purpose {
|
|
font-size: 0.75rem;
|
|
color: var(--mood-text-muted);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
max-width: 16rem;
|
|
}
|
|
|
|
.ws__chevron {
|
|
font-size: 0.875rem;
|
|
color: var(--mood-text-muted);
|
|
flex-shrink: 0;
|
|
transition: transform 0.15s ease;
|
|
}
|
|
.ws__chevron--open {
|
|
transform: rotate(180deg);
|
|
}
|
|
|
|
/* --- Menu --- */
|
|
.ws__menu {
|
|
position: absolute;
|
|
top: calc(100% + 0.5rem);
|
|
left: 0;
|
|
z-index: 40;
|
|
min-width: 15rem;
|
|
max-width: 20rem;
|
|
padding: 0.375rem;
|
|
box-shadow: var(--shadow-raised);
|
|
}
|
|
|
|
.ws__item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.625rem;
|
|
width: 100%;
|
|
min-height: 2.25rem;
|
|
padding: 0.5rem 0.75rem;
|
|
background: none;
|
|
border-radius: var(--r-input);
|
|
font-size: 0.875rem;
|
|
font-weight: 600;
|
|
color: var(--mood-text);
|
|
text-decoration: none;
|
|
cursor: pointer;
|
|
text-align: left;
|
|
transition: background 0.12s ease;
|
|
}
|
|
.ws__item:hover {
|
|
background: var(--mood-accent-soft);
|
|
}
|
|
.ws__item--active {
|
|
color: var(--mood-accent);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.ws__item-name {
|
|
flex: 1;
|
|
min-width: 0;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.ws__item-check {
|
|
font-size: 0.875rem;
|
|
color: var(--mood-accent);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.ws__item-icon {
|
|
font-size: 1rem;
|
|
color: var(--mood-text-muted);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.ws__sep {
|
|
height: 1px;
|
|
margin: 0.375rem 0.5rem;
|
|
background: color-mix(in srgb, var(--mood-accent) 12%, transparent);
|
|
}
|
|
|
|
/* --- Transition --- */
|
|
.ws-pop-enter-active,
|
|
.ws-pop-leave-active {
|
|
transition: opacity 0.12s ease, transform 0.12s ease;
|
|
}
|
|
.ws-pop-enter-from,
|
|
.ws-pop-leave-to {
|
|
opacity: 0;
|
|
transform: translateY(-4px);
|
|
}
|
|
</style>
|