feat: carousel
This commit is contained in:
@@ -155,6 +155,12 @@ section {
|
|||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Carousel progress bar */
|
||||||
|
@keyframes progress {
|
||||||
|
from { width: 0; }
|
||||||
|
to { width: 100%; }
|
||||||
|
}
|
||||||
|
|
||||||
/* Fade in animation for sections */
|
/* Fade in animation for sections */
|
||||||
@keyframes fade-up {
|
@keyframes fade-up {
|
||||||
from {
|
from {
|
||||||
|
|||||||
@@ -1,8 +1,72 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState, useEffect, useRef } from 'react'
|
||||||
import { Eye } from 'lucide-react'
|
import { Eye } from 'lucide-react'
|
||||||
|
|
||||||
|
const slides = [
|
||||||
|
{
|
||||||
|
label: 'Dashboard',
|
||||||
|
src: '/dashboard.png',
|
||||||
|
alt: 'GreyProxy dashboard overview showing connection stats and activity',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Pending requests',
|
||||||
|
src: '/pending_requests.png',
|
||||||
|
alt: 'GreyProxy pending network requests with Allow and Deny controls',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Rules',
|
||||||
|
src: '/rules.png',
|
||||||
|
alt: 'GreyProxy domain rules configuration for allow and deny policies',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Logs',
|
||||||
|
src: '/logs.png',
|
||||||
|
alt: 'GreyProxy connection logs showing all outbound network activity',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const INTERVAL = 4000
|
||||||
|
|
||||||
export function Observability() {
|
export function Observability() {
|
||||||
|
const [active, setActive] = useState(0)
|
||||||
|
const [paused, setPaused] = useState(false)
|
||||||
|
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||||
|
// Key to force re-mount of the progress bar so animation restarts
|
||||||
|
const [tick, setTick] = useState(0)
|
||||||
|
|
||||||
|
function goTo(i: number) {
|
||||||
|
setActive(i)
|
||||||
|
setTick((t) => t + 1)
|
||||||
|
resetTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
function advance() {
|
||||||
|
setActive((i) => (i + 1) % slides.length)
|
||||||
|
setTick((t) => t + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetTimer() {
|
||||||
|
if (timerRef.current) clearInterval(timerRef.current)
|
||||||
|
if (!paused) {
|
||||||
|
timerRef.current = setInterval(advance, INTERVAL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (paused) {
|
||||||
|
if (timerRef.current) clearInterval(timerRef.current)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
timerRef.current = setInterval(advance, INTERVAL)
|
||||||
|
return () => {
|
||||||
|
if (timerRef.current) clearInterval(timerRef.current)
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [paused])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="features" className="py-24 px-6 border-t border-border/30">
|
<section id="features" className="py-24 px-4 sm:px-6 border-t border-border/30">
|
||||||
<div className="mx-auto max-w-5xl">
|
<div className="mx-auto max-w-5xl">
|
||||||
<div className="max-w-2xl mb-16">
|
<div className="max-w-2xl mb-16">
|
||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
@@ -21,21 +85,60 @@ export function Observability() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mx-auto max-w-3xl">
|
<div
|
||||||
<div className="flex items-center gap-3 mb-4">
|
className="mx-auto max-w-3xl"
|
||||||
<div className="flex items-center justify-center w-8 h-8 rounded-md bg-primary/10 text-primary">
|
onMouseEnter={() => setPaused(true)}
|
||||||
<Eye className="h-4 w-4" />
|
onMouseLeave={() => setPaused(false)}
|
||||||
</div>
|
>
|
||||||
<h3 className="font-sans font-semibold text-sm">GreyProxy dashboard</h3>
|
{/* Screenshot with crossfade */}
|
||||||
|
<div className="relative rounded-lg border border-border/40 overflow-hidden bg-card/30">
|
||||||
|
{slides.map((slide, i) => (
|
||||||
|
<img
|
||||||
|
key={slide.label}
|
||||||
|
src={slide.src}
|
||||||
|
alt={slide.alt}
|
||||||
|
className={`w-full h-auto transition-opacity duration-700 ${
|
||||||
|
i === active ? 'opacity-100 relative' : 'opacity-0 absolute inset-0'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg border border-border/40 overflow-hidden bg-card/30">
|
|
||||||
<img
|
{/* Progress indicators + labels */}
|
||||||
src="/greyproxy.png"
|
<div className="flex items-center justify-center gap-4 mt-5">
|
||||||
alt="GreyProxy dashboard showing pending network requests with Allow and Deny controls"
|
{slides.map((slide, i) => (
|
||||||
className="w-full h-auto"
|
<button
|
||||||
/>
|
key={slide.label}
|
||||||
|
onClick={() => goTo(i)}
|
||||||
|
className="flex items-center gap-2 group"
|
||||||
|
>
|
||||||
|
<div className="relative h-1.5 w-8 rounded-full bg-border/50 overflow-hidden">
|
||||||
|
{i === active ? (
|
||||||
|
<div
|
||||||
|
key={tick}
|
||||||
|
className="absolute inset-y-0 left-0 rounded-full bg-primary"
|
||||||
|
style={
|
||||||
|
paused
|
||||||
|
? { width: '100%' }
|
||||||
|
: { animation: `progress ${INTERVAL}ms linear forwards` }
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="absolute inset-0 rounded-full bg-transparent group-hover:bg-muted-foreground/30 transition-colors" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={`text-xs font-sans transition-colors hidden sm:inline ${
|
||||||
|
i === active ? 'text-foreground font-medium' : 'text-muted-foreground'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{slide.label}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground font-serif leading-relaxed mt-4">
|
|
||||||
|
<p className="text-xs text-muted-foreground font-serif leading-relaxed mt-5 text-center">
|
||||||
Every outbound request is visible. Allow trusted domains, block unknown ones,
|
Every outbound request is visible. Allow trusted domains, block unknown ones,
|
||||||
and adjust policies live as your agent works.
|
and adjust policies live as your agent works.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export function Problem() {
|
|||||||
{/* Section 1: Stochastic risk */}
|
{/* Section 1: Stochastic risk */}
|
||||||
<div className="mb-12 sm:mb-16">
|
<div className="mb-12 sm:mb-16">
|
||||||
<h2 className="font-serif text-2xl sm:text-3xl md:text-4xl font-semibold tracking-tight mb-3 max-w-3xl">
|
<h2 className="font-serif text-2xl sm:text-3xl md:text-4xl font-semibold tracking-tight mb-3 max-w-3xl">
|
||||||
Your agent runs as <em className="italic text-primary">you</em>.
|
Your agent runs as <em className="italic text-red-400/90">you</em>.
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-muted-foreground font-serif text-sm sm:text-base leading-relaxed max-w-full mt-4 mb-10">
|
<p className="text-muted-foreground font-serif text-sm sm:text-base leading-relaxed max-w-full mt-4 mb-10">
|
||||||
Agents inherit your full permissions and decide what to access at runtime. Here's what that looks like...
|
Agents inherit your full permissions and decide what to access at runtime. Here's what that looks like...
|
||||||
|
|||||||
BIN
public/dashboard.png
Normal file
BIN
public/dashboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 179 KiB |
BIN
public/logs.png
Normal file
BIN
public/logs.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 352 KiB |
|
Before Width: | Height: | Size: 203 KiB After Width: | Height: | Size: 203 KiB |
BIN
public/rules.png
Normal file
BIN
public/rules.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 265 KiB |
Reference in New Issue
Block a user