feat(viz): refonte du donut + jeu d'essai démo de 20 projets
Donut (DonutPage.vue) — alignement avec la maquette "Capture 2026-05-28 205310" :
- Suppression des secteurs en couleur vive : 6 secteurs séparés par des
lignes radiales en pointillé gris léger
- 2 anneaux gris texturés (pattern diagonal) : "Plafond écologique" (extérieur)
et "Plancher social" (intérieur), avec leurs labels
- Espace sûr cream entre les deux où vivent les projets
- Positionnement des projets :
angle : secteur de la catégorie + jitter stable (hash slug)
rayon : bande définie par la dimension de la catégorie
(social -> 108..140, ecologique -> 160..192, transversal -> 142..158)
puis modulé par progress_percent (plus avancé = plus éloigné du centre)
- Projets archivés rendus en POINTS CREUX (stroke only, sans fill)
- Tooltip survol -> RouterLink cliquable vers la fiche projet
- Légende droite réécrite avec les phrases exactes de la maquette
("L'anneau intérieur représente le plancher social, ..." etc.)
- Liste des catégories par dimension avec compte de projets cliquables
Jeu d'essai (tests/seed-demo.sh) :
- 10 actifs / 6 réalisés / 4 archivés (= 20 projets), réalistes Marseille
- 10 membres (4 collectives/orgs, 4 personnes, 2 collectives + AMU)
- 26 liens project_members, 27 étapes de roadmap (mix done/à faire)
- 6 discussions, 8 ressources, 8 events d'activité antérieurs
- Lit DB_* depuis .env, force --default-character-set=utf8mb4
- TRUNCATE+INSERT idempotent (la table categories est préservée)
- Hors /migrations/ pour ne PAS être appliqué automatiquement par apply.sh
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+183
-138
@@ -1,20 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Visualisation Donut — SVG custom.
|
||||
* Visualisation Donut — SVG custom, version maquette « Capture d'écran 2026-05-28 205310 ».
|
||||
*
|
||||
* Modèle Donut Kate Raworth adapté :
|
||||
* - Anneau extérieur (vert pâle) = "Plafond écologique" qu'on ne doit pas dépasser
|
||||
* - Anneau intérieur (orange pâle) = "Plancher social" qu'on ne doit pas franchir vers le bas
|
||||
* - Zone centrale entre les deux = "espace sûr" où vivent les projets citoyens
|
||||
* Anneaux concentriques sobres (pas de couleurs vives sur les secteurs) :
|
||||
* - Anneau extérieur gris = "Plafond écologique" (limites planétaires)
|
||||
* - Espace sûr cream = zone où l'on veut que vivent les projets
|
||||
* - Anneau intérieur gris = "Plancher social" (besoins fondamentaux)
|
||||
*
|
||||
* 6 secteurs colorés (60° chacun) = les 6 catégories thématiques.
|
||||
* Chaque projet = 1 point coloré (par catégorie) placé dans son secteur,
|
||||
* à un rayon qui varie légèrement (hash stable de l'id) pour éviter les chevauchements.
|
||||
* Survol → tooltip avec titre du projet et lien vers sa fiche.
|
||||
* Positionnement des projets :
|
||||
* - Angle : par catégorie (6 secteurs de 60°), jitter angulaire
|
||||
* - Rayon : déterminé par la DIMENSION
|
||||
* social -> bande intérieure (proche du plancher)
|
||||
* ecologique -> bande extérieure (proche du plafond)
|
||||
* transversal -> milieu de la zone sûre
|
||||
* puis modulé par le PROGRESS (plus progressé -> plus éloigné du centre,
|
||||
* dans les limites de la bande de la dimension)
|
||||
* - Projets archivés rendus en POINTS CREUX (stroke only).
|
||||
*/
|
||||
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { RouterLink, useRouter } from 'vue-router'
|
||||
import { api, ApiError } from '@/api/client'
|
||||
import type { Category, Dimension, Paginated, ProjectListRow, Wrapped } from '@/types/api'
|
||||
import LoadingState from '@/components/LoadingState.vue'
|
||||
@@ -31,6 +36,8 @@ async function load() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
// /api/projects sans filtre renvoie déjà tous les non soft-deletés
|
||||
// (active + completed + archived) — utile pour les points creux des archives.
|
||||
const [cats, projs] = await Promise.all([
|
||||
api.get<Wrapped<Category>>('/api/categories'),
|
||||
api.get<Paginated<ProjectListRow>>('/api/projects', { query: { per_page: 100 } }),
|
||||
@@ -45,14 +52,14 @@ async function load() {
|
||||
}
|
||||
onMounted(load)
|
||||
|
||||
// --- Géométrie (rayons en unités du viewBox -250..250) ---
|
||||
const R_PROJECT_OUTER = 200
|
||||
const R_PROJECT_INNER = 110
|
||||
const R_FLOOR_OUTER = 95
|
||||
const R_FLOOR_INNER = 70
|
||||
const R_CENTER = 60
|
||||
// --- Géométrie (en unités du viewBox -260..260) ---
|
||||
const R_CEILING_OUTER = 230
|
||||
const R_CEILING_INNER = 200
|
||||
const R_FLOOR_OUTER = 100
|
||||
const R_FLOOR_INNER = 78
|
||||
const R_CENTER = 56
|
||||
|
||||
/** deg→rad avec offset -90° pour que 0° soit en haut (12 h). */
|
||||
/** deg → rad avec offset -90° (0° en haut). */
|
||||
function rad(deg: number): number {
|
||||
return (deg - 90) * Math.PI / 180
|
||||
}
|
||||
@@ -60,45 +67,34 @@ function pt(angleDeg: number, r: number): [number, number] {
|
||||
const a = rad(angleDeg)
|
||||
return [Math.cos(a) * r, Math.sin(a) * r]
|
||||
}
|
||||
function wedgePath(startDeg: number, endDeg: number, rInner: number, rOuter: number): string {
|
||||
const [x1, y1] = pt(startDeg, rOuter)
|
||||
const [x2, y2] = pt(endDeg, rOuter)
|
||||
const [x3, y3] = pt(endDeg, rInner)
|
||||
const [x4, y4] = pt(startDeg, rInner)
|
||||
const largeArc = endDeg - startDeg > 180 ? 1 : 0
|
||||
return [
|
||||
`M ${x1.toFixed(2)} ${y1.toFixed(2)}`,
|
||||
`A ${rOuter} ${rOuter} 0 ${largeArc} 1 ${x2.toFixed(2)} ${y2.toFixed(2)}`,
|
||||
`L ${x3.toFixed(2)} ${y3.toFixed(2)}`,
|
||||
`A ${rInner} ${rInner} 0 ${largeArc} 0 ${x4.toFixed(2)} ${y4.toFixed(2)}`,
|
||||
'Z',
|
||||
].join(' ')
|
||||
|
||||
/** Hash 32-bit basique pour stabiliser le jitter par projet. */
|
||||
function strHash(s: string): number {
|
||||
let h = 0
|
||||
for (let i = 0; i < s.length; i++) h = ((h << 5) - h + s.charCodeAt(i)) | 0
|
||||
return Math.abs(h)
|
||||
}
|
||||
|
||||
interface WedgeView {
|
||||
cat: Category
|
||||
startDeg: number
|
||||
endDeg: number
|
||||
centerDeg: number
|
||||
path: string
|
||||
labelPos: [number, number]
|
||||
}
|
||||
/** Lignes radiales de séparation des secteurs (entre plancher et plafond). */
|
||||
const wedgeDividers = computed(() => {
|
||||
const n = categories.value.length || 6
|
||||
const step = 360 / n
|
||||
return Array.from({ length: n }, (_, i) => {
|
||||
const angle = i * step
|
||||
const [x1, y1] = pt(angle, R_FLOOR_OUTER)
|
||||
const [x2, y2] = pt(angle, R_CEILING_INNER)
|
||||
return { x1, y1, x2, y2, key: i }
|
||||
})
|
||||
})
|
||||
|
||||
const wedges = computed<WedgeView[]>(() => {
|
||||
/** Position du label catégorie (à l'extérieur du plafond). */
|
||||
const categoryLabels = computed(() => {
|
||||
const n = categories.value.length || 6
|
||||
const step = 360 / n
|
||||
return categories.value.map((cat, i) => {
|
||||
const startDeg = i * step
|
||||
const endDeg = (i + 1) * step
|
||||
const centerDeg = (startDeg + endDeg) / 2
|
||||
return {
|
||||
cat,
|
||||
startDeg,
|
||||
endDeg,
|
||||
centerDeg,
|
||||
path: wedgePath(startDeg, endDeg, R_PROJECT_INNER, R_PROJECT_OUTER),
|
||||
labelPos: pt(centerDeg, (R_PROJECT_INNER + R_PROJECT_OUTER) / 2 + 25),
|
||||
}
|
||||
const center = i * step + step / 2
|
||||
const [x, y] = pt(center, R_CEILING_OUTER + 18)
|
||||
return { cat, x, y }
|
||||
})
|
||||
})
|
||||
|
||||
@@ -106,51 +102,80 @@ interface DotView {
|
||||
project: ProjectListRow
|
||||
x: number
|
||||
y: number
|
||||
r: number
|
||||
isHollow: boolean
|
||||
color: string
|
||||
}
|
||||
|
||||
/** Hash 32-bit basique sur une chaîne (stabilité de positionnement entre rendus). */
|
||||
function strHash(s: string): number {
|
||||
let h = 0
|
||||
for (let i = 0; i < s.length; i++) h = ((h << 5) - h + s.charCodeAt(i)) | 0
|
||||
return Math.abs(h)
|
||||
}
|
||||
|
||||
const dots = computed<DotView[]>(() => {
|
||||
const byCat = new Map<number, ProjectListRow[]>()
|
||||
for (const p of projects.value) {
|
||||
const arr = byCat.get(p.category.id) ?? []
|
||||
arr.push(p)
|
||||
byCat.set(p.category.id, arr)
|
||||
if (categories.value.length === 0) return []
|
||||
// index catégorie par slug -> position du secteur (0..5)
|
||||
const catIndex = new Map<string, number>()
|
||||
categories.value.forEach((c, i) => catIndex.set(c.slug, i))
|
||||
const n = categories.value.length
|
||||
const step = 360 / n
|
||||
|
||||
// Bands selon la dimension (intérieur/extérieur/milieu).
|
||||
const BANDS: Record<Dimension, [number, number]> = {
|
||||
social: [R_FLOOR_OUTER + 8, R_FLOOR_OUTER + 40], // 108..140
|
||||
ecologique: [R_CEILING_INNER - 40, R_CEILING_INNER - 8], // 160..192
|
||||
transversal: [R_FLOOR_OUTER + 42, R_CEILING_INNER - 42], // 142..158
|
||||
}
|
||||
|
||||
// Groupage par catégorie pour répartir angulairement.
|
||||
const byCat = new Map<string, ProjectListRow[]>()
|
||||
for (const p of projects.value) {
|
||||
const list = byCat.get(p.category.slug) ?? []
|
||||
list.push(p)
|
||||
byCat.set(p.category.slug, list)
|
||||
}
|
||||
|
||||
const out: DotView[] = []
|
||||
for (const w of wedges.value) {
|
||||
const ps = byCat.get(w.cat.id) ?? []
|
||||
const span = w.endDeg - w.startDeg
|
||||
ps.forEach((p, idx) => {
|
||||
// Répartit angulairement les projets sur la largeur du secteur (avec marge),
|
||||
// et ajoute un petit jitter de rayon pour ne pas tous les aligner.
|
||||
const t = (idx + 1) / (ps.length + 1)
|
||||
const angleDeg = w.startDeg + span * t
|
||||
const jitter = ((strHash(p.slug) % 100) / 100 - 0.5) * (R_PROJECT_OUTER - R_PROJECT_INNER - 30)
|
||||
const r = (R_PROJECT_INNER + R_PROJECT_OUTER) / 2 + jitter
|
||||
const [x, y] = pt(angleDeg, r)
|
||||
// Diamètre du point en fonction de la progression (un peu plus gros si avancé).
|
||||
const radius = 4 + (p.progress_percent / 100) * 4
|
||||
out.push({ project: p, x, y, r: radius, color: p.category.color_hex })
|
||||
for (const [slug, list] of byCat.entries()) {
|
||||
const i = catIndex.get(slug) ?? 0
|
||||
const wedgeStart = i * step + 3
|
||||
const wedgeEnd = (i + 1) * step - 3
|
||||
const span = wedgeEnd - wedgeStart
|
||||
|
||||
list.forEach((p, idx) => {
|
||||
// Angle : répartition + jitter stable
|
||||
const t = (idx + 1) / (list.length + 1)
|
||||
const angleJitter = ((strHash(p.slug + 'a') % 100) / 100 - 0.5) * Math.min(6, span / Math.max(2, list.length))
|
||||
const angle = wedgeStart + span * t + angleJitter
|
||||
|
||||
// Rayon : bande de dimension + progress
|
||||
const [rMin, rMax] = BANDS[p.category.dimension] ?? BANDS.transversal
|
||||
const progressR = rMin + (rMax - rMin) * (p.progress_percent / 100)
|
||||
const rJitter = ((strHash(p.slug + 'r') % 100) / 100 - 0.5) * 4
|
||||
const r = Math.max(rMin, Math.min(rMax, progressR + rJitter))
|
||||
|
||||
const [x, y] = pt(angle, r)
|
||||
out.push({
|
||||
project: p,
|
||||
x, y,
|
||||
isHollow: p.status === 'archived',
|
||||
color: p.category.color_hex,
|
||||
})
|
||||
})
|
||||
}
|
||||
return out
|
||||
})
|
||||
|
||||
// Légende groupée par dimension
|
||||
// Légende — catégories regroupées par dimension avec leur count.
|
||||
const projectsByCategorySlug = computed(() => {
|
||||
const m: Record<string, number> = {}
|
||||
for (const p of projects.value) {
|
||||
m[p.category.slug] = (m[p.category.slug] ?? 0) + 1
|
||||
}
|
||||
return m
|
||||
})
|
||||
|
||||
const DIMENSION_ORDER: Dimension[] = ['social', 'ecologique', 'transversal']
|
||||
const DIMENSION_META: Record<Dimension, { label: string; color: string }> = {
|
||||
social: { label: 'Plancher social', color: '#F97316' },
|
||||
ecologique: { label: 'Plafond écologique', color: '#22C55E' },
|
||||
transversal: { label: 'Transversal', color: '#6B7280' },
|
||||
social: { label: 'Plancher social', color: '#F97316' },
|
||||
ecologique: { label: 'Plafond écologique', color: '#22C55E' },
|
||||
transversal: { label: 'Transversal', color: '#6B7280' },
|
||||
}
|
||||
|
||||
const categoriesByDimension = computed(() => {
|
||||
const map: Record<Dimension, Category[]> = { social: [], ecologique: [], transversal: [] }
|
||||
for (const c of categories.value) map[c.dimension].push(c)
|
||||
@@ -168,7 +193,7 @@ function gotoProject(p: ProjectListRow) {
|
||||
<h1 class="text-3xl font-semibold text-donut-text">Le Donut des Projets Citoyens</h1>
|
||||
<p class="text-donut-muted mt-2">
|
||||
Inspiré du modèle de l'économie du Donut de Kate Raworth. L'Infolab des projets citoyens
|
||||
explore les initiatives qui poussent une vie marseillaise dans l'espace sûr : entre
|
||||
explore les initiatives qui poussent une vie marseillaise dans l'espace sûr et juste : entre
|
||||
plancher social et plafond écologique.
|
||||
</p>
|
||||
</header>
|
||||
@@ -176,103 +201,119 @@ function gotoProject(p: ProjectListRow) {
|
||||
<LoadingState v-if="loading" class="mt-12 justify-center" />
|
||||
<ErrorState v-else-if="error" :message="error" :retry="load" class="mt-12" />
|
||||
|
||||
<div v-else class="mt-10 grid grid-cols-1 lg:grid-cols-3 gap-10 items-start">
|
||||
<!-- Donut SVG (2/3 sur grands écrans) -->
|
||||
<div class="lg:col-span-2 flex flex-col items-center">
|
||||
<svg viewBox="-250 -250 500 500" class="w-full max-w-[560px] aspect-square">
|
||||
<!-- Plafond écologique (anneau externe) -->
|
||||
<circle r="240" fill="#22C55E14" />
|
||||
<circle r="215" fill="#FAFAF6" />
|
||||
<text
|
||||
v-for="i in 1" :key="`ceil-${i}`"
|
||||
x="0" y="-228" text-anchor="middle" font-size="9"
|
||||
fill="#22C55E" font-weight="600" style="letter-spacing:1px"
|
||||
>PLAFOND ÉCOLOGIQUE</text>
|
||||
<div v-else class="mt-10 grid grid-cols-1 lg:grid-cols-5 gap-10 items-start">
|
||||
<!-- Donut SVG (3/5 sur grands écrans) -->
|
||||
<div class="lg:col-span-3 flex flex-col items-center">
|
||||
<svg viewBox="-260 -260 520 520" class="w-full max-w-[600px] aspect-square">
|
||||
<defs>
|
||||
<pattern id="ring-stripes" patternUnits="userSpaceOnUse" width="6" height="6" patternTransform="rotate(45)">
|
||||
<rect width="6" height="6" fill="#E5E7EB" />
|
||||
<line x1="0" y1="0" x2="0" y2="6" stroke="#D1D5DB" stroke-width="1.2" />
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<!-- Plancher social (anneau interne) -->
|
||||
<circle :r="R_FLOOR_OUTER" fill="#F9731614" />
|
||||
<!-- 1) Anneau extérieur PLAFOND ÉCOLOGIQUE (gris) -->
|
||||
<circle :r="R_CEILING_OUTER" fill="url(#ring-stripes)" />
|
||||
<circle :r="R_CEILING_INNER" fill="#FAFAF6" />
|
||||
|
||||
<!-- 2) Anneau intérieur PLANCHER SOCIAL (gris) -->
|
||||
<circle :r="R_FLOOR_OUTER" fill="url(#ring-stripes)" />
|
||||
<circle :r="R_FLOOR_INNER" fill="#FAFAF6" />
|
||||
|
||||
<!-- 6 secteurs catégories -->
|
||||
<g>
|
||||
<path
|
||||
v-for="w in wedges" :key="w.cat.id"
|
||||
:d="w.path"
|
||||
:fill="w.cat.color_hex + '14'"
|
||||
:stroke="w.cat.color_hex + '40'"
|
||||
stroke-width="0.5"
|
||||
<!-- 3) Séparateurs radiaux entre secteurs (dotted) -->
|
||||
<g stroke="#D1D5DB" stroke-width="0.8" stroke-dasharray="2 3">
|
||||
<line
|
||||
v-for="d in wedgeDividers" :key="d.key"
|
||||
:x1="d.x1" :y1="d.y1" :x2="d.x2" :y2="d.y2"
|
||||
/>
|
||||
</g>
|
||||
|
||||
<!-- Étiquettes catégories à l'extérieur des secteurs -->
|
||||
<!-- 4) Étiquettes catégories autour du plafond -->
|
||||
<g>
|
||||
<text
|
||||
v-for="w in wedges" :key="`label-${w.cat.id}`"
|
||||
:x="w.labelPos[0]" :y="w.labelPos[1]"
|
||||
v-for="l in categoryLabels" :key="`label-${l.cat.id}`"
|
||||
:x="l.x" :y="l.y"
|
||||
text-anchor="middle"
|
||||
font-size="10"
|
||||
:fill="w.cat.color_hex"
|
||||
:fill="l.cat.color_hex"
|
||||
font-weight="600"
|
||||
>{{ w.cat.name }}</text>
|
||||
>{{ l.cat.name }}</text>
|
||||
</g>
|
||||
|
||||
<!-- Points projets -->
|
||||
<!-- 5) Labels des anneaux -->
|
||||
<text x="0" :y="-(R_CEILING_OUTER + 4)" text-anchor="middle" font-size="9" fill="#22C55E" font-weight="700"
|
||||
style="letter-spacing:1px">PLAFOND ÉCOLOGIQUE</text>
|
||||
<text x="0" :y="-(R_FLOOR_INNER - 5)" text-anchor="middle" font-size="8" fill="#F97316" font-weight="700"
|
||||
style="letter-spacing:1px">PLANCHER SOCIAL</text>
|
||||
|
||||
<!-- 6) Points-projets -->
|
||||
<g>
|
||||
<circle
|
||||
v-for="d in dots" :key="d.project.id"
|
||||
:cx="d.x" :cy="d.y" :r="d.r"
|
||||
:fill="d.color"
|
||||
stroke="white" stroke-width="1.5"
|
||||
:cx="d.x" :cy="d.y" r="6"
|
||||
:fill="d.isHollow ? '#FAFAF6' : d.color"
|
||||
:stroke="d.color"
|
||||
:stroke-width="d.isHollow ? 2 : 1.5"
|
||||
class="cursor-pointer transition-transform hover:scale-150 origin-center"
|
||||
:class="hoveredProject?.id === d.project.id ? 'scale-150' : ''"
|
||||
@mouseenter="hoveredProject = d.project"
|
||||
@mouseleave="hoveredProject = null"
|
||||
@click="gotoProject(d.project)"
|
||||
>
|
||||
<title>{{ d.project.title }}</title>
|
||||
<title>{{ d.project.title }} — {{ d.project.progress_percent }}%</title>
|
||||
</circle>
|
||||
</g>
|
||||
|
||||
<!-- Centre -->
|
||||
<!-- 7) Centre -->
|
||||
<circle :r="R_CENTER" fill="#FFFFFF" stroke="#E5E7EB" />
|
||||
<text x="0" y="-4" text-anchor="middle" font-size="14" fill="#111827" font-weight="700"
|
||||
style="letter-spacing:2px">LEDONUT</text>
|
||||
<text x="0" y="14" text-anchor="middle" font-size="9" fill="#6B7280">
|
||||
<text x="0" y="-4" text-anchor="middle" font-size="13" fill="#111827" font-weight="700"
|
||||
style="letter-spacing:2px">LE DONUT</text>
|
||||
<text x="0" y="12" text-anchor="middle" font-size="9" fill="#6B7280">
|
||||
{{ projects.length }} projet{{ projects.length > 1 ? 's' : '' }}
|
||||
</text>
|
||||
</svg>
|
||||
|
||||
<!-- Tooltip projet survolé -->
|
||||
<div class="mt-4 h-12 flex items-center justify-center">
|
||||
<div class="mt-2 h-12 flex items-center justify-center">
|
||||
<transition name="fade">
|
||||
<div
|
||||
<RouterLink
|
||||
v-if="hoveredProject"
|
||||
class="px-3 py-1.5 rounded-full bg-white border border-donut-border shadow-sm text-sm flex items-center gap-2"
|
||||
:to="{ name: 'project-detail', params: { slug: hoveredProject.slug } }"
|
||||
class="px-3 py-1.5 rounded-full bg-white border border-donut-border shadow-sm text-sm flex items-center gap-2 hover:shadow-md transition-shadow"
|
||||
>
|
||||
<span
|
||||
class="w-2 h-2 rounded-full"
|
||||
:style="{ background: hoveredProject.category.color_hex }"
|
||||
/>
|
||||
<span class="font-medium">{{ hoveredProject.title }}</span>
|
||||
<span class="text-donut-muted text-xs">— {{ hoveredProject.progress_percent }}%</span>
|
||||
</div>
|
||||
<span class="text-donut-muted text-xs">
|
||||
— {{ hoveredProject.progress_percent }}%
|
||||
<template v-if="hoveredProject.status === 'archived'"> · archivé</template>
|
||||
<template v-else-if="hoveredProject.status === 'completed'"> · réalisé</template>
|
||||
</span>
|
||||
</RouterLink>
|
||||
<div v-else class="text-xs text-donut-muted">
|
||||
Survolez un point pour voir le détail d'un projet
|
||||
Survolez un point pour voir le projet · cliquez pour ouvrir
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Légende -->
|
||||
<aside class="space-y-6">
|
||||
<aside class="lg:col-span-2 space-y-6">
|
||||
<div class="rounded-xl border border-donut-border bg-donut-surface p-5">
|
||||
<h2 class="text-base font-semibold text-donut-text">Comment lire le donut ?</h2>
|
||||
<p class="text-sm text-donut-muted mt-2 leading-relaxed">
|
||||
Chaque <strong class="text-donut-text">point</strong> est un projet citoyen, placé dans son
|
||||
secteur de catégorie. L'anneau extérieur représente le
|
||||
<span class="font-medium" style="color:#22C55E">plafond écologique</span> et l'anneau
|
||||
intérieur le <span class="font-medium" style="color:#F97316">plancher social</span>.
|
||||
L'objectif : que tous les projets contribuent à rester dans l'espace sûr entre les deux.
|
||||
</p>
|
||||
<h2 class="text-base font-semibold text-donut-text">Comment lire ce donut ?</h2>
|
||||
<ul class="mt-3 space-y-2.5 text-sm text-donut-muted leading-relaxed">
|
||||
<li>L'<strong class="text-donut-text">anneau intérieur</strong> représente le
|
||||
<span class="font-medium" style="color:#F97316">plancher social</span>, qui définit les besoins fondamentaux.</li>
|
||||
<li>L'<strong class="text-donut-text">anneau extérieur</strong> représente le
|
||||
<span class="font-medium" style="color:#22C55E">plafond écologique</span>, qui définit les limites planétaires.</li>
|
||||
<li>L'espace entre les deux est l'<strong class="text-donut-text">espace sûr et juste</strong>.</li>
|
||||
<li>Les <strong class="text-donut-text">projets sociaux</strong> sont positionnés vers l'intérieur.</li>
|
||||
<li>Les <strong class="text-donut-text">projets écologiques</strong> sont positionnés vers l'extérieur.</li>
|
||||
<li>Plus un point est <strong class="text-donut-text">éloigné du centre</strong>, plus le projet est progressé.</li>
|
||||
<li>Les <strong class="text-donut-text">points creux</strong> sont les projets historiques.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -281,7 +322,7 @@ function gotoProject(p: ProjectListRow) {
|
||||
class="rounded-xl border border-donut-border bg-donut-surface p-5"
|
||||
>
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<span class="w-2 h-2 rounded-full" :style="{ background: DIMENSION_META[dim].color }" />
|
||||
<span class="w-2.5 h-2.5 rounded-full" :style="{ background: DIMENSION_META[dim].color }" />
|
||||
<h3 class="text-xs font-semibold uppercase tracking-wider" :style="{ color: DIMENSION_META[dim].color }">
|
||||
{{ DIMENSION_META[dim].label }}
|
||||
</h3>
|
||||
@@ -290,14 +331,18 @@ function gotoProject(p: ProjectListRow) {
|
||||
<li
|
||||
v-for="c in categoriesByDimension[dim]"
|
||||
:key="c.id"
|
||||
class="flex items-center gap-2 text-sm"
|
||||
>
|
||||
<span class="w-2.5 h-2.5 rounded-full shrink-0" :style="{ background: c.color_hex }" />
|
||||
<RouterLink
|
||||
:to="{ name: 'projects', query: { category: c.slug } }"
|
||||
class="text-donut-text hover:underline"
|
||||
class="flex items-center justify-between gap-2 text-sm group"
|
||||
>
|
||||
{{ c.name }}
|
||||
<span class="flex items-center gap-2 min-w-0">
|
||||
<span class="w-2.5 h-2.5 rounded-full shrink-0" :style="{ background: c.color_hex }" />
|
||||
<span class="text-donut-text truncate group-hover:underline">{{ c.name }}</span>
|
||||
</span>
|
||||
<span class="text-donut-muted tabular-nums shrink-0">
|
||||
{{ projectsByCategorySlug[c.slug] ?? 0 }}
|
||||
</span>
|
||||
</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
Reference in New Issue
Block a user