feat(frontend): micro-interactions — page-enter transition + MetricTile lift

- Add animate-fade-up utility (reduced-motion aware) and a PageTransition
  wrapper; every dashboard view now rises + settles on mount/route change.
- MetricTile gains a subtle hover lift (-translate-y) + ring-focus glow.
- Theme toggle and command palette (⌘K) already existed and persist; no-op.
This commit is contained in:
asepharyana
2026-08-18 09:50:52 +07:00
parent d78d7a0181
commit 95f2903067
11 changed files with 72 additions and 12 deletions
@@ -1,7 +1,12 @@
import { PageTransition } from "@/components/shared";
import { AnalysisView } from "./view";
export const dynamic = "force-dynamic";
export default function AnalysisPage() {
return <AnalysisView />;
return (
<PageTransition>
<AnalysisView />
</PageTransition>
);
}
@@ -1,3 +1,4 @@
import { PageTransition } from "@/components/shared";
import { getActivity, getDashboardStats } from "@/lib/api/server";
import { DashboardView } from "./view";
@@ -14,5 +15,9 @@ export default async function DashboardPage() {
} catch {
// Backend unavailable — client hooks will surface the error state.
}
return <DashboardView initialStats={stats} initialActivity={activity} />;
return (
<PageTransition>
<DashboardView initialStats={stats} initialActivity={activity} />
</PageTransition>
);
}
@@ -1,3 +1,4 @@
import { PageTransition } from "@/components/shared";
import { getMediaStatus } from "@/lib/api/server";
import { MediaView } from "./view";
@@ -10,5 +11,9 @@ export default async function MediaPage() {
} catch {
/* client hooks surface errors */
}
return <MediaView initialStatus={status} />;
return (
<PageTransition>
<MediaView initialStatus={status} />
</PageTransition>
);
}
@@ -1,3 +1,4 @@
import { PageTransition } from "@/components/shared";
import { getConfig, getGuilds } from "@/lib/api/server";
import { MessagesView } from "./view";
@@ -12,9 +13,11 @@ export default async function MessagesPage() {
/* client hooks surface errors */
}
return (
<MessagesView
initialGuilds={guilds}
initialGuildId={config?.monitorGuildId ?? null}
/>
<PageTransition>
<MessagesView
initialGuilds={guilds}
initialGuildId={config?.monitorGuildId ?? null}
/>
</PageTransition>
);
}
@@ -1,3 +1,4 @@
import { PageTransition } from "@/components/shared";
import { getModerationActions, getModerationStats } from "@/lib/api/server";
import { ModerationView } from "./view";
@@ -14,5 +15,9 @@ export default async function ModerationPage() {
} catch {
/* client hooks surface errors */
}
return <ModerationView initialStats={stats} initialActions={actions} />;
return (
<PageTransition>
<ModerationView initialStats={stats} initialActions={actions} />
</PageTransition>
);
}
@@ -1,3 +1,4 @@
import { PageTransition } from "@/components/shared";
import { getRecordings } from "@/lib/api/server";
import { RecordingsView } from "./view";
@@ -12,5 +13,9 @@ export default async function RecordingsPage() {
} catch {
/* client hooks surface errors */
}
return <RecordingsView initialItems={recordings?.items} />;
return (
<PageTransition>
<RecordingsView initialItems={recordings?.items} />
</PageTransition>
);
}
@@ -1,3 +1,4 @@
import { PageTransition } from "@/components/shared";
import { getGuilds, getVoiceStatus } from "@/lib/api/server";
import { VoiceView } from "./view";
@@ -11,5 +12,9 @@ export default async function VoicePage() {
} catch {
/* client hooks surface errors */
}
return <VoiceView initialStatus={status} initialGuilds={guilds} />;
return (
<PageTransition>
<VoiceView initialStatus={status} initialGuilds={guilds} />
</PageTransition>
);
}
+7 -1
View File
@@ -276,6 +276,11 @@
}
.animate-breathe { animation: breathe 3.5s ease-in-out infinite; }
/* Page-enter: subtle rise+settle. Respect reduced-motion (handled globally). */
.animate-fade-up {
animation: fade-up 0.4s cubic-bezier(0.22, 1, 0.36, 1) both;
}
@media (prefers-reduced-motion: reduce) {
.scan-line::after,
.scan-line,
@@ -283,7 +288,8 @@
.animate-spin-disc,
.animate-pulse-ring,
.animate-shimmer,
.animate-breathe {
.animate-breathe,
.animate-fade-up {
animation: none !important;
}
.scan-line {
@@ -1,5 +1,6 @@
export { GuildChannelPicker } from "./guild-picker";
export { MarkdownLite } from "./markdown";
export { PageTransition } from "./page-transition";
export { MetricTile, SectionHeader } from "./section";
export {
EmptyState,
@@ -0,0 +1,15 @@
import { cn } from "@/lib/utils";
/**
* Wraps a view's root so the whole panel stack rises + settles on mount.
* The animation is disabled under prefers-reduced-motion via globals.css.
*/
export function PageTransition({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) {
return <div className={cn("animate-fade-up", className)}>{children}</div>;
}
@@ -55,7 +55,12 @@ export function MetricTile({
? "var(--color-signal)"
: "var(--color-ink)";
return (
<div className={cn("glass p-4", className)}>
<div
className={cn(
"glass p-4 ring-focus transition-transform duration-200 hover:-translate-y-0.5",
className,
)}
>
<div className="flex items-center justify-between">
<div className="eyebrow">{label}</div>
{icon && <span className="text-ink-faint">{icon}</span>}