feat: Frutiger Aero redesign — glass panels, Aero shimmer, interaction moments
This commit is contained in:
@@ -48,10 +48,10 @@
|
||||
/* Frutiger Aero Gradients */
|
||||
--gradient-fa-blue: 135deg, hsl(217 91% 60%) 0%, hsl(200 90% 70%) 100%;
|
||||
--gradient-fa-green: 135deg, hsl(155 70% 55%) 0%, hsl(170 80% 65%) 100%;
|
||||
--gradient-fa-card: 180deg, hsl(var(--card)) 0%, hsl(var(--card)) 90%,
|
||||
hsl(var(--card)) 100%;
|
||||
--gradient-fa-gloss: 135deg, rgba(255, 255, 255, 0.2) 0%,
|
||||
rgba(255, 255, 255, 0) 100%;
|
||||
--gradient-fa-card:
|
||||
180deg, hsl(var(--card)) 0%, hsl(var(--card)) 90%, hsl(var(--card)) 100%;
|
||||
--gradient-fa-gloss:
|
||||
135deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 100%;
|
||||
|
||||
--shadow-fa-sm: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
--shadow-fa-md: 0 4px 6px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.06);
|
||||
@@ -183,11 +183,6 @@
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
background-image: url("/background.avif");
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-attachment: fixed;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.glossy-effect::before {
|
||||
@@ -312,3 +307,165 @@
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Frutiger Aero interaction keyframes ── */
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
max-height: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
max-height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0) rotate(0deg);
|
||||
}
|
||||
15% {
|
||||
transform: translateX(-4px) rotate(-1.5deg);
|
||||
}
|
||||
30% {
|
||||
transform: translateX(4px) rotate(1.5deg);
|
||||
}
|
||||
45% {
|
||||
transform: translateX(-3px) rotate(-1deg);
|
||||
}
|
||||
60% {
|
||||
transform: translateX(3px) rotate(1deg);
|
||||
}
|
||||
75% {
|
||||
transform: translateX(-1px) rotate(-0.5deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scale(0.9) translateY(8px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes floatBob {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmerAero {
|
||||
0% {
|
||||
background-position: -400px 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 400px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.animate-slide-down {
|
||||
overflow: hidden;
|
||||
animation: slideDown 0.22s ease-out forwards;
|
||||
}
|
||||
.animate-shake {
|
||||
animation: shake 0.45s ease-out;
|
||||
}
|
||||
.animate-fade-out {
|
||||
animation: fadeOut 0.3s ease-out forwards;
|
||||
}
|
||||
.animate-float-bob {
|
||||
animation: floatBob 2.8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Aero-tinted shimmer for skeleton loaders */
|
||||
.shimmer-aero {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(96, 165, 250, 0.12) 25%,
|
||||
rgba(96, 165, 250, 0.3) 50%,
|
||||
rgba(96, 165, 250, 0.12) 75%
|
||||
);
|
||||
background-size: 800px 100%;
|
||||
background-repeat: no-repeat;
|
||||
animation: shimmerAero 1.5s infinite linear;
|
||||
}
|
||||
|
||||
/* Widget title icon badges */
|
||||
.widget-icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.widget-icon-blue {
|
||||
background: linear-gradient(135deg, #60a5fa, #2563eb);
|
||||
box-shadow:
|
||||
0 2px 4px rgba(37, 99, 235, 0.3),
|
||||
inset 0 1px 1px rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
.widget-icon-green {
|
||||
background: linear-gradient(135deg, #6ee7b7, #10b981);
|
||||
box-shadow:
|
||||
0 2px 4px rgba(16, 185, 129, 0.3),
|
||||
inset 0 1px 1px rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
.widget-icon-purple {
|
||||
background: linear-gradient(135deg, #c4b5fd, #7c3aed);
|
||||
box-shadow:
|
||||
0 2px 4px rgba(124, 58, 237, 0.3),
|
||||
inset 0 1px 1px rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* Landing page ambient orbs */
|
||||
.orb {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(40px);
|
||||
opacity: 0.45;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Gradient avatar fallback */
|
||||
.avatar-gradient {
|
||||
background: linear-gradient(135deg, #60a5fa, #34d399);
|
||||
box-shadow:
|
||||
0 0 0 2px white,
|
||||
0 0 0 3.5px rgba(59, 130, 246, 0.45);
|
||||
}
|
||||
}
|
||||
|
||||
/* Respect reduced motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.animate-slide-down {
|
||||
animation: none;
|
||||
}
|
||||
.animate-shake {
|
||||
animation: none;
|
||||
}
|
||||
.animate-fade-out {
|
||||
animation: none;
|
||||
}
|
||||
.animate-float-bob {
|
||||
animation: none;
|
||||
}
|
||||
.shimmer-aero {
|
||||
animation: none;
|
||||
background: rgba(96, 165, 250, 0.18);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { AuthProvider } from "@/hooks/use-auth";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { Header } from "@/components/header";
|
||||
import localFont from "next/font/local";
|
||||
import Image from "next/image";
|
||||
import InstallPrompt from "@/components/install-prompt";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -52,6 +53,14 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={`${frutiger.className} antialiased`}>
|
||||
<Image
|
||||
src="/bg1.avif"
|
||||
alt=""
|
||||
fill
|
||||
priority
|
||||
quality={85}
|
||||
className="fixed inset-0 -z-10 object-cover object-center"
|
||||
/>
|
||||
<AuthProvider>
|
||||
<Header />
|
||||
<main className="flex-1">{children}</main>
|
||||
|
||||
@@ -13,7 +13,11 @@ import { UsersCount } from "@/components/users-count";
|
||||
import { PaginationNav } from "@/components/pagination-nav";
|
||||
import { redirect } from "next/navigation";
|
||||
import { Suspense } from "react";
|
||||
import { ProfileSkeleton, TagsSkeleton, CountSkeleton } from "@/components/loading-skeleton";
|
||||
import {
|
||||
ProfileSkeleton,
|
||||
TagsSkeleton,
|
||||
CountSkeleton,
|
||||
} from "@/components/loading-skeleton";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Home",
|
||||
@@ -86,9 +90,7 @@ async function FeedPage({
|
||||
</header>
|
||||
<ThoughtForm />
|
||||
|
||||
<div className="block lg:hidden space-y-6">
|
||||
{sidebar}
|
||||
</div>
|
||||
<div className="block lg:hidden space-y-6">{sidebar}</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{thoughtThreads.map((thought) => (
|
||||
@@ -99,7 +101,13 @@ async function FeedPage({
|
||||
/>
|
||||
))}
|
||||
{thoughtThreads.length === 0 && (
|
||||
<EmptyState message="Your feed is empty. Follow some users to see their thoughts!" />
|
||||
<EmptyState
|
||||
emoji="💭"
|
||||
title="Your feed is quiet"
|
||||
message="Your feed is empty. Follow some users to see their thoughts!"
|
||||
ctaLabel="Discover people ✨"
|
||||
ctaHref="/users/all"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<PaginationNav
|
||||
@@ -110,9 +118,7 @@ async function FeedPage({
|
||||
</main>
|
||||
|
||||
<aside className="hidden lg:block lg:col-span-1">
|
||||
<div className="sticky top-20 space-y-6">
|
||||
{sidebar}
|
||||
</div>
|
||||
<div className="sticky top-20 space-y-6">{sidebar}</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
@@ -121,28 +127,112 @@ async function FeedPage({
|
||||
|
||||
function LandingPage() {
|
||||
return (
|
||||
<>
|
||||
<div className="font-sans min-h-screen text-gray-800 flex items-center justify-center">
|
||||
<div className="container mx-auto max-w-2xl p-4 sm:p-6 text-center glass-effect glossy-effect bottom rounded-md shadow-fa-lg">
|
||||
<h1
|
||||
className="text-5xl font-bold"
|
||||
style={{ textShadow: "2px 2px 4px rgba(0,0,0,0.1)" }}
|
||||
<div className="font-sans min-h-screen flex items-center justify-center relative overflow-hidden">
|
||||
{/* Ambient orbs */}
|
||||
<div
|
||||
className="orb"
|
||||
style={{
|
||||
width: 280,
|
||||
height: 280,
|
||||
background:
|
||||
"radial-gradient(circle, #ffffff 0%, #87ceeb 60%, transparent 100%)",
|
||||
top: "-80px",
|
||||
left: "-60px",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="orb"
|
||||
style={{
|
||||
width: 220,
|
||||
height: 220,
|
||||
background:
|
||||
"radial-gradient(circle, #b2f5ea 0%, #48bb78 60%, transparent 100%)",
|
||||
bottom: "-40px",
|
||||
right: "5%",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="orb"
|
||||
style={{
|
||||
width: 160,
|
||||
height: 160,
|
||||
background:
|
||||
"radial-gradient(circle, #e0f2fe 0%, #38bdf8 60%, transparent 100%)",
|
||||
top: "35%",
|
||||
left: "65%",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Hero card */}
|
||||
<div
|
||||
className="container mx-auto max-w-lg p-4 sm:p-6 text-center relative z-10"
|
||||
style={{
|
||||
background: "rgba(255,255,255,0.28)",
|
||||
backdropFilter: "blur(20px)",
|
||||
WebkitBackdropFilter: "blur(20px)",
|
||||
border: "1px solid rgba(255,255,255,0.55)",
|
||||
borderRadius: "20px",
|
||||
boxShadow:
|
||||
"0 8px 32px rgba(0,0,0,0.10), inset 0 1px 0 rgba(255,255,255,0.6)",
|
||||
}}
|
||||
>
|
||||
{/* Gloss sweep */}
|
||||
<div
|
||||
aria-hidden
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: "55%",
|
||||
background:
|
||||
"linear-gradient(180deg, rgba(255,255,255,0.38) 0%, transparent 100%)",
|
||||
borderRadius: "20px 20px 0 0",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
|
||||
<h1
|
||||
className="text-5xl font-bold relative"
|
||||
style={{
|
||||
textShadow:
|
||||
"0 2px 4px rgba(255,255,255,0.6), 0 1px 2px rgba(0,0,0,0.1)",
|
||||
}}
|
||||
>
|
||||
Welcome to Thoughts
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-3 relative">
|
||||
A federated social network for short-form thoughts.
|
||||
<br />
|
||||
Connect with the Fediverse.
|
||||
</p>
|
||||
|
||||
<div className="mt-8 flex justify-center gap-4 relative">
|
||||
<Button asChild className="px-7">
|
||||
<Link href="/login">Login</Link>
|
||||
</Button>
|
||||
<Button asChild variant="secondary" className="px-7">
|
||||
<Link href="/register">Register</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Fediverse badge */}
|
||||
<div className="mt-5 relative flex justify-center">
|
||||
<span
|
||||
className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full text-xs text-muted-foreground"
|
||||
style={{
|
||||
background: "rgba(255,255,255,0.3)",
|
||||
border: "1px solid rgba(255,255,255,0.5)",
|
||||
}}
|
||||
>
|
||||
Welcome to Thoughts
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Throwback to the golden age of microblogging.
|
||||
</p>
|
||||
<div className="mt-8 flex justify-center gap-4">
|
||||
<Button asChild>
|
||||
<Link href="/login">Login</Link>
|
||||
</Button>
|
||||
<Button variant="secondary" asChild>
|
||||
<Link href="/register">Register</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<span
|
||||
className="w-2 h-2 rounded-full bg-emerald-400 inline-block"
|
||||
style={{ boxShadow: "0 0 4px #34d399" }}
|
||||
/>
|
||||
Works with Mastodon, Pixelfed & more
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ export default async function SearchPage({ searchParams }: SearchPageProps) {
|
||||
<RemoteUserCard actor={remoteActor} />
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState message={`No user found at ${query}`} />
|
||||
<EmptyState emoji="🔍" title="No results" message={`No user found at ${query}`} />
|
||||
)
|
||||
) : results ? (
|
||||
<Tabs defaultValue="thoughts" className="w-full">
|
||||
@@ -91,7 +91,7 @@ export default async function SearchPage({ searchParams }: SearchPageProps) {
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
) : (
|
||||
<EmptyState message="No results found or an error occurred." />
|
||||
<EmptyState emoji="🔍" title="No results" message="No results found or an error occurred." />
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -67,7 +67,7 @@ export default async function TagPage({ params }: TagPageProps) {
|
||||
/>
|
||||
))}
|
||||
{thoughtThreads.length === 0 && (
|
||||
<EmptyState message="No thoughts found for this tag." />
|
||||
<EmptyState emoji="🏷" title="No thoughts here yet" message="No thoughts found for this tag." />
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -270,7 +270,7 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
|
||||
/>
|
||||
))}
|
||||
{thoughtThreads.length === 0 && (
|
||||
<EmptyState message="This user hasn't posted any public thoughts yet." />
|
||||
<EmptyState emoji="💭" title="Nothing here yet" message="This user hasn't posted any public thoughts yet." />
|
||||
)}
|
||||
</TabsContent>
|
||||
{isOwnProfile && (
|
||||
|
||||
Reference in New Issue
Block a user