© {new Date().getFullYear()} Thesys Inc. All Rights Reserved
diff --git a/docs/app/(home)/sections/Navbar/Navbar.tsx b/docs/app/(home)/sections/Navbar/Navbar.tsx
index 4f93a0e1a..20c992804 100644
--- a/docs/app/(home)/sections/Navbar/Navbar.tsx
+++ b/docs/app/(home)/sections/Navbar/Navbar.tsx
@@ -1,5 +1,3 @@
-"use client";
-
import { SiteMarketingHeader } from "@/components/site-marketing-header";
export function Navbar() {
diff --git a/docs/components/brand-logo.module.css b/docs/components/brand-logo.module.css
index 17a2f0f37..b24185178 100644
--- a/docs/components/brand-logo.module.css
+++ b/docs/components/brand-logo.module.css
@@ -1,3 +1,126 @@
+.thesysLink {
+ position: relative;
+ display: block;
+ flex-shrink: 0;
+ width: 1.5rem;
+ height: 1.5rem;
+ cursor: pointer;
+}
+
+.thesysMark {
+ position: absolute;
+ z-index: 1;
+ display: block;
+ width: 100%;
+ height: 100%;
+ transition: transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
+}
+
+.thesysRect,
+.thesysPath,
+.thesysGlow {
+ transition:
+ fill 160ms ease,
+ opacity 160ms ease,
+ transform 160ms ease;
+}
+
+.thesysRect {
+ fill: rgb(0 0 0 / 4%);
+}
+
+.thesysPath {
+ fill: #000;
+}
+
+.thesysGlow {
+ position: absolute;
+ inset: -0.25rem;
+ z-index: 0;
+ border-radius: 0.5rem;
+ background: rgb(0 0 0 / 5%);
+ opacity: 0;
+ transform: scale(0.5);
+}
+
+.thesysLink:hover .thesysMark,
+.thesysLink:focus-visible .thesysMark {
+ transform: scale(1.15) rotate(8deg);
+}
+
+.thesysLink:hover .thesysRect,
+.thesysLink:focus-visible .thesysRect {
+ fill: #000;
+}
+
+.thesysLink:hover .thesysPath,
+.thesysLink:focus-visible .thesysPath {
+ fill: #fff;
+}
+
+.thesysLink:hover .thesysGlow,
+.thesysLink:focus-visible .thesysGlow {
+ opacity: 1;
+ transform: scale(1);
+}
+
+.thesysLinkDark .thesysRect {
+ fill: rgb(255 255 255 / 12%);
+}
+
+.thesysLinkDark .thesysPath {
+ fill: #fff;
+}
+
+.thesysLinkDark .thesysGlow {
+ background: rgb(255 255 255 / 10%);
+}
+
+.thesysLinkDark:hover .thesysRect,
+.thesysLinkDark:focus-visible .thesysRect {
+ fill: #fff;
+}
+
+.thesysLinkDark:hover .thesysPath,
+.thesysLinkDark:focus-visible .thesysPath {
+ fill: #000;
+}
+
+.openuiLink {
+ display: flex;
+ align-items: center;
+ gap: 0.125rem;
+ text-decoration: none;
+}
+
+.openuiMascot {
+ position: relative;
+ flex-shrink: 0;
+ width: 2rem;
+ height: 2rem;
+}
+
+.openuiMascotImage {
+ position: absolute;
+ inset: 0;
+ display: block;
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+}
+
+.openuiText {
+ color: #000;
+ font-family: "Inter", sans-serif;
+ font-size: 15px;
+ font-weight: 600;
+ line-height: 1.5rem;
+}
+
+.openuiTextDark {
+ color: #fff;
+}
+
.githubButton {
position: relative;
display: inline-flex;
diff --git a/docs/components/brand-logo.tsx b/docs/components/brand-logo.tsx
index 4176b0d23..79c652cc0 100644
--- a/docs/components/brand-logo.tsx
+++ b/docs/components/brand-logo.tsx
@@ -1,104 +1,46 @@
"use client";
import svgPaths from "@/imports/svg-urruvoh2be";
-import { AnimatePresence, motion } from "motion/react";
import Link from "next/link";
import { useEffect, useState } from "react";
import styles from "./brand-logo.module.css";
-const COUNT_UP_DURATION = 1500;
-
-const LOGO_SPRING = { type: "spring", stiffness: 400, damping: 15 } as const;
-const LOGO_COLOR_TRANSITION = { duration: 0.25 } as const;
-const GLOW_TRANSITION = { duration: 0.2 } as const;
-
export type LogoVariant = "light" | "dark";
function cx(...classNames: Array) {
return classNames.filter(Boolean).join(" ");
}
-export function ThesysLogo({
- isHovered,
- onHoverChange,
- variant = "light",
-}: {
- isHovered: boolean;
- onHoverChange: (hovered: boolean) => void;
- variant?: LogoVariant;
-}) {
+export function ThesysLogo({ variant = "light" }: { variant?: LogoVariant }) {
const isDark = variant === "dark";
- const rectIdleColor = isDark ? "rgba(255,255,255,0.12)" : "rgba(0,0,0,0.04)";
- const rectHoveredColor = isDark ? "#ffffff" : "#000000";
- const pathIdleColor = isDark ? "#ffffff" : "#000000";
- const pathHoveredColor = isDark ? "#000000" : "#ffffff";
- const glowClass = isDark
- ? "absolute -inset-1 rounded-lg bg-white/10"
- : "absolute -inset-1 rounded-lg bg-black/5";
-
return (
onHoverChange(true)}
- onMouseLeave={() => onHoverChange(false)}
+ aria-label="Visit Thesys"
+ className={`${styles.thesysLink} ${isDark ? styles.thesysLinkDark : ""}`.trim()}
>
-
-
-
-
-
-
- {isHovered && (
-
- )}
-
+
+
);
}
export function OpenUILogo({ variant = "light" }: { variant?: LogoVariant }) {
const isDark = variant === "dark";
- const textClass = isDark
- ? "font-['Geist',sans-serif] font-semibold text-[15px] text-white leading-6"
- : "font-['Geist',sans-serif] font-semibold text-[15px] text-black leading-6";
+ const textClass = `${styles.openuiText} ${isDark ? styles.openuiTextDark : ""}`.trim();
return (
-
+
{/* Shiro mascot */}
-
+
{/* eslint-disable-next-line @next/next/no-img-element */}
-

+
OpenUI
@@ -144,33 +86,23 @@ function fetchGitHubStarCount(repo: string): Promise
{
return inflight;
}
-export function useGitHubStarCount(repo: string) {
+export function useGitHubStarCount(repo: string, enabled = true) {
const [count, setCount] = useState(null);
useEffect(() => {
+ if (!enabled) return;
+
let cancelled = false;
void fetchGitHubStarCount(repo).then((target) => {
if (cancelled || target === null) return;
- const startCount = Math.max(target - 50, 0);
- const startTime = performance.now();
-
- setCount(startCount);
-
- const tick = () => {
- if (cancelled) return;
- const progress = Math.min((performance.now() - startTime) / COUNT_UP_DURATION, 1);
- setCount(Math.round(startCount + (target - startCount) * progress));
- if (progress < 1) requestAnimationFrame(tick);
- };
-
- requestAnimationFrame(tick);
+ setCount(target);
});
return () => {
cancelled = true;
};
- }, [repo]);
+ }, [enabled, repo]);
return count;
}
diff --git a/docs/components/site-header.tsx b/docs/components/site-header.tsx
index ecd1fb2ac..5ca60fd38 100644
--- a/docs/components/site-header.tsx
+++ b/docs/components/site-header.tsx
@@ -1,6 +1,4 @@
-"use client";
-
-import { useState, type CSSProperties, type ReactNode } from "react";
+import type { CSSProperties, ReactNode } from "react";
import { OpenUILogo, ThesysLogo, type LogoVariant } from "./brand-logo";
import styles from "./site-header.module.css";
@@ -9,11 +7,9 @@ function joinClasses(...classNames: Array) {
}
export function SiteHeaderBrand({ variant = "light" }: { variant?: LogoVariant }) {
- const [isHovered, setIsHovered] = useState(false);
-
return (
-
+
diff --git a/docs/components/site-marketing-header.module.css b/docs/components/site-marketing-header.module.css
index 4a50436e0..7d95e71bd 100644
--- a/docs/components/site-marketing-header.module.css
+++ b/docs/components/site-marketing-header.module.css
@@ -52,6 +52,7 @@
cursor: pointer;
background: rgb(0 0 0 / 60%);
backdrop-filter: blur(12px);
+ animation: mobileBackdropIn 0.2s ease both;
}
.mobileTrayWrap {
@@ -61,6 +62,27 @@
right: 0;
z-index: 50;
pointer-events: none;
+ animation: mobileTrayIn 0.2s ease both;
+}
+
+@keyframes mobileBackdropIn {
+ from {
+ opacity: 0;
+ }
+}
+
+@keyframes mobileTrayIn {
+ from {
+ opacity: 0;
+ transform: translateY(-8px);
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .mobileBackdrop,
+ .mobileTrayWrap {
+ animation: none;
+ }
}
.mobileTray {
diff --git a/docs/components/site-marketing-header.tsx b/docs/components/site-marketing-header.tsx
index ecd176aa2..15e6bf8f6 100644
--- a/docs/components/site-marketing-header.tsx
+++ b/docs/components/site-marketing-header.tsx
@@ -10,7 +10,6 @@ import {
} from "@/components/site-primary-nav";
import { ThemeToggle } from "@/components/theme-toggle";
import { ArrowRight } from "lucide-react";
-import { AnimatePresence, motion } from "motion/react";
import { useTheme } from "next-themes";
import Link from "next/link";
import { useCallback, useEffect, useState, type ReactNode } from "react";
@@ -116,21 +115,8 @@ function MobileMenu({ onClose }: { onClose: () => void }) {
return (
<>
-
-
+
+
@@ -156,6 +142,7 @@ function MobileMenu({ onClose }: { onClose: () => void }) {
void }) {
-
+
>
);
}
@@ -183,14 +170,11 @@ export function SiteMarketingHeader({
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [isScrolled, setIsScrolled] = useState(borderMode === "always");
const { resolvedTheme } = useTheme();
- const [mounted, setMounted] = useState(false);
- useEffect(() => setMounted(true), []);
const resolvedBrandVariant =
- brandVariant ?? (mounted && resolvedTheme === "dark" ? "dark" : "light");
+ brandVariant ?? (resolvedTheme === "dark" ? "dark" : "light");
useEffect(() => {
if (borderMode === "always") {
- setIsScrolled(true);
return;
}
@@ -235,6 +219,7 @@ export function SiteMarketingHeader({
@@ -258,9 +243,7 @@ export function SiteMarketingHeader({
}
);
}
diff --git a/docs/components/theme-toggle.tsx b/docs/components/theme-toggle.tsx
index e264cc7fa..0b69b1758 100644
--- a/docs/components/theme-toggle.tsx
+++ b/docs/components/theme-toggle.tsx
@@ -3,7 +3,6 @@
import { cn } from "@/lib/cn";
import { Moon, Sun } from "@phosphor-icons/react";
import { useTheme } from "next-themes";
-import { useEffect, useState } from "react";
import styles from "./theme-toggle.module.css";
type ThemeToggleProps = {
@@ -15,9 +14,7 @@ type ThemeToggleProps = {
export function ThemeToggle({ className, onToggle, title, ariaLabel }: ThemeToggleProps) {
const { setTheme, resolvedTheme } = useTheme();
- const [mounted, setMounted] = useState(false);
- useEffect(() => setMounted(true), []);
- const isDark = mounted && resolvedTheme === "dark";
+ const isDark = resolvedTheme === "dark";
const nextTheme = isDark ? "light" : "dark";
const defaultLabel = `Switch to ${nextTheme} mode`;