Files
nexus/frontend/src/components/dashboard/v2/TaskStrip.vue
T
reviewer 0f8939306d
CI - Build & Test / Backend (.NET) (push) Successful in 26s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 16s
CI - Build & Test / Security Check (push) Successful in 3s
feat: mobile-responsive dashboard v2
2026-06-14 12:16:06 +02:00

166 lines
3.4 KiB
Vue

<script setup lang="ts">
import type { TaskItem } from './types'
defineProps<{
tasks: TaskItem[]
loading?: boolean
error?: string | null
}>()
function prioLabel(p: TaskItem['priority']): string {
return p === 'high' ? 'P0' : p === 'medium' ? 'P1' : 'P2'
}
function prioColor(p: TaskItem['priority']): string {
return p === 'high' ? '#fda4b0' : p === 'medium' ? '#fcd34d' : '#9db6ff'
}
function dotClass(s: TaskItem['status']): string {
return s === 'active' ? 'work' : s === 'blocked' ? 'block' : 'queue'
}
function statusLabel(s: TaskItem['status']): string {
return s === 'active' ? 'Läuft' : s === 'blocked' ? 'Blocker' : 'Queue'
}
</script>
<template>
<div class="tstrip">
<template v-if="loading">
<div v-for="n in 4" :key="n" class="tcard skeleton"></div>
</template>
<div v-else-if="error" class="tstrip-msg"> {{ error }}</div>
<div v-else-if="!tasks.length" class="tstrip-msg">Keine aktiven Tasks</div>
<template v-else>
<div
v-for="task in tasks.slice(0, 4)"
:key="task.id"
class="tcard"
:class="{ block: task.status === 'blocked' }"
>
<div class="tcard-row">
<span class="pr" :style="{ background: 'rgba(124,108,255,.14)', color: prioColor(task.priority) }">
{{ prioLabel(task.priority) }}
</span>
<span class="dot" :class="dotClass(task.status)"></span>
<span class="stl">{{ statusLabel(task.status) }}</span>
</div>
<div class="tt">{{ task.title }}</div>
<div class="ow">{{ task.agent }}</div>
</div>
</template>
</div>
</template>
<style scoped>
.tstrip {
display: flex;
gap: 10px;
overflow: hidden;
flex: 0 0 auto;
}
.tcard {
flex: 1;
min-width: 0;
padding: 11px 13px;
border-radius: 12px;
background: var(--glass);
border: 1px solid var(--line);
display: flex;
flex-direction: column;
}
.tcard.block {
border-color: rgba(251,113,133,.35);
background: rgba(251,113,133,.07);
}
.tcard-row {
display: flex;
align-items: center;
gap: 7px;
}
.pr {
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
font-weight: 600;
padding: 1px 6px;
border-radius: 5px;
flex: 0 0 auto;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex: 0 0 auto;
}
.dot.work { background: var(--st-work); animation: pulse-work 1.8s infinite; }
.dot.queue { background: var(--st-queue); }
.dot.block { background: var(--st-block); animation: pulse-block 1.4s infinite; }
.dot.idle { background: var(--st-idle); }
.stl {
margin-left: auto;
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
color: var(--tx-3);
}
.tt {
font-size: 12px;
font-weight: 600;
margin-top: 7px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: var(--tx);
}
.ow {
font-size: 10.5px;
color: var(--tx-3);
margin-top: 5px;
}
.skeleton {
height: 78px;
background: var(--glass);
animation: skeleton-pulse 1.5s ease-in-out infinite;
}
@keyframes skeleton-pulse {
0%, 100% { opacity: 0.5; }
50% { opacity: 0.8; }
}
.tstrip-msg {
font-family: 'Manrope', sans-serif;
font-size: 11px;
color: var(--tx-3);
padding: 12px;
white-space: nowrap;
}
@media (max-width: 767px) {
.tstrip {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.tstrip::-webkit-scrollbar {
display: none;
}
.tcard {
flex: 0 0 200px;
}
}
</style>