diff --git a/docs/app/(home)/components/Accordion/Accordion.tsx b/docs/app/(home)/components/Accordion/Accordion.tsx new file mode 100644 index 000000000..3ddc498a4 --- /dev/null +++ b/docs/app/(home)/components/Accordion/Accordion.tsx @@ -0,0 +1,87 @@ +"use client"; + +import { AnimatePresence, motion, type TargetAndTransition, type Transition } from "motion/react"; +import type { ReactNode } from "react"; + +const NO_SHADOW = "0px 0px 0px rgba(0,0,0,0)"; +const DEFAULT_PANEL_INITIAL = { height: 0, opacity: 0 }; +const DEFAULT_PANEL_ANIMATE = { height: "auto", opacity: 1 }; +const DEFAULT_PANEL_EXIT = { height: 0, opacity: 0 }; + +interface AccordionItemProps { + open: boolean; + expandedHeight: number; + collapsedHeight: number; + className?: string; + activeShadow?: string; + zIndexOpen?: number; + zIndexClosed?: number; + transition?: Transition; + onActivate?: () => void; + children: ReactNode; +} + +export function AccordionItem({ + open, + expandedHeight, + collapsedHeight, + className, + activeShadow = NO_SHADOW, + zIndexOpen = 2, + zIndexClosed = 1, + transition, + onActivate, + children, +}: AccordionItemProps) { + return ( + + {children} + + ); +} + +interface AccordionPanelProps { + open: boolean; + className?: string; + transition?: Transition; + initial?: TargetAndTransition; + animate?: TargetAndTransition; + exit?: TargetAndTransition; + children: ReactNode; +} + +export function AccordionPanel({ + open, + className, + transition, + initial = DEFAULT_PANEL_INITIAL, + animate = DEFAULT_PANEL_ANIMATE, + exit = DEFAULT_PANEL_EXIT, + children, +}: AccordionPanelProps) { + return ( + + {open && ( + + {children} + + )} + + ); +} diff --git a/docs/app/(home)/components/BuildChatSection/BuildChatSection.tsx b/docs/app/(home)/components/BuildChatSection/BuildChatSection.tsx deleted file mode 100644 index 59510327d..000000000 --- a/docs/app/(home)/components/BuildChatSection/BuildChatSection.tsx +++ /dev/null @@ -1,130 +0,0 @@ -"use client"; - -import dashboardImg from "@/public/images/home/d67b5e94653944c1d0d4998c6b169c37f98060ad.png"; -import Image from "next/image"; -import { useEffect, useRef, useState } from "react"; -import { CopyIcon } from "../shared/shared"; -import styles from "./BuildChatSection.module.css"; - -// --------------------------------------------------------------------------- -// Constants -// --------------------------------------------------------------------------- - -const CARD_SHADOW = "0px 8px 16px -4px rgba(22,34,51,0.08)"; -const COMMAND = "npx @openuidev/cli@latest create"; - -// --------------------------------------------------------------------------- -// Sub-components -// --------------------------------------------------------------------------- - -function SectionTitle() { - return

Build a Generative UI chat in minutes

; -} - -function CtaButton() { - const [copied, setCopied] = useState(false); - const resetTimeoutRef = useRef | null>(null); - - useEffect(() => { - return () => { - if (resetTimeoutRef.current) { - clearTimeout(resetTimeoutRef.current); - } - }; - }, []); - - const handleClick = async () => { - if (copied) return; - - try { - await navigator.clipboard.writeText(COMMAND); - setCopied(true); - resetTimeoutRef.current = setTimeout(() => { - setCopied(false); - }, 3000); - } catch { - setCopied(false); - } - }; - - return ( -
- -
- ); -} - -function DashboardIllustration() { - return ( - AI chat dashboard illustration - ); -} - -// --------------------------------------------------------------------------- -// Main component -// --------------------------------------------------------------------------- - -export function BuildChatSection() { - return ( -
-
-
- {/* Border + shadow overlay */} - -
-
- ); -} diff --git a/docs/app/(home)/components/Button/Button.module.css b/docs/app/(home)/components/Button/Button.module.css new file mode 100644 index 000000000..a5657db06 --- /dev/null +++ b/docs/app/(home)/components/Button/Button.module.css @@ -0,0 +1,35 @@ +.clipboardCommandButton { + font-family: var(--font-geist-mono); +} + +.copyIcon { + height: 1rem; + width: 1rem; + flex-shrink: 0; +} + +.copyIconFrame { + position: relative; + display: flex; + height: 1rem; + width: 1rem; + align-items: center; + justify-content: center; +} + +.iconLayer { + position: absolute; + transition: + opacity 0.3s ease, + transform 0.3s ease; +} + +.iconVisible { + opacity: 1; + transform: scale(1); +} + +.iconHidden { + opacity: 0; + transform: scale(0.5); +} diff --git a/docs/app/(home)/components/Button/Button.tsx b/docs/app/(home)/components/Button/Button.tsx new file mode 100644 index 000000000..18779c396 --- /dev/null +++ b/docs/app/(home)/components/Button/Button.tsx @@ -0,0 +1,174 @@ +"use client"; + +import { Check, Copy } from "lucide-react"; +import Link from "next/link"; +import { + useEffect, + useRef, + useState, + type ButtonHTMLAttributes, + type ReactNode, +} from "react"; +import styles from "./Button.module.css"; + +type ButtonType = ButtonHTMLAttributes["type"]; +const COPY_FEEDBACK_MS = 3000; + +function CopyIcon({ color = "white" }: { color?: string }) { + return ; +} + +function CheckIcon({ color = "white" }: { color?: string }) { + return ; +} + +interface CopyStatusIconProps { + copied: boolean; + className?: string; + frameClassName?: string; + color?: string; +} + +function CopyStatusIcon({ + copied, + className = "", + frameClassName = "", + color = "white", +}: CopyStatusIconProps) { + return ( + + + + + + + + + ); +} + +interface ClipboardCommandButtonProps { + command: string; + children: ReactNode; + className?: string; + iconContainerClassName?: string; + iconFrameClassName?: string; + iconPosition?: "start" | "end"; + copyIconColor?: string; + type?: ButtonType; +} + +export function ClipboardCommandButton({ + command, + children, + className = "", + iconContainerClassName = "", + iconFrameClassName = "", + iconPosition = "end", + copyIconColor = "white", + type = "button", +}: ClipboardCommandButtonProps) { + const [copied, setCopied] = useState(false); + const resetTimeoutRef = useRef | null>(null); + + useEffect(() => { + return () => { + if (resetTimeoutRef.current) { + clearTimeout(resetTimeoutRef.current); + } + }; + }, []); + + const handleClick = async () => { + if (copied) return; + + try { + await navigator.clipboard.writeText(command); + setCopied(true); + if (resetTimeoutRef.current) { + clearTimeout(resetTimeoutRef.current); + } + resetTimeoutRef.current = setTimeout(() => { + setCopied(false); + }, COPY_FEEDBACK_MS); + } catch { + setCopied(false); + } + }; + + const icon = ( + + + + ); + + return ( + + ); +} + +interface PillLinkProps { + href: string; + children: ReactNode; + className?: string; + arrow?: ReactNode; + external?: boolean; +} + +export function PillLink({ + href, + children, + className = "", + arrow, + external = false, +}: PillLinkProps) { + const content = ( + <> + {children} + {arrow} + + ); + + if (external) { + return ( + + {content} + + ); + } + + return ( + + {content} + + ); +} diff --git a/docs/app/(home)/components/FadeInSection/FadeInSection.module.css b/docs/app/(home)/components/FadeInSection/FadeInSection.module.css deleted file mode 100644 index 88de80d08..000000000 --- a/docs/app/(home)/components/FadeInSection/FadeInSection.module.css +++ /dev/null @@ -1,3 +0,0 @@ -.root { - width: 100%; -} diff --git a/docs/app/(home)/components/FadeInSection/FadeInSection.tsx b/docs/app/(home)/components/FadeInSection/FadeInSection.tsx deleted file mode 100644 index adf65cea7..000000000 --- a/docs/app/(home)/components/FadeInSection/FadeInSection.tsx +++ /dev/null @@ -1,17 +0,0 @@ -"use client"; - -import type { ReactNode } from "react"; -import styles from "./FadeInSection.module.css"; - -interface FadeInSectionProps { - children: ReactNode; - className?: string; -} - -export function FadeInSection({ children, className = "" }: FadeInSectionProps) { - return ( -
- {children} -
- ); -} diff --git a/docs/app/(home)/components/FeatureList/FeatureList.module.css b/docs/app/(home)/components/FeatureList/FeatureList.module.css new file mode 100644 index 000000000..88e8ed1e4 --- /dev/null +++ b/docs/app/(home)/components/FeatureList/FeatureList.module.css @@ -0,0 +1,108 @@ +.featureIcon { + display: flex; + height: 2.25rem; + width: 2.25rem; + flex-shrink: 0; + align-items: center; + justify-content: center; + border: 1px solid var(--home-border-default); + border-radius: var(--home-radius-pill); + background: var(--home-color-surface); +} + +.featureIconSvg { + height: 18px; + width: 18px; +} + +.desktopList { + display: none; +} + +.mobileList { + display: block; +} + +.desktopRow { + display: flex; + height: 72px; + align-items: center; + justify-content: space-between; +} + +.desktopRowLead { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.desktopTitle, +.desktopDescription { + font-family: "Inter Display", sans-serif; + font-size: 22px; + line-height: 1.4; +} + +.desktopTitle { + color: #000; +} + +.desktopDescription { + text-align: right; + color: var(--home-color-text-muted); +} + +.mobileRow { + display: flex; + align-items: center; + gap: 0.375rem; + padding-block: 1rem; +} + +.mobileCopy { + display: flex; + flex: 1; + flex-direction: column; + gap: 0.25rem; +} + +.mobileTitle, +.mobileDescription { + font-family: "Inter", sans-serif; + font-size: 16px; +} + +.mobileTitle { + font-weight: 500; + line-height: 1.25; + color: #000; +} + +.mobileDescription { + line-height: 1.4; + color: var(--home-color-text-muted); +} + +.divider { + height: 1px; + width: 100vw; + margin-left: calc(-50vw + 50%); +} + +.desktopDivider { + background: var(--home-border-default); +} + +.mobileDivider { + background: var(--home-color-surface-muted); +} + +@media (min-width: 1024px) { + .desktopList { + display: block; + } + + .mobileList { + display: none; + } +} diff --git a/docs/app/(home)/components/FeatureList/FeatureList.tsx b/docs/app/(home)/components/FeatureList/FeatureList.tsx new file mode 100644 index 000000000..5040a3079 --- /dev/null +++ b/docs/app/(home)/components/FeatureList/FeatureList.tsx @@ -0,0 +1,104 @@ +"use client"; + +import styles from "./FeatureList.module.css"; + +export interface FeatureListItem { + title: string; + description: string; + iconPath: string; +} + +interface FeatureListProps { + items: FeatureListItem[]; +} + +function FeatureIcon({ path, index }: { path: string; index: number }) { + const clipId = `clip_feat_${index}`; + + return ( +
+ + + + + + + + + + +
+ ); +} + +function DesktopFeatureRow({ + item, + index, +}: { + item: FeatureListItem; + index: number; +}) { + return ( +
+
+
+ +
+ {item.title} +
+ {item.description} +
+ ); +} + +function MobileFeatureRow({ + item, + index, + iconIndexOffset, +}: { + item: FeatureListItem; + index: number; + iconIndexOffset: number; +}) { + return ( +
+
+ {item.title} + {item.description} +
+
+ +
+
+ ); +} + +function Divider({ className }: { className: string }) { + return
; +} + +export function FeatureList({ items }: FeatureListProps) { + const lastItemIndex = items.length - 1; + + return ( + <> +
+ {items.map((item, index) => ( +
+ + {index < lastItemIndex && } +
+ ))} +
+ +
+ {items.map((item, index) => ( +
+ + {index < lastItemIndex && } +
+ ))} +
+ + ); +} diff --git a/docs/app/(home)/components/FeaturesSection/FeaturesSection.module.css b/docs/app/(home)/components/FeaturesSection/FeaturesSection.module.css deleted file mode 100644 index 8dac57417..000000000 --- a/docs/app/(home)/components/FeaturesSection/FeaturesSection.module.css +++ /dev/null @@ -1,191 +0,0 @@ -.section { - width: 100%; - padding-inline: 1.25rem; -} - -.container { - max-width: 75rem; - margin-inline: auto; -} - -.featureIcon { - display: flex; - height: 2.25rem; - width: 2.25rem; - flex-shrink: 0; - align-items: center; - justify-content: center; - border: 1px solid rgb(0 0 0 / 10%); - border-radius: 999px; - background: #fff; -} - -.featureIconSvg { - height: 18px; - width: 18px; -} - -.desktopList { - display: none; -} - -.mobileList { - display: block; -} - -.desktopRow { - display: flex; - height: 72px; - align-items: center; - justify-content: space-between; -} - -.desktopRowLead { - display: flex; - align-items: center; - gap: 0.75rem; -} - -.desktopTitle, -.desktopDescription { - font-family: "Inter Display", sans-serif; - font-size: 22px; - line-height: 1.4; -} - -.desktopTitle { - color: #000; -} - -.desktopDescription { - text-align: right; - color: rgb(0 0 0 / 40%); -} - -.mobileRow { - display: flex; - align-items: center; - gap: 0.375rem; - padding-block: 1rem; -} - -.mobileCopy { - display: flex; - flex: 1; - flex-direction: column; - gap: 0.25rem; -} - -.mobileTitle, -.mobileDescription { - font-family: "Inter", sans-serif; - font-size: 16px; -} - -.mobileTitle { - font-weight: 500; - line-height: 1.25; - color: #000; -} - -.mobileDescription { - line-height: 1.4; - color: rgb(0 0 0 / 40%); -} - -.divider { - height: 1px; - width: 100vw; - margin-left: calc(-50vw + 50%); -} - -.desktopDivider { - background: rgb(0 0 0 / 10%); -} - -.mobileDivider { - background: rgb(0 0 0 / 8%); -} - -.ctaWrap { - display: flex; - justify-content: center; - margin-top: 2.5rem; -} - -.ctaLink { - width: 100%; - max-width: 280px; - text-decoration: none; -} - -.ctaButton { - display: flex; - height: 3rem; - width: 100%; - align-items: center; - justify-content: center; - padding-inline: 1rem; - border: 1.25px solid rgb(0 0 0 / 8%); - border-radius: 999px; - background: #fff; - box-shadow: var(--features-cta-shadow, none); - color: #000; - cursor: pointer; - transition: - transform 0.2s ease, - box-shadow 0.2s ease; - font-family: "Inter", sans-serif; - font-size: 16px; - font-weight: 500; - line-height: 1.5rem; -} - -.ctaButton:hover { - transform: scale(0.99); - box-shadow: none; -} - -.mobileLabel { - display: inline; -} - -.desktopLabel { - display: none; -} - -@media (min-width: 1024px) { - .section { - padding-inline: 2rem; - } - - .desktopList { - display: block; - } - - .mobileList { - display: none; - } - - .ctaWrap { - margin-top: 5rem; - } - - .ctaLink { - width: auto; - max-width: none; - } - - .ctaButton { - width: auto; - font-size: 18px; - } - - .mobileLabel { - display: none; - } - - .desktopLabel { - display: inline; - } -} diff --git a/docs/app/(home)/components/FeaturesSection/FeaturesSection.tsx b/docs/app/(home)/components/FeaturesSection/FeaturesSection.tsx deleted file mode 100644 index c64d53872..000000000 --- a/docs/app/(home)/components/FeaturesSection/FeaturesSection.tsx +++ /dev/null @@ -1,164 +0,0 @@ -"use client"; - -import svgPaths from "@/imports/svg-urruvoh2be"; -import Link from "next/link"; -import type { CSSProperties } from "react"; -import { BUTTON_SHADOW } from "../shared/shared"; -import styles from "./FeaturesSection.module.css"; - -// --------------------------------------------------------------------------- -// Data -// --------------------------------------------------------------------------- - -interface Feature { - title: string; - description: string; - iconPath: string; -} - -const FEATURES: Feature[] = [ - { - title: "Performance Optimized", - description: "Up to 3.0x faster rendering than json-render", - iconPath: svgPaths.p7658f00, - }, - { - title: "Token efficient", - description: "Up to 67.1% lesser tokens than json-render", - iconPath: svgPaths.p2a8ddd80, - }, - { - title: "Native Types", - description: "Performant and memory safe", - iconPath: svgPaths.p10e86100, - }, - { - title: "Works across platforms", - description: "JS Runtime. Native support for iOS & Android coming soon", - iconPath: svgPaths.p2cbb5d00, - }, - { - title: "Streaming Native", - description: "Streaming and partial responses", - iconPath: svgPaths.p33780400, - }, - { - title: "Interactive", - description: "Handles inputs and interactive flows", - iconPath: svgPaths.p17c7f700, - }, - { - title: "Safe by Default", - description: "No arbitrary code execution", - iconPath: svgPaths.p16eec200, - }, -]; - -const LAST_FEATURE_INDEX = FEATURES.length - 1; -const FEATURES_CTA_STYLE = { - "--features-cta-shadow": BUTTON_SHADOW, -} as CSSProperties; - -// --------------------------------------------------------------------------- -// Sub-components -// --------------------------------------------------------------------------- - -function FeatureIcon({ path, index }: { path: string; index: number }) { - const clipId = `clip_feat_${index}`; - return ( -
- - - - - - - - - - -
- ); -} - -function DesktopFeatureRow({ feature, index }: { feature: Feature; index: number }) { - return ( -
-
-
- -
- - {feature.title} - -
- - {feature.description} - -
- ); -} - -function MobileFeatureRow({ feature, index }: { feature: Feature; index: number }) { - return ( -
-
- - {feature.title} - - - {feature.description} - -
-
- -
-
- ); -} - -function Divider({ className = "" }: { className?: string }) { - return
; -} - -// --------------------------------------------------------------------------- -// Main component -// --------------------------------------------------------------------------- - -export function FeaturesSection() { - return ( -
-
- {/* Desktop */} -
- {FEATURES.map((f, i) => ( -
- - {i < LAST_FEATURE_INDEX && } -
- ))} -
- - {/* Mobile */} -
- {FEATURES.map((f, i) => ( -
- - {i < LAST_FEATURE_INDEX && } -
- ))} -
- - {/* CTA button */} -
- - - Detailed comparison - View Comparison - - -
-
-
- ); -} diff --git a/docs/app/(home)/components/GradientDivider/GradientDivider.module.css b/docs/app/(home)/components/GradientDivider/GradientDivider.module.css deleted file mode 100644 index 73efe5a11..000000000 --- a/docs/app/(home)/components/GradientDivider/GradientDivider.module.css +++ /dev/null @@ -1,24 +0,0 @@ -.divider { - display: flex; - width: 100%; - flex-direction: column; - gap: 2px; - transform: rotate(180deg); -} - -.bar { - height: var(--bar-h-mobile); - width: 100%; - flex-shrink: 0; - background: rgb(0 0 0 / 4%); -} - -@media (min-width: 1024px) { - .divider { - gap: 6px; - } - - .bar { - height: var(--bar-h); - } -} diff --git a/docs/app/(home)/components/GradientDivider/GradientDivider.tsx b/docs/app/(home)/components/GradientDivider/GradientDivider.tsx deleted file mode 100644 index b61b4b41a..000000000 --- a/docs/app/(home)/components/GradientDivider/GradientDivider.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import type { CSSProperties } from "react"; -import styles from "./GradientDivider.module.css"; - -const BAR_HEIGHTS = [ - 60, 48, 32, 20.942, 16.754, 12.565, 10.471, 8.377, 6.283, 4.188, 3.141, 2.094, 1.047, -]; - -export function GradientDivider({ direction = "down" }: { direction?: "down" | "up" }) { - const bars = direction === "up" ? [...BAR_HEIGHTS].reverse() : BAR_HEIGHTS; - - return ( -
- {bars.map((h) => ( -
- ))} -
- ); -} diff --git a/docs/app/(home)/components/StepsSection/StepsSection.module.css b/docs/app/(home)/components/StepsAccordion/StepsAccordion.module.css similarity index 82% rename from docs/app/(home)/components/StepsSection/StepsSection.module.css rename to docs/app/(home)/components/StepsAccordion/StepsAccordion.module.css index 43a9021d5..e73b176b8 100644 --- a/docs/app/(home)/components/StepsSection/StepsSection.module.css +++ b/docs/app/(home)/components/StepsAccordion/StepsAccordion.module.css @@ -1,20 +1,13 @@ -.section { - width: 100%; - padding-inline: 1.25rem; -} - -.container { - max-width: 75rem; - margin-inline: auto; -} - .card { + border: 1px solid var(--home-border-subtle); border-radius: 18px; - background: #fff; + background: var(--home-color-surface); + box-shadow: var(--home-shadow-elevated); } .desktopSteps { display: none; + height: 733px; overflow: hidden; } @@ -29,18 +22,18 @@ flex-shrink: 0; align-items: center; justify-content: center; - border: 1px solid rgb(0 0 0 / 10%); - border-radius: 999px; + border: 1px solid var(--home-border-default); + border-radius: var(--home-radius-pill); } .stepBadgeActive { - background: #000; + background: var(--home-color-surface-inverse); color: #fff; } .stepBadgeInactive { - background: #fff; - color: #000; + background: var(--home-color-surface); + color: var(--home-color-text-primary); } .stepBadgeLabel { @@ -53,7 +46,7 @@ .divider { height: 1px; width: 100%; - background: rgb(0 0 0 / 8%); + background: var(--home-color-surface-muted); } .stepDetails { @@ -66,7 +59,7 @@ font-family: "Inter Display", sans-serif; font-size: 18px; line-height: 1.2; - color: rgb(0 0 0 / 40%); + color: var(--home-color-text-muted); } .stepDetailsTitle { @@ -102,6 +95,9 @@ .mobileIllustrationScale { position: absolute; inset: 0; + width: 610px; + height: 432px; + transform: scale(var(--mobile-illustration-scale, 1)); transform-origin: top left; } @@ -126,6 +122,7 @@ flex: 1; flex-direction: column; gap: 1.5rem; + padding-right: 60px; } .desktopStepTitle { @@ -188,10 +185,6 @@ } @media (min-width: 1024px) { - .section { - padding-inline: 2rem; - } - .desktopSteps { display: block; } diff --git a/docs/app/(home)/components/StepsAccordion/StepsAccordion.tsx b/docs/app/(home)/components/StepsAccordion/StepsAccordion.tsx new file mode 100644 index 000000000..8faed721c --- /dev/null +++ b/docs/app/(home)/components/StepsAccordion/StepsAccordion.tsx @@ -0,0 +1,231 @@ +"use client"; + +import { useCallback, useEffect, useRef, useState, type ComponentType } from "react"; +import { AccordionItem, AccordionPanel } from "../Accordion/Accordion"; +import styles from "./StepsAccordion.module.css"; + +export interface StepsAccordionItem { + number: number; + title: string; + description: string; + detailsTitle?: string; + details: string[]; + Illustration: ComponentType; +} + +interface StepsAccordionProps { + steps: StepsAccordionItem[]; +} + +const EXPANDED_HEIGHT = 480; +const COLLAPSED_HEIGHT = 84; +const ACTIVE_STEP_SHADOW = "0px 1px 3px rgba(22,34,51,0.08), 0px 10px 20px rgba(22,34,51,0.03)"; + +const TRANSITION = { + expand: { duration: 0.5, ease: [0.4, 0, 0.2, 1] as const }, + content: { duration: 0.3, delay: 0.15 }, + preview: { duration: 0.4, delay: 0.1 }, + mobile: { duration: 0.35, ease: [0.4, 0, 0.2, 1] as const }, +} as const; + +function StepBadge({ num, isActive }: { num: number; isActive: boolean }) { + return ( +
+ {num} +
+ ); +} + +function Divider() { + return
; +} + +function StepDetails({ step, hideDetails }: { step: StepsAccordionItem; hideDetails?: boolean }) { + return ( +
+

{step.description}

+ {!hideDetails && step.details.length > 0 && ( +
+ {step.detailsTitle &&

{step.detailsTitle}

} +
    + {step.details.map((line) => ( +
  • {line}
  • + ))} +
+
+ )} +
+ ); +} + +function StepIllustration({ step, mobile }: { step: StepsAccordionItem; mobile?: boolean }) { + const containerRef = useRef(null); + const scaleRef = useRef(null); + const [scale, setScale] = useState(1); + + useEffect(() => { + if (!mobile || !containerRef.current) return; + + const updateScale = () => { + if (containerRef.current) { + setScale(containerRef.current.offsetWidth / 610); + } + }; + + updateScale(); + const observer = new ResizeObserver(updateScale); + observer.observe(containerRef.current); + + return () => observer.disconnect(); + }, [mobile]); + + useEffect(() => { + if (!mobile || !scaleRef.current) return; + scaleRef.current.style.setProperty("--mobile-illustration-scale", String(scale)); + }, [mobile, scale]); + + const Illustration = step.Illustration; + + if (!Illustration) { + return
; + } + + if (mobile) { + return ( +
+
+ +
+
+ ); + } + + return ( +
+ +
+ ); +} + +function DesktopStep({ + step, + isActive, + onActivate, +}: { + step: StepsAccordionItem; + isActive: boolean; + onActivate: () => void; +}) { + return ( + +
+ + +
+

{step.title}

+ + + + +
+
+ + + + +
+ ); +} + +function MobileStep({ + step, + isActive, + onToggle, +}: { + step: StepsAccordionItem; + isActive: boolean; + onToggle: () => void; +}) { + return ( +
+ + + +
+
+ +
+
+ +
+
+
+
+ ); +} + +export function StepsAccordion({ steps }: StepsAccordionProps) { + const [activeStep, setActiveStep] = useState(1); + const lastStepIndex = steps.length - 1; + const activate = useCallback((stepNumber: number) => setActiveStep(stepNumber), []); + + return ( +
+
+ {steps.map((step, index) => ( +
+ activate(step.number)} + /> + {index < lastStepIndex && } +
+ ))} +
+ +
+ {steps.map((step, index) => ( +
+ activate(step.number)} + /> + {index < lastStepIndex && } +
+ ))} +
+
+ ); +} diff --git a/docs/app/(home)/components/StepsSection/StepsSection.tsx b/docs/app/(home)/components/StepsSection/StepsSection.tsx deleted file mode 100644 index c21a70d9c..000000000 --- a/docs/app/(home)/components/StepsSection/StepsSection.tsx +++ /dev/null @@ -1,326 +0,0 @@ -"use client"; - -import LlmRespondsInOpenUiLang from "@/imports/LlmRespondsInOpenUiLang"; -import OpenUiGeneratesSchema from "@/imports/OpenUiGeneratesSchema"; -import OpenUiRendererRendersIt from "@/imports/OpenUiRendererRendersIt-43-427"; -import YouRegisterComponents from "@/imports/YouRegisterComponents-43-365"; -import { AnimatePresence, motion } from "motion/react"; -import { useCallback, useEffect, useRef, useState, type ComponentType } from "react"; -import styles from "./StepsSection.module.css"; - -// --------------------------------------------------------------------------- -// Types & data -// --------------------------------------------------------------------------- - -interface Step { - number: number; - title: string; - description: string; - detailsTitle?: string; - details: string[]; -} - -const STEPS: Step[] = [ - { - number: 1, - title: "You define your library", - description: "Register components with defineComponent and createLibrary.", - details: [], - }, - { - number: 2, - title: "OpenUI generates system prompt", - description: - "Generate a system prompt from your library with the OpenUI CLI or library.prompt() and send it to the LLM.", - details: [], - }, - { - number: 3, - title: "LLM responds in OpenUI Lang", - description: "The model returns token-efficient, line-oriented OpenUI Lang (not markdown).", - details: [], - }, - { - number: 4, - title: "Renderer parses and renders UI", - description: "Renderer parses the output and renders interactive UI in real time.", - details: [], - }, -]; - -/** Maps step number → Figma illustration component */ -const STEP_ILLUSTRATIONS: Record = { - 1: YouRegisterComponents, - 2: OpenUiGeneratesSchema, - 3: LlmRespondsInOpenUiLang, - 4: OpenUiRendererRendersIt, -}; - -// --------------------------------------------------------------------------- -// Layout constants -// --------------------------------------------------------------------------- - -const EXPANDED_HEIGHT = 480; -const COLLAPSED_HEIGHT = 84; -const LAST_STEP_INDEX = STEPS.length - 1; -const TOTAL_DESKTOP_HEIGHT = EXPANDED_HEIGHT + LAST_STEP_INDEX * COLLAPSED_HEIGHT + LAST_STEP_INDEX; - -const CARD_SHADOW = "0px 1px 3px 0px rgba(22,34,51,0.08), 0px 12px 24px 0px rgba(22,34,51,0.04)"; -const CARD_BORDER = "0.391px solid rgba(0,0,0,0.08)"; - -// --------------------------------------------------------------------------- -// Animation presets -// --------------------------------------------------------------------------- - -const TRANSITION = { - expand: { duration: 0.5, ease: [0.4, 0, 0.2, 1] as const }, - content: { duration: 0.3, delay: 0.15 }, - preview: { duration: 0.4, delay: 0.1 }, - mobile: { duration: 0.35, ease: [0.4, 0, 0.2, 1] as const }, -} as const; - -// --------------------------------------------------------------------------- -// Shared sub-components -// --------------------------------------------------------------------------- - -function StepBadge({ num, isActive }: { num: number; isActive: boolean }) { - return ( -
- - {num} - -
- ); -} - -function Divider() { - return
; -} - -function StepDetails({ step, hideDetails }: { step: Step; hideDetails?: boolean }) { - return ( -
-

- {step.description} -

- {!hideDetails && step.details.length > 0 && ( -
- {step.detailsTitle &&

{step.detailsTitle}

} -
    - {step.details.map((line) => ( -
  • {line}
  • - ))} -
-
- )} -
- ); -} - -function StepIllustration({ stepNumber, mobile }: { stepNumber: number; mobile?: boolean }) { - const Illustration = STEP_ILLUSTRATIONS[stepNumber]; - const containerRef = useRef(null); - const [scale, setScale] = useState(1); - - useEffect(() => { - if (!mobile || !containerRef.current) return; - const updateScale = () => { - if (containerRef.current) { - setScale(containerRef.current.offsetWidth / 610); - } - }; - updateScale(); - const ro = new ResizeObserver(updateScale); - ro.observe(containerRef.current); - return () => ro.disconnect(); - }, [mobile]); - - if (!Illustration) return
; - - if (mobile) { - return ( -
-
- -
-
- ); - } - - return ( -
- -
- ); -} - -// --------------------------------------------------------------------------- -// Desktop step row -// --------------------------------------------------------------------------- - -function DesktopStep({ - step, - isActive, - onActivate, -}: { - step: Step; - isActive: boolean; - onActivate: () => void; -}) { - return ( - - {/* Left: number + text */} -
- - -
-

- {step.title} -

- - - {isActive && ( - - - - )} - -
-
- - {/* Right: illustration preview */} - - {isActive && ( - - - - )} - -
- ); -} - -// --------------------------------------------------------------------------- -// Mobile step row -// --------------------------------------------------------------------------- - -function MobileStep({ - step, - isActive, - onToggle, -}: { - step: Step; - isActive: boolean; - onToggle: () => void; -}) { - return ( -
- - - - {isActive && ( - -
-
- -
-
- -
-
-
- )} -
-
- ); -} - -// --------------------------------------------------------------------------- -// Main component -// --------------------------------------------------------------------------- - -export function StepsSection() { - const [activeStep, setActiveStep] = useState(1); - const activate = useCallback((n: number) => setActiveStep(n), []); - - return ( -
-
-
- {/* Desktop */} -
- {STEPS.map((step, i) => ( -
- activate(step.number)} - /> - {i < LAST_STEP_INDEX && } -
- ))} -
- - {/* Mobile */} -
- {STEPS.map((step, i) => ( -
- activate(step.number)} - /> - {i < LAST_STEP_INDEX && } -
- ))} -
-
-
-
- ); -} diff --git a/docs/app/(home)/components/shared/shared.module.css b/docs/app/(home)/components/shared/shared.module.css deleted file mode 100644 index 5a98b10b2..000000000 --- a/docs/app/(home)/components/shared/shared.module.css +++ /dev/null @@ -1,5 +0,0 @@ -.copyIcon { - height: 1rem; - width: 1rem; - flex-shrink: 0; -} diff --git a/docs/app/(home)/components/shared/shared.tsx b/docs/app/(home)/components/shared/shared.tsx deleted file mode 100644 index d7078a5db..000000000 --- a/docs/app/(home)/components/shared/shared.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import svgPaths from "@/imports/svg-urruvoh2be"; -import styles from "./shared.module.css"; - -// --------------------------------------------------------------------------- -// Design tokens shared across multiple sections -// --------------------------------------------------------------------------- - -export const BUTTON_SHADOW = "0px 1px 3px 0px rgba(22,34,51,0.08), 0px 12px 24px 0px rgba(22,34,51,0.04)"; - -// --------------------------------------------------------------------------- -// Shared components -// --------------------------------------------------------------------------- - -/** Clipboard/copy icon used in CTA buttons across Hero, BuildChat, etc. */ -export function CopyIcon({ color = "white" }: { color?: string }) { - return ( - - - - ); -} diff --git a/docs/app/(home)/globals.css b/docs/app/(home)/globals.css new file mode 100644 index 000000000..4a06e60bc --- /dev/null +++ b/docs/app/(home)/globals.css @@ -0,0 +1,27 @@ +.homeTheme { + --home-color-text-primary: #000; + --home-color-text-inverse: #fff; + --home-color-text-muted: rgb(0 0 0 / 40%); + --home-color-text-subtle: rgb(0 0 0 / 30%); + --home-color-text-secondary: rgb(0 0 0 / 60%); + --home-color-surface: #fff; + --home-color-surface-inverse: #000; + --home-color-surface-soft: rgb(0 0 0 / 5%); + --home-color-surface-muted: rgb(0 0 0 / 8%); + --home-color-surface-subtle: rgb(0 0 0 / 4%); + --home-border-soft: rgb(0 0 0 / 6%); + --home-border-subtle: rgb(0 0 0 / 8%); + --home-border-default: rgb(0 0 0 / 10%); + --home-shadow-elevated: 0 1px 3px 0 rgb(22 34 51 / 8%), 0 12px 24px 0 rgb(22 34 51 / 4%); + --home-shadow-soft-card: 0 1px 2px 0 rgb(22 34 51 / 4%), 0 8px 16px 0 rgb(22 34 51 / 4%); + --home-shadow-panel: 0 10px 20px rgb(0 0 0 / 10%); + --home-radius-pill: 999px; + --home-pill-padding-inline: 1.25rem; + --home-section-padding-inline: 1.25rem; +} + +@media (min-width: 1024px) { + .homeTheme { + --home-section-padding-inline: 2rem; + } +} diff --git a/docs/app/(home)/layout.tsx b/docs/app/(home)/layout.tsx index ddf1f5288..9fa9c2bc7 100644 --- a/docs/app/(home)/layout.tsx +++ b/docs/app/(home)/layout.tsx @@ -1,10 +1,11 @@ -import { Navbar } from "./components/Navbar/Navbar"; +import "./globals.css"; +import { Navbar } from "./sections/Navbar/Navbar"; export default function Layout({ children }: { children: React.ReactNode }) { return ( - <> +
{children} - +
); } diff --git a/docs/app/(home)/page.module.css b/docs/app/(home)/page.module.css index dfebcea5d..70fa2ab41 100644 --- a/docs/app/(home)/page.module.css +++ b/docs/app/(home)/page.module.css @@ -6,10 +6,6 @@ background: #fff; } -.mascotSection { - margin-top: 4rem; -} - .contentSection { position: relative; margin-top: 4rem; @@ -36,7 +32,6 @@ } @media (min-width: 1024px) { - .mascotSection, .contentSection { margin-top: 6rem; } diff --git a/docs/app/(home)/page.tsx b/docs/app/(home)/page.tsx index 394a7f9ed..5c8dc74b7 100644 --- a/docs/app/(home)/page.tsx +++ b/docs/app/(home)/page.tsx @@ -1,23 +1,20 @@ -import { BuildChatSection } from "./components/BuildChatSection/BuildChatSection"; -import { CompatibilitySection } from "./components/CompatibilitySection/CompatibilitySection"; -import { FadeInSection } from "./components/FadeInSection/FadeInSection"; -import { FeaturesSection } from "./components/FeaturesSection/FeaturesSection"; -import { Footer } from "./components/Footer/Footer"; -import { GradientDivider } from "./components/GradientDivider/GradientDivider"; -import { HeroSection } from "./components/HeroSection/HeroSection"; -import { PossibilitiesSection } from "./components/PossibilitiesSection/PossibilitiesSection"; -import { ShiroMascot } from "./components/ShiroMascot/ShiroMascot"; -import { StepsSection } from "./components/StepsSection/StepsSection"; -import { UILibrariesSection } from "./components/UILibrariesSection/UILibrariesSection"; +import { BuildChatSection } from "./sections/BuildChatSection/BuildChatSection"; +import { CompatibilitySection } from "./sections/CompatibilitySection/CompatibilitySection"; +import { FeaturesSection } from "./sections/FeaturesSection/FeaturesSection"; +import { Footer } from "./sections/Footer/Footer"; +import { GradientDivider } from "./sections/GradientDivider/GradientDivider"; +import { HeroSection } from "./sections/HeroSection/HeroSection"; +import { PossibilitiesSection } from "./sections/PossibilitiesSection/PossibilitiesSection"; +import { ShiroMascot } from "./sections/ShiroMascot/ShiroMascot"; +import { StepsSection } from "./sections/StepsSection/StepsSection"; +import { UILibrariesSection } from "./sections/UILibrariesSection/UILibrariesSection"; import styles from "./page.module.css"; export default function HomePage() { return (
-
- -
+
@@ -27,9 +24,7 @@ export default function HomePage() {
- - - +
diff --git a/docs/app/(home)/components/BuildChatSection/BuildChatSection.module.css b/docs/app/(home)/sections/BuildChatSection/BuildChatSection.module.css similarity index 56% rename from docs/app/(home)/components/BuildChatSection/BuildChatSection.module.css rename to docs/app/(home)/sections/BuildChatSection/BuildChatSection.module.css index 6f3767d44..a4f9a3a51 100644 --- a/docs/app/(home)/components/BuildChatSection/BuildChatSection.module.css +++ b/docs/app/(home)/sections/BuildChatSection/BuildChatSection.module.css @@ -1,6 +1,6 @@ .section { width: 100%; - padding-inline: 1.25rem; + padding-inline: var(--home-section-padding-inline); } .container { @@ -12,15 +12,16 @@ position: relative; overflow: hidden; border-radius: 32px; - background: #fff; + background: var(--home-color-surface); } .overlay { position: absolute; inset: 0; pointer-events: none; - border: 1px solid rgb(0 0 0 / 10%); + border: 1px solid var(--home-border-default); border-radius: 32px; + box-shadow: var(--home-shadow-elevated); } .content { @@ -49,7 +50,7 @@ font-size: 32px; font-weight: 600; line-height: 1.2; - color: #000; + color: var(--home-color-text-primary); margin: auto; text-align: center; } @@ -62,7 +63,7 @@ font-family: "Inter Display", sans-serif; font-size: 18px; line-height: 1.4; - color: rgb(0 0 0 / 40%); + color: var(--home-color-text-muted); } .ctaWrap { @@ -81,11 +82,10 @@ justify-content: center; gap: 0.625rem; height: 3rem; - padding-left: 1.25rem; - padding-right: 0.5rem; + padding-inline: var(--home-pill-padding-inline); border: 0; border-radius: 999px; - background: #000; + background: var(--home-color-surface-inverse); cursor: pointer; transition: transform 0.2s ease; } @@ -95,85 +95,14 @@ box-shadow: none; } -.ctaDesktopLabel { - display: none; - white-space: nowrap; - font-family: "Inter Display", sans-serif; - font-size: 18px; - font-weight: 500; - line-height: 1.5rem; - color: #fff; -} - -.ctaMobileLabel { +.ctaLabel { position: relative; - display: block; - min-width: 0; - flex: 1; - overflow: hidden; -} - -.ctaTicker { - display: flex; - width: max-content; - align-items: center; - animation: ctaTickerScroll 10s linear infinite; -} - -.ctaTickerText { white-space: nowrap; - padding-right: 2rem; - font-family: "Inter Display", sans-serif; - font-size: 18px; - font-weight: 500; + font-family: var(--font-geist-mono); + font-size: 12px; + font-weight: 400; line-height: 1.5rem; - color: #fff; -} - -.iconBadge { - display: flex; - height: 2rem; - width: 2rem; - flex-shrink: 0; - align-items: center; - justify-content: center; - border-radius: 999px; - background: rgb(255 255 255 / 15%); -} - -.iconFrame { - position: relative; - display: flex; - height: 1rem; - width: 1rem; - align-items: center; - justify-content: center; -} - -.iconLayer { - position: absolute; - transition: - opacity 0.3s ease, - transform 0.3s ease; -} - -.iconVisible { - opacity: 1; - transform: scale(1); -} - -.iconHidden { - opacity: 0; - transform: scale(0.5); -} - -@keyframes ctaTickerScroll { - 0% { - transform: translateX(0); - } - 100% { - transform: translateX(-50%); - } + color: var(--home-color-text-inverse); } .mediaColumn { @@ -189,10 +118,6 @@ } @media (min-width: 1024px) { - .section { - padding-inline: 2rem; - } - .content { flex-direction: row; gap: 0; @@ -200,16 +125,17 @@ .copyColumn { min-height: 0; - padding: 16px; + padding:16px; } + .title { max-width: 430px; font-family: "Inter", sans-serif; font-size: 36px; font-weight: 600; line-height: 1.2; - color: #000; + color: var(--home-color-text-primary); margin: 4px 0 0 4px; text-align: left; } @@ -225,12 +151,8 @@ justify-content: flex-start; } - .ctaDesktopLabel { - display: inline; - } - - .ctaMobileLabel { - display: none; + .ctaLabel { + font-size: 16px; } .mediaColumn { diff --git a/docs/app/(home)/sections/BuildChatSection/BuildChatSection.tsx b/docs/app/(home)/sections/BuildChatSection/BuildChatSection.tsx new file mode 100644 index 000000000..fb3a6f652 --- /dev/null +++ b/docs/app/(home)/sections/BuildChatSection/BuildChatSection.tsx @@ -0,0 +1,73 @@ +"use client"; + +import dashboardImg from "@/public/images/home/d67b5e94653944c1d0d4998c6b169c37f98060ad.png"; +import Image from "next/image"; +import { ClipboardCommandButton } from "../../components/Button/Button"; +import styles from "./BuildChatSection.module.css"; + +// --------------------------------------------------------------------------- +// Sub-components +// --------------------------------------------------------------------------- + +function SectionTitle() { + return

Build a Generative UI chat in minutes

; +} + +function CtaButton() { + return ( +
+ + + npx @openuidev/cli@latest create + + +
+ ); +} + +function DashboardIllustration() { + return ( + AI chat dashboard illustration + ); +} + +// --------------------------------------------------------------------------- +// Main component +// --------------------------------------------------------------------------- + +export function BuildChatSection() { + return ( +
+
+
+ {/* Border + shadow overlay */} + +
+
+ ); +} diff --git a/docs/app/(home)/components/CompatibilitySection/CompatibilitySection.module.css b/docs/app/(home)/sections/CompatibilitySection/CompatibilitySection.module.css similarity index 65% rename from docs/app/(home)/components/CompatibilitySection/CompatibilitySection.module.css rename to docs/app/(home)/sections/CompatibilitySection/CompatibilitySection.module.css index e087df6a3..ae04d9414 100644 --- a/docs/app/(home)/components/CompatibilitySection/CompatibilitySection.module.css +++ b/docs/app/(home)/sections/CompatibilitySection/CompatibilitySection.module.css @@ -1,6 +1,6 @@ .section { width: 100%; - padding-inline: 1.25rem; + padding-inline: var(--home-section-padding-inline); } .container { @@ -28,7 +28,7 @@ line-height: 1; letter-spacing: 0.08em; text-transform: uppercase; - color: rgb(0 0 0 / 30%); + color: var(--home-color-text-subtle); } .chips { @@ -43,9 +43,9 @@ align-items: center; gap: 0.375rem; padding: 0.375rem 0.625rem; - border: 1px solid rgb(0 0 0 / 8%); - border-radius: 999px; - background: #fff; + border: 1px solid var(--home-border-subtle); + border-radius: var(--home-radius-pill); + background: var(--home-color-surface); box-shadow: 0 2px 8px -2px rgb(22 34 51 / 8%); } @@ -60,8 +60,36 @@ border-radius: 5px; } +.badgeWhite { + background: var(--home-color-surface); +} + +.badgeBlack { + background: var(--home-color-surface-inverse); +} + +.badgeAnthropic { + background: #d4a574; +} + +.badgeMistral { + background: #ff7000; +} + +.badgeLangChain { + background: #1c3144; +} + +.badgeCrewAi { + background: #ff4b4b; +} + +.badgeGoogle { + background: #4285f4; +} + .badgeWithBorder { - border: 1px solid rgb(0 0 0 / 10%); + border: 1px solid var(--home-border-default); } .badgeImage { @@ -76,7 +104,7 @@ font-size: 13px; line-height: 1; white-space: nowrap; - color: rgb(0 0 0 / 60%); + color: var(--home-color-text-secondary); } .chipLabel { @@ -85,7 +113,7 @@ .more { margin-left: 0.25rem; - color: rgb(0 0 0 / 30%); + color: var(--home-color-text-subtle); } .divider { @@ -94,10 +122,6 @@ } @media (min-width: 1024px) { - .section { - padding-inline: 2rem; - } - .stack { gap: 1.25rem; } diff --git a/docs/app/(home)/components/CompatibilitySection/CompatibilitySection.tsx b/docs/app/(home)/sections/CompatibilitySection/CompatibilitySection.tsx similarity index 78% rename from docs/app/(home)/components/CompatibilitySection/CompatibilitySection.tsx rename to docs/app/(home)/sections/CompatibilitySection/CompatibilitySection.tsx index b2717d257..571b1c8a4 100644 --- a/docs/app/(home)/components/CompatibilitySection/CompatibilitySection.tsx +++ b/docs/app/(home)/sections/CompatibilitySection/CompatibilitySection.tsx @@ -9,8 +9,7 @@ interface CompatibilityItem { slug?: string; localSrc?: string; iconColor: string; - badgeBg: string; - border?: boolean; + badgeClassName?: string; } const LLMS: CompatibilityItem[] = [ @@ -18,27 +17,35 @@ const LLMS: CompatibilityItem[] = [ name: "OpenAI", localSrc: "/brand-icons/openai.svg", iconColor: "000000", - badgeBg: "#ffffff", - border: true, + badgeClassName: `${styles.badgeWhite} ${styles.badgeWithBorder}`, }, - { name: "Anthropic", slug: "anthropic", iconColor: "ffffff", badgeBg: "#D4A574" }, - { name: "Gemini", slug: "googlegemini", iconColor: "000000", badgeBg: "#ffffff", border: true }, - { name: "Mistral", slug: "mistralai", iconColor: "ffffff", badgeBg: "#FF7000" }, + { name: "Anthropic", slug: "anthropic", iconColor: "ffffff", badgeClassName: styles.badgeAnthropic }, + { + name: "Gemini", + slug: "googlegemini", + iconColor: "000000", + badgeClassName: `${styles.badgeWhite} ${styles.badgeWithBorder}`, + }, + { name: "Mistral", slug: "mistralai", iconColor: "ffffff", badgeClassName: styles.badgeMistral }, ]; const FRAMEWORKS: CompatibilityItem[] = [ - { name: "Vercel AI SDK", slug: "vercel", iconColor: "ffffff", badgeBg: "#000000" }, - { name: "LangChain", slug: "langchain", iconColor: "ffffff", badgeBg: "#1C3144" }, - { name: "CrewAI", slug: "crewai", iconColor: "ffffff", badgeBg: "#FF4B4B" }, + { name: "Vercel AI SDK", slug: "vercel", iconColor: "ffffff", badgeClassName: styles.badgeBlack }, + { name: "LangChain", slug: "langchain", iconColor: "ffffff", badgeClassName: styles.badgeLangChain }, + { name: "CrewAI", slug: "crewai", iconColor: "ffffff", badgeClassName: styles.badgeCrewAi }, { name: "OpenAI Agents SDK", localSrc: "/brand-icons/openai.svg", iconColor: "000000", - badgeBg: "#ffffff", - border: true, + badgeClassName: `${styles.badgeWhite} ${styles.badgeWithBorder}`, + }, + { + name: "Anthropic Agents SDK", + slug: "anthropic", + iconColor: "ffffff", + badgeClassName: styles.badgeAnthropic, }, - { name: "Anthropic Agents SDK", slug: "anthropic", iconColor: "ffffff", badgeBg: "#D4A574" }, - { name: "Google ADK", slug: "google", iconColor: "ffffff", badgeBg: "#4285F4" }, + { name: "Google ADK", slug: "google", iconColor: "ffffff", badgeClassName: styles.badgeGoogle }, ]; // --------------------------------------------------------------------------- @@ -49,10 +56,7 @@ function Chip({ item }: { item: CompatibilityItem }) { const imgSrc = item.localSrc ?? `https://cdn.simpleicons.org/${item.slug}/${item.iconColor}`; return (
-
+
{/* eslint-disable-next-line @next/next/no-img-element */} +
+ + + {/* CTA button */} +
+ + + Detailed comparison + View Comparison + + +
+
+
+ ); +} diff --git a/docs/app/(home)/components/Footer/Footer.module.css b/docs/app/(home)/sections/Footer/Footer.module.css similarity index 68% rename from docs/app/(home)/components/Footer/Footer.module.css rename to docs/app/(home)/sections/Footer/Footer.module.css index 9dc6d9e0d..20bb4e943 100644 --- a/docs/app/(home)/components/Footer/Footer.module.css +++ b/docs/app/(home)/sections/Footer/Footer.module.css @@ -11,7 +11,7 @@ .handcraftedSection { width: 100%; - padding-inline: 1.25rem; + padding-inline: var(--home-section-padding-inline); } .handcraftedContainer { @@ -26,8 +26,8 @@ .mascotWrap { position: relative; - height: 140px; - width: 140px; + height: 96px; + width: 96px; flex-shrink: 0; } @@ -39,15 +39,22 @@ .handcraftedCopy { text-align: center; - font-family: "Playwrite US Trad", serif; - font-size: 16px; + font-family: var(--font-geist-mono); + font-size: 18px; line-height: 1.2; - color: #000; + color: var(--home-color-text-primary); + margin-top: 12px; +} + +.handcraftedCursor { + display: inline-block; + margin-left: 0.06em; + animation: handcraftedCursorBlink 1s steps(1, end) infinite; } .contentSection { - padding: 60px 1.25rem 1rem; - background: #fff; + padding: 60px var(--home-section-padding-inline) 1rem; + background: var(--home-color-surface); } .contentContainer { @@ -87,7 +94,7 @@ font-family: "Inter", sans-serif; font-size: 15px; line-height: 1.5; - color: rgb(0 0 0 / 40%); + color: var(--home-color-text-muted); } .desktopMetaLeft { @@ -121,6 +128,26 @@ position: absolute; } +.socialIconTwitter { + inset: 10.82% 4.33% 18.35% 4.17%; +} + +.socialIconYoutube { + left: 50%; + top: 50%; + width: 22px; + height: 15.469px; + transform: translate(-50%, -50%); +} + +.socialIconLinkedIn { + left: 50%; + top: 50%; + width: 19px; + height: 19px; + transform: translate(-50%, -50%); +} + .mobileMeta { margin-top: 2.5rem; text-align: center; @@ -130,18 +157,22 @@ font-family: "Inter", sans-serif; font-size: 12px; line-height: 1.5; - color: rgb(0 0 0 / 40%); + color: var(--home-color-text-muted); } -@media (min-width: 1024px) { - .handcraftedSection, - .contentSection { - padding-inline: 2rem; +@keyframes handcraftedCursorBlink { + 0%, + 49% { + opacity: 1; } - .handcraftedCopy { - font-size: 22px; + 50%, + 100% { + opacity: 0; } +} + +@media (min-width: 1024px) { .desktopLogoRow { display: flex; diff --git a/docs/app/(home)/components/Footer/Footer.tsx b/docs/app/(home)/sections/Footer/Footer.tsx similarity index 90% rename from docs/app/(home)/components/Footer/Footer.tsx rename to docs/app/(home)/sections/Footer/Footer.tsx index af87b02c9..d86a8af2d 100644 --- a/docs/app/(home)/components/Footer/Footer.tsx +++ b/docs/app/(home)/sections/Footer/Footer.tsx @@ -1,6 +1,4 @@ "use client"; - -import type { CSSProperties } from "react"; import svgPaths from "@/imports/svg-urruvoh2be"; import mascotSvgPaths from "@/imports/svg-xeurqn3j1r"; import styles from "./Footer.module.css"; @@ -14,7 +12,7 @@ interface SocialLink { href: string; viewBox: string; path: string; - wrapperStyle?: CSSProperties; + wrapperClassName?: string; clipId?: string; clipSize?: { width: string; height: string }; } @@ -25,7 +23,7 @@ const SOCIAL_LINKS: SocialLink[] = [ href: "https://x.com/thesysdev", viewBox: "0 0 24 24", path: svgPaths.pa1e7100, - wrapperStyle: { inset: "10.82% 4.33% 18.35% 4.17%" }, + wrapperClassName: styles.socialIconTwitter, }, { label: "Discord", @@ -38,13 +36,7 @@ const SOCIAL_LINKS: SocialLink[] = [ href: "https://www.youtube.com/@thesysdev", viewBox: "0 0 22 15.4688", path: svgPaths.p23dbbd00, - wrapperStyle: { - left: "50%", - top: "50%", - width: "22px", - height: "15.469px", - transform: "translate(-50%, -50%)", - }, + wrapperClassName: styles.socialIconYoutube, clipId: "clip_yt", clipSize: { width: "22", height: "15.4688" }, }, @@ -53,13 +45,7 @@ const SOCIAL_LINKS: SocialLink[] = [ href: "https://www.linkedin.com/company/thesysdev/", viewBox: "0 0 19 19", path: svgPaths.p26fc3100, - wrapperStyle: { - left: "50%", - top: "50%", - width: "19px", - height: "19px", - transform: "translate(-50%, -50%)", - }, + wrapperClassName: styles.socialIconLinkedIn, clipId: "clip_li", clipSize: { width: "19", height: "19" }, }, @@ -95,8 +81,8 @@ function SocialIcon({ link }: { link: SocialLink }) { className={styles.socialLink} aria-label={link.label} > - {link.wrapperStyle ? ( -
+ {link.wrapperClassName ? ( +
{svgContent}
) : ( @@ -159,7 +145,10 @@ export function Footer() {

- Handcrafted with a lot of love. + Handcrafted with a lot of love +

diff --git a/docs/app/(home)/sections/GradientDivider/GradientDivider.module.css b/docs/app/(home)/sections/GradientDivider/GradientDivider.module.css new file mode 100644 index 000000000..231e1e927 --- /dev/null +++ b/docs/app/(home)/sections/GradientDivider/GradientDivider.module.css @@ -0,0 +1,123 @@ +.divider { + display: flex; + width: 100%; + flex-direction: column; + gap: 2px; + transform: rotate(180deg); +} + +.bar { + width: 100%; + flex-shrink: 0; + background: rgb(0 0 0 / 4%); +} + +.barHeight60 { + height: 30px; +} + +.barHeight48 { + height: 24px; +} + +.barHeight32 { + height: 16px; +} + +.barHeight20942 { + height: 10.471px; +} + +.barHeight16754 { + height: 8.377px; +} + +.barHeight12565 { + height: 6.2825px; +} + +.barHeight10471 { + height: 5.2355px; +} + +.barHeight8377 { + height: 4.1885px; +} + +.barHeight6283 { + height: 3.1415px; +} + +.barHeight4188 { + height: 2.094px; +} + +.barHeight3141 { + height: 1.5705px; +} + +.barHeight2094 { + height: 1.047px; +} + +.barHeight1047 { + height: 0.5235px; +} + +@media (min-width: 1024px) { + .divider { + gap: 6px; + } + + .barHeight60 { + height: 60px; + } + + .barHeight48 { + height: 48px; + } + + .barHeight32 { + height: 32px; + } + + .barHeight20942 { + height: 20.942px; + } + + .barHeight16754 { + height: 16.754px; + } + + .barHeight12565 { + height: 12.565px; + } + + .barHeight10471 { + height: 10.471px; + } + + .barHeight8377 { + height: 8.377px; + } + + .barHeight6283 { + height: 6.283px; + } + + .barHeight4188 { + height: 4.188px; + } + + .barHeight3141 { + height: 3.141px; + } + + .barHeight2094 { + height: 2.094px; + } + + .barHeight1047 { + height: 1.047px; + } +} diff --git a/docs/app/(home)/sections/GradientDivider/GradientDivider.tsx b/docs/app/(home)/sections/GradientDivider/GradientDivider.tsx new file mode 100644 index 000000000..b28070648 --- /dev/null +++ b/docs/app/(home)/sections/GradientDivider/GradientDivider.tsx @@ -0,0 +1,32 @@ +import styles from "./GradientDivider.module.css"; + +const BAR_HEIGHT_CLASSES = [ + styles.barHeight60, + styles.barHeight48, + styles.barHeight32, + styles.barHeight20942, + styles.barHeight16754, + styles.barHeight12565, + styles.barHeight10471, + styles.barHeight8377, + styles.barHeight6283, + styles.barHeight4188, + styles.barHeight3141, + styles.barHeight2094, + styles.barHeight1047, +]; + +export function GradientDivider({ direction = "down" }: { direction?: "down" | "up" }) { + const bars = direction === "up" ? [...BAR_HEIGHT_CLASSES].reverse() : BAR_HEIGHT_CLASSES; + + return ( +
+ {bars.map((barClassName) => ( +
+ ))} +
+ ); +} diff --git a/docs/app/(home)/components/HeroSection/HeroSection.module.css b/docs/app/(home)/sections/HeroSection/HeroSection.module.css similarity index 76% rename from docs/app/(home)/components/HeroSection/HeroSection.module.css rename to docs/app/(home)/sections/HeroSection/HeroSection.module.css index 346663af9..62d489b81 100644 --- a/docs/app/(home)/components/HeroSection/HeroSection.module.css +++ b/docs/app/(home)/sections/HeroSection/HeroSection.module.css @@ -2,23 +2,6 @@ width: 100%; } -.iconLayer { - position: absolute; - transition: - opacity 0.3s ease, - transform 0.3s ease; -} - -.iconVisible { - opacity: 1; - transform: scale(1); -} - -.iconHidden { - opacity: 0; - transform: scale(0.5); -} - .npmButton { display: flex; height: 3rem; @@ -28,10 +11,10 @@ gap: 0.625rem; padding-left: 1.25rem; padding-right: 0.5rem; - border: 1px solid rgb(0 0 0 / 6%); - border-radius: 999px; - background: #fff; - box-shadow: var(--hero-button-shadow, none); + border: 1px solid var(--home-border-soft); + border-radius: var(--home-radius-pill); + background: var(--home-color-surface); + box-shadow: var(--home-shadow-elevated); cursor: pointer; transition: transform 0.2s ease, @@ -44,23 +27,28 @@ } .desktopPlaygroundButton:hover { - background: rgb(0 0 0 / 5%); + background: var(--home-color-surface-soft); } .npmDesktopLabel, -.desktopPlaygroundButton, .npmTickerText, +.desktopPlaygroundButton, .mobilePlaygroundLabel { - font-family: "Inter Display", sans-serif; font-size: 18px; font-weight: 500; line-height: 1.5rem; } +.desktopPlaygroundButton, +.mobilePlaygroundLabel { + font-family: "Inter Display", sans-serif; +} + .npmDesktopLabel { display: none; + font-family: var(--font-geist-mono); white-space: nowrap; - color: #000; + color: var(--home-color-text-primary); } .npmMobileLabel { @@ -79,9 +67,10 @@ } .npmTickerText { + font-family: var(--font-geist-mono); white-space: nowrap; padding-right: 2rem; - color: #000; + color: var(--home-color-text-primary); } .npmIconBadge { @@ -92,16 +81,7 @@ align-items: center; justify-content: center; border-radius: 999px; - background: #000; -} - -.npmIconFrame { - position: relative; - display: flex; - height: 1rem; - width: 1rem; - align-items: center; - justify-content: center; + background: var(--home-color-surface-inverse); } .desktopPlaygroundButton, @@ -116,16 +96,16 @@ } .desktopPlaygroundButton { - padding-inline: 1.25rem; - color: #000; + padding-inline: var(--home-pill-padding-inline); + color: var(--home-color-text-primary); transition: background-color 0.2s ease; } .mobilePlaygroundButton { flex-shrink: 0; - padding-inline: 1.25rem; + padding-inline: var(--home-pill-padding-inline); border-radius: 100px; - background: #000; + background: var(--home-color-surface-inverse); transition: background-color 0.2s ease; } @@ -135,11 +115,11 @@ .mobilePlaygroundLabel { white-space: nowrap; - color: #fff; + color: var(--home-color-text-inverse); } .mobilePlaygroundArrow { - color: #fff; + color: var(--home-color-text-inverse); font-size: 20px; line-height: 1; } @@ -169,16 +149,11 @@ visibility: hidden; } -.heroLayerFade, .heroLayerMotion { position: absolute; inset: 0; } -.heroLayerFade { - will-change: opacity; -} - .heroLayerMotion, .desktopCtaLayer { will-change: transform, opacity; @@ -189,26 +164,10 @@ height: 100%; } -.leftFade, -.rightFade { - position: absolute; - inset-block: 0; - width: 40px; - pointer-events: none; -} - -.leftFade { - left: 0; - background: linear-gradient(to right, #fff, transparent); -} - -.rightFade { - right: 0; - background: linear-gradient(to left, #fff, transparent); -} - .desktopCtaLayer { position: absolute; + left: 50%; + top: 71.19%; } .desktopCtaStack { @@ -228,7 +187,7 @@ .mobileHeroIntro { position: relative; - padding: 60px 1.25rem 1.25rem; + padding: 32px var(--home-section-padding-inline) var(--home-section-padding-inline); } .mobileHeroStack { @@ -242,6 +201,13 @@ padding-block: 2.5rem; } +.mobileBrandGroup { + display: flex; + flex-direction: column; + align-items: center; + gap: 0rem; +} + .mobileGithubBanner { display: inline-flex; align-items: center; @@ -249,9 +215,9 @@ gap: 0.5rem; min-height: 2.5rem; padding-inline: 1rem; - border-radius: 999px; - background: rgb(0 0 0 / 5%); - color: #000; + border-radius: var(--home-radius-pill); + background: var(--home-color-surface-soft); + color: var(--home-color-text-primary); text-decoration: none; font-family: "Inter Display", sans-serif; font-size: 16px; @@ -261,7 +227,7 @@ } .mobileGithubBanner:hover { - background: rgb(0 0 0 / 8%); + background: var(--home-color-surface-muted); } .mobileGithubBannerLead { @@ -284,8 +250,8 @@ .mobileMascotWrap { position: relative; - height: 140px; - width: 140px; + height: 120px; + width: 120px; flex-shrink: 0; } @@ -293,16 +259,18 @@ text-align: center; font-family: "Inter Display", sans-serif; font-size: 80px; + font-weight: 600; line-height: 1.25; - color: #000; + color: var(--home-color-text-primary); } .mobileSubtitle { text-align: center; font-family: "Inter Display", sans-serif; font-size: 22px; + font-weight: 500; line-height: 1.2; - color: rgb(0 0 0 / 40%); + color: var(--home-color-text-muted); } .mobileCtaStack { @@ -338,6 +306,9 @@ .previewScaleFrame { position: absolute; left: 50%; + width: 1600px; + height: 518px; + transform: translateX(-50%) scale(var(--preview-scale, 1)); transform-origin: top center; } @@ -355,12 +326,16 @@ .previewFrame { position: relative; + width: calc(100% + 240px); + max-width: 100vw; + aspect-ratio: 1600 / 518; + margin-inline: -120px; overflow: hidden; } .taglineSection { margin-top: 1.25rem; - padding-inline: 1.25rem; + padding-inline: var(--home-section-padding-inline); } .taglineContainer { @@ -374,7 +349,7 @@ font-size: 22px; font-weight: 500; line-height: 1.4; - color: rgb(0 0 0 / 40%); + color: var(--home-color-text-muted); } .taglineBreak { @@ -406,7 +381,7 @@ position: relative; width: 100%; overflow: hidden; - padding: 5.5rem 2rem 5.5rem; + padding: 5.5rem var(--home-section-padding-inline) 5.5rem; } .previewSection { @@ -419,7 +394,7 @@ .taglineSection { margin-top: 2.5rem; - padding: 2.5rem 2rem 0; + padding: 2.5rem var(--home-section-padding-inline) 0; } .tagline { diff --git a/docs/app/(home)/components/HeroSection/HeroSection.tsx b/docs/app/(home)/sections/HeroSection/HeroSection.tsx similarity index 59% rename from docs/app/(home)/components/HeroSection/HeroSection.tsx rename to docs/app/(home)/sections/HeroSection/HeroSection.tsx index 4a7ef8423..bf47ab6b2 100644 --- a/docs/app/(home)/components/HeroSection/HeroSection.tsx +++ b/docs/app/(home)/sections/HeroSection/HeroSection.tsx @@ -4,122 +4,72 @@ import { GitHubIcon } from "@/components/brand-logo"; import HeroPreviewFrame from "@/imports/Frame2147239423"; import svgMascotPaths from "@/imports/svg-148i9mcxjn"; import svgHeroPaths from "@/imports/svg-a5kdrdeeao"; -import { lazy, Suspense, useEffect, useRef, useState, type CSSProperties } from "react"; -import { CopyIcon } from "../shared/shared"; +import { lazy, Suspense, useEffect, useRef, useState } from "react"; +import { ClipboardCommandButton, PillLink } from "../../components/Button/Button"; import styles from "./HeroSection.module.css"; const LazyMobileActionFigure = lazy(() => import("@/imports/MobileActionFigure")); -const HERO_BUTTON_SHADOW = "0 1.5px 5px 0 rgba(22, 34, 51, 0.06), 0 12px 24px 0 rgba(22, 34, 51, 0.04)"; -const HERO_BUTTON_STYLE = { - "--hero-button-shadow": HERO_BUTTON_SHADOW, -} as CSSProperties; - // CTAs const primaryCTA = "npx @openuidev/cli@latest create"; const secondaryCTA = "Try Playground"; -const COPIED_FEEDBACK_MS = 3000; // --------------------------------------------------------------------------- // Buttons // --------------------------------------------------------------------------- function NpmButton({ className = "" }: { className?: string }) { - const [copied, setCopied] = useState(false); - const copiedTimeoutRef = useRef | null>(null); - - useEffect(() => { - return () => { - if (copiedTimeoutRef.current) { - clearTimeout(copiedTimeoutRef.current); - } - }; - }, []); - - const onCopy = async () => { - try { - await navigator.clipboard.writeText(primaryCTA); - setCopied(true); - if (copiedTimeoutRef.current) { - clearTimeout(copiedTimeoutRef.current); - } - copiedTimeoutRef.current = setTimeout(() => { - setCopied(false); - }, COPIED_FEEDBACK_MS); - } catch { - setCopied(false); - } - }; - return ( - <> - - + + ); } function DesktopPlaygroundButton({ className = "" }: { className?: string }) { return ( - + ); } function MobilePlaygroundButton({ className = "" }: { className?: string }) { return ( - + + ); } @@ -176,39 +126,6 @@ function DesktopHero() { - {/* Grid lines */} -
- - - {[189, 787].map((x) => ( - - - - - - - ))} - - - - - - -
- {/* Title */}
@@ -227,15 +144,8 @@ function DesktopHero() {
- {/* Edge fades */} -
-
- {/* CTA buttons */} -
+
@@ -271,17 +181,20 @@ function MobileHero() { → -
- -
- {/* Title */} -

- OpenUI -

+
+
+ +
+ + {/* Title */} +

+ OpenUI +

+
{/* Subtitle */} -

+

The Open Standard
for Generative UI @@ -310,11 +223,11 @@ function MobileHero() { // --------------------------------------------------------------------------- const PREVIEW_NATIVE_WIDTH = 1600; -const PREVIEW_NATIVE_HEIGHT = 518; const PREVIEW_SCALE_MULTIPLIER = 1.25; function ScaledPreview() { const containerRef = useRef(null); + const frameRef = useRef(null); const [scale, setScale] = useState(1); useEffect(() => { @@ -327,16 +240,14 @@ function ScaledPreview() { return () => observer.disconnect(); }, []); + useEffect(() => { + if (!frameRef.current) return; + frameRef.current.style.setProperty("--preview-scale", String(scale * PREVIEW_SCALE_MULTIPLIER)); + }, [scale]); + return (

-
+
@@ -347,15 +258,7 @@ function PreviewImage() { return (
-
+
diff --git a/docs/app/(home)/components/Navbar/Navbar.module.css b/docs/app/(home)/sections/Navbar/Navbar.module.css similarity index 91% rename from docs/app/(home)/components/Navbar/Navbar.module.css rename to docs/app/(home)/sections/Navbar/Navbar.module.css index ee02c7cc1..9978c3a58 100644 --- a/docs/app/(home)/components/Navbar/Navbar.module.css +++ b/docs/app/(home)/sections/Navbar/Navbar.module.css @@ -8,6 +8,10 @@ transition: border-color 0.2s ease; } +.navScrolled { + border-bottom: 1px solid var(--home-border-default); +} + .navInner { display: flex; max-width: 75rem; @@ -25,7 +29,7 @@ .logoDivider { height: 1rem; width: 1px; - background: rgb(0 0 0 / 10%); + background: var(--home-border-default); } .desktopTabs, @@ -62,7 +66,6 @@ .hamburgerIcon { height: 1.25rem; width: 1.25rem; - color: #000; } .mobileMenuButton { @@ -105,7 +108,7 @@ border-bottom-left-radius: 18px; border-bottom-right-radius: 18px; background: #fff; - box-shadow: 0 10px 20px rgb(0 0 0 / 10%); + box-shadow: var(--home-shadow-panel); } .mobileTrayInner { @@ -170,9 +173,9 @@ position: absolute; inset: 0; pointer-events: none; - border: 1px solid rgb(0 0 0 / 10%); - border-radius: 999px; - box-shadow: var(--mobile-github-button-shadow, none); + border: 1px solid var(--home-border-default); + border-radius: var(--home-radius-pill); + box-shadow: var(--home-shadow-elevated); transition: box-shadow 0.2s ease; } diff --git a/docs/app/(home)/components/Navbar/Navbar.tsx b/docs/app/(home)/sections/Navbar/Navbar.tsx similarity index 88% rename from docs/app/(home)/components/Navbar/Navbar.tsx rename to docs/app/(home)/sections/Navbar/Navbar.tsx index 5da6d286a..625658596 100644 --- a/docs/app/(home)/components/Navbar/Navbar.tsx +++ b/docs/app/(home)/sections/Navbar/Navbar.tsx @@ -9,8 +9,7 @@ import { useGitHubStarCount, } from "@/components/brand-logo"; import { AnimatePresence, motion } from "motion/react"; -import { useCallback, useEffect, useState, type CSSProperties } from "react"; -import { BUTTON_SHADOW } from "../shared/shared"; +import { useCallback, useEffect, useState } from "react"; import styles from "./Navbar.module.css"; // --------------------------------------------------------------------------- @@ -25,10 +24,6 @@ const TAB_URLS: Record = { Components: "/docs/components", Blog: "/blog", }; -const NAVBAR_BORDER_COLOR = "rgba(0,0,0,0.1)"; -const MOBILE_GITHUB_BUTTON_STYLE = { - "--mobile-github-button-shadow": BUTTON_SHADOW, -} as CSSProperties; // Sub-components // --------------------------------------------------------------------------- @@ -76,7 +71,7 @@ function HamburgerIcon({ isOpen }: { isOpen: boolean }) { ); } -function MobileMenu({ starCount, onClose }: { starCount: number; onClose: () => void }) { +function MobileMenu({ starCount, onClose }: { starCount: number | null; onClose: () => void }) { return ( <> {/* Backdrop overlay — below navbar (absolute top-full), covers rest of viewport */} @@ -122,11 +117,7 @@ function MobileMenu({ starCount, onClose }: { starCount: number; onClose: () => rel="noopener noreferrer" className={styles.mobileGithubButton} > -