feat: Dashboard-Enhancements – Filter, Checkboxen, Animationen
- 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:
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user