افزودن سیستم Motion به تمام بخشهای رابط کاربری
واژگان مشترک حرکت در globals.css: شش @keyframes و شش کلاس .anim-* بههمراه .anim-stagger برای ورود آبشاری. همهٔ مدتزمانها پشت custom property هستند تا یک override کل اپ را تنظیم کند. - packages/ui: Button، Card، Stat، Loading/Empty/ErrorState، UsageBar، Timeline، Modal، Drawer، Table، ToastStack و ArtifactViewer - ریشهٔ هر صفحهٔ (app) با .anim-stagger وارد میشود - AppShell: محتوا fade، منوی کنار stagger، منوهای کشویی slide-up - prefers-reduced-motion مدتزمانها را به ۰.۰۱ms میرساند تا وضعیت پایانی حفظ شود - قرارداد motion در src/test/motion.test.tsx تست میشود تا بازنویسی بعدی آن را بیصدا حذف نکند - تستهای فرانتاند: ۳۵ ← ۴۵
This commit is contained in:
+22
-19
@@ -55,7 +55,9 @@ export function Button({
|
||||
{...rest}
|
||||
disabled={disabled || loading}
|
||||
className={cx(
|
||||
"inline-flex items-center justify-center gap-2 rounded-lg border font-medium transition-colors",
|
||||
"inline-flex items-center justify-center gap-2 rounded-lg border font-medium",
|
||||
"transition-[color,background-color,border-color,transform] duration-150",
|
||||
"active:scale-[0.97]",
|
||||
"focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/60",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
BUTTON_VARIANTS[variant],
|
||||
@@ -88,6 +90,7 @@ export function Card({
|
||||
{...rest}
|
||||
className={cx(
|
||||
"rounded-xl border border-surface-border bg-surface-raised p-5",
|
||||
"transition-colors duration-200",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
@@ -148,7 +151,7 @@ export function Stat({
|
||||
tone?: Tone;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-xl border border-surface-border bg-surface-raised p-4">
|
||||
<div className="anim-rise rounded-xl border border-surface-border bg-surface-raised p-4">
|
||||
<p className="text-xs text-slate-400">{label}</p>
|
||||
<p
|
||||
className={cx(
|
||||
@@ -184,7 +187,7 @@ export function Field({ label, hint, error, className, id, ...rest }: FieldProps
|
||||
id={inputId}
|
||||
{...rest}
|
||||
className={cx(
|
||||
"h-10 rounded-lg border bg-surface-overlay px-3 text-sm text-slate-100",
|
||||
"h-10 rounded-lg border bg-surface-overlay px-3 text-sm text-slate-100 transition-colors duration-150",
|
||||
"placeholder:text-slate-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/60",
|
||||
error ? "border-danger" : "border-surface-border",
|
||||
className,
|
||||
@@ -219,7 +222,7 @@ export function Select({ label, hint, error, options, className, id, ...rest }:
|
||||
id={selectId}
|
||||
{...rest}
|
||||
className={cx(
|
||||
"h-10 rounded-lg border bg-surface-overlay px-3 text-sm text-slate-100",
|
||||
"h-10 rounded-lg border bg-surface-overlay px-3 text-sm text-slate-100 transition-colors duration-150",
|
||||
"focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/60",
|
||||
error ? "border-danger" : "border-surface-border",
|
||||
className,
|
||||
@@ -266,7 +269,7 @@ export function Checkbox({
|
||||
|
||||
export function LoadingState({ label = "در حال بارگذاری…" }: { label?: string }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-3" role="status" aria-live="polite">
|
||||
<div className="anim-fade-in flex flex-col gap-3" role="status" aria-live="polite">
|
||||
<p className="text-xs text-slate-400">{label}</p>
|
||||
<div className="h-4 w-1/3 animate-pulse rounded bg-surface-overlay" />
|
||||
<div className="h-4 w-2/3 animate-pulse rounded bg-surface-overlay" />
|
||||
@@ -285,7 +288,7 @@ export function EmptyState({
|
||||
action?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-3 rounded-xl border border-dashed border-surface-border bg-surface-raised/50 px-6 py-12 text-center">
|
||||
<div className="anim-rise flex flex-col items-center justify-center gap-3 rounded-xl border border-dashed border-surface-border bg-surface-raised/50 px-6 py-12 text-center">
|
||||
<p className="text-sm font-medium text-slate-200">{title}</p>
|
||||
{description && <p className="max-w-md text-xs text-slate-400">{description}</p>}
|
||||
{action}
|
||||
@@ -305,7 +308,7 @@ export function ErrorState({
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
className="flex flex-col items-start gap-3 rounded-xl border border-danger/40 bg-danger/5 px-5 py-4"
|
||||
className="anim-rise flex flex-col items-start gap-3 rounded-xl border border-danger/40 bg-danger/5 px-5 py-4"
|
||||
>
|
||||
<p className="text-sm font-medium text-danger">{title}</p>
|
||||
{message && <p className="text-xs text-slate-300">{message}</p>}
|
||||
@@ -343,7 +346,7 @@ export function UsageBar({
|
||||
<div className="h-2 w-full overflow-hidden rounded-full bg-surface-overlay">
|
||||
<div
|
||||
className={cx(
|
||||
"h-full rounded-full",
|
||||
"h-full rounded-full transition-[width] duration-500 ease-out",
|
||||
pct > 90 ? "bg-danger" : pct > 70 ? "bg-warning" : "bg-accent",
|
||||
)}
|
||||
style={{ width: `${pct}%` }}
|
||||
@@ -365,7 +368,7 @@ export function Timeline({ items }: { items: TimelineItem[] }) {
|
||||
return <p className="text-xs text-slate-500">رویدادی ثبت نشده است.</p>;
|
||||
}
|
||||
return (
|
||||
<ol className="flex flex-col gap-3">
|
||||
<ol className="anim-stagger flex flex-col gap-3">
|
||||
{items.map((item) => (
|
||||
<li key={item.id} className="flex gap-3">
|
||||
<span
|
||||
@@ -485,9 +488,9 @@ export function ConfirmDialog({
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={title}
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4"
|
||||
className="anim-fade-in fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4"
|
||||
>
|
||||
<div className="w-full max-w-md rounded-xl border border-surface-border bg-surface-raised p-5">
|
||||
<div className="anim-scale-in w-full max-w-md rounded-xl border border-surface-border bg-surface-raised p-5">
|
||||
<p className="text-sm font-medium text-slate-100">{title}</p>
|
||||
{description && <p className="mt-2 text-xs text-slate-400">{description}</p>}
|
||||
<div className="mt-4 flex justify-end gap-2">
|
||||
@@ -536,14 +539,14 @@ export function Modal({
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={title}
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4"
|
||||
className="anim-fade-in fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
// Clicks inside must not bubble up and close the dialog.
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
className={cx(
|
||||
"w-full rounded-xl border border-surface-border bg-surface-raised p-5",
|
||||
"anim-scale-in w-full rounded-xl border border-surface-border bg-surface-raised p-5",
|
||||
MODAL_SIZES[size],
|
||||
)}
|
||||
>
|
||||
@@ -583,13 +586,13 @@ export function Drawer({
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={title}
|
||||
className="fixed inset-0 z-50 flex bg-black/60"
|
||||
className="anim-fade-in fixed inset-0 z-50 flex bg-black/60"
|
||||
onClick={onClose}
|
||||
>
|
||||
<aside
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
className={cx(
|
||||
"flex h-full w-full max-w-md flex-col border-surface-border bg-surface-raised",
|
||||
"anim-drawer-start flex h-full w-full max-w-md flex-col border-surface-border bg-surface-raised",
|
||||
side === "start" ? "border-l" : "border-r",
|
||||
)}
|
||||
>
|
||||
@@ -658,7 +661,7 @@ export function Table<T>({
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="overflow-x-auto rounded-xl border border-surface-border">
|
||||
<div className="anim-fade-in overflow-x-auto rounded-xl border border-surface-border">
|
||||
<table className="w-full border-collapse text-sm">
|
||||
{caption && <caption className="sr-only">{caption}</caption>}
|
||||
<thead>
|
||||
@@ -677,7 +680,7 @@ export function Table<T>({
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody className="anim-stagger">
|
||||
{rows.map((row) => (
|
||||
<tr key={rowKey(row)} className="border-t border-surface-border">
|
||||
{columns.map((column) => (
|
||||
@@ -775,7 +778,7 @@ export function ToastStack({
|
||||
key={toast.id}
|
||||
role="status"
|
||||
className={cx(
|
||||
"pointer-events-auto flex items-start justify-between gap-3 rounded-lg border px-4 py-3",
|
||||
"anim-slide-up pointer-events-auto flex items-start justify-between gap-3 rounded-lg border px-4 py-3",
|
||||
TONES[toast.tone ?? "neutral"],
|
||||
)}
|
||||
>
|
||||
@@ -817,7 +820,7 @@ export function ArtifactViewer({
|
||||
createdAt?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 rounded-xl border border-surface-border bg-surface-raised">
|
||||
<div className="anim-fade-in flex flex-col gap-2 rounded-xl border border-surface-border bg-surface-raised">
|
||||
<div className="flex items-center justify-between gap-3 border-b border-surface-border px-4 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-medium text-slate-200">{name}</span>
|
||||
|
||||
Reference in New Issue
Block a user