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:
@@ -1,7 +1,12 @@
|
|||||||
|
import { PageTransition } from "@/components/shared";
|
||||||
import { AnalysisView } from "./view";
|
import { AnalysisView } from "./view";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export default function AnalysisPage() {
|
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 { getActivity, getDashboardStats } from "@/lib/api/server";
|
||||||
import { DashboardView } from "./view";
|
import { DashboardView } from "./view";
|
||||||
|
|
||||||
@@ -14,5 +15,9 @@ export default async function DashboardPage() {
|
|||||||
} catch {
|
} catch {
|
||||||
// Backend unavailable — client hooks will surface the error state.
|
// 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 { getMediaStatus } from "@/lib/api/server";
|
||||||
import { MediaView } from "./view";
|
import { MediaView } from "./view";
|
||||||
|
|
||||||
@@ -10,5 +11,9 @@ export default async function MediaPage() {
|
|||||||
} catch {
|
} catch {
|
||||||
/* client hooks surface errors */
|
/* 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 { getConfig, getGuilds } from "@/lib/api/server";
|
||||||
import { MessagesView } from "./view";
|
import { MessagesView } from "./view";
|
||||||
|
|
||||||
@@ -12,9 +13,11 @@ export default async function MessagesPage() {
|
|||||||
/* client hooks surface errors */
|
/* client hooks surface errors */
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<MessagesView
|
<PageTransition>
|
||||||
initialGuilds={guilds}
|
<MessagesView
|
||||||
initialGuildId={config?.monitorGuildId ?? null}
|
initialGuilds={guilds}
|
||||||
/>
|
initialGuildId={config?.monitorGuildId ?? null}
|
||||||
|
/>
|
||||||
|
</PageTransition>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { PageTransition } from "@/components/shared";
|
||||||
import { getModerationActions, getModerationStats } from "@/lib/api/server";
|
import { getModerationActions, getModerationStats } from "@/lib/api/server";
|
||||||
import { ModerationView } from "./view";
|
import { ModerationView } from "./view";
|
||||||
|
|
||||||
@@ -14,5 +15,9 @@ export default async function ModerationPage() {
|
|||||||
} catch {
|
} catch {
|
||||||
/* client hooks surface errors */
|
/* 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 { getRecordings } from "@/lib/api/server";
|
||||||
import { RecordingsView } from "./view";
|
import { RecordingsView } from "./view";
|
||||||
|
|
||||||
@@ -12,5 +13,9 @@ export default async function RecordingsPage() {
|
|||||||
} catch {
|
} catch {
|
||||||
/* client hooks surface errors */
|
/* 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 { getGuilds, getVoiceStatus } from "@/lib/api/server";
|
||||||
import { VoiceView } from "./view";
|
import { VoiceView } from "./view";
|
||||||
|
|
||||||
@@ -11,5 +12,9 @@ export default async function VoicePage() {
|
|||||||
} catch {
|
} catch {
|
||||||
/* client hooks surface errors */
|
/* client hooks surface errors */
|
||||||
}
|
}
|
||||||
return <VoiceView initialStatus={status} initialGuilds={guilds} />;
|
return (
|
||||||
|
<PageTransition>
|
||||||
|
<VoiceView initialStatus={status} initialGuilds={guilds} />
|
||||||
|
</PageTransition>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -276,6 +276,11 @@
|
|||||||
}
|
}
|
||||||
.animate-breathe { animation: breathe 3.5s ease-in-out infinite; }
|
.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) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.scan-line::after,
|
.scan-line::after,
|
||||||
.scan-line,
|
.scan-line,
|
||||||
@@ -283,7 +288,8 @@
|
|||||||
.animate-spin-disc,
|
.animate-spin-disc,
|
||||||
.animate-pulse-ring,
|
.animate-pulse-ring,
|
||||||
.animate-shimmer,
|
.animate-shimmer,
|
||||||
.animate-breathe {
|
.animate-breathe,
|
||||||
|
.animate-fade-up {
|
||||||
animation: none !important;
|
animation: none !important;
|
||||||
}
|
}
|
||||||
.scan-line {
|
.scan-line {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export { GuildChannelPicker } from "./guild-picker";
|
export { GuildChannelPicker } from "./guild-picker";
|
||||||
export { MarkdownLite } from "./markdown";
|
export { MarkdownLite } from "./markdown";
|
||||||
|
export { PageTransition } from "./page-transition";
|
||||||
export { MetricTile, SectionHeader } from "./section";
|
export { MetricTile, SectionHeader } from "./section";
|
||||||
export {
|
export {
|
||||||
EmptyState,
|
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-signal)"
|
||||||
: "var(--color-ink)";
|
: "var(--color-ink)";
|
||||||
return (
|
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="flex items-center justify-between">
|
||||||
<div className="eyebrow">{label}</div>
|
<div className="eyebrow">{label}</div>
|
||||||
{icon && <span className="text-ink-faint">{icon}</span>}
|
{icon && <span className="text-ink-faint">{icon}</span>}
|
||||||
|
|||||||
Reference in New Issue
Block a user