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)' },
|
||||
completed: { label: 'Completed', color: '#3b82f6', bg: 'rgba(59,130,246,0.1)' },
|
||||
}
|
||||
|
||||
function onInitiativeClick(title: string) {
|
||||
console.log('[Dashboard] Open initiative:', title)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -37,6 +41,10 @@ const statusMeta: Record<InitiativeStatus, { label: string; color: string; bg: s
|
||||
v-for="(init, idx) in initiatives"
|
||||
:key="idx"
|
||||
:class="['initiative-card', 'status-' + init.status]"
|
||||
@click="onInitiativeClick(init.title)"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@keyup.enter="onInitiativeClick(init.title)"
|
||||
>
|
||||
<div class="init-head">
|
||||
<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-radius: 14px;
|
||||
padding: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.initiative-card:hover {
|
||||
transform: scale(1.02);
|
||||
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 {
|
||||
display: flex;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { CheckCircle2, Circle, AlertTriangle } from '@lucide/vue'
|
||||
|
||||
interface AgendaItem {
|
||||
@@ -9,6 +9,8 @@ interface AgendaItem {
|
||||
overdue?: boolean
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'nexus-agenda-done'
|
||||
|
||||
const agendaToday = ref<AgendaItem[]>([
|
||||
{ text: 'Teammeeting', time: '14:00' },
|
||||
{ text: 'Deutsch lernen', time: '18:00' },
|
||||
@@ -25,9 +27,48 @@ const agendaOverdue = ref<AgendaItem[]>([
|
||||
{ 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) {
|
||||
item.done = !item.done
|
||||
saveDoneStates()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadDoneStates()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,22 +1,38 @@
|
||||
<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 { useTime } from '../../composables/useTime'
|
||||
import { useOperationsStore } from '../../stores/operations'
|
||||
|
||||
const { greeting } = useTime()
|
||||
|
||||
const chatInput = ref('')
|
||||
function sendChat() {
|
||||
if (!chatInput.value.trim()) return
|
||||
alert(`[Iris] Received: "${chatInput.value}"`)
|
||||
chatInput.value = ''
|
||||
interface Suggestion {
|
||||
text: string
|
||||
}
|
||||
|
||||
const meters = {
|
||||
openTasks: 12,
|
||||
blocked: 3,
|
||||
overdue: 2,
|
||||
todayAppointments: 4,
|
||||
const { greeting } = useTime()
|
||||
const store = useOperationsStore()
|
||||
|
||||
const chatInput = ref('')
|
||||
|
||||
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>
|
||||
|
||||
@@ -56,21 +72,13 @@ const meters = {
|
||||
|
||||
<div class="suggestions">
|
||||
<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" />
|
||||
<span>Du solltest zuerst das Dungeon-System abschließen.</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>
|
||||
<span>{{ s.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -81,15 +81,17 @@ const statusColor = (s: FeedStatus): string => {
|
||||
<div class="feed-list">
|
||||
<template v-for="group in feedGroups" :key="group.date">
|
||||
<div class="feed-date-heading">{{ group.date }}</div>
|
||||
<TransitionGroup name="feed-item" tag="div" class="feed-group-items">
|
||||
<div
|
||||
v-for="(item, idx) in group.items"
|
||||
:key="idx"
|
||||
:key="group.date + '-' + idx"
|
||||
class="feed-item"
|
||||
>
|
||||
<span class="feed-time">{{ item.time }}</span>
|
||||
<span class="feed-dot" :style="{ background: statusColor(item.status) }"></span>
|
||||
<span class="feed-text">{{ item.text }}</span>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -163,6 +165,9 @@ const statusColor = (s: FeedStatus): string => {
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
.feed-group-items {
|
||||
display: contents;
|
||||
}
|
||||
.feed-date-heading {
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
@@ -202,6 +207,25 @@ const statusColor = (s: FeedStatus): string => {
|
||||
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) {
|
||||
.feed-panel {
|
||||
order: 2;
|
||||
|
||||
@@ -9,13 +9,25 @@ const recentlyFinished = ref([
|
||||
'TeamView deployt',
|
||||
'Config-Editor live',
|
||||
])
|
||||
|
||||
function onChipClick(item: string) {
|
||||
console.log('[Dashboard] Recently finished:', item)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="finished-section">
|
||||
<h3>Recently Finished</h3>
|
||||
<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 }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -66,6 +78,10 @@ const recentlyFinished = ref([
|
||||
border-color: rgba(139, 124, 246, 0.2);
|
||||
color: #e8eaf0;
|
||||
}
|
||||
.finished-chip:focus-visible {
|
||||
outline: 2px solid #a78bfa;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.finished-section {
|
||||
|
||||
@@ -49,6 +49,35 @@ import RecentlyFinished from '../components/dashboard/RecentlyFinished.vue'
|
||||
margin: 0 auto;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user