166 lines
3.4 KiB
Vue
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>
|