diff --git a/docs/app/(home)/Openclaw-OS/page.tsx b/docs/app/(home)/Openclaw-OS/page.tsx new file mode 100644 index 000000000..6bb3f117b --- /dev/null +++ b/docs/app/(home)/Openclaw-OS/page.tsx @@ -0,0 +1,93 @@ +import styles from "../page.module.css"; +import { FeaturesSection, OPENCLAW_FEATURES } from "../sections/FeaturesSection/FeaturesSection"; +import { Footer } from "../sections/Footer/Footer"; +import { GradientDivider } from "../sections/GradientDivider/GradientDivider"; +import { HeroSection } from "../sections/HeroSection/HeroSection"; +import heroStyles from "../sections/HeroSection/HeroSection.module.css"; +import { PossibilitiesSection } from "../sections/PossibilitiesSection/PossibilitiesSection"; +import { StuckInChatSection } from "../sections/StuckInChatSection/StuckInChatSection"; + +const INSTALL_COMMAND = "curl -fsSL https://openui.com/openclaw-os/install.sh | bash"; + +export default function OpenClawOSPage() { + return ( +
+
+ + OpenClaw OS + + } + subtitle={ + <> + The Default workspace for{" "} + + OpenClaw. + + } + command={INSTALL_COMMAND} + compact + showBanner={false} + showPlaygroundButton={false} + desktopPreviewImage="/OpenclawOS-hero.png" + desktopPreviewImageAlt="OpenClaw OS desktop preview" + desktopPreviewImageWidth={3200} + desktopPreviewImageHeight={1036} + mobilePreviewImage="/openclaw-os-mobile-hero.png" + mobilePreviewImageAlt="OpenClaw OS mobile preview" + mobilePreviewImageWidth={804} + mobilePreviewImageHeight={880} + mobilePreviewImageCropTopPercent={20} + showGitHubBanner={false} + widePreview + showTagline + taglineCompact + tagline={ + <> + OpenClaw OS is a workspace for managing and operating your OpenClaw agent.{" "} +
+ Generate interactive apps and artifacts, instantly for any use case. + + } + /> +
+
+
+ + + +
+ +
+
+ ); +} diff --git a/docs/app/(home)/components/Button/Button.tsx b/docs/app/(home)/components/Button/Button.tsx index 3653a5613..82528d19b 100644 --- a/docs/app/(home)/components/Button/Button.tsx +++ b/docs/app/(home)/components/Button/Button.tsx @@ -6,7 +6,7 @@ import { useEffect, useRef, useState, type ButtonHTMLAttributes, type ReactNode import styles from "./Button.module.css"; type ButtonType = ButtonHTMLAttributes["type"]; -const COPY_FEEDBACK_MS = 3000; +const COPY_FEEDBACK_MS = 1800; function CopyIcon({ color = "white" }: { color?: string }) { return ; @@ -49,6 +49,37 @@ function CopyStatusIcon({ ); } +async function copyText(text: string): Promise { + if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) { + try { + await navigator.clipboard.writeText(text); + return true; + } catch { + // fall through to execCommand fallback + } + } + + if (typeof document === "undefined") return false; + + try { + const textarea = document.createElement("textarea"); + textarea.value = text; + textarea.setAttribute("readonly", ""); + textarea.style.position = "fixed"; + textarea.style.top = "0"; + textarea.style.left = "0"; + textarea.style.opacity = "0"; + textarea.style.pointerEvents = "none"; + document.body.appendChild(textarea); + textarea.select(); + const ok = document.execCommand("copy"); + document.body.removeChild(textarea); + return ok; + } catch { + return false; + } +} + interface ClipboardCommandButtonProps { command: string; children: ReactNode; @@ -58,6 +89,7 @@ interface ClipboardCommandButtonProps { iconPosition?: "start" | "end"; copyIconColor?: string; type?: ButtonType; + onCopyChange?: (copied: boolean) => void; } export function ClipboardCommandButton({ @@ -69,6 +101,7 @@ export function ClipboardCommandButton({ iconPosition = "end", copyIconColor = "white", type = "button", + onCopyChange, }: ClipboardCommandButtonProps) { const [copied, setCopied] = useState(false); const resetTimeoutRef = useRef | null>(null); @@ -82,20 +115,20 @@ export function ClipboardCommandButton({ }, []); 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 ok = await copyText(command); + if (!ok) { + onCopyChange?.(false); + return; } + setCopied(true); + onCopyChange?.(true); + if (resetTimeoutRef.current) { + clearTimeout(resetTimeoutRef.current); + } + resetTimeoutRef.current = setTimeout(() => { + setCopied(false); + onCopyChange?.(false); + }, COPY_FEEDBACK_MS); }; const icon = ( diff --git a/docs/app/(home)/components/FeatureList/FeatureList.module.css b/docs/app/(home)/components/FeatureList/FeatureList.module.css index b433438dd..1d5ed5a71 100644 --- a/docs/app/(home)/components/FeatureList/FeatureList.module.css +++ b/docs/app/(home)/components/FeatureList/FeatureList.module.css @@ -11,6 +11,9 @@ } .featureIconSvg { + display: inline-flex; + align-items: center; + justify-content: center; height: 18px; width: 18px; color: var(--openui-text-neutral-primary); diff --git a/docs/app/(home)/components/FeatureList/FeatureList.tsx b/docs/app/(home)/components/FeatureList/FeatureList.tsx index 1af38c8cf..975e27df3 100644 --- a/docs/app/(home)/components/FeatureList/FeatureList.tsx +++ b/docs/app/(home)/components/FeatureList/FeatureList.tsx @@ -1,42 +1,32 @@ "use client"; +import type { ReactNode } from "react"; import styles from "./FeatureList.module.css"; export interface FeatureListItem { title: string; description: string; - iconPath: string; + icon: ReactNode; } interface FeatureListProps { items: FeatureListItem[]; } -function FeatureIcon({ path, index }: { path: string; index: number }) { - const clipId = `clip_feat_${index}`; - +function FeatureIcon({ icon }: { icon: ReactNode }) { return (
- - - - - - - - - - + {icon}
); } -function DesktopFeatureRow({ item, index }: { item: FeatureListItem; index: number }) { +function DesktopFeatureRow({ item }: { item: FeatureListItem }) { return (
- +
{item.title}
@@ -45,15 +35,7 @@ function DesktopFeatureRow({ item, index }: { item: FeatureListItem; index: numb ); } -function MobileFeatureRow({ - item, - index, - iconIndexOffset, -}: { - item: FeatureListItem; - index: number; - iconIndexOffset: number; -}) { +function MobileFeatureRow({ item }: { item: FeatureListItem }) { return (
@@ -61,7 +43,7 @@ function MobileFeatureRow({ {item.description}
- +
); @@ -79,7 +61,7 @@ export function FeatureList({ items }: FeatureListProps) {
{items.map((item, index) => (
- + {index < lastItemIndex && }
))} @@ -88,7 +70,7 @@ export function FeatureList({ items }: FeatureListProps) {
{items.map((item, index) => (
- + {index < lastItemIndex && }
))} diff --git a/docs/app/(home)/components/TweetWall/TweetWall.tsx b/docs/app/(home)/components/TweetWall/TweetWall.tsx index 5e5ba4a0b..6f359c6e0 100644 --- a/docs/app/(home)/components/TweetWall/TweetWall.tsx +++ b/docs/app/(home)/components/TweetWall/TweetWall.tsx @@ -72,9 +72,8 @@ export function TweetWall() { const columns = splitIntoColumns(HOME_TWEETS, columnCount); useEffect(() => { - if (window.twttr?.widgets?.createTweet) { - setScriptReady(true); - } + if (!window.twttr?.widgets?.createTweet) return; + queueMicrotask(() => setScriptReady(true)); }, []); useEffect(() => { @@ -91,11 +90,11 @@ export function TweetWall() { ); if (!shouldRehydrate) { - setIsWallReady(true); + queueMicrotask(() => setIsWallReady(true)); return; } - setIsWallReady(false); + queueMicrotask(() => setIsWallReady(false)); async function hydrateEmbeds() { await Promise.allSettled( diff --git a/docs/app/(home)/page.module.css b/docs/app/(home)/page.module.css index 7535425e6..e7d6e4886 100644 --- a/docs/app/(home)/page.module.css +++ b/docs/app/(home)/page.module.css @@ -24,10 +24,18 @@ background: var(--openui-background); } +.contentShellTight { + gap: 4rem; +} + @media (min-width: 768px) { .contentShell { gap: 15rem; } + + .contentShellTight { + gap: 6rem; + } } @media (min-width: 1024px) { diff --git a/docs/app/(home)/sections/FeaturesSection/FeaturesSection.module.css b/docs/app/(home)/sections/FeaturesSection/FeaturesSection.module.css index d424dc0e7..548b639ee 100644 --- a/docs/app/(home)/sections/FeaturesSection/FeaturesSection.module.css +++ b/docs/app/(home)/sections/FeaturesSection/FeaturesSection.module.css @@ -1,6 +1,6 @@ .section { width: 100%; - padding-inline: 1.25rem; + padding: 2rem 1.25rem; } .container { @@ -57,7 +57,7 @@ @media (min-width: 1024px) { .section { - padding-inline: 2rem; + padding: 3rem 2rem; } .ctaWrap { diff --git a/docs/app/(home)/sections/FeaturesSection/FeaturesSection.tsx b/docs/app/(home)/sections/FeaturesSection/FeaturesSection.tsx index d3c7b9724..906bf1d30 100644 --- a/docs/app/(home)/sections/FeaturesSection/FeaturesSection.tsx +++ b/docs/app/(home)/sections/FeaturesSection/FeaturesSection.tsx @@ -1,74 +1,139 @@ "use client"; +import { + Browsers, + Eye, + MagicWand, + PushPin, + SlidersHorizontal, + SquaresFour, +} from "@phosphor-icons/react"; import svgPaths from "@/imports/svg-urruvoh2be"; import { PillLink } from "../../components/Button/Button"; import { FeatureList, type FeatureListItem } from "../../components/FeatureList/FeatureList"; import styles from "./FeaturesSection.module.css"; -// --------------------------------------------------------------------------- -// Data -// --------------------------------------------------------------------------- +const PHOSPHOR_ICON_SIZE = 18; -const FEATURES: FeatureListItem[] = [ +function SvgPathIcon({ path, index }: { path: string; index: number }) { + const clipId = `clip_feat_${index}`; + return ( + + ); +} + +const DEFAULT_FEATURES: FeatureListItem[] = [ { title: "Performance Optimized", description: "3x faster rendering than json-render", - iconPath: svgPaths.p7658f00, + icon: , }, { title: "Token efficient", description: "Up to 67.1% lesser tokens than json-render", - iconPath: svgPaths.p2a8ddd80, + icon: , }, { title: "Live data", description: "Query your tools and MCP servers at runtime", - iconPath: svgPaths.p10e86100, + icon: , }, { title: "Works across platforms", description: "React, React Native, Vue, etc", - iconPath: svgPaths.p2cbb5d00, + icon: , }, { title: "Native Streaming", description: "UI renders in real time", - iconPath: svgPaths.p33780400, + icon: , }, { title: "Interactive", description: "Reactive state, inputs, and tool-connected actions", - iconPath: svgPaths.p17c7f700, + icon: , }, { title: "Safe by Default", description: "No arbitrary code execution", - iconPath: svgPaths.p16eec200, + icon: , }, ]; -// --------------------------------------------------------------------------- -// Main component -// --------------------------------------------------------------------------- +export const OPENCLAW_FEATURES: FeatureListItem[] = [ + { + title: "Generative UI", + description: "Build apps, dashboards, and artifacts on demand", + icon: , + }, + { + title: "Persistent apps", + description: "Apps stay in place and refresh with live data automatically", + icon: , + }, + { + title: "Structured workspace", + description: "Agents, sessions, artifacts, and apps in one organized space", + icon: , + }, + { + title: "Full visibility", + description: "Inspect tool calls, context, and agent actions in real time", + icon: , + }, + { + title: "Direct control", + description: "Permissions, schedules, and execution from one interface", + icon: , + }, + { + title: "Elegant interface", + description: "Built for clarity with responsive layouts and themes", + icon: , + }, +]; -export function FeaturesSection() { +export function FeaturesSection({ + features = DEFAULT_FEATURES, + showCta = true, +}: { + features?: FeatureListItem[]; + showCta?: boolean; +} = {}) { return (
- + - {/* CTA button */} -
- - - Detailed comparison - View Comparison - - -
+ {showCta && ( +
+ + + Detailed comparison + View Comparison + + +
+ )}
); diff --git a/docs/app/(home)/sections/HeroSection/HeroSection.module.css b/docs/app/(home)/sections/HeroSection/HeroSection.module.css index e0fc7d729..a7b91d2ef 100644 --- a/docs/app/(home)/sections/HeroSection/HeroSection.module.css +++ b/docs/app/(home)/sections/HeroSection/HeroSection.module.css @@ -18,12 +18,26 @@ letter-spacing: -3px; } +.desktopTitleCompact { + font-size: 6rem; + letter-spacing: -2px; +} + +.titleAccent { + color: #9ca3af; +} + .mobileTitle { text-align: center; font-size: clamp(96px, 25vw, 120px); letter-spacing: -2px; } +.mobileTitleCompact { + font-size: clamp(40px, 10vw, 52px); + letter-spacing: -1px; +} + .desktopSubtitle, .mobileSubtitle, .tagline { @@ -55,6 +69,11 @@ line-height: 1.4; } +.taglineCompact { + font-size: 16px; + color: var(--openui-text-neutral-primary); +} + .taglineBreak { display: none; } @@ -99,6 +118,41 @@ /* ── NPM button ── */ +.npmButtonWrapper { + position: relative; + z-index: 5; + display: inline-block; + width: max-content; + max-width: 100%; +} + +.copyToast { + position: absolute; + top: 100%; + left: 50%; + z-index: 50; + margin-top: 0.375rem; + padding: 0.3125rem 0.75rem; + border-radius: var(--openui-radius-full); + background: rgba(0, 0, 0, 0.06); + color: var(--openui-text-neutral-secondary); + font-family: "Inter", sans-serif; + font-size: 0.75rem; + line-height: 1; + white-space: nowrap; + pointer-events: none; + opacity: 0; + transform: translate(-50%, -6px); + transition: + opacity 0.2s ease, + transform 0.2s ease; +} + +.copyToastVisible { + opacity: 1; + transform: translate(-50%, 0); +} + .npmButton { display: flex; height: 3rem; @@ -342,6 +396,42 @@ margin-top: 1rem; } +.desktopCtaStackShadowRoom { + padding-bottom: 0.75rem; +} + +.subtitleLogo { + display: block; + margin: 0.75rem auto 0; + width: 64px; + height: 64px; +} + +.subtitleLogoTile { + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.3em; + height: 1.3em; + margin: 0 0.3em 0 0.15em; + padding: 0.2em; + border-radius: 0.35em; + background: #ffffff; + border: 1px solid rgba(15, 23, 42, 0.08); + box-shadow: + 0 1px 2px rgba(15, 23, 42, 0.06), + 0 4px 8px rgba(15, 23, 42, 0.05); + vertical-align: middle; + position: relative; + top: -0.15em; +} + +.subtitleLogoTile img { + width: 100%; + height: 100%; + display: block; +} + /* ── Mobile hero layout ── */ .mobileHero { @@ -379,7 +469,7 @@ align-items: center; margin-top: 40px; gap: 1rem; - padding: 0.5rem 2.5rem 4rem; + padding: 0.5rem 2.5rem 1rem; } .mobileCtaButtonWidth { @@ -408,6 +498,10 @@ max-width: 75rem; } +.previewFrameWide { + max-width: 130rem; +} + .mobileIllustrationImage, .previewImage { display: block; @@ -415,6 +509,12 @@ height: auto; } +.previewFrameWide .previewImage { + width: 118%; + margin-inline: -9%; + max-width: none; +} + .previewSection { width: 100%; overflow: hidden; @@ -422,6 +522,10 @@ padding-inline: 1rem; } +.previewSectionTight { + margin-top: 0.5rem; +} + .previewDesktopOnly { display: none; width: 100%; @@ -475,7 +579,6 @@ display: block; position: relative; width: 100%; - overflow: hidden; padding: 8rem 2rem 0; } @@ -499,7 +602,7 @@ .previewSection { margin-top: 2.5rem; - padding-inline: 2rem; + padding-inline: 1rem; } .previewDesktopOnly { @@ -515,6 +618,10 @@ font-size: 28px; } + .taglineCompact { + font-size: 18px; + } + .taglineBreak { display: inline; } diff --git a/docs/app/(home)/sections/HeroSection/HeroSection.tsx b/docs/app/(home)/sections/HeroSection/HeroSection.tsx index 4acac1b62..7bfd87232 100644 --- a/docs/app/(home)/sections/HeroSection/HeroSection.tsx +++ b/docs/app/(home)/sections/HeroSection/HeroSection.tsx @@ -3,9 +3,12 @@ import { GitHubIcon } from "@/components/brand-logo"; import { ArrowRight } from "lucide-react"; import Link from "next/link"; +import { useEffect, useRef, useState, type ReactNode } from "react"; import { ClipboardCommandButton, PillLink } from "../../components/Button/Button"; import styles from "./HeroSection.module.css"; +export const heroStyles = styles; + // CTAs const primaryCTA = "npx @openuidev/cli@latest create"; const secondaryCTA = "Try Playground"; @@ -34,24 +37,58 @@ function TrailingArrow() { ); } -function NpmButton({ className = "" }: { className?: string }) { +const COPY_TOAST_MS = 1800; + +export function NpmButton({ className = "", command }: { className?: string; command: string }) { + const [showToast, setShowToast] = useState(false); + const toastTimeoutRef = useRef | null>(null); + + useEffect(() => { + return () => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + }; + }, []); + + const handleCopyChange = (copied: boolean) => { + if (!copied) return; + setShowToast(true); + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + toastTimeoutRef.current = setTimeout(() => { + setShowToast(false); + }, COPY_TOAST_MS); + }; + return ( - - {primaryCTA} - - - {primaryCTA} -