feat: add toast notification system
This commit is contained in:
36
src/App.vue
36
src/App.vue
@@ -1,38 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// Main App component - layout placeholder
|
import TitleBar from './components/TitleBar.vue'
|
||||||
|
import NavRail from './components/NavRail.vue'
|
||||||
|
import ToastNotification from './components/ToastNotification.vue'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-full w-full flex flex-col bg-background">
|
<div class="h-full w-full flex flex-col bg-bg-base">
|
||||||
<!-- TitleBar placeholder -->
|
<TitleBar />
|
||||||
<header class="h-10 flex items-center justify-between px-4 bg-surface border-b border-border" data-tauri-drag-region>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="text-amber font-semibold">ZeroClock</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<!-- Window controls would go here -->
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- TimerBar placeholder -->
|
|
||||||
<div class="h-16 flex items-center justify-center bg-surface border-b border-border">
|
|
||||||
<span class="text-text-secondary">Timer Bar Placeholder</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main content area -->
|
|
||||||
<div class="flex-1 flex overflow-hidden">
|
<div class="flex-1 flex overflow-hidden">
|
||||||
<!-- Sidebar placeholder -->
|
<NavRail />
|
||||||
<aside class="w-16 flex flex-col items-center py-4 bg-surface border-r border-border">
|
<main class="flex-1 overflow-auto">
|
||||||
<div class="text-text-secondary text-sm">Sidebar</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<!-- Main content -->
|
|
||||||
<main class="flex-1 p-6 overflow-auto">
|
|
||||||
<router-view />
|
<router-view />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ToastNotification />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
</style>
|
|
||||||
|
|||||||
28
src/components/ToastNotification.vue
Normal file
28
src/components/ToastNotification.vue
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useToastStore } from '../stores/toast'
|
||||||
|
import { Check, AlertCircle, Info } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
const toastStore = useToastStore()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="fixed top-4 left-1/2 -translate-x-1/2 z-[100] flex flex-col gap-2 pointer-events-none" style="margin-left: 24px;">
|
||||||
|
<div
|
||||||
|
v-for="toast in toastStore.toasts"
|
||||||
|
:key="toast.id"
|
||||||
|
@click="toastStore.removeToast(toast.id)"
|
||||||
|
class="w-80 flex items-center gap-3 px-4 py-3 bg-bg-surface border border-border-subtle rounded shadow-lg cursor-pointer pointer-events-auto border-l-[3px]"
|
||||||
|
:class="[
|
||||||
|
toast.exiting ? 'animate-toast-exit' : 'animate-toast-enter',
|
||||||
|
toast.type === 'success' ? 'border-l-status-running' : '',
|
||||||
|
toast.type === 'error' ? 'border-l-status-error' : '',
|
||||||
|
toast.type === 'info' ? 'border-l-accent' : ''
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<Check v-if="toast.type === 'success'" class="w-4 h-4 text-status-running shrink-0" :stroke-width="2" />
|
||||||
|
<AlertCircle v-if="toast.type === 'error'" class="w-4 h-4 text-status-error shrink-0" :stroke-width="2" />
|
||||||
|
<Info v-if="toast.type === 'info'" class="w-4 h-4 text-accent shrink-0" :stroke-width="2" />
|
||||||
|
<span class="text-sm text-text-primary">{{ toast.message }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
43
src/stores/toast.ts
Normal file
43
src/stores/toast.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
export interface Toast {
|
||||||
|
id: number
|
||||||
|
message: string
|
||||||
|
type: 'success' | 'error' | 'info'
|
||||||
|
exiting?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useToastStore = defineStore('toast', () => {
|
||||||
|
const toasts = ref<Toast[]>([])
|
||||||
|
let nextId = 0
|
||||||
|
|
||||||
|
function addToast(message: string, type: Toast['type'] = 'info') {
|
||||||
|
const id = nextId++
|
||||||
|
toasts.value.push({ id, message, type })
|
||||||
|
|
||||||
|
// Max 3 visible
|
||||||
|
if (toasts.value.length > 3) {
|
||||||
|
toasts.value.shift()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-dismiss after 3s
|
||||||
|
setTimeout(() => removeToast(id), 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeToast(id: number) {
|
||||||
|
const toast = toasts.value.find(t => t.id === id)
|
||||||
|
if (toast) {
|
||||||
|
toast.exiting = true
|
||||||
|
setTimeout(() => {
|
||||||
|
toasts.value = toasts.value.filter(t => t.id !== id)
|
||||||
|
}, 150)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function success(message: string) { addToast(message, 'success') }
|
||||||
|
function error(message: string) { addToast(message, 'error') }
|
||||||
|
function info(message: string) { addToast(message, 'info') }
|
||||||
|
|
||||||
|
return { toasts, addToast, removeToast, success, error, info }
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user