v2 : couche données complète + shell + primitives communes
- 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>
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user