Merge pull request #12 from ibelick/feat/add-landing-page

feat: add landing page, init pnpm workspace
This commit is contained in:
Julien Thibeaut
2026-02-07 09:14:19 +01:00
committed by GitHub
94 changed files with 379 additions and 10031 deletions
-11
View File
@@ -1,11 +0,0 @@
{
"files.watcherExclude": {
"**/routeTree.gen.ts": true
},
"search.exclude": {
"**/routeTree.gen.ts": true
},
"files.readonlyInclude": {
"**/routeTree.gen.ts": true
}
}
+11 -11
View File
@@ -2,17 +2,17 @@
## Overview
WebClaw built with React + TanStack Router + Tailwind CSS v4.
WebClaw app lives in `apps/webclaw` and is built with React + TanStack Router + Tailwind CSS v4.
## Commands
- `npm run dev` — Start development server
- `npm run build` — Build for production
- `npm run preview` — Preview production build
- `npm run test` — Run tests
- `npm run lint` — Run ESLint
- `npm run format` — Run Prettier
- `npm run check` — Format and lint fix
- `pnpm -C apps/webclaw dev` — Start development server
- `pnpm -C apps/webclaw build` — Build for production
- `pnpm -C apps/webclaw preview` — Preview production build
- `pnpm -C apps/webclaw test` — Run tests
- `pnpm -C apps/webclaw lint` — Run ESLint
- `pnpm -C apps/webclaw format` — Run Prettier
- `pnpm -C apps/webclaw check` — Format and lint fix
## Conventions
@@ -26,9 +26,9 @@ WebClaw built with React + TanStack Router + Tailwind CSS v4.
### Routing & Structure
- Routes live in `src/routes` using TanStack file routing.
- Global styles and CSS variables live in `src/styles.css`.
- Local environment values go in `.env.local`.
- Routes live in `apps/webclaw/src/routes` using TanStack file routing.
- Global styles and CSS variables live in `apps/webclaw/src/styles.css`.
- Local environment values go in `apps/webclaw/.env.local`.
### UI & Styling
+8 -5
View File
@@ -1,16 +1,19 @@
# WebClaw
![Cover](./public/cover.webp)
![Cover](./apps/webclaw/public/cover.webp)
Fast web client for OpenClaw
Fast web client for OpenClaw.
Currently in beta.
## Setup
Create `.env.local` with `CLAWDBOT_GATEWAY_URL` and either `CLAWDBOT_GATEWAY_TOKEN` (recommended) or `CLAWDBOT_GATEWAY_PASSWORD`. These map to your OpenClaw Gateway auth (`gateway.auth.token` or `gateway.auth.password`). Default URL is `ws://127.0.0.1:18789`. Docs: https://docs.openclaw.ai/gateway
Create `apps/webclaw/.env.local` with `CLAWDBOT_GATEWAY_URL` and either
`CLAWDBOT_GATEWAY_TOKEN` (recommended) or `CLAWDBOT_GATEWAY_PASSWORD`. These map
to your OpenClaw Gateway auth (`gateway.auth.token` or `gateway.auth.password`).
Default URL is `ws://127.0.0.1:18789`. Docs: https://docs.openclaw.ai/gateway
```bash
npm install
npm run dev
pnpm install
pnpm dev
```
+28
View File
@@ -0,0 +1,28 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>WebClaw</title>
<meta name="description" content="Fast web client for OpenClaw." />
<meta name="theme-color" content="#fcfcfc" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="canonical" href="https://webclaw.dev/" />
<meta property="og:type" content="website" />
<meta property="og:title" content="WebClaw" />
<meta property="og:description" content="Fast web client for OpenClaw." />
<meta property="og:url" content="https://webclaw.dev/" />
<meta property="og:image" content="/webclaw-cover.webp" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="WebClaw" />
<meta name="twitter:description" content="Fast web client for OpenClaw." />
<meta name="twitter:image" content="/webclaw-cover.webp" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+24
View File
@@ -0,0 +1,24 @@
{
"name": "webclaw-landing",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite dev --port 3001",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
"devDependencies": {
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@vitejs/plugin-react": "^5.0.4",
"@tailwindcss/vite": "^4.1.18",
"tailwindcss": "^4.1.18",
"typescript": "^5.7.2",
"vite": "^7.1.7"
}
}

Before

Width:  |  Height:  |  Size: 897 B

After

Width:  |  Height:  |  Size: 897 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

+95
View File
@@ -0,0 +1,95 @@
import { CodeBlock, CodeBlockCode } from "@/components/ui/code-block";
export function App() {
return (
<div
className="min-h-screen text-neutral-900 selection:bg-neutral-900 selection:text-white"
style={{
fontFamily: '"Inter", system-ui, -apple-system, "Segoe UI", sans-serif',
}}
>
<main className="mx-auto flex min-h-screen max-w-4xl flex-col space-y-32 px-6 py-20">
<div className="flex flex-col space-y-14">
<div className="space-y-6">
<h1 className="font-medium">WebClaw</h1>
<p className="text-neutral-500">Fast web client for OpenClaw.</p>
<div className="flex flex-wrap gap-3">
<a
className="inline-flex items-center gap-1.5 rounded-full bg-neutral-900 px-5 py-2.5 text-white select-none"
href="https://github.com/ibelick/webclaw"
target="_blank"
rel="noopener noreferrer"
>
Github Repository
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
className="size-4"
>
<path
d="M10 20.5675C6.57143 21.7248 3.71429 20.5675 2 17"
stroke="#ffffff"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
></path>
<path
d="M10 22V18.7579C10 18.1596 10.1839 17.6396 10.4804 17.1699C10.6838 16.8476 10.5445 16.3904 10.1771 16.2894C7.13394 15.4528 5 14.1077 5 9.64606C5 8.48611 5.38005 7.39556 6.04811 6.4464C6.21437 6.21018 6.29749 6.09208 6.31748 5.9851C6.33746 5.87813 6.30272 5.73852 6.23322 5.45932C5.95038 4.32292 5.96871 3.11619 6.39322 2.02823C6.39322 2.02823 7.27042 1.74242 9.26698 2.98969C9.72282 3.27447 9.95075 3.41686 10.1515 3.44871C10.3522 3.48056 10.6206 3.41384 11.1573 3.28041C11.8913 3.09795 12.6476 3 13.5 3C14.3524 3 15.1087 3.09795 15.8427 3.28041C16.3794 3.41384 16.6478 3.48056 16.8485 3.44871C17.0493 3.41686 17.2772 3.27447 17.733 2.98969C19.7296 1.74242 20.6068 2.02823 20.6068 2.02823C21.0313 3.11619 21.0496 4.32292 20.7668 5.45932C20.6973 5.73852 20.6625 5.87813 20.6825 5.9851C20.7025 6.09207 20.7856 6.21019 20.9519 6.4464C21.6199 7.39556 22 8.48611 22 9.64606C22 14.1077 19.8661 15.4528 16.8229 16.2894C16.4555 16.3904 16.3162 16.8476 16.5196 17.1699C16.8161 17.6396 17 18.1596 17 18.7579V22"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
></path>
</svg>
</a>
</div>
</div>
<img
alt="OpenClaw interface preview"
className="pointer-events-none h-full w-full rounded-[6px] select-none"
src="/webclaw-cover.webp"
style={{
boxShadow: `
0px 0px 0px 1px rgba(0,0,0,0.08),
0px 8px 12px 0px rgba(0, 0, 0, 0.04),
0px 24px 32px 0px rgba(0, 0, 0, 0.04)
`,
}}
/>
</div>
<div className="space-y-6">
<h2 className="font-[450]">Installation</h2>
<CodeBlock className="rounded-[6px] border-none bg-[#FCFCFC] shadow-2xs outline-1 outline-neutral-950/10 [&>div]:bg-[#FCFCFC] [&>div>pre]:!bg-[#FCFCFC]">
<CodeBlockCode
code={`gh repo clone ibelick/webclaw
cd webclaw
pnpm i
pnpm run dev`}
language="bash"
/>
</CodeBlock>
</div>
</main>
<footer
className="flex w-full items-center justify-center gap-3 px-4 pt-24 pb-6 text-center text-sm font-medium text-neutral-400 sm:px-0"
style={{ pointerEvents: "auto" }}
>
<span>
<span>©2026</span>{" "}
<a
href="https://interfaceoffice.com"
target="_blank"
rel="noopener noreferrer"
className="text-neutral-400 hover:text-neutral-900"
>
Interface Office
</a>
</span>
</footer>
</div>
);
}
@@ -0,0 +1,63 @@
import type { ReactNode } from "react";
type CodeBlockProps = {
className?: string;
children: ReactNode;
};
type CodeBlockCodeProps = {
code: string;
language?: string;
};
export function CodeBlock({ className, children }: CodeBlockProps) {
return (
<div className={className}>
<div>
<pre className="m-0 overflow-x-auto rounded-[6px] bg-white p-4 text-[13px]">
{children}
</pre>
</div>
</div>
);
}
export function CodeBlockCode({ code, language }: CodeBlockCodeProps) {
const resolvedLanguage = language ?? "text";
const lines = code.split("\n");
if (resolvedLanguage === "bash") {
return (
<code className="whitespace-pre" data-language={resolvedLanguage}>
{lines.map((line, index) => {
const match = /^(gh|cd|pnpm)\s(.+)$/.exec(line);
const command = match?.[1];
const rest = match ? ` ${match[2]}` : line;
return (
<span key={`${line}-${index}`}>
{command === "gh" ? (
<span style={{ color: "rgb(111, 66, 193)" }}>gh</span>
) : command === "cd" ? (
<span style={{ color: "#005CC5" }}>cd</span>
) : command === "pnpm" ? (
<span style={{ color: "rgb(111, 66, 193)" }}>pnpm</span>
) : null}
<span style={{ color: "#032F62" }}>{rest}</span>
{index < lines.length - 1 ? "\n" : ""}
</span>
);
})}
</code>
);
}
return (
<code
className="whitespace-pre text-neutral-900"
data-language={resolvedLanguage}
>
{code}
</code>
);
}
+16
View File
@@ -0,0 +1,16 @@
import { createRoot } from "react-dom/client";
import { StrictMode } from "react";
import { App } from "./app";
import "./styles.css";
const root = document.getElementById("root");
if (!root) {
throw new Error("Root element not found");
}
createRoot(root).render(
<StrictMode>
<App />
</StrictMode>,
);
+15
View File
@@ -0,0 +1,15 @@
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap");
@import "tailwindcss";
body {
margin: 0;
}
html,
body {
@apply m-0 font-sans;
letter-spacing: -0.15px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #fcfcfc;
}
+24
View File
@@ -0,0 +1,24 @@
{
"include": ["src"],
"compilerOptions": {
"target": "ES2022",
"jsx": "react-jsx",
"module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["vite/client"],
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": false,
"noEmit": true,
"skipLibCheck": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
+13
View File
@@ -0,0 +1,13 @@
import { URL, fileURLToPath } from "node:url";
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
plugins: [react(), tailwindcss()],
});
+57
View File
@@ -0,0 +1,57 @@
{
"name": "webclaw",
"private": true,
"type": "module",
"scripts": {
"dev": "vite dev --port 3000",
"build": "vite build",
"preview": "vite preview",
"test": "vitest run",
"lint": "eslint",
"format": "prettier",
"check": "prettier --write . && eslint --fix"
},
"dependencies": {
"@base-ui/react": "^1.1.0",
"@hugeicons/core-free-icons": "^3.1.1",
"@hugeicons/react": "^1.1.4",
"@tailwindcss/vite": "^4.1.18",
"@tanstack/react-query": "^5.84.1",
"@tanstack/react-router": "^1.132.0",
"@tanstack/react-router-devtools": "^1.132.0",
"@tanstack/react-router-ssr-query": "^1.131.7",
"@tanstack/react-start": "^1.132.0",
"@tanstack/router-plugin": "^1.132.0",
"class-variance-authority": "^0.7.1",
"marked": "^17.0.1",
"motion": "^12.29.2",
"nitro": "npm:nitro-nightly@latest",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-markdown": "^10.1.0",
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.1",
"shiki": "^3.21.0",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18",
"use-stick-to-bottom": "^1.1.2",
"vite-tsconfig-paths": "^6.0.2",
"ws": "^8.19.0",
"zustand": "^5.0.11"
},
"devDependencies": {
"@tanstack/eslint-config": "^0.3.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.2.0",
"@types/node": "^22.10.2",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@vitejs/plugin-react": "^5.0.4",
"jsdom": "^27.0.0",
"prettier": "^3.5.3",
"typescript": "^5.7.2",
"vite": "^7.1.7",
"vitest": "^3.0.5",
"web-vitals": "^5.1.0"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 80 80" fill="none">
<rect x="0.5" y="0.5" width="79" height="79" fill="oklch(0.25 0.003 80)" />
<path d="M56.343 44.1513C56.7813 43.2831 57.1357 42.581 57.408 41.6289C61.865 32.3874 62.4007 22.0378 60.0033 15.1717C59.0614 12.3089 56.9095 12.312 56.8172 14.6528C55.9163 20.6969 54.1958 24.5178 52.98 26.9262C49.9119 33.0037 45.1248 38.6396 40.4708 42.1544C32.919 47.8252 30.4248 43.4776 29.9327 38.7481C29.5892 34.7 30.1781 30.0994 31.2457 24.9825C31.8513 22.9009 29.949 21.9406 28.6338 23.664C27.147 25.7271 25.849 27.8855 24.8831 30.2116C21.9386 35.6315 20.1097 43.9829 20.8551 52.6203C21.4024 56.7713 22.0623 60.1929 22.7221 63.6144C23.5382 67.7022 26.2713 67.9925 29.8909 66.9397C40.7669 64.1786 50.8494 53.4948 54.7601 46.6301C55.474 45.8726 55.9123 45.0044 56.343 44.1513Z" fill="oklch(0.95 0.004 80)" />
</svg>

After

Width:  |  Height:  |  Size: 897 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

@@ -16,9 +16,9 @@ export function clearSessionDeleted(id: string) {
tombstones.delete(id)
}
export function filterSessionsWithTombstones<T extends { key: string; friendlyId: string }>(
sessions: Array<T>,
) {
export function filterSessionsWithTombstones<
T extends { key: string; friendlyId: string },
>(sessions: Array<T>) {
if (tombstones.size === 0) return sessions
const now = Date.now()
let changed = false
@@ -12,7 +12,7 @@
--color-current: currentColor;
--color-surface: var(--color-white);
--color-ink: var(--color-black);
--color-primary-50: oklch(0.992 0.002 80);
--color-primary-100: oklch(0.9821 0 89.88);
--color-primary-200: oklch(0.9461 0 89.88);
@@ -28,7 +28,7 @@
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
--font-serif: 'EB Garamond', serif;
--font-mono: "JetBrains Mono", monospace;
--font-mono: 'JetBrains Mono', monospace;
}
.dark {
+1 -1
View File
@@ -4,7 +4,7 @@
"**/*.tsx",
"eslint.config.js",
"prettier.config.js",
"vite.config.js"
"vite.config.ts"
],
"compilerOptions": {
-9945
View File
File diff suppressed because it is too large Load Diff
+12 -52
View File
@@ -1,57 +1,17 @@
{
"name": "webclaw",
"name": "webclaw-monorepo",
"private": true,
"type": "module",
"packageManager": "pnpm@9.15.4",
"scripts": {
"dev": "vite dev --port 3000",
"build": "vite build",
"preview": "vite preview",
"test": "vitest run",
"lint": "eslint",
"format": "prettier",
"check": "prettier --write . && eslint --fix"
},
"dependencies": {
"@base-ui/react": "^1.1.0",
"@hugeicons/core-free-icons": "^3.1.1",
"@hugeicons/react": "^1.1.4",
"@tailwindcss/vite": "^4.1.18",
"@tanstack/react-query": "^5.84.1",
"@tanstack/react-router": "^1.132.0",
"@tanstack/react-router-devtools": "^1.132.0",
"@tanstack/react-router-ssr-query": "^1.131.7",
"@tanstack/react-start": "^1.132.0",
"@tanstack/router-plugin": "^1.132.0",
"class-variance-authority": "^0.7.1",
"marked": "^17.0.1",
"motion": "^12.29.2",
"nitro": "npm:nitro-nightly@latest",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-markdown": "^10.1.0",
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.1",
"shiki": "^3.21.0",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18",
"use-stick-to-bottom": "^1.1.2",
"vite-tsconfig-paths": "^6.0.2",
"ws": "^8.19.0",
"zustand": "^5.0.11"
},
"devDependencies": {
"@tanstack/eslint-config": "^0.3.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.2.0",
"@types/node": "^22.10.2",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@vitejs/plugin-react": "^5.0.4",
"jsdom": "^27.0.0",
"prettier": "^3.5.3",
"typescript": "^5.7.2",
"vite": "^7.1.7",
"vitest": "^3.0.5",
"web-vitals": "^5.1.0"
"dev": "pnpm -C apps/webclaw dev",
"build": "pnpm -C apps/webclaw build",
"landing:dev": "pnpm -C apps/landing dev",
"landing:build": "pnpm -C apps/landing build",
"landing:preview": "pnpm -C apps/landing preview",
"preview": "pnpm -C apps/webclaw preview",
"test": "pnpm -C apps/webclaw test",
"lint": "pnpm -C apps/webclaw lint",
"format": "pnpm -C apps/webclaw format",
"check": "pnpm -C apps/webclaw check"
}
}
+2
View File
@@ -0,0 +1,2 @@
packages:
- apps/*
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB