feat: updated homepage
- Added the 'lenis' library for smooth scrolling functionality. - Integrated 'UseCasesGrid' component into the homepage layout. - Updated 'HeroSection' to replace image elements with 'WalkthroughScroll' for enhanced user experience.
|
|
@ -5,11 +5,13 @@ import { FeaturesBentoGrid } from "@/components/homepage/features-bento-grid";
|
|||
import { FeaturesCards } from "@/components/homepage/features-card";
|
||||
import { HeroSection } from "@/components/homepage/hero-section";
|
||||
import ExternalIntegrations from "@/components/homepage/integrations";
|
||||
import { UseCasesGrid } from "@/components/homepage/use-cases-grid";
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<main className="min-h-screen bg-gradient-to-b from-gray-50 to-gray-100 text-gray-900 dark:from-black dark:to-gray-900 dark:text-white">
|
||||
<HeroSection />
|
||||
<UseCasesGrid />
|
||||
<FeaturesCards />
|
||||
<FeaturesBentoGrid />
|
||||
<ExternalIntegrations />
|
||||
|
|
|
|||
|
|
@ -141,9 +141,7 @@ export const InlineMentionEditor = forwardRef<InlineMentionEditorRef, InlineMent
|
|||
|
||||
// Preserve mention chips as inline @title tokens.
|
||||
if (element.hasAttribute(CHIP_DATA_ATTR)) {
|
||||
const title = element
|
||||
.querySelector("[data-mention-title='true']")
|
||||
?.textContent?.trim();
|
||||
const title = element.querySelector("[data-mention-title='true']")?.textContent?.trim();
|
||||
if (title) {
|
||||
return `@${title}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
"use client";
|
||||
import { useFeatureFlagVariantKey } from "@posthog/react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import Balancer from "react-wrap-balancer";
|
||||
import { WalkthroughScroll } from "@/components/ui/walkthrough-scroll";
|
||||
import { AUTH_TYPE, BACKEND_URL } from "@/lib/env-config";
|
||||
import { trackLoginAttempt } from "@/lib/posthog/events";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
|
@ -40,41 +40,37 @@ export function HeroSection() {
|
|||
return (
|
||||
<div
|
||||
ref={parentRef}
|
||||
className="relative flex min-h-screen flex-col items-center justify-center overflow-hidden px-4 py-20 md:px-8 md:py-40"
|
||||
className="relative flex min-h-screen flex-col items-center justify-center overflow-hidden px-4 py-12 md:px-8 md:py-24"
|
||||
>
|
||||
<BackgroundGrids />
|
||||
<CollisionMechanism
|
||||
parentRef={parentRef}
|
||||
beamOptions={{
|
||||
initialX: -400,
|
||||
translateX: 600,
|
||||
duration: 7,
|
||||
repeatDelay: 3,
|
||||
}}
|
||||
containerRef={containerRef}
|
||||
parentRef={parentRef}
|
||||
/>
|
||||
<CollisionMechanism
|
||||
parentRef={parentRef}
|
||||
beamOptions={{
|
||||
initialX: -200,
|
||||
translateX: 800,
|
||||
duration: 4,
|
||||
repeatDelay: 3,
|
||||
}}
|
||||
containerRef={containerRef}
|
||||
parentRef={parentRef}
|
||||
/>
|
||||
<CollisionMechanism
|
||||
parentRef={parentRef}
|
||||
beamOptions={{
|
||||
initialX: 200,
|
||||
translateX: 1200,
|
||||
duration: 5,
|
||||
repeatDelay: 3,
|
||||
}}
|
||||
containerRef={containerRef}
|
||||
parentRef={parentRef}
|
||||
/>
|
||||
<CollisionMechanism
|
||||
containerRef={containerRef}
|
||||
parentRef={parentRef}
|
||||
beamOptions={{
|
||||
initialX: 400,
|
||||
|
|
@ -106,34 +102,12 @@ export function HeroSection() {
|
|||
<p className="relative z-50 mx-auto mt-0 max-w-lg px-4 text-center text-base/6 text-gray-600 dark:text-gray-200">
|
||||
Then chat with it in real-time, even alongside your team.
|
||||
</p>
|
||||
<div className="mb-10 mt-8 flex w-full flex-col items-center justify-center gap-4 px-8 sm:flex-row md:mb-20">
|
||||
<div className="mb-6 mt-6 flex w-full flex-col items-center justify-center gap-4 px-8 sm:flex-row md:mb-10">
|
||||
<GetStartedButton />
|
||||
<ContactSalesButton />
|
||||
</div>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative mx-auto max-w-7xl rounded-[32px] border border-neutral-200/50 bg-neutral-100 p-2 backdrop-blur-lg md:p-4 dark:border-neutral-700 dark:bg-neutral-800/50"
|
||||
>
|
||||
<div className="rounded-[24px] border border-neutral-200 bg-white p-2 dark:border-neutral-700 dark:bg-black">
|
||||
{/* Light mode image */}
|
||||
<Image
|
||||
src="/homepage/main_demo.webp"
|
||||
alt="header"
|
||||
width={1920}
|
||||
height={1080}
|
||||
className="rounded-[20px] block dark:hidden"
|
||||
unoptimized
|
||||
/>
|
||||
{/* Dark mode image */}
|
||||
<Image
|
||||
src="/homepage/main_demo.webp"
|
||||
alt="header"
|
||||
width={1920}
|
||||
height={1080}
|
||||
className="rounded-[20px] hidden dark:block"
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
<div ref={containerRef} className="relative w-full">
|
||||
<WalkthroughScroll />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -236,24 +210,23 @@ const BackgroundGrids = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const CollisionMechanism = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
{
|
||||
containerRef: React.RefObject<HTMLDivElement | null>;
|
||||
parentRef: React.RefObject<HTMLDivElement | null>;
|
||||
beamOptions?: {
|
||||
initialX?: number;
|
||||
translateX?: number;
|
||||
initialY?: number;
|
||||
translateY?: number;
|
||||
rotate?: number;
|
||||
className?: string;
|
||||
duration?: number;
|
||||
delay?: number;
|
||||
repeatDelay?: number;
|
||||
};
|
||||
}
|
||||
>(({ parentRef, containerRef, beamOptions = {} }, ref) => {
|
||||
const CollisionMechanism = ({
|
||||
parentRef,
|
||||
beamOptions = {},
|
||||
}: {
|
||||
parentRef: React.RefObject<HTMLDivElement | null>;
|
||||
beamOptions?: {
|
||||
initialX?: number;
|
||||
translateX?: number;
|
||||
initialY?: number;
|
||||
translateY?: number;
|
||||
rotate?: number;
|
||||
className?: string;
|
||||
duration?: number;
|
||||
delay?: number;
|
||||
repeatDelay?: number;
|
||||
};
|
||||
}) => {
|
||||
const beamRef = useRef<HTMLDivElement>(null);
|
||||
const [collision, setCollision] = useState<{
|
||||
detected: boolean;
|
||||
|
|
@ -264,14 +237,14 @@ const CollisionMechanism = React.forwardRef<
|
|||
|
||||
useEffect(() => {
|
||||
const checkCollision = () => {
|
||||
if (beamRef.current && containerRef.current && parentRef.current && !cycleCollisionDetected) {
|
||||
if (beamRef.current && parentRef.current && !cycleCollisionDetected) {
|
||||
const beamRect = beamRef.current.getBoundingClientRect();
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
const parentRect = parentRef.current.getBoundingClientRect();
|
||||
const rightEdge = parentRect.right;
|
||||
|
||||
if (beamRect.bottom >= containerRect.top) {
|
||||
const relativeX = beamRect.left - parentRect.left + beamRect.width / 2;
|
||||
const relativeY = beamRect.bottom - parentRect.top;
|
||||
if (beamRect.right >= rightEdge - 20) {
|
||||
const relativeX = parentRect.width - 20;
|
||||
const relativeY = beamRect.top - parentRect.top + beamRect.height / 2;
|
||||
|
||||
setCollision({
|
||||
detected: true,
|
||||
|
|
@ -288,7 +261,7 @@ const CollisionMechanism = React.forwardRef<
|
|||
const animationInterval = setInterval(checkCollision, 100);
|
||||
|
||||
return () => clearInterval(animationInterval);
|
||||
}, [cycleCollisionDetected, containerRef]);
|
||||
}, [cycleCollisionDetected, parentRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (collision.detected && collision.coordinates) {
|
||||
|
|
@ -354,9 +327,7 @@ const CollisionMechanism = React.forwardRef<
|
|||
</AnimatePresence>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
CollisionMechanism.displayName = "CollisionMechanism";
|
||||
};
|
||||
|
||||
const Explosion = ({ ...props }: React.HTMLProps<HTMLDivElement>) => {
|
||||
const spans = Array.from({ length: 20 }, (_, index) => ({
|
||||
|
|
|
|||
107
surfsense_web/components/homepage/use-cases-grid.tsx
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
"use client";
|
||||
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { ExpandedGifOverlay, useExpandedGif } from "@/components/ui/expanded-gif-overlay";
|
||||
|
||||
const useCases = [
|
||||
{
|
||||
title: "Search & Citation",
|
||||
description: "Ask questions and get Perplexity-style cited responses from your knowledge base.",
|
||||
src: "/homepage/hero_tutorial/BSNCGif.gif",
|
||||
},
|
||||
{
|
||||
title: "Document Mention QNA",
|
||||
description: "Mention specific documents in your queries for targeted answers.",
|
||||
src: "/homepage/hero_tutorial/BQnaGif_compressed.gif",
|
||||
},
|
||||
{
|
||||
title: "Report Generation",
|
||||
description: "Generate and export reports in many formats.",
|
||||
src: "/homepage/hero_tutorial/ReportGenGif_compressed.gif",
|
||||
},
|
||||
{
|
||||
title: "Podcast Generation",
|
||||
description: "Turn your knowledge into podcasts in under 20 seconds.",
|
||||
src: "/homepage/hero_tutorial/PodcastGenGif.gif",
|
||||
},
|
||||
{
|
||||
title: "Image Generation",
|
||||
description: "Generate images directly from your conversations.",
|
||||
src: "/homepage/hero_tutorial/ImageGenGif.gif",
|
||||
},
|
||||
];
|
||||
|
||||
function UseCaseCard({
|
||||
title,
|
||||
description,
|
||||
src,
|
||||
className,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
src: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const { expanded, open, close } = useExpandedGif();
|
||||
|
||||
return (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-60px" }}
|
||||
transition={{ duration: 0.5, ease: "easeOut" }}
|
||||
className={`group overflow-hidden rounded-2xl border border-neutral-200/60 bg-white shadow-sm transition-shadow duration-300 hover:shadow-xl dark:border-neutral-700/60 dark:bg-neutral-900 ${className ?? ""}`}
|
||||
>
|
||||
<div
|
||||
className="cursor-pointer overflow-hidden bg-neutral-50 p-2 dark:bg-neutral-950"
|
||||
onClick={open}
|
||||
>
|
||||
<img
|
||||
src={src}
|
||||
alt={title}
|
||||
className="w-full rounded-xl object-cover transition-transform duration-500 group-hover:scale-[1.02]"
|
||||
/>
|
||||
</div>
|
||||
<div className="px-5 py-4">
|
||||
<h3 className="text-base font-semibold text-neutral-900 dark:text-white">{title}</h3>
|
||||
<p className="mt-1 text-sm text-neutral-500 dark:text-neutral-400">{description}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<AnimatePresence>
|
||||
{expanded && <ExpandedGifOverlay src={src} alt={title} onClose={close} />}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function UseCasesGrid() {
|
||||
return (
|
||||
<section className="relative mx-auto max-w-7xl px-4 py-4 sm:px-6 sm:py-8 lg:px-8">
|
||||
<div className="mb-6 text-center">
|
||||
<h2 className="text-3xl font-semibold tracking-tight text-neutral-900 sm:text-4xl dark:text-white">
|
||||
What You Can Do
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* First row: 2 larger cards */}
|
||||
<div className="grid grid-cols-1 gap-5 md:grid-cols-2">
|
||||
{useCases.slice(0, 2).map((useCase) => (
|
||||
<UseCaseCard key={useCase.title} {...useCase} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Second row: 3 equal cards */}
|
||||
<div className="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{useCases.slice(2).map((useCase) => (
|
||||
<UseCaseCard key={useCase.title} {...useCase} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="mt-8 text-center text-sm text-neutral-500 dark:text-neutral-400">
|
||||
And more coming soon.
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
53
surfsense_web/components/ui/expanded-gif-overlay.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
"use client";
|
||||
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
function ExpandedGifOverlay({
|
||||
src,
|
||||
alt,
|
||||
onClose,
|
||||
}: {
|
||||
src: string;
|
||||
alt: string;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") onClose();
|
||||
};
|
||||
document.addEventListener("keydown", handleKey);
|
||||
return () => document.removeEventListener("keydown", handleKey);
|
||||
}, [onClose]);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="fixed inset-0 z-100 flex items-center justify-center bg-black/70 p-4 backdrop-blur-sm sm:p-8"
|
||||
onClick={onClose}
|
||||
>
|
||||
<motion.img
|
||||
initial={{ scale: 0.85, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.85, opacity: 0 }}
|
||||
transition={{ duration: 0.25, ease: "easeOut" }}
|
||||
src={src}
|
||||
alt={alt}
|
||||
className="max-h-[90vh] max-w-[90vw] rounded-2xl shadow-2xl"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
function useExpandedGif() {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const open = useCallback(() => setExpanded(true), []);
|
||||
const close = useCallback(() => setExpanded(false), []);
|
||||
return { expanded, open, close };
|
||||
}
|
||||
|
||||
export { ExpandedGifOverlay, useExpandedGif };
|
||||
123
surfsense_web/components/ui/walkthrough-scroll.tsx
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
"use client";
|
||||
|
||||
import { AnimatePresence, motion, useScroll, useTransform } from "motion/react";
|
||||
import { useRef } from "react";
|
||||
import { ExpandedGifOverlay, useExpandedGif } from "@/components/ui/expanded-gif-overlay";
|
||||
|
||||
const walkthroughSteps = [
|
||||
{
|
||||
step: 1,
|
||||
title: "Login",
|
||||
description: "Login to get started.",
|
||||
src: "/homepage/hero_tutorial/LoginFlowGif.gif",
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
title: "Connect & Sync",
|
||||
description: "Connect your connectors and sync. Enable periodic syncing to keep them updated.",
|
||||
src: "/homepage/hero_tutorial/ConnectorFlowGif.gif",
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
title: "Upload Documents",
|
||||
description: "While connectors index, upload your documents directly.",
|
||||
src: "/homepage/hero_tutorial/DocUploadGif.gif",
|
||||
},
|
||||
];
|
||||
|
||||
function WalkthroughCard({
|
||||
i,
|
||||
step,
|
||||
title,
|
||||
description,
|
||||
src,
|
||||
progress,
|
||||
range,
|
||||
targetScale,
|
||||
}: {
|
||||
i: number;
|
||||
step: number;
|
||||
title: string;
|
||||
description: string;
|
||||
src: string;
|
||||
progress: ReturnType<typeof useScroll>["scrollYProgress"];
|
||||
range: [number, number];
|
||||
targetScale: number;
|
||||
}) {
|
||||
const container = useRef<HTMLDivElement>(null);
|
||||
const scale = useTransform(progress, range, [1, targetScale]);
|
||||
const { expanded, open, close } = useExpandedGif();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={container}
|
||||
className="sticky top-0 flex items-center justify-center px-4 sm:px-6 lg:px-8"
|
||||
>
|
||||
<motion.div
|
||||
style={{
|
||||
scale,
|
||||
top: `calc(10vh + ${i * 30}px)`,
|
||||
}}
|
||||
className="relative flex origin-top flex-col overflow-hidden rounded-2xl border border-neutral-200/60 bg-white shadow-xl sm:rounded-3xl dark:border-neutral-700/60 dark:bg-neutral-900
|
||||
w-full max-w-[340px] sm:max-w-[520px] md:max-w-[680px] lg:max-w-[900px]"
|
||||
>
|
||||
<div className="flex items-center gap-3 border-b border-neutral-200/60 px-4 py-3 sm:px-6 sm:py-4 dark:border-neutral-700/60">
|
||||
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-neutral-900 text-xs font-semibold text-white sm:h-8 sm:w-8 sm:text-sm dark:bg-white dark:text-neutral-900">
|
||||
{step}
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<h3 className="truncate text-sm font-semibold text-neutral-900 sm:text-base dark:text-white">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="hidden text-xs text-neutral-500 sm:block dark:text-neutral-400">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="cursor-pointer bg-neutral-50 p-2 sm:p-3 dark:bg-neutral-950"
|
||||
onClick={open}
|
||||
>
|
||||
<img src={src} alt={title} className="w-full rounded-lg object-cover sm:rounded-xl" />
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{expanded && <ExpandedGifOverlay src={src} alt={title} onClose={close} />}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function WalkthroughScroll() {
|
||||
const container = useRef<HTMLDivElement>(null);
|
||||
const { scrollYProgress } = useScroll({
|
||||
target: container,
|
||||
offset: ["start start", "end end"],
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={container}
|
||||
className="relative flex w-full flex-col items-center justify-center pb-[15vh] pt-[1vh] sm:pb-[18vh] sm:pt-[2vh]"
|
||||
>
|
||||
{walkthroughSteps.map((project, i) => {
|
||||
const targetScale = Math.max(0.6, 1 - (walkthroughSteps.length - i - 1) * 0.05);
|
||||
return (
|
||||
<WalkthroughCard
|
||||
key={`walkthrough_${i}`}
|
||||
i={i}
|
||||
{...project}
|
||||
progress={scrollYProgress}
|
||||
range={[i * (1 / walkthroughSteps.length), 1]}
|
||||
targetScale={targetScale}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { WalkthroughScroll, WalkthroughCard };
|
||||
|
|
@ -82,6 +82,7 @@
|
|||
"jotai": "^2.15.1",
|
||||
"jotai-tanstack-query": "^0.11.0",
|
||||
"katex": "^0.16.28",
|
||||
"lenis": "^1.3.17",
|
||||
"lucide-react": "^0.477.0",
|
||||
"motion": "^12.23.22",
|
||||
"next": "^16.1.0",
|
||||
|
|
|
|||
28
surfsense_web/pnpm-lock.yaml
generated
|
|
@ -191,6 +191,9 @@ importers:
|
|||
katex:
|
||||
specifier: ^0.16.28
|
||||
version: 0.16.28
|
||||
lenis:
|
||||
specifier: ^1.3.17
|
||||
version: 1.3.17(react@19.2.3)
|
||||
lucide-react:
|
||||
specifier: ^0.477.0
|
||||
version: 0.477.0(react@19.2.3)
|
||||
|
|
@ -5380,6 +5383,20 @@ packages:
|
|||
resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==}
|
||||
engines: {node: '>=0.10'}
|
||||
|
||||
lenis@1.3.17:
|
||||
resolution: {integrity: sha512-k9T9rgcxne49ggJOvXCraWn5dt7u2mO+BNkhyu6yxuEnm9c092kAW5Bus5SO211zUvx7aCCEtzy9UWr0RB+oJw==}
|
||||
peerDependencies:
|
||||
'@nuxt/kit': '>=3.0.0'
|
||||
react: '>=17.0.0'
|
||||
vue: '>=3.0.0'
|
||||
peerDependenciesMeta:
|
||||
'@nuxt/kit':
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
vue:
|
||||
optional: true
|
||||
|
||||
levn@0.4.1:
|
||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
|
@ -6702,6 +6719,9 @@ packages:
|
|||
tailwind-merge@3.4.0:
|
||||
resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==}
|
||||
|
||||
tailwind-merge@3.4.1:
|
||||
resolution: {integrity: sha512-2OA0rFqWOkITEAOFWSBSApYkDeH9t2B3XSJuI4YztKBzK3mX0737A2qtxDZ7xkw9Zfh0bWl+r34sF3HXV+Ig7Q==}
|
||||
|
||||
tailwindcss-animate@1.0.7:
|
||||
resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==}
|
||||
peerDependencies:
|
||||
|
|
@ -11236,7 +11256,7 @@ snapshots:
|
|||
react: 19.2.3
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
react-easy-sort: 1.8.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
tailwind-merge: 3.4.0
|
||||
tailwind-merge: 3.4.1
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
- '@types/react-dom'
|
||||
|
|
@ -12532,6 +12552,10 @@ snapshots:
|
|||
dependencies:
|
||||
language-subtag-registry: 0.3.23
|
||||
|
||||
lenis@1.3.17(react@19.2.3):
|
||||
optionalDependencies:
|
||||
react: 19.2.3
|
||||
|
||||
levn@0.4.1:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
|
|
@ -14394,6 +14418,8 @@ snapshots:
|
|||
|
||||
tailwind-merge@3.4.0: {}
|
||||
|
||||
tailwind-merge@3.4.1: {}
|
||||
|
||||
tailwindcss-animate@1.0.7(tailwindcss@4.1.18):
|
||||
dependencies:
|
||||
tailwindcss: 4.1.18
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 4.1 MiB |
BIN
surfsense_web/public/homepage/hero_tutorial/BSNCGif.gif
Normal file
|
After Width: | Height: | Size: 8.1 MiB |
BIN
surfsense_web/public/homepage/hero_tutorial/ConnectorFlowGif.gif
Normal file
|
After Width: | Height: | Size: 5.2 MiB |
BIN
surfsense_web/public/homepage/hero_tutorial/DocUploadGif.gif
Normal file
|
After Width: | Height: | Size: 7.2 MiB |
BIN
surfsense_web/public/homepage/hero_tutorial/ImageGenGif.gif
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
surfsense_web/public/homepage/hero_tutorial/LoginFlowGif.gif
Normal file
|
After Width: | Height: | Size: 4.1 MiB |
BIN
surfsense_web/public/homepage/hero_tutorial/PodcastGenGif.gif
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
|
After Width: | Height: | Size: 6 MiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 58 KiB |