feat(design): neon/AI look with gradient loading states

This commit is contained in:
opencode
2026-08-06 07:08:01 +02:00
parent f3b4fa553b
commit 01982964bb
49 changed files with 4811 additions and 25 deletions
@@ -23,7 +23,7 @@ export function CompareBar({
if (tools.length === 0) return null;
return (
<div className="fixed bottom-4 left-1/2 -translate-x-1/2 z-50 w-[min(96vw,42rem)]">
<div className="rounded-xl border bg-popover/95 backdrop-blur shadow-lg p-3">
<div className="neon-glow rounded-xl border bg-popover/95 backdrop-blur shadow-lg p-3">
<div className="flex items-center gap-2">
<Scale className="w-4 h-4 text-muted-foreground shrink-0" />
<div className="flex items-center gap-1.5 flex-1 min-w-0 overflow-x-auto">
@@ -16,7 +16,7 @@ export function ToolCard({
watchlist?: { watched: boolean; onToggle: () => void };
}) {
return (
<Card className="hover-elevate transition-all flex flex-col h-full cursor-pointer hover:border-primary/50">
<Card className="hover-elevate transition-all flex flex-col h-full cursor-pointer hover:border-primary/50 hover:neon-glow-primary">
<Link href={`/tools/${tool.id}`} className="flex flex-col h-full p-0 m-0">
<CardHeader className="pb-3">
<div className="flex items-start gap-3 mb-2">
@@ -12,14 +12,14 @@ const buttonVariants = cva(
variant: {
default:
// @replit: no hover, and add primary border
"bg-primary text-primary-foreground border border-primary-border",
"bg-primary text-primary-foreground border border-primary-border hover:neon-glow-primary focus-visible:neon-glow-primary transition-shadow",
destructive:
"bg-destructive text-destructive-foreground shadow-sm border-destructive-border",
outline:
// @replit Shows the background color of whatever card / sidebar / accent background it is inside of.
// Inherits the current text color. Uses shadow-xs. no shadow on active
// No hover state
" border [border-color:var(--button-outline)] shadow-xs active:shadow-none ",
" border [border-color:var(--button-outline)] shadow-xs active:shadow-none hover:neon-glow focus-visible:neon-glow transition-shadow",
secondary:
// @replit border, no hover, no shadow, secondary border.
"border bg-secondary text-secondary-foreground border border-secondary-border ",
@@ -8,7 +8,7 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:neon-glow disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
@@ -9,19 +9,19 @@ const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-2 w-full overflow-hidden rounded-full border border-border/60 bg-primary/10",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className="loading-gradient h-full w-full flex-1 transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
))
Progress.displayName = ProgressPrimitive.Root.displayName
@@ -6,7 +6,7 @@ function Skeleton({
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-primary/10", className)}
className={cn("loading-gradient rounded-md border border-border/60", className)}
{...props}
/>
)
@@ -7,7 +7,10 @@ function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
<Loader2Icon
role="status"
aria-label="Loading"
className={cn("size-4 animate-spin", className)}
className={cn(
"size-4 animate-spin text-primary [filter:drop-shadow(0_0_6px_hsl(var(--glow-1)/0.55))]",
className
)}
{...props}
/>
)
@@ -9,7 +9,7 @@ const Textarea = React.forwardRef<
return (
<textarea
className={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:neon-glow disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
+91
View File
@@ -123,6 +123,10 @@
--chart-4: 43 100% 50%;
--chart-5: 280 80% 50%;
--glow-1: 187 100% 50%; /* cyan */
--glow-2: 265 100% 68%; /* violet */
--glow-3: 14 100% 57%; /* orange (primary) */
--app-font-sans: 'Inter', sans-serif;
--app-font-serif: Georgia, serif;
--app-font-mono: 'JetBrains Mono', Menlo, monospace;
@@ -372,4 +376,91 @@
.border.hover-elevate:not(.no-hover-interaction-elevate)::after {
inset: -1px;
}
/* ------------------------------------------------------------------ */
/* Neon / AI glow system
* Dezent: funktioniert in Light + Dark. Animationen respektieren
* prefers-reduced-motion.
* ------------------------------------------------------------------ */
.neon-glow {
box-shadow:
0 0 0 1px hsl(var(--glow-1) / 0.35),
0 0 18px -2px hsl(var(--glow-1) / 0.45),
0 0 42px -8px hsl(var(--glow-2) / 0.35);
}
.neon-glow-primary {
box-shadow:
0 0 0 1px hsl(var(--glow-3) / 0.4),
0 0 16px -2px hsl(var(--glow-3) / 0.38),
0 0 34px -8px hsl(var(--glow-3) / 0.3);
}
/* Animated gradient background for loading states (Google-AI style flow) */
.loading-gradient {
background-image: linear-gradient(
100deg,
hsl(var(--glow-3) / 0.14) 0%,
hsl(var(--glow-2) / 0.22) 25%,
hsl(var(--glow-1) / 0.24) 50%,
hsl(var(--glow-2) / 0.22) 75%,
hsl(var(--glow-3) / 0.14) 100%
);
background-size: 300% 100%;
animation: neon-loading-sweep 7s ease-in-out infinite;
}
.loading-gradient-text {
background-image: linear-gradient(
100deg,
hsl(var(--glow-3)) 0%,
hsl(var(--glow-1)) 45%,
hsl(var(--glow-2)) 75%,
hsl(var(--glow-3)) 100%
);
background-size: 250% 100%;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
animation: neon-loading-sweep 8s ease-in-out infinite;
}
.neon-pulse {
animation: neon-pulse 3.5s ease-in-out infinite;
}
.neon-spin {
animation: neon-rotate 1.4s linear infinite;
background: linear-gradient(
hsl(var(--glow-3)),
hsl(var(--glow-1)),
hsl(var(--glow-2)),
hsl(var(--glow-3))
);
animation: neon-rotate 1.4s linear infinite;
}
@keyframes neon-loading-sweep {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
@keyframes neon-pulse {
0%, 100% { opacity: 0.55; box-shadow: 0 0 6px -1px hsl(var(--glow-1) / 0.25); }
50% { opacity: 1; box-shadow: 0 0 16px 0 hsl(var(--glow-1) / 0.5); }
}
@keyframes neon-rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
.loading-gradient { animation: none; }
.loading-gradient-text { animation: none; }
.neon-pulse { animation: none; }
.neon-spin { animation: none; }
}
}
+3 -3
View File
@@ -39,7 +39,7 @@ export default function Home() {
{/* Stats Banner */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card className="bg-primary/5 border-primary/20">
<Card className="neon-glow-primary bg-primary/5 border-primary/20">
<CardContent className="p-6 flex items-center gap-4">
<div className="p-3 bg-primary/10 rounded-lg text-primary">
<Wrench className="w-6 h-6" />
@@ -55,7 +55,7 @@ export default function Home() {
</CardContent>
</Card>
<Card className="bg-primary/5 border-primary/20">
<Card className="neon-glow-primary bg-primary/5 border-primary/20">
<CardContent className="p-6 flex items-center gap-4">
<div className="p-3 bg-primary/10 rounded-lg text-primary">
<MessageSquare className="w-6 h-6" />
@@ -71,7 +71,7 @@ export default function Home() {
</CardContent>
</Card>
<Card className="bg-primary/5 border-primary/20">
<Card className="neon-glow-primary bg-primary/5 border-primary/20">
<CardContent className="p-6 flex items-center gap-4">
<div className="p-3 bg-primary/10 rounded-lg text-primary">
<Star className="w-6 h-6" />
+1 -1
View File
@@ -58,7 +58,7 @@ export default function Login() {
<p className="text-muted-foreground text-sm">{t("auth.loginSubtitle")}</p>
</div>
<Card>
<Card className="neon-glow">
<CardHeader className="pb-4">
<CardTitle className="text-lg">{t("auth.signIn")}</CardTitle>
<CardDescription>{t("auth.loginDescription")}</CardDescription>