refactor: replace PrimeVue components with custom App components for buttons, dialogs, and inputs
- Updated DangerZone.vue to use AppButton and AppDialog, replacing PrimeVue Button and Dialog components. - Refactored DomainsDns.vue to utilize AppButton, AppDialog, and AppInput, enhancing the UI consistency. - Modified NotificationSettings.vue and PlayerSettings.vue to implement AppButton and AppSwitch for better styling. - Replaced PrimeVue components in SecurityNConnected.vue with AppButton, AppDialog, and AppInput for a cohesive design. - Introduced AppConfirmHost for handling confirmation dialogs with a custom design. - Created AppToastHost for managing toast notifications with custom styling and behavior. - Added utility composables useAppConfirm and useAppToast for managing confirmation dialogs and toast notifications. - Implemented AppProgressBar and AppSwitch components for improved UI elements.
This commit is contained in:
101
src/components/app/AppToastHost.vue
Normal file
101
src/components/app/AppToastHost.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<script setup lang="ts">
|
||||
import CheckCircleIcon from '@/components/icons/CheckCircleIcon.vue';
|
||||
import InfoIcon from '@/components/icons/InfoIcon.vue';
|
||||
import AlertTriangleIcon from '@/components/icons/AlertTriangleIcon.vue';
|
||||
import XCircleIcon from '@/components/icons/XCircleIcon.vue';
|
||||
import XIcon from '@/components/icons/XIcon.vue';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { onBeforeUnmount, watchEffect } from 'vue';
|
||||
import { useAppToast, type AppToastSeverity } from '@/composables/useAppToast';
|
||||
|
||||
const { toasts, remove } = useAppToast();
|
||||
|
||||
const timers = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
const dismiss = (id: string) => {
|
||||
const timer = timers.get(id);
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timers.delete(id);
|
||||
}
|
||||
remove(id);
|
||||
};
|
||||
|
||||
const iconFor = (severity: AppToastSeverity) => {
|
||||
switch (severity) {
|
||||
case 'success':
|
||||
return CheckCircleIcon;
|
||||
case 'warn':
|
||||
return AlertTriangleIcon;
|
||||
case 'error':
|
||||
return XCircleIcon;
|
||||
case 'info':
|
||||
default:
|
||||
return InfoIcon;
|
||||
}
|
||||
};
|
||||
|
||||
const toneClass = (severity: AppToastSeverity) => {
|
||||
switch (severity) {
|
||||
case 'success':
|
||||
return 'border-success/25 bg-success/5';
|
||||
case 'warn':
|
||||
return 'border-warning/25 bg-warning/5';
|
||||
case 'error':
|
||||
return 'border-danger/25 bg-danger/5';
|
||||
case 'info':
|
||||
default:
|
||||
return 'border-info/25 bg-info/5';
|
||||
}
|
||||
};
|
||||
|
||||
watchEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
for (const t of toasts.value) {
|
||||
if (timers.has(t.id)) continue;
|
||||
const life = Math.max(0, t.life ?? 3000);
|
||||
const timer = setTimeout(() => {
|
||||
dismiss(t.id);
|
||||
}, life);
|
||||
timers.set(t.id, timer);
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
for (const timer of timers.values()) clearTimeout(timer);
|
||||
timers.clear();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="fixed top-4 right-4 z-[10000] flex flex-col gap-2 w-[360px] max-w-[calc(100vw-2rem)]">
|
||||
<TransitionGroup
|
||||
enter-active-class="transition-all duration-200 ease-out"
|
||||
enter-from-class="opacity-0 translate-y-1"
|
||||
enter-to-class="opacity-100 translate-y-0"
|
||||
leave-active-class="transition-all duration-150 ease-in"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0 translate-y-1"
|
||||
>
|
||||
<div
|
||||
v-for="t in toasts"
|
||||
:key="t.id"
|
||||
:class="cn('flex items-start gap-3 p-3 rounded-lg border shadow-sm', toneClass(t.severity))"
|
||||
>
|
||||
<component :is="iconFor(t.severity)" class="w-5 h-5 mt-0.5" :class="t.severity === 'success' ? 'text-success' : t.severity === 'warn' ? 'text-warning' : t.severity === 'error' ? 'text-danger' : 'text-info'" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-semibold text-foreground truncate">{{ t.summary }}</p>
|
||||
<p v-if="t.detail" class="text-xs text-foreground/70 mt-0.5 break-words">{{ t.detail }}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="p-1 rounded-md text-foreground/50 hover:text-foreground hover:bg-muted/50 transition-all"
|
||||
@click="dismiss(t.id)"
|
||||
aria-label="Dismiss"
|
||||
>
|
||||
<XIcon class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user