refactor(ui): replace modal dialogs with toast/sheet components

Remove DialogVant/NotifyVant and add Toast and Sheet components with
useToast, useReveal, and usePageTransition composables. Rework
Navigation, RegisterView, and styles across both apps; rebuild dist assets.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
s.esmaeili
2026-10-04 13:57:25 +03:30
co-authored by Claude Sonnet 5
parent 148b99c4a6
commit c05352d6fd
205 changed files with 3362 additions and 1701 deletions
@@ -0,0 +1,43 @@
/**
* Lets the router hold its scroll until MasterLayout's outgoing page has
* faded out.
*
* vue-router runs scrollBehavior right after the new route renders, which with
* an out-in transition is while the OLD page is still fading — so a scrolled
* page would visibly snap to the top on its way out. MasterLayout reports the
* leave here, and scrollBehavior awaits `afterPageLeave()`, so the scroll lands
* while the new page is still fully transparent.
*
* Routes in FullPageLayout have no transition: no leave is ever reported for
* them and `afterPageLeave()` resolves at once.
*/
// Never hold the scroll longer than this, even if a leave hook goes missing.
const MAX_WAIT_MS = 400
let leaving: Promise<void> | null = null
let finish: (() => void) | null = null
export function pageLeaveStarted() {
finish?.()
leaving = new Promise<void>((resolve) => {
const timer = setTimeout(done, MAX_WAIT_MS)
function done() {
clearTimeout(timer)
if (finish === done) {
finish = null
leaving = null
}
resolve()
}
finish = done
})
}
export function pageLeaveEnded() {
finish?.()
}
export function afterPageLeave(): Promise<void> {
return leaving ?? Promise.resolve()
}
@@ -0,0 +1,43 @@
import { nextTick, ref } from 'vue'
/**
* First-paint entrance shared by the tab screens (Home, Dashboard, Cart):
* once a screen's first data load lands, its sections rise and fade in one
* after another. The styles are the global `.reveal` / `.is-revealed` pair in
* assets/styles/style.less.
*
* MasterLayout renders its routes without keep-alive, so a screen remounts on
* every return to its tab. Which screens have already played is kept here in
* module scope, so the entrance is a first-run flourish that resets only on a
* full reload — replaying it on each tab switch would fight the page
* transition rather than add to it. (A `let` at the top of <script setup>
* would not do this: that code runs once per mount.)
*/
const played = new Set<string>()
export function useReveal(key: string, stepMs = 70) {
const revealed = ref(played.has(key))
// Each item starts a beat after the one before it. Kept small, so the whole
// sequence finishes well under a second and reads as the page arriving.
function revealDelay(i: number) {
return { '--reveal-delay': `${i * stepMs}ms` }
}
// Call once the first load has populated the DOM. Waits for the new nodes
// to be mounted, then for one painted frame in their "before" state — with
// a single frame the browser can resolve both states in the same style pass
// and there is nothing to animate.
async function reveal() {
if (revealed.value) return
await nextTick()
requestAnimationFrame(() =>
requestAnimationFrame(() => {
revealed.value = true
played.add(key)
}),
)
}
return { revealed, revealDelay, reveal }
}
+72
View File
@@ -0,0 +1,72 @@
import { reactive } from 'vue'
/**
* App-wide toasts. One shared queue, rendered by <ToastHost> (mounted once in
* App.vue), so any view or store can call `toast.success(...)` without placing
* a component in its own template.
*/
export type ToastType = 'success' | 'error' | 'info' | 'warning'
export interface ToastOptions {
title?: string
/** ms before it closes by itself; 0 keeps it until the user dismisses it. */
duration?: number
/** Custom icon name; defaults to one per type. */
icon?: string
}
export interface ToastItem {
id: number
type: ToastType
message: string
title?: string
icon: string
duration: number
}
const DEFAULT_DURATION = 3200
// Older toasts beyond this are dropped so the stack never covers the screen.
const MAX_VISIBLE = 3
const ICONS: Record<ToastType, string> = {
success: 'vuesax-linear:tick-circle',
error: 'vuesax-linear:close-circle',
info: 'vuesax-linear:info-circle',
warning: 'vuesax-linear:warning-2',
}
const state = reactive<{ items: ToastItem[] }>({ items: [] })
let nextId = 1
function show(type: ToastType, message: string, options: ToastOptions = {}): number {
const id = nextId++
state.items.push({
id,
type,
message,
title: options.title,
icon: options.icon ?? ICONS[type],
duration: options.duration ?? DEFAULT_DURATION,
})
if (state.items.length > MAX_VISIBLE) state.items.splice(0, state.items.length - MAX_VISIBLE)
return id
}
function dismiss(id: number) {
const i = state.items.findIndex((t) => t.id === id)
if (i !== -1) state.items.splice(i, 1)
}
export const toast = {
show,
dismiss,
success: (message: string, options?: ToastOptions) => show('success', message, options),
error: (message: string, options?: ToastOptions) => show('error', message, options),
info: (message: string, options?: ToastOptions) => show('info', message, options),
warning: (message: string, options?: ToastOptions) => show('warning', message, options),
}
export function useToast() {
return { toasts: state.items, ...toast }
}