feat: Dashboard-Enhancements – Filter, Checkboxen, Animationen
CI - Build & Test / Backend (.NET) (push) Successful in 25s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 17s
CI - Build & Test / Security Check (push) Successful in 3s

- IrisPanel: Task-Zahlen aus OperationsStore, Suggestions als Array, Chat via console.log
- OperationsFeed: Filter-Pills filtern Feed-Items, TransitionGroup-Animationen
- AgendaPanel: Checkboxen mit localStorage-Persistenz (nexus-agenda-done)
- ActiveInitiatives: Cards hover-scale, Klick-Handler, Progress-Bars animiert
- RecentlyFinished: Chips klickbar + a11y (role, tabindex, keyup)
- DashboardView: fade-in Animation, Custom-Scrollbar
- VERSION: 0.1.0 initial
- Deploy-Workflow: Version-Bump-Semantik (major=x.0.0, minor=1.x.0, patch=1.0.x)
This commit is contained in:
2026-06-09 19:51:25 +02:00
parent 635e43457d
commit 13d4c2f157
6 changed files with 171 additions and 39 deletions
@@ -27,6 +27,10 @@ const statusMeta: Record<InitiativeStatus, { label: string; color: string; bg: s
paused: { label: 'Paused', color: '#6b7280', bg: 'rgba(107,114,128,0.1)' }, paused: { label: 'Paused', color: '#6b7280', bg: 'rgba(107,114,128,0.1)' },
completed: { label: 'Completed', color: '#3b82f6', bg: 'rgba(59,130,246,0.1)' }, completed: { label: 'Completed', color: '#3b82f6', bg: 'rgba(59,130,246,0.1)' },
} }
function onInitiativeClick(title: string) {
console.log('[Dashboard] Open initiative:', title)
}
</script> </script>
<template> <template>
@@ -37,6 +41,10 @@ const statusMeta: Record<InitiativeStatus, { label: string; color: string; bg: s
v-for="(init, idx) in initiatives" v-for="(init, idx) in initiatives"
:key="idx" :key="idx"
:class="['initiative-card', 'status-' + init.status]" :class="['initiative-card', 'status-' + init.status]"
@click="onInitiativeClick(init.title)"
role="button"
tabindex="0"
@keyup.enter="onInitiativeClick(init.title)"
> >
<div class="init-head"> <div class="init-head">
<h3>{{ init.title }}</h3> <h3>{{ init.title }}</h3>
@@ -103,11 +111,17 @@ const statusMeta: Record<InitiativeStatus, { label: string; color: string; bg: s
border: 1px solid rgba(139, 124, 246, 0.08); border: 1px solid rgba(139, 124, 246, 0.08);
border-radius: 14px; border-radius: 14px;
padding: 14px; padding: 14px;
cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.initiative-card:hover { .initiative-card:hover {
transform: scale(1.02);
border-color: rgba(139, 124, 246, 0.2); border-color: rgba(139, 124, 246, 0.2);
box-shadow: 0 2px 12px rgba(0,0,0,0.2); box-shadow: 0 8px 24px rgba(0,0,0,0.3);
}
.initiative-card:focus-visible {
outline: 2px solid #a78bfa;
outline-offset: 2px;
} }
.init-head { .init-head {
display: flex; display: flex;
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, onMounted } from 'vue'
import { CheckCircle2, Circle, AlertTriangle } from '@lucide/vue' import { CheckCircle2, Circle, AlertTriangle } from '@lucide/vue'
interface AgendaItem { interface AgendaItem {
@@ -9,6 +9,8 @@ interface AgendaItem {
overdue?: boolean overdue?: boolean
} }
const STORAGE_KEY = 'nexus-agenda-done'
const agendaToday = ref<AgendaItem[]>([ const agendaToday = ref<AgendaItem[]>([
{ text: 'Teammeeting', time: '14:00' }, { text: 'Teammeeting', time: '14:00' },
{ text: 'Deutsch lernen', time: '18:00' }, { text: 'Deutsch lernen', time: '18:00' },
@@ -25,9 +27,48 @@ const agendaOverdue = ref<AgendaItem[]>([
{ text: 'Hangfire konfigurieren', overdue: true }, { text: 'Hangfire konfigurieren', overdue: true },
]) ])
function loadDoneStates() {
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (!raw) return
const keys: string[] = JSON.parse(raw)
const set = new Set(keys)
const sections = [
{ items: agendaToday.value, prefix: 'today' },
{ items: agendaTomorrow.value, prefix: 'tomorrow' },
{ items: agendaOverdue.value, prefix: 'overdue' },
]
for (const { items, prefix } of sections) {
items.forEach((item, i) => {
if (set.has(`${prefix}-${i}`)) item.done = true
})
}
} catch { /* ignore malformed storage */ }
}
function saveDoneStates() {
const keys: string[] = []
const sections = [
{ items: agendaToday.value, prefix: 'today' },
{ items: agendaTomorrow.value, prefix: 'tomorrow' },
{ items: agendaOverdue.value, prefix: 'overdue' },
]
for (const { items, prefix } of sections) {
items.forEach((item, i) => {
if (item.done) keys.push(`${prefix}-${i}`)
})
}
localStorage.setItem(STORAGE_KEY, JSON.stringify(keys))
}
function toggleAgendaItem(item: AgendaItem) { function toggleAgendaItem(item: AgendaItem) {
item.done = !item.done item.done = !item.done
saveDoneStates()
} }
onMounted(() => {
loadDoneStates()
})
</script> </script>
<template> <template>
+35 -27
View File
@@ -1,22 +1,38 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { computed, ref } from 'vue'
import { Bot, Sparkles, MessageSquareText, ListTodo, Zap, FileText, Send, Lightbulb } from '@lucide/vue' import { Bot, Sparkles, MessageSquareText, ListTodo, Zap, FileText, Send, Lightbulb } from '@lucide/vue'
import { useTime } from '../../composables/useTime' import { useTime } from '../../composables/useTime'
import { useOperationsStore } from '../../stores/operations'
const { greeting } = useTime() interface Suggestion {
text: string
const chatInput = ref('')
function sendChat() {
if (!chatInput.value.trim()) return
alert(`[Iris] Received: "${chatInput.value}"`)
chatInput.value = ''
} }
const meters = { const { greeting } = useTime()
openTasks: 12, const store = useOperationsStore()
blocked: 3,
overdue: 2, const chatInput = ref('')
todayAppointments: 4,
const meters = computed(() => {
const tasks = store.snapshot.tasks
return {
openTasks: store.snapshot.metrics.queuedTasks,
blocked: store.snapshot.metrics.incidents,
overdue: tasks.filter(t => t.state === 'Blocked').length,
todayAppointments: tasks.filter(t => t.state === 'In progress').length,
}
})
const suggestions = ref<Suggestion[]>([
{ text: 'Du solltest zuerst das Dungeon-System abschließen.' },
{ text: 'Die Dokumentation wurde seit 3 Tagen nicht aktualisiert.' },
{ text: 'Das Projekt OpenClaw benötigt Aufmerksamkeit.' },
])
function sendChat() {
if (!chatInput.value.trim()) return
console.log('[Iris] Chat received:', chatInput.value)
chatInput.value = ''
} }
</script> </script>
@@ -56,21 +72,13 @@ const meters = {
<div class="suggestions"> <div class="suggestions">
<h3><Sparkles :size="14" /> Vorschläge</h3> <h3><Sparkles :size="14" /> Vorschläge</h3>
<div class="suggestion-card"> <div
v-for="(s, idx) in suggestions"
:key="idx"
class="suggestion-card"
>
<Lightbulb :size="14" class="bulb" /> <Lightbulb :size="14" class="bulb" />
<span>Du solltest zuerst das Dungeon-System abschließen.</span> <span>{{ s.text }}</span>
</div>
<div class="suggestion-card">
<Lightbulb :size="14" class="bulb" />
<span>Die Dokumentation wurde seit 3 Tagen nicht aktualisiert.</span>
</div>
<div class="suggestion-card">
<Lightbulb :size="14" class="bulb" />
<span>Das Projekt OpenClaw benötigt Aufmerksamkeit.</span>
</div>
<div class="suggestion-card">
<Lightbulb :size="14" class="bulb" />
<span>Deine wöchentliche Zusammenfassung ist bereit.</span>
</div> </div>
</div> </div>
@@ -81,15 +81,17 @@ const statusColor = (s: FeedStatus): string => {
<div class="feed-list"> <div class="feed-list">
<template v-for="group in feedGroups" :key="group.date"> <template v-for="group in feedGroups" :key="group.date">
<div class="feed-date-heading">{{ group.date }}</div> <div class="feed-date-heading">{{ group.date }}</div>
<TransitionGroup name="feed-item" tag="div" class="feed-group-items">
<div <div
v-for="(item, idx) in group.items" v-for="(item, idx) in group.items"
:key="idx" :key="group.date + '-' + idx"
class="feed-item" class="feed-item"
> >
<span class="feed-time">{{ item.time }}</span> <span class="feed-time">{{ item.time }}</span>
<span class="feed-dot" :style="{ background: statusColor(item.status) }"></span> <span class="feed-dot" :style="{ background: statusColor(item.status) }"></span>
<span class="feed-text">{{ item.text }}</span> <span class="feed-text">{{ item.text }}</span>
</div> </div>
</TransitionGroup>
</template> </template>
</div> </div>
</div> </div>
@@ -163,6 +165,9 @@ const statusColor = (s: FeedStatus): string => {
overflow-y: auto; overflow-y: auto;
flex: 1; flex: 1;
} }
.feed-group-items {
display: contents;
}
.feed-date-heading { .feed-date-heading {
font-size: 9px; font-size: 9px;
font-weight: 700; font-weight: 700;
@@ -202,6 +207,25 @@ const statusColor = (s: FeedStatus): string => {
color: #7e8799; color: #7e8799;
} }
/* TransitionGroup animations */
.feed-item-enter-active {
transition: all 0.3s ease;
}
.feed-item-leave-active {
transition: all 0.3s ease;
}
.feed-item-enter-from {
opacity: 0;
transform: translateX(-12px);
}
.feed-item-leave-to {
opacity: 0;
transform: translateX(12px);
}
.feed-item-move {
transition: all 0.3s ease;
}
@media (max-width: 900px) { @media (max-width: 900px) {
.feed-panel { .feed-panel {
order: 2; order: 2;
@@ -9,13 +9,25 @@ const recentlyFinished = ref([
'TeamView deployt', 'TeamView deployt',
'Config-Editor live', 'Config-Editor live',
]) ])
function onChipClick(item: string) {
console.log('[Dashboard] Recently finished:', item)
}
</script> </script>
<template> <template>
<div class="finished-section"> <div class="finished-section">
<h3>Recently Finished</h3> <h3>Recently Finished</h3>
<div class="finished-scroll"> <div class="finished-scroll">
<span v-for="(item, idx) in recentlyFinished" :key="idx" class="finished-chip"> <span
v-for="(item, idx) in recentlyFinished"
:key="idx"
class="finished-chip"
role="button"
tabindex="0"
@click="onChipClick(item)"
@keyup.enter="onChipClick(item)"
>
{{ item }} {{ item }}
</span> </span>
</div> </div>
@@ -66,6 +78,10 @@ const recentlyFinished = ref([
border-color: rgba(139, 124, 246, 0.2); border-color: rgba(139, 124, 246, 0.2);
color: #e8eaf0; color: #e8eaf0;
} }
.finished-chip:focus-visible {
outline: 2px solid #a78bfa;
outline-offset: 2px;
}
@media (max-width: 900px) { @media (max-width: 900px) {
.finished-section { .finished-section {
+29
View File
@@ -49,6 +49,35 @@ import RecentlyFinished from '../components/dashboard/RecentlyFinished.vue'
margin: 0 auto; margin: 0 auto;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
color: var(--text-primary); color: var(--text-primary);
animation: dashboard-fade-in 0.4s ease-out;
}
@keyframes dashboard-fade-in {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Custom scrollbar */
.dashboard ::-webkit-scrollbar {
width: 5px;
height: 5px;
}
.dashboard ::-webkit-scrollbar-track {
background: transparent;
}
.dashboard ::-webkit-scrollbar-thumb {
background: rgba(139, 124, 246, 0.2);
border-radius: 3px;
}
.dashboard ::-webkit-scrollbar-thumb:hover {
background: rgba(139, 124, 246, 0.35);
} }
.topbar { .topbar {