From 8749593773c1ec66aacfcbb791c7c79e36858268 Mon Sep 17 00:00:00 2001
From: Musa
Date: Sat, 31 Jan 2026 21:25:59 -0800
Subject: [PATCH] adding logo cloud component to katanemo-www (#718)
---
apps/katanemo-www/public/logos/chase.svg | 68 ++
apps/katanemo-www/public/logos/hp.svg | 4 +
.../katanemo-www/public/logos/huggingface.svg | 9 +
apps/katanemo-www/public/logos/sandisk.svg | 3 +
apps/katanemo-www/public/logos/tmobile.svg | 165 +++++
apps/katanemo-www/src/app/page.tsx | 17 +-
.../src/components/LogoSlider.tsx | 608 ++++++++++++++++++
7 files changed, 870 insertions(+), 4 deletions(-)
create mode 100644 apps/katanemo-www/public/logos/chase.svg
create mode 100644 apps/katanemo-www/public/logos/hp.svg
create mode 100644 apps/katanemo-www/public/logos/huggingface.svg
create mode 100644 apps/katanemo-www/public/logos/sandisk.svg
create mode 100644 apps/katanemo-www/public/logos/tmobile.svg
create mode 100644 apps/katanemo-www/src/components/LogoSlider.tsx
diff --git a/apps/katanemo-www/public/logos/chase.svg b/apps/katanemo-www/public/logos/chase.svg
new file mode 100644
index 00000000..40d2ae76
--- /dev/null
+++ b/apps/katanemo-www/public/logos/chase.svg
@@ -0,0 +1,68 @@
+
+
+
diff --git a/apps/katanemo-www/public/logos/hp.svg b/apps/katanemo-www/public/logos/hp.svg
new file mode 100644
index 00000000..e53232c8
--- /dev/null
+++ b/apps/katanemo-www/public/logos/hp.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/apps/katanemo-www/public/logos/huggingface.svg b/apps/katanemo-www/public/logos/huggingface.svg
new file mode 100644
index 00000000..7e254084
--- /dev/null
+++ b/apps/katanemo-www/public/logos/huggingface.svg
@@ -0,0 +1,9 @@
+
diff --git a/apps/katanemo-www/public/logos/sandisk.svg b/apps/katanemo-www/public/logos/sandisk.svg
new file mode 100644
index 00000000..d5fe1092
--- /dev/null
+++ b/apps/katanemo-www/public/logos/sandisk.svg
@@ -0,0 +1,3 @@
+
diff --git a/apps/katanemo-www/public/logos/tmobile.svg b/apps/katanemo-www/public/logos/tmobile.svg
new file mode 100644
index 00000000..6616be40
--- /dev/null
+++ b/apps/katanemo-www/public/logos/tmobile.svg
@@ -0,0 +1,165 @@
+
+
+
+
diff --git a/apps/katanemo-www/src/app/page.tsx b/apps/katanemo-www/src/app/page.tsx
index 263da803..eb3aa532 100644
--- a/apps/katanemo-www/src/app/page.tsx
+++ b/apps/katanemo-www/src/app/page.tsx
@@ -1,5 +1,6 @@
import Image from "next/image";
import Link from "next/link";
+import LogoSlider from "../components/LogoSlider";
export default function HomePage() {
return (
@@ -23,25 +24,34 @@ export default function HomePage() {
Bringing industry-leading research and open-source technologies to
accelerate the development of AI agents.
+
+ Trusted by leading companies to deliver agents to production.
+
+
Models Research
- ↗
+
+ ↗
+
Plano - Open Source Agent Infrastructure
- ↗
+
+ ↗
+
- Move faster and more reliably by letting Katanemo do the heavy-lifting.
+ Move faster and more reliably by letting Katanemo do the
+ heavy-lifting.
-
React.ReactNode;
+ ariaLabel?: string;
+ className?: string;
+ style?: React.CSSProperties;
+}
+
+const ANIMATION_CONFIG = {
+ SMOOTH_TAU: 0.25,
+ MIN_COPIES: 2,
+ COPY_HEADROOM: 2,
+} as const;
+
+const toCssLength = (value?: number | string): string | undefined =>
+ typeof value === "number" ? `${value}px` : (value ?? undefined);
+
+const cx = (...parts: Array) =>
+ parts.filter(Boolean).join(" ");
+
+const isNodeItem = (
+ item: LogoItem,
+): item is Extract => "node" in item;
+
+const isImageItem = (
+ item: LogoItem,
+): item is Extract => "src" in item;
+
+const useResizeObserver = (
+ callback: () => void,
+ elements: Array>,
+ dependencies: React.DependencyList,
+) => {
+ useEffect(() => {
+ if (!window.ResizeObserver) {
+ const handleResize = () => callback();
+ window.addEventListener("resize", handleResize);
+ callback();
+ return () => window.removeEventListener("resize", handleResize);
+ }
+
+ const observers: Array = [];
+ for (const ref of elements) {
+ if (!ref.current) {
+ observers.push(null);
+ continue;
+ }
+ const observer = new ResizeObserver(callback);
+ observer.observe(ref.current);
+ observers.push(observer);
+ }
+
+ callback();
+
+ return () => {
+ for (const observer of observers) {
+ observer?.disconnect();
+ }
+ };
+ }, [callback, elements, ...elements, ...dependencies]);
+};
+
+const useImageLoader = (
+ seqRef: React.RefObject,
+ onLoad: () => void,
+ dependencies: React.DependencyList,
+) => {
+ useEffect(() => {
+ const images = seqRef.current?.querySelectorAll("img") ?? [];
+
+ if (images.length === 0) {
+ onLoad();
+ return;
+ }
+
+ let remainingImages = images.length;
+ const handleImageLoad = () => {
+ remainingImages -= 1;
+ if (remainingImages === 0) {
+ onLoad();
+ }
+ };
+
+ images.forEach((img) => {
+ const htmlImg = img as HTMLImageElement;
+ if (htmlImg.complete) {
+ handleImageLoad();
+ } else {
+ htmlImg.addEventListener("load", handleImageLoad, { once: true });
+ htmlImg.addEventListener("error", handleImageLoad, { once: true });
+ }
+ });
+
+ return () => {
+ images.forEach((img) => {
+ img.removeEventListener("load", handleImageLoad);
+ img.removeEventListener("error", handleImageLoad);
+ });
+ };
+ }, [onLoad, seqRef, ...dependencies]);
+};
+
+const useAnimationLoop = (
+ trackRef: React.RefObject,
+ targetVelocity: number,
+ seqWidth: number,
+ seqHeight: number,
+ isHovered: boolean,
+ hoverSpeed: number | undefined,
+ isVertical: boolean,
+) => {
+ const rafRef = useRef(null);
+ const lastTimestampRef = useRef(null);
+ const offsetRef = useRef(0);
+ const velocityRef = useRef(0);
+
+ useEffect(() => {
+ const track = trackRef.current;
+ if (!track) return;
+
+ const prefersReduced =
+ typeof window !== "undefined" &&
+ window.matchMedia &&
+ window.matchMedia("(prefers-reduced-motion: reduce)").matches;
+
+ const seqSize = isVertical ? seqHeight : seqWidth;
+
+ if (seqSize > 0) {
+ offsetRef.current = ((offsetRef.current % seqSize) + seqSize) % seqSize;
+ const transformValue = isVertical
+ ? `translate3d(0, ${-offsetRef.current}px, 0)`
+ : `translate3d(${-offsetRef.current}px, 0, 0)`;
+ track.style.transform = transformValue;
+ }
+
+ if (prefersReduced) {
+ track.style.transform = isVertical
+ ? "translate3d(0, 0, 0)"
+ : "translate3d(0, 0, 0)";
+ return () => {
+ lastTimestampRef.current = null;
+ };
+ }
+
+ const animate = (timestamp: number) => {
+ if (lastTimestampRef.current === null) {
+ lastTimestampRef.current = timestamp;
+ }
+
+ const deltaTime =
+ Math.max(0, timestamp - lastTimestampRef.current) / 1000;
+ lastTimestampRef.current = timestamp;
+
+ const target =
+ isHovered && hoverSpeed !== undefined ? hoverSpeed : targetVelocity;
+
+ const easingFactor =
+ 1 - Math.exp(-deltaTime / ANIMATION_CONFIG.SMOOTH_TAU);
+ velocityRef.current += (target - velocityRef.current) * easingFactor;
+
+ if (seqSize > 0) {
+ let nextOffset = offsetRef.current + velocityRef.current * deltaTime;
+ nextOffset = ((nextOffset % seqSize) + seqSize) % seqSize;
+ offsetRef.current = nextOffset;
+
+ const transformValue = isVertical
+ ? `translate3d(0, ${-offsetRef.current}px, 0)`
+ : `translate3d(${-offsetRef.current}px, 0, 0)`;
+ track.style.transform = transformValue;
+ }
+
+ rafRef.current = requestAnimationFrame(animate);
+ };
+
+ rafRef.current = requestAnimationFrame(animate);
+
+ return () => {
+ if (rafRef.current !== null) {
+ cancelAnimationFrame(rafRef.current);
+ rafRef.current = null;
+ }
+ lastTimestampRef.current = null;
+ };
+ }, [
+ trackRef,
+ targetVelocity,
+ seqWidth,
+ seqHeight,
+ isHovered,
+ hoverSpeed,
+ isVertical,
+ ]);
+};
+
+export const LogoLoop = React.memo(
+ ({
+ logos,
+ speed = 120,
+ direction = "left",
+ width = "100%",
+ logoHeight = 28,
+ gap = 32,
+ pauseOnHover,
+ hoverSpeed,
+ fadeOut = false,
+ fadeOutColor,
+ scaleOnHover = false,
+ renderItem,
+ ariaLabel = "Partner logos",
+ className,
+ style,
+ }) => {
+ const containerRef = useRef(null);
+ const trackRef = useRef(null);
+ const seqRef = useRef(null);
+
+ const [seqWidth, setSeqWidth] = useState(0);
+ const [seqHeight, setSeqHeight] = useState(0);
+ const [copyCount, setCopyCount] = useState(
+ ANIMATION_CONFIG.MIN_COPIES,
+ );
+ const [isHovered, setIsHovered] = useState(false);
+
+ const effectiveHoverSpeed = useMemo(() => {
+ if (hoverSpeed !== undefined) return hoverSpeed;
+ if (pauseOnHover === true) return 0;
+ if (pauseOnHover === false) return undefined;
+ return 0;
+ }, [hoverSpeed, pauseOnHover]);
+
+ const isVertical = direction === "up" || direction === "down";
+
+ const targetVelocity = useMemo(() => {
+ const magnitude = Math.abs(speed);
+ let directionMultiplier: number;
+ if (isVertical) {
+ directionMultiplier = direction === "up" ? 1 : -1;
+ } else {
+ directionMultiplier = direction === "left" ? 1 : -1;
+ }
+ const speedMultiplier = speed < 0 ? -1 : 1;
+ return magnitude * directionMultiplier * speedMultiplier;
+ }, [speed, direction, isVertical]);
+
+ const updateDimensions = useCallback(() => {
+ const containerWidth = containerRef.current?.clientWidth ?? 0;
+ const sequenceRect = seqRef.current?.getBoundingClientRect?.();
+ const sequenceWidth = sequenceRect?.width ?? 0;
+ const sequenceHeight = sequenceRect?.height ?? 0;
+ if (isVertical) {
+ const parentHeight =
+ containerRef.current?.parentElement?.clientHeight ?? 0;
+ if (containerRef.current && parentHeight > 0) {
+ const targetHeight = Math.ceil(parentHeight);
+ if (containerRef.current.style.height !== `${targetHeight}px`)
+ containerRef.current.style.height = `${targetHeight}px`;
+ }
+ if (sequenceHeight > 0) {
+ setSeqHeight(Math.ceil(sequenceHeight));
+ const viewport =
+ containerRef.current?.clientHeight ??
+ parentHeight ??
+ sequenceHeight;
+ const copiesNeeded =
+ Math.ceil(viewport / sequenceHeight) +
+ ANIMATION_CONFIG.COPY_HEADROOM;
+ setCopyCount(Math.max(ANIMATION_CONFIG.MIN_COPIES, copiesNeeded));
+ }
+ } else if (sequenceWidth > 0) {
+ setSeqWidth(Math.ceil(sequenceWidth));
+ const copiesNeeded =
+ Math.ceil(containerWidth / sequenceWidth) +
+ ANIMATION_CONFIG.COPY_HEADROOM;
+ setCopyCount(Math.max(ANIMATION_CONFIG.MIN_COPIES, copiesNeeded));
+ }
+ }, [isVertical]);
+
+ useResizeObserver(
+ updateDimensions,
+ [containerRef, seqRef],
+ [logos, gap, logoHeight, isVertical],
+ );
+
+ useImageLoader(seqRef, updateDimensions, [
+ logos,
+ gap,
+ logoHeight,
+ isVertical,
+ ]);
+
+ useAnimationLoop(
+ trackRef,
+ targetVelocity,
+ seqWidth,
+ seqHeight,
+ isHovered,
+ effectiveHoverSpeed,
+ isVertical,
+ );
+
+ const cssVariables = useMemo(
+ () =>
+ ({
+ "--logoloop-gap": `${gap}px`,
+ "--logoloop-logoHeight": `${logoHeight}px`,
+ ...(fadeOutColor && { "--logoloop-fadeColor": fadeOutColor }),
+ }) as React.CSSProperties,
+ [gap, logoHeight, fadeOutColor],
+ );
+
+ const rootClasses = useMemo(
+ () =>
+ cx(
+ "relative group",
+ isVertical
+ ? "overflow-hidden h-full inline-block"
+ : "overflow-x-hidden",
+ "[--logoloop-gap:32px]",
+ "[--logoloop-logoHeight:28px]",
+ "[--logoloop-fadeColorAuto:#ffffff]",
+ "dark:[--logoloop-fadeColorAuto:#0b0b0b]",
+ scaleOnHover && "py-[calc(var(--logoloop-logoHeight)*0.1)]",
+ className,
+ ),
+ [isVertical, scaleOnHover, className],
+ );
+
+ const handleMouseEnter = useCallback(() => {
+ if (effectiveHoverSpeed !== undefined) setIsHovered(true);
+ }, [effectiveHoverSpeed]);
+ const handleMouseLeave = useCallback(() => {
+ if (effectiveHoverSpeed !== undefined) setIsHovered(false);
+ }, [effectiveHoverSpeed]);
+
+ const renderLogoItem = useCallback(
+ (item: LogoItem, key: React.Key) => {
+ if (renderItem) {
+ return (
+
+ {renderItem(item, key)}
+
+ );
+ }
+
+ const content = isNodeItem(item) ? (
+
+ {item.node}
+
+ ) : (
+
+ );
+
+ const itemAriaLabel = isNodeItem(item)
+ ? (item.ariaLabel ?? item.title)
+ : (item.alt ?? item.title);
+
+ const inner = item.href ? (
+
+ {content}
+
+ ) : (
+ content
+ );
+
+ return (
+
+ {inner}
+
+ );
+ },
+ [isVertical, scaleOnHover, renderItem],
+ );
+
+ const logoLists = useMemo(
+ () =>
+ Array.from({ length: copyCount }, (_, copyIndex) => (
+ 0}
+ ref={copyIndex === 0 ? seqRef : undefined}
+ >
+ {logos.map((item, itemIndex) =>
+ renderLogoItem(item, `${copyIndex}-${itemIndex}`),
+ )}
+
+ )),
+ [copyCount, logos, renderLogoItem, isVertical],
+ );
+
+ const containerStyle = useMemo(
+ (): React.CSSProperties => ({
+ width: isVertical
+ ? toCssLength(width) === "100%"
+ ? undefined
+ : toCssLength(width)
+ : (toCssLength(width) ?? "100%"),
+ ...cssVariables,
+ ...style,
+ }),
+ [width, cssVariables, style, isVertical],
+ );
+
+ return (
+
+ {fadeOut &&
+ (isVertical ? (
+ <>
+
+
+ >
+ ) : (
+ <>
+
+
+ >
+ ))}
+
+
+ {logoLists}
+
+
+ );
+ },
+);
+
+LogoLoop.displayName = "LogoLoop";
+
+const logos: LogoItem[] = [
+ { src: "/logos/chase.svg", alt: "Chase" },
+ { src: "/logos/hp.svg", alt: "HP" },
+ { src: "/logos/huggingface.svg", alt: "Hugging Face" },
+ { src: "/logos/sandisk.svg", alt: "SanDisk" },
+ { src: "/logos/tmobile.svg", alt: "T-Mobile" },
+];
+
+export default function LogoSlider() {
+ return (
+
+ {
+ if (isImageItem(item)) {
+ return (
+
+ );
+ }
+
+ return {item.node};
+ }}
+ className="relative"
+ style={{
+ WebkitMaskImage:
+ "linear-gradient(to right, transparent 0%, black 10%, black 90%, transparent 100%)",
+ maskImage:
+ "linear-gradient(to right, transparent 0%, black 10%, black 90%, transparent 100%)",
+ }}
+ />
+
+ );
+}