mirror of
https://github.com/Ocelot-Social-Community/ocelot.social.git
synced 2026-04-06 01:25:18 +00:00
535 lines
16 KiB
Vue
535 lines
16 KiB
Vue
<template>
|
|
<div class="roadmap" :class="{ 'roadmap--animated': animated, 'roadmap--expanded': showAllPast || showAllFuture }">
|
|
<div class="roadmap-legend">
|
|
<span class="roadmap-legend-item">
|
|
<span class="roadmap-legend-dot roadmap-legend-dot--done"></span> {{ labelDone }}
|
|
</span>
|
|
<span class="roadmap-legend-item">
|
|
<span class="roadmap-legend-dot roadmap-legend-dot--in-progress"></span> {{ labelInProgress }}
|
|
</span>
|
|
<span class="roadmap-legend-item">
|
|
<span class="roadmap-legend-dot roadmap-legend-dot--planned"></span> {{ labelPlanned }}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="roadmap-timeline">
|
|
<!-- Drei Punkte: es wurde bereits viel umgesetzt -->
|
|
<div v-if="hasHiddenPast" class="roadmap-past roadmap-expandable" @click="expandPast">
|
|
<div class="roadmap-past-dots">
|
|
<span class="roadmap-past-dot"></span>
|
|
<span class="roadmap-past-dot"></span>
|
|
<span class="roadmap-past-dot"></span>
|
|
</div>
|
|
<span class="roadmap-past-label">{{ labelPreviouslyCompleted }}</span>
|
|
</div>
|
|
|
|
<div
|
|
v-for="(item, index) in items"
|
|
:key="item.id"
|
|
class="roadmap-station"
|
|
:class="'roadmap-station--' + item.status"
|
|
:style="{ '--i': index }"
|
|
>
|
|
<!-- Verbindungslinie zum nächsten Punkt -->
|
|
<div v-if="index < items.length - 1 || (index === items.length - 1 && hasMorePlanned)" class="roadmap-connector" :style="index < items.length - 1 ? connectorStyle(index) : { '--conn-color': '#059669' }"></div>
|
|
|
|
<!-- Station-Marker -->
|
|
<div class="roadmap-marker" :class="'roadmap-marker--' + item.status">
|
|
<span v-if="item.status === 'done'" class="roadmap-marker-icon">✓</span>
|
|
<svg v-else-if="item.status === 'in-progress'" class="roadmap-marker-svg" viewBox="0 0 512 512" fill="#fff"><path d="M352 320c88.4 0 160-71.6 160-160c0-15.3-2.2-30.1-6.2-44.2c-3.1-10.8-16.4-13.2-24.3-5.3l-76.8 76.8c-3 3-7.1 4.7-11.3 4.7H336c-8.8 0-16-7.2-16-16v-57.4c0-4.2 1.7-8.3 4.7-11.3l76.8-76.8c7.9-7.9 5.4-21.2-5.3-24.3C382.1 2.2 367.3 0 352 0C263.6 0 192 71.6 192 160c0 19.1 3.4 37.5 9.5 54.5L19.9 396.1C7.2 408.8 0 426.1 0 444.1C0 481.6 30.4 512 67.9 512c18 0 35.3-7.2 48-19.9l181.6-181.6c17 6.2 35.4 9.5 54.5 9.5zM80 456c-13.3 0-24-10.7-24-24s10.7-24 24-24s24 10.7 24 24s-10.7 24-24 24z"/></svg>
|
|
</div>
|
|
|
|
<!-- Inhalt -->
|
|
<div class="roadmap-content">
|
|
<div class="roadmap-content-header">
|
|
<strong class="roadmap-content-title">{{ item.title }}</strong>
|
|
<span class="roadmap-content-badge" :class="'roadmap-content-badge--' + item.status">
|
|
{{ statusLabel(item.status) }}
|
|
</span>
|
|
</div>
|
|
<p class="roadmap-content-description">{{ item.description }}</p>
|
|
<div v-if="item.issues && item.issues.length" class="roadmap-content-issues">
|
|
<a
|
|
v-for="issue in item.issues"
|
|
:key="issue"
|
|
:href="'https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/' + issue"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>#{{ issue }}</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Drei Punkte am Ende: weitere geplante Features -->
|
|
<div v-if="hasMorePlanned" class="roadmap-future roadmap-expandable" @click="expandFuture" :style="{ '--i': items.length - 1 }">
|
|
<div class="roadmap-future-dots">
|
|
<span class="roadmap-future-dot"></span>
|
|
<span class="roadmap-future-dot"></span>
|
|
<span class="roadmap-future-dot"></span>
|
|
</div>
|
|
<span class="roadmap-future-label">{{ labelMorePlanned }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted } from "vue"
|
|
|
|
const props = defineProps({
|
|
items: { type: Array, required: true },
|
|
labelDone: { type: String, required: true },
|
|
labelInProgress: { type: String, required: true },
|
|
labelPlanned: { type: String, required: true },
|
|
labelPreviouslyCompleted: { type: String, required: true },
|
|
labelMorePlanned: { type: String, required: true },
|
|
})
|
|
|
|
const animated = ref(false)
|
|
|
|
onMounted(() => {
|
|
requestAnimationFrame(() => {
|
|
animated.value = true
|
|
})
|
|
})
|
|
|
|
const MAX_VISIBLE = 7
|
|
|
|
const showAllPast = ref(false)
|
|
const showAllFuture = ref(false)
|
|
|
|
const allDone = computed(() => props.items.filter(i => i.status === 'done'))
|
|
const allInProgress = computed(() => props.items.filter(i => i.status === 'in-progress'))
|
|
const allPlanned = computed(() => props.items.filter(i => i.status === 'planned'))
|
|
|
|
const hiddenPastCount = computed(() => Math.max(0, allDone.value.length - 1))
|
|
const hasHiddenPast = computed(() => hiddenPastCount.value > 0 && !showAllPast.value)
|
|
|
|
// Basis-Anzahl Planned ohne Expansion
|
|
const basePlannedCount = computed(() => MAX_VISIBLE - 1 - Math.min(1, allInProgress.value.length))
|
|
|
|
const items = computed(() => {
|
|
const done = showAllPast.value ? allDone.value : allDone.value.slice(-1)
|
|
const inProgress = allInProgress.value.slice(0, 1)
|
|
const maxPlanned = showAllFuture.value ? allPlanned.value.length : basePlannedCount.value
|
|
return [...done, ...inProgress, ...allPlanned.value.slice(0, maxPlanned)]
|
|
})
|
|
|
|
const hasMorePlanned = computed(() => {
|
|
if (showAllFuture.value) return false
|
|
return allPlanned.value.length > basePlannedCount.value
|
|
})
|
|
|
|
const expandPast = () => { showAllPast.value = true }
|
|
const expandFuture = () => { showAllFuture.value = true }
|
|
|
|
const statusLabel = (status) => {
|
|
switch (status) {
|
|
case 'done': return props.labelDone
|
|
case 'in-progress': return props.labelInProgress
|
|
case 'planned': return props.labelPlanned
|
|
default: return props.labelPlanned
|
|
}
|
|
}
|
|
|
|
const statusColor = {
|
|
'done': '#eab308',
|
|
'in-progress': '#6366f1',
|
|
'planned': '#059669',
|
|
}
|
|
|
|
const connectorStyle = (index) => {
|
|
const list = items.value
|
|
const from = statusColor[list[index].status]
|
|
const to = statusColor[list[index + 1].status]
|
|
if (from === to) return { '--conn-color': from }
|
|
return { '--conn-from': from, '--conn-to': to }
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* Color tokens */
|
|
.roadmap {
|
|
margin: 1.5rem 0;
|
|
--c-done: #eab308;
|
|
--c-progress: #6366f1;
|
|
--c-planned: #059669;
|
|
}
|
|
|
|
/* === Legend === */
|
|
.roadmap-legend {
|
|
display: flex;
|
|
gap: 20px;
|
|
flex-wrap: wrap;
|
|
margin-bottom: 28px;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.roadmap-legend-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
color: var(--vp-c-text-2, #666);
|
|
}
|
|
|
|
.roadmap-legend-dot {
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.roadmap-legend-dot--done { background: var(--c-done); }
|
|
.roadmap-legend-dot--in-progress { background: var(--c-progress); }
|
|
.roadmap-legend-dot--planned { background: var(--c-planned); }
|
|
|
|
/* === Timeline === */
|
|
.roadmap-timeline {
|
|
position: relative;
|
|
}
|
|
|
|
/* === Past & Future expandable rows === */
|
|
.roadmap-past,
|
|
.roadmap-future {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
margin-left: -12px;
|
|
padding-left: 12px;
|
|
position: relative;
|
|
opacity: 0;
|
|
}
|
|
|
|
.roadmap-past { padding-top: 12px; padding-bottom: 20px; }
|
|
.roadmap-future { padding-top: 20px; padding-bottom: 12px; }
|
|
|
|
.roadmap-past-dots,
|
|
.roadmap-future-dots {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 6px;
|
|
width: 24px;
|
|
}
|
|
|
|
.roadmap-past-dot,
|
|
.roadmap-future-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.roadmap-past-dot { background: var(--c-done); opacity: 0.5; }
|
|
.roadmap-past-dot:nth-child(2) { opacity: 0.7; }
|
|
.roadmap-past-dot:nth-child(3) { opacity: 0.9; }
|
|
|
|
.roadmap-future-dot { background: var(--c-planned); opacity: 0.9; }
|
|
.roadmap-future-dot:nth-child(2) { opacity: 0.7; }
|
|
.roadmap-future-dot:nth-child(3) { opacity: 0.5; }
|
|
|
|
.roadmap-past-label,
|
|
.roadmap-future-label {
|
|
font-size: 0.85em;
|
|
color: var(--vp-c-text);
|
|
opacity: 0.6;
|
|
font-style: italic;
|
|
}
|
|
|
|
/* Connector line from past dots to first entry */
|
|
.roadmap-past::after {
|
|
content: '';
|
|
position: absolute;
|
|
left: 24px;
|
|
transform: translateX(-50%);
|
|
bottom: -28px;
|
|
height: 48px;
|
|
width: 3px;
|
|
background: var(--c-done);
|
|
z-index: 1;
|
|
}
|
|
|
|
/* === Expandable hover === */
|
|
.roadmap-expandable {
|
|
cursor: pointer;
|
|
border-radius: 10px;
|
|
transition: background-color 0.25s ease;
|
|
}
|
|
|
|
.roadmap-expandable:hover { background-color: rgba(128, 128, 128, 0.08); }
|
|
.roadmap-past.roadmap-expandable:hover { background-color: rgba(234, 179, 8, 0.1); }
|
|
.roadmap-future.roadmap-expandable:hover { background-color: rgba(5, 150, 105, 0.1); }
|
|
|
|
.roadmap-expandable:hover .roadmap-past-label,
|
|
.roadmap-expandable:hover .roadmap-future-label { opacity: 0.85; }
|
|
|
|
.roadmap-expandable:hover .roadmap-past-dot,
|
|
.roadmap-expandable:hover .roadmap-future-dot { opacity: 1; }
|
|
|
|
/* === Station === */
|
|
.roadmap-station {
|
|
position: relative;
|
|
display: grid;
|
|
grid-template-columns: 24px 1fr;
|
|
column-gap: 20px;
|
|
min-height: 80px;
|
|
margin-left: -12px;
|
|
padding-left: 12px;
|
|
cursor: default;
|
|
border-radius: 10px;
|
|
transition: background-color 0.25s ease;
|
|
}
|
|
|
|
/* === Connector === */
|
|
.roadmap-connector {
|
|
position: absolute;
|
|
left: 24px;
|
|
transform: translateX(-50%);
|
|
top: 28px;
|
|
bottom: -28px;
|
|
width: 3px;
|
|
z-index: 1;
|
|
background: var(--conn-color, linear-gradient(to bottom, var(--conn-from), var(--conn-to)));
|
|
}
|
|
|
|
/* === Marker === */
|
|
.roadmap-marker {
|
|
grid-column: 1;
|
|
grid-row: 1;
|
|
align-self: start;
|
|
margin-top: 16px;
|
|
width: 24px;
|
|
height: 24px;
|
|
border-radius: 50%;
|
|
box-sizing: border-box;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
position: relative;
|
|
z-index: 2;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.roadmap-marker--done {
|
|
background: var(--c-done);
|
|
border: 3px solid var(--c-done);
|
|
color: #fff;
|
|
font-size: 14px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.roadmap-marker--in-progress {
|
|
background: var(--c-progress);
|
|
border: 3px solid var(--c-progress);
|
|
box-shadow: 0 0 0 5px rgba(99, 102, 241, 0.2);
|
|
}
|
|
|
|
.roadmap-marker-svg { width: 10px; height: 10px; }
|
|
|
|
.roadmap-marker--planned {
|
|
background: var(--vp-c-bg, #fff);
|
|
border: 3px solid var(--c-planned);
|
|
}
|
|
|
|
/* === Content === */
|
|
.roadmap-content {
|
|
grid-column: 2;
|
|
grid-row: 1;
|
|
padding: 14px 16px 24px 0;
|
|
}
|
|
|
|
.roadmap-content-header {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: baseline;
|
|
gap: 10px;
|
|
}
|
|
|
|
strong.roadmap-content-title {
|
|
font-size: 1.15em;
|
|
font-weight: 800;
|
|
color: var(--vp-c-text);
|
|
}
|
|
|
|
.roadmap-content-badge {
|
|
font-size: 0.75em;
|
|
padding: 2px 10px;
|
|
border-radius: 12px;
|
|
font-weight: 600;
|
|
letter-spacing: 0.02em;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.roadmap-content-badge--done { background: #fef9c3; color: #854d0e; }
|
|
.roadmap-content-badge--in-progress { background: #e0e7ff; color: #4338ca; }
|
|
.roadmap-content-badge--planned { background: #d1fae5; color: #065f46; }
|
|
|
|
.roadmap-content-description {
|
|
margin: 6px 0 0;
|
|
font-size: 0.9em;
|
|
color: var(--vp-c-text);
|
|
opacity: 0.75;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.roadmap-content-issues {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-top: 6px;
|
|
font-size: 0.82em;
|
|
}
|
|
|
|
.roadmap-content-issues a { color: var(--vp-c-accent-bg); text-decoration: none; font-weight: 500; }
|
|
.roadmap-content-issues a:hover { text-decoration: underline; }
|
|
|
|
/* === Hover effects === */
|
|
.roadmap-station:hover .roadmap-marker {
|
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
|
transform: scale(1.25);
|
|
}
|
|
|
|
.roadmap-station--done:hover { background-color: rgba(234, 179, 8, 0.1); }
|
|
.roadmap-station--in-progress:hover { background-color: rgba(99, 102, 241, 0.1); }
|
|
.roadmap-station--planned:hover { background-color: rgba(5, 150, 105, 0.1); }
|
|
|
|
.roadmap-station--done:hover .roadmap-marker { box-shadow: 0 0 12px 4px rgba(234, 179, 8, 0.35); }
|
|
.roadmap-station--in-progress:hover .roadmap-marker { box-shadow: 0 0 12px 4px rgba(99, 102, 241, 0.35); }
|
|
.roadmap-station--planned:hover .roadmap-marker { box-shadow: 0 0 12px 4px rgba(5, 150, 105, 0.35); }
|
|
|
|
/* === Animations: initial state === */
|
|
.roadmap-station .roadmap-marker,
|
|
.roadmap-station .roadmap-content {
|
|
opacity: 0;
|
|
transform: scale(0.5);
|
|
}
|
|
|
|
.roadmap-station .roadmap-connector {
|
|
transform: translateX(-50%) scaleY(0);
|
|
transform-origin: top center;
|
|
}
|
|
|
|
/* Normal entrance */
|
|
.roadmap--animated .roadmap-past {
|
|
animation: contentFadeIn 0.4s ease 0.1s forwards;
|
|
}
|
|
|
|
.roadmap--animated .roadmap-future {
|
|
animation: contentFadeIn 0.3s ease calc(0.85s + var(--i, 0) * 0.5s) forwards;
|
|
}
|
|
|
|
.roadmap--animated .roadmap-station .roadmap-marker {
|
|
animation: markerPop 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) forwards,
|
|
markerGlow 0.5s ease forwards;
|
|
animation-delay: calc(0.3s + var(--i) * 0.5s);
|
|
}
|
|
|
|
.roadmap--animated .roadmap-station .roadmap-content {
|
|
animation: contentFadeIn 0.4s ease forwards;
|
|
animation-delay: calc(0.3s + var(--i) * 0.5s);
|
|
}
|
|
|
|
.roadmap--animated .roadmap-station .roadmap-connector {
|
|
animation: lineGrow 0.4s ease forwards;
|
|
animation-delay: calc(0.45s + var(--i) * 0.5s);
|
|
}
|
|
|
|
.roadmap--animated .roadmap-station--in-progress .roadmap-marker {
|
|
animation: markerPop 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) forwards,
|
|
markerGlow 0.5s ease forwards,
|
|
inProgressPulse 2s ease-in-out infinite;
|
|
animation-delay: calc(0.3s + var(--i) * 0.5s),
|
|
calc(0.3s + var(--i) * 0.5s),
|
|
calc(0.8s + var(--i) * 0.5s);
|
|
}
|
|
|
|
/* Expanded entrance (fast stagger) */
|
|
.roadmap--expanded.roadmap--animated .roadmap-station .roadmap-marker {
|
|
animation: markerPop 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) forwards,
|
|
markerGlow 0.7s ease forwards;
|
|
animation-delay: calc(var(--i) * 0.04s);
|
|
}
|
|
|
|
.roadmap--expanded.roadmap--animated .roadmap-station .roadmap-content {
|
|
animation: contentFadeIn 0.6s ease forwards;
|
|
animation-delay: calc(var(--i) * 0.04s);
|
|
}
|
|
|
|
.roadmap--expanded.roadmap--animated .roadmap-station .roadmap-connector {
|
|
animation: lineGrow 0.6s ease forwards;
|
|
animation-delay: calc(var(--i) * 0.04s + 0.1s);
|
|
}
|
|
|
|
.roadmap--expanded.roadmap--animated .roadmap-station--in-progress .roadmap-marker {
|
|
animation: markerPop 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) forwards,
|
|
markerGlow 0.7s ease forwards,
|
|
inProgressPulse 2s ease-in-out infinite;
|
|
animation-delay: calc(var(--i) * 0.04s),
|
|
calc(var(--i) * 0.04s),
|
|
calc(var(--i) * 0.04s + 0.6s);
|
|
}
|
|
|
|
.roadmap--expanded.roadmap--animated .roadmap-future {
|
|
animation: contentFadeIn 0.6s ease forwards;
|
|
animation-delay: calc(var(--i, 0) * 0.04s + 0.7s);
|
|
}
|
|
|
|
/* === Keyframes === */
|
|
@keyframes markerPop {
|
|
0% { opacity: 0; transform: scale(0); }
|
|
60% { opacity: 1; transform: scale(1.3); }
|
|
100% { opacity: 1; transform: scale(1); }
|
|
}
|
|
|
|
@keyframes markerGlow {
|
|
0% { box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.6); }
|
|
50% { box-shadow: 0 0 16px 6px rgba(255, 255, 255, 0.4); }
|
|
100% { box-shadow: none; }
|
|
}
|
|
|
|
@keyframes contentFadeIn {
|
|
0% { opacity: 0; transform: translateX(-10px) scale(0.95); }
|
|
100% { opacity: 1; transform: translateX(0) scale(1); }
|
|
}
|
|
|
|
@keyframes lineGrow {
|
|
0% { transform: translateX(-50%) scaleY(0); opacity: 0.5; }
|
|
100% { transform: translateX(-50%) scaleY(1); opacity: 1; }
|
|
}
|
|
|
|
@keyframes inProgressPulse {
|
|
0%, 100% { box-shadow: 0 0 0 5px rgba(99, 102, 241, 0.2); }
|
|
50% { box-shadow: 0 0 0 9px rgba(99, 102, 241, 0.35); }
|
|
}
|
|
|
|
/* === Responsive === */
|
|
@media (max-width: 600px) {
|
|
.roadmap-station { column-gap: 14px; }
|
|
.roadmap-content { padding: 8px 0 20px 0; }
|
|
strong.roadmap-content-title { font-size: 1.05em; }
|
|
}
|
|
|
|
/* === Reduced motion === */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.roadmap-station .roadmap-marker,
|
|
.roadmap-station .roadmap-content {
|
|
opacity: 1;
|
|
transform: none;
|
|
}
|
|
|
|
.roadmap-station .roadmap-connector {
|
|
transform: translateX(-50%);
|
|
}
|
|
|
|
.roadmap--animated .roadmap-station .roadmap-marker,
|
|
.roadmap--animated .roadmap-station .roadmap-content {
|
|
animation: none;
|
|
opacity: 1;
|
|
transform: none;
|
|
}
|
|
|
|
.roadmap--animated .roadmap-station .roadmap-connector {
|
|
animation: none;
|
|
transform: translateX(-50%);
|
|
}
|
|
}
|
|
</style>
|