- 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>
101 lines
3.1 KiB
Vue
101 lines
3.1 KiB
Vue
<script setup lang="ts">
|
|
// <!-- ld-v2 --> Stacked initials avatars — deterministic tint from id, -8px overlap.
|
|
// origin glyphs: 'computed' = full dot (concerné·e en premier lieu),
|
|
// 'declared' = raised hand (en second lieu). Tap → parent shows inclusion reason.
|
|
import type { Person } from '~/types/domain'
|
|
|
|
const props = withDefaults(defineProps<{
|
|
people: { person: Person; origin?: 'computed' | 'declared'; reason?: string }[]
|
|
max?: number
|
|
size?: number
|
|
}>(), { max: 6, size: 30 })
|
|
|
|
const emit = defineEmits<{ (e: 'tap', entry: { person: Person; origin?: string; reason?: string }): void }>()
|
|
|
|
const visible = computed(() => props.people.slice(0, props.max))
|
|
const overflow = computed(() => Math.max(0, props.people.length - props.max))
|
|
|
|
function initials(name: string): string {
|
|
return name.split(/[\s-]+/).map(w => w[0] ?? '').join('').slice(0, 2).toUpperCase()
|
|
}
|
|
function tint(id: string): string {
|
|
let h = 0
|
|
for (let i = 0; i < id.length; i++) h = (h * 31 + id.charCodeAt(i)) % 360
|
|
return `oklch(0.72 0.09 ${h})`
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<!-- ld-v2 -->
|
|
<div class="ld-avatars" :style="{ '--av-size': size + 'px' }">
|
|
<button
|
|
v-for="entry in visible"
|
|
:key="entry.person.id"
|
|
class="ld-avatars__item"
|
|
:style="{ background: tint(entry.person.id) }"
|
|
:title="entry.reason ? `${entry.person.displayName} — ${entry.reason}` : entry.person.displayName"
|
|
type="button"
|
|
@click="emit('tap', entry)"
|
|
>
|
|
<span>{{ initials(entry.person.displayName) }}</span>
|
|
<span v-if="entry.origin === 'declared'" class="ld-avatars__glyph">
|
|
<UIcon name="i-lucide-hand" />
|
|
</span>
|
|
<span v-else-if="entry.origin === 'computed'" class="ld-avatars__dot" />
|
|
</button>
|
|
<span v-if="overflow > 0" class="ld-avatars__more">+{{ overflow }}</span>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.ld-avatars { display: inline-flex; align-items: center; }
|
|
.ld-avatars__item {
|
|
position: relative;
|
|
width: var(--av-size);
|
|
height: var(--av-size);
|
|
border-radius: 50%;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: calc(var(--av-size) * 0.36);
|
|
font-weight: 700;
|
|
color: rgba(255, 255, 255, 0.95);
|
|
margin-left: -8px;
|
|
box-shadow: 0 0 0 2px var(--mood-surface);
|
|
cursor: pointer;
|
|
transition: transform 0.1s ease;
|
|
}
|
|
.ld-avatars__item:first-child { margin-left: 0; }
|
|
.ld-avatars__item:hover { transform: translateY(-1px); z-index: 1; }
|
|
.ld-avatars__glyph {
|
|
position: absolute;
|
|
right: -3px;
|
|
bottom: -3px;
|
|
width: calc(var(--av-size) * 0.48);
|
|
height: calc(var(--av-size) * 0.48);
|
|
border-radius: 50%;
|
|
background: var(--mood-surface);
|
|
color: var(--mood-accent);
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: calc(var(--av-size) * 0.3);
|
|
}
|
|
.ld-avatars__dot {
|
|
position: absolute;
|
|
right: -1px;
|
|
bottom: -1px;
|
|
width: calc(var(--av-size) * 0.28);
|
|
height: calc(var(--av-size) * 0.28);
|
|
border-radius: 50%;
|
|
background: var(--mood-accent);
|
|
box-shadow: 0 0 0 2px var(--mood-surface);
|
|
}
|
|
.ld-avatars__more {
|
|
margin-left: 0.4rem;
|
|
font-size: 0.8125rem;
|
|
font-weight: 700;
|
|
color: var(--mood-text-muted);
|
|
}
|
|
</style>
|