feat(web): animated mesh hero with peer constellation + comparison section
- New hero section with a live animated mesh background: three equal Claude Code peers in a triangle layout + six desaturated background peers, all rendered pixel-perfect from pure React/CSS using the exact Unicode characters and colors from Claude Code's own source. - User prompts type into the bottom prompt-input box and "submit" to scrollback (matching real Claude Code behavior). Mesh sends fly as envelope icons with fading trails between peers; receivers pulse on arrival. Dynamic routing by peer displayName. - Radial vignette overlay keeps the hero title crisp while letting the corner peers pulse visibly around the edges. Top/bottom linear fades bleed into adjacent sections. - Responsive scaling via ResizeObserver: cover-fit in hero bg context, contain-fit for standalone use. - Features section: added Skills, MCPs, and Commands as the first three tabs — the mesh's real differentiators. Updated subtitle copy. - New "Where claudemesh fits" section positioned between Features and WhatIsClaudemesh: four-card comparison (vs MCP, vs subagents, vs OpenClaw, and the positive claim) framing claudemesh as a wire between Claude Code sessions, not a replacement for any of them. All work is additive: 10 new files in apps/web/src/modules/marketing/ home/fake-claude-code/ plus hero-mesh-animation.tsx, hero-with-mesh.tsx, and where-mesh-fits.tsx. Single edit each to features.tsx and (marketing)/page.tsx to swap in the new hero and mount the new section. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { fccTheme } from "./theme";
|
||||
|
||||
export type ClawdPose = "default" | "arms-up" | "look-left" | "look-right";
|
||||
|
||||
const APPLE_EYES: Record<ClawdPose, string> = {
|
||||
default: " \u2597 \u2596 ",
|
||||
"look-left": " \u2598 \u2598 ",
|
||||
"look-right": " \u259d \u259d ",
|
||||
"arms-up": " \u2597 \u2596 ",
|
||||
};
|
||||
|
||||
export function Clawd({ pose = "default" }: { pose?: ClawdPose }) {
|
||||
const monoStyle: React.CSSProperties = {
|
||||
fontFamily: fccTheme.fontMono,
|
||||
color: fccTheme.clawdBody,
|
||||
lineHeight: 1,
|
||||
letterSpacing: 0,
|
||||
fontVariantLigatures: "none",
|
||||
fontFeatureSettings: '"liga" 0, "calt" 0',
|
||||
whiteSpace: "pre",
|
||||
};
|
||||
|
||||
const eyesStyle: React.CSSProperties = {
|
||||
backgroundColor: fccTheme.clawdBody,
|
||||
color: fccTheme.clawdBackground,
|
||||
};
|
||||
|
||||
const bodyRowStyle: React.CSSProperties = {
|
||||
backgroundColor: fccTheme.clawdBody,
|
||||
color: fccTheme.clawdBody,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
...monoStyle,
|
||||
}}
|
||||
aria-label="Claude Code mascot"
|
||||
>
|
||||
<div>
|
||||
<span>{"\u2597"}</span>
|
||||
<span style={eyesStyle}>{APPLE_EYES[pose]}</span>
|
||||
<span>{"\u2596"}</span>
|
||||
</div>
|
||||
<div style={bodyRowStyle}>{" "}</div>
|
||||
<div>{"\u2598\u2598 \u259d\u259d"}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Session, type SessionEvent, type SessionStep } from "./session";
|
||||
import { fccTheme } from "./theme";
|
||||
|
||||
type Point = { x: number; y: number };
|
||||
|
||||
type SessionConfig = {
|
||||
id: string;
|
||||
/** Display name used by mesh-send `to` fields to route particles */
|
||||
displayName: string;
|
||||
title: string;
|
||||
cwd: string;
|
||||
script: SessionStep[];
|
||||
startDelayMs?: number;
|
||||
position: {
|
||||
xPct: number;
|
||||
yPct: number;
|
||||
scale?: number;
|
||||
rotate?: number;
|
||||
opacity?: number;
|
||||
zIndex?: number;
|
||||
/** 0..1 — 1 is full color, 0 is grayscale */
|
||||
saturate?: number;
|
||||
/** pixels — adds depth-of-field bokeh blur to background peers */
|
||||
blurPx?: number;
|
||||
};
|
||||
};
|
||||
|
||||
type ArcConfig = {
|
||||
fromId: string;
|
||||
toId: string;
|
||||
triggerStepKind: "mesh-send";
|
||||
};
|
||||
|
||||
type FlyingParticle = {
|
||||
id: number;
|
||||
fromId: string;
|
||||
toId: string;
|
||||
bornAt: number;
|
||||
};
|
||||
|
||||
type MeshHeroProps = {
|
||||
sessions: SessionConfig[];
|
||||
arcs?: ArcConfig[];
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
|
||||
const PARTICLE_LIFE_MS = 1100;
|
||||
const TRAIL_SEGMENTS = 18;
|
||||
const TRAIL_SPAN = 0.34;
|
||||
const ICON_W = 38;
|
||||
const ICON_H = 26;
|
||||
|
||||
export function MeshHero({
|
||||
sessions,
|
||||
arcs = [],
|
||||
width = 1440,
|
||||
height = 720,
|
||||
}: MeshHeroProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const anchorsRef = useRef<Record<string, Point>>({});
|
||||
const [particles, setParticles] = useState<FlyingParticle[]>([]);
|
||||
const particleIdRef = useRef(0);
|
||||
const [, forceTick] = useState(0);
|
||||
const [reactions, setReactions] = useState<
|
||||
Record<string, { nonce: number; kind: "receive" | "send" | "arrive" }>
|
||||
>({});
|
||||
const reactionTimersRef = useRef<Record<string, ReturnType<typeof setTimeout>>>(
|
||||
{},
|
||||
);
|
||||
const arrivedParticlesRef = useRef<Set<number>>(new Set());
|
||||
|
||||
const bumpReaction = (
|
||||
sessionId: string,
|
||||
kind: "receive" | "send" | "arrive",
|
||||
) => {
|
||||
setReactions((prev) => ({
|
||||
...prev,
|
||||
[sessionId]: { nonce: (prev[sessionId]?.nonce ?? 0) + 1, kind },
|
||||
}));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let raf = 0;
|
||||
const loop = () => {
|
||||
forceTick((n) => (n + 1) % 1_000_000);
|
||||
raf = requestAnimationFrame(loop);
|
||||
};
|
||||
raf = requestAnimationFrame(loop);
|
||||
return () => cancelAnimationFrame(raf);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (particles.length === 0) return;
|
||||
const now = performance.now();
|
||||
const next = particles.filter((p) => now - p.bornAt < PARTICLE_LIFE_MS);
|
||||
if (next.length !== particles.length) setParticles(next);
|
||||
});
|
||||
|
||||
const handleEvent = (e: SessionEvent) => {
|
||||
if (e.kind !== "mesh-send") return;
|
||||
// Resolve destination by matching the mesh-send `to` field against
|
||||
// session displayNames. Fall back to the configured arcs if provided.
|
||||
const normalize = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
||||
const target = normalize(e.to);
|
||||
const toSession = sessions.find(
|
||||
(s) => normalize(s.displayName) === target,
|
||||
);
|
||||
let fromId = e.sessionId;
|
||||
let toId = toSession?.id;
|
||||
if (!toId) {
|
||||
const arc = arcs.find((a) => a.fromId === e.sessionId);
|
||||
if (!arc) return;
|
||||
toId = arc.toId;
|
||||
}
|
||||
if (fromId === toId) return;
|
||||
bumpReaction(fromId, "send");
|
||||
const id = particleIdRef.current++;
|
||||
setParticles((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id,
|
||||
fromId,
|
||||
toId,
|
||||
bornAt: performance.now(),
|
||||
},
|
||||
]);
|
||||
const timer = setTimeout(
|
||||
() => bumpReaction(toId!, "arrive"),
|
||||
PARTICLE_LIFE_MS - 60,
|
||||
);
|
||||
reactionTimersRef.current[`${id}`] = timer;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
Object.values(reactionTimersRef.current).forEach(clearTimeout);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const setAnchor = (id: string) => (el: HTMLDivElement | null) => {
|
||||
if (!el || !containerRef.current) return;
|
||||
const container = containerRef.current.getBoundingClientRect();
|
||||
const rect = el.getBoundingClientRect();
|
||||
anchorsRef.current[id] = {
|
||||
x: rect.left - container.left + rect.width / 2,
|
||||
y: rect.top - container.top + rect.height / 2,
|
||||
};
|
||||
};
|
||||
|
||||
const arcForParticle = (fromId: string, toId: string) => {
|
||||
const from = anchorsRef.current[fromId];
|
||||
const to = anchorsRef.current[toId];
|
||||
if (!from || !to) return null;
|
||||
const midX = (from.x + to.x) / 2;
|
||||
const midY = (from.y + to.y) / 2 - Math.abs(to.x - from.x) * 0.08 - 30;
|
||||
return { from, to, midX, midY };
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{
|
||||
position: "relative",
|
||||
width,
|
||||
height,
|
||||
background:
|
||||
"radial-gradient(ellipse at 50% 40%, rgba(215,119,87,0.07) 0%, rgba(0,0,0,0) 55%), #0a0a0a",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{sessions.map((s) => {
|
||||
const left = (s.position.xPct / 100) * width;
|
||||
const top = (s.position.yPct / 100) * height;
|
||||
const scale = s.position.scale ?? 1;
|
||||
const rotate = s.position.rotate ?? 0;
|
||||
const opacity = s.position.opacity ?? 1;
|
||||
const zIndex = s.position.zIndex ?? 1;
|
||||
const saturate = s.position.saturate ?? 1;
|
||||
const blurPx = s.position.blurPx ?? 0;
|
||||
const filters = [
|
||||
"drop-shadow(0 30px 50px rgba(0,0,0,0.6))",
|
||||
saturate !== 1 ? `saturate(${saturate})` : "",
|
||||
blurPx > 0 ? `blur(${blurPx}px)` : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
return (
|
||||
<div
|
||||
key={s.id}
|
||||
ref={setAnchor(s.id)}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left,
|
||||
top,
|
||||
transform: `translate(-50%, -50%) scale(${scale}) rotate(${rotate}deg)`,
|
||||
transformOrigin: "center center",
|
||||
filter: filters,
|
||||
opacity,
|
||||
zIndex,
|
||||
}}
|
||||
>
|
||||
<Session
|
||||
sessionId={s.id}
|
||||
script={s.script}
|
||||
title={s.title}
|
||||
cwd={s.cwd}
|
||||
width={720}
|
||||
height={480}
|
||||
startDelayMs={s.startDelayMs}
|
||||
onEvent={handleEvent}
|
||||
reactionNonce={reactions[s.id]?.nonce ?? 0}
|
||||
reactionKind={reactions[s.id]?.kind ?? "receive"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
pointerEvents: "none",
|
||||
mixBlendMode: "screen",
|
||||
}}
|
||||
>
|
||||
<defs>
|
||||
<filter id="meshGlow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur stdDeviation="3" result="blur" />
|
||||
<feMerge>
|
||||
<feMergeNode in="blur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
<symbol id="meshMsgIcon" viewBox="0 0 38 26">
|
||||
<rect
|
||||
x="1.5"
|
||||
y="1.5"
|
||||
width="35"
|
||||
height="23"
|
||||
rx="3"
|
||||
ry="3"
|
||||
fill={fccTheme.clawdBody}
|
||||
stroke={fccTheme.claudeShimmer}
|
||||
strokeWidth="1"
|
||||
/>
|
||||
<path
|
||||
d="M 4 5 L 19 15 L 34 5"
|
||||
stroke={fccTheme.clawdBackground}
|
||||
strokeWidth="2.2"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</symbol>
|
||||
</defs>
|
||||
{particles.map((p) => {
|
||||
const arc = arcForParticle(p.fromId, p.toId);
|
||||
if (!arc) return null;
|
||||
const age = (performance.now() - p.bornAt) / PARTICLE_LIFE_MS;
|
||||
if (age > 1) return null;
|
||||
const head = Math.min(1, Math.max(0, age));
|
||||
|
||||
const pointAt = (t: number) => {
|
||||
const tt = Math.max(0, Math.min(1, t));
|
||||
const inv = 1 - tt;
|
||||
return {
|
||||
x:
|
||||
inv * inv * arc.from.x +
|
||||
2 * inv * tt * arc.midX +
|
||||
tt * tt * arc.to.x,
|
||||
y:
|
||||
inv * inv * arc.from.y +
|
||||
2 * inv * tt * arc.midY +
|
||||
tt * tt * arc.to.y,
|
||||
};
|
||||
};
|
||||
|
||||
const trailNodes = Array.from({ length: TRAIL_SEGMENTS }, (_, i) => {
|
||||
const frac = i / TRAIL_SEGMENTS;
|
||||
const t = head - frac * TRAIL_SPAN;
|
||||
if (t < 0) return null;
|
||||
const pt = pointAt(t);
|
||||
const falloff = Math.pow(1 - frac, 2.2);
|
||||
return {
|
||||
x: pt.x,
|
||||
y: pt.y,
|
||||
r: 2 + falloff * 5,
|
||||
opacity: 0.75 * falloff,
|
||||
};
|
||||
}).filter((n): n is NonNullable<typeof n> => n !== null);
|
||||
|
||||
const headPt = pointAt(head);
|
||||
const iconOpacity = Math.min(1, Math.sin(head * Math.PI) * 1.2 + 0.15);
|
||||
|
||||
return (
|
||||
<g key={p.id} filter="url(#meshGlow)">
|
||||
{trailNodes.map((n, i) => (
|
||||
<circle
|
||||
key={i}
|
||||
cx={n.x}
|
||||
cy={n.y}
|
||||
r={n.r}
|
||||
fill={fccTheme.clawdBody}
|
||||
opacity={n.opacity}
|
||||
/>
|
||||
))}
|
||||
<use
|
||||
href="#meshMsgIcon"
|
||||
x={headPt.x - ICON_W / 2}
|
||||
y={headPt.y - ICON_H / 2}
|
||||
width={ICON_W}
|
||||
height={ICON_H}
|
||||
opacity={iconOpacity}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { fccTheme } from "./theme";
|
||||
|
||||
type BaseProps = { children: ReactNode };
|
||||
|
||||
export function UserPromptRow({ children }: BaseProps) {
|
||||
return (
|
||||
<div style={{ display: "flex", gap: "1ch", marginTop: 6 }}>
|
||||
<span style={{ color: fccTheme.dim }}>{"\u003e"}</span>
|
||||
<span style={{ color: fccTheme.text }}>{children}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function BashRunRow({
|
||||
command,
|
||||
lines,
|
||||
}: {
|
||||
command: string;
|
||||
lines?: string[];
|
||||
}) {
|
||||
return (
|
||||
<div style={{ marginTop: 10, marginBottom: 6 }}>
|
||||
<div style={{ display: "flex", gap: "0.7ch", alignItems: "baseline" }}>
|
||||
<span style={{ color: fccTheme.success }}>{"\u25cf"}</span>
|
||||
<span style={{ fontWeight: 700 }}>Bash</span>
|
||||
<span style={{ color: fccTheme.dim }}>({command})</span>
|
||||
</div>
|
||||
{lines?.map((l, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
paddingLeft: "2.2ch",
|
||||
color: fccTheme.dim,
|
||||
}}
|
||||
>
|
||||
<span style={{ color: fccTheme.subtle, marginRight: "0.7ch" }}>
|
||||
{"\u2514"}
|
||||
</span>
|
||||
{l}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function BulletRow({
|
||||
color = "success",
|
||||
children,
|
||||
}: {
|
||||
color?: "success" | "error" | "dim";
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const c =
|
||||
color === "error"
|
||||
? fccTheme.error
|
||||
: color === "dim"
|
||||
? fccTheme.dim
|
||||
: fccTheme.success;
|
||||
return (
|
||||
<div style={{ display: "flex", gap: "0.7ch", marginTop: 8 }}>
|
||||
<span style={{ color: c }}>{"\u25cf"}</span>
|
||||
<span>{children}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ToolUseRow({
|
||||
name,
|
||||
args,
|
||||
result,
|
||||
}: {
|
||||
name: string;
|
||||
args?: string;
|
||||
result?: string;
|
||||
}) {
|
||||
return (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<div style={{ display: "flex", gap: "0.7ch" }}>
|
||||
<span style={{ color: fccTheme.clawdBody }}>{"\u25cf"}</span>
|
||||
<span style={{ fontWeight: 700 }}>{name}</span>
|
||||
{args && <span style={{ color: fccTheme.dim }}>({args})</span>}
|
||||
</div>
|
||||
{result && (
|
||||
<div style={{ paddingLeft: "2.2ch", color: fccTheme.dim }}>
|
||||
<span style={{ color: fccTheme.subtle, marginRight: "0.7ch" }}>
|
||||
{"\u2514"}
|
||||
</span>
|
||||
{result}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AssistantTextRow({ children }: BaseProps) {
|
||||
return (
|
||||
<div style={{ marginTop: 8, color: fccTheme.text }}>
|
||||
<span style={{ color: fccTheme.clawdBody, marginRight: "0.7ch" }}>
|
||||
{"\u25cf"}
|
||||
</span>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MeshMessageRow({
|
||||
direction,
|
||||
peer,
|
||||
message,
|
||||
}: {
|
||||
direction: "out" | "in";
|
||||
peer: string;
|
||||
message: string;
|
||||
}) {
|
||||
const arrow = direction === "out" ? "\u2192" : "\u2190";
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 8,
|
||||
padding: "6px 10px",
|
||||
border: `1px solid ${fccTheme.clawdBody}`,
|
||||
borderRadius: 4,
|
||||
color: fccTheme.text,
|
||||
display: "flex",
|
||||
gap: "0.7ch",
|
||||
alignItems: "baseline",
|
||||
}}
|
||||
>
|
||||
<span style={{ color: fccTheme.clawdBody }}>mesh</span>
|
||||
<span style={{ color: fccTheme.dim }}>{arrow}</span>
|
||||
<span style={{ color: fccTheme.clawdBody, fontWeight: 700 }}>
|
||||
{peer}
|
||||
</span>
|
||||
<span style={{ color: fccTheme.dim }}>:</span>
|
||||
<span>{message}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { fccTheme } from "./theme";
|
||||
|
||||
type PromptInputProps = {
|
||||
value?: string;
|
||||
caret?: boolean;
|
||||
};
|
||||
|
||||
export function PromptInput({ value = "", caret = true }: PromptInputProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 12,
|
||||
border: `1px solid ${fccTheme.promptBorder}`,
|
||||
borderRadius: 4,
|
||||
padding: "6px 10px",
|
||||
color: fccTheme.text,
|
||||
display: "flex",
|
||||
alignItems: "baseline",
|
||||
gap: "0.7ch",
|
||||
}}
|
||||
>
|
||||
<span style={{ color: fccTheme.dim }}>{"\u003e"}</span>
|
||||
<span>{value}</span>
|
||||
{caret && (
|
||||
<span
|
||||
aria-hidden
|
||||
style={{
|
||||
display: "inline-block",
|
||||
width: "0.6ch",
|
||||
height: "1em",
|
||||
backgroundColor: fccTheme.text,
|
||||
animation: "fccCaret 1s steps(1) infinite",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<style>{`@keyframes fccCaret { 50% { opacity: 0; } }`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
398
apps/web/src/modules/marketing/home/fake-claude-code/session.tsx
Normal file
398
apps/web/src/modules/marketing/home/fake-claude-code/session.tsx
Normal file
@@ -0,0 +1,398 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
// useRef is still used for onEventRef below
|
||||
import { fccTheme } from "./theme";
|
||||
import { TerminalWindow } from "./terminal-window";
|
||||
import { Welcome } from "./welcome";
|
||||
import {
|
||||
AssistantTextRow,
|
||||
BulletRow,
|
||||
MeshMessageRow,
|
||||
ToolUseRow,
|
||||
UserPromptRow,
|
||||
} from "./message-row";
|
||||
import { PromptInput } from "./prompt-input";
|
||||
import { StatusBar } from "./status-bar";
|
||||
import { ThinkingSpinner } from "./thinking-spinner";
|
||||
|
||||
export type SessionStep =
|
||||
| { type: "user-input"; text: string; typeMs?: number }
|
||||
| { type: "thinking"; durationMs: number; label?: string }
|
||||
| { type: "assistant-text"; text: string; streamMs?: number }
|
||||
| { type: "tool-use"; name: string; args?: string; result?: string }
|
||||
| { type: "bullet"; text: string; color?: "success" | "error" | "dim" }
|
||||
| {
|
||||
type: "mesh-send";
|
||||
to: string;
|
||||
message: string;
|
||||
}
|
||||
| {
|
||||
type: "mesh-receive";
|
||||
from: string;
|
||||
message: string;
|
||||
}
|
||||
| { type: "pause"; durationMs: number };
|
||||
|
||||
export type SessionEvent =
|
||||
| { kind: "mesh-send"; sessionId: string; to: string; message: string; stepIndex: number }
|
||||
| { kind: "mesh-receive"; sessionId: string; from: string; message: string; stepIndex: number }
|
||||
| { kind: "step-start"; sessionId: string; stepIndex: number }
|
||||
| { kind: "script-complete"; sessionId: string };
|
||||
|
||||
export type SessionReaction = "receive" | "send" | "arrive";
|
||||
|
||||
export type SessionProps = {
|
||||
sessionId: string;
|
||||
script: SessionStep[];
|
||||
title?: string;
|
||||
cwd?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
contextPct?: number;
|
||||
loop?: boolean;
|
||||
startDelayMs?: number;
|
||||
onEvent?: (event: SessionEvent) => void;
|
||||
/**
|
||||
* Bumps to trigger a reaction animation. Parent increments this to fire the
|
||||
* matching effect — e.g. an "arrive" pulse when a mesh particle lands.
|
||||
*/
|
||||
reactionNonce?: number;
|
||||
reactionKind?: SessionReaction;
|
||||
};
|
||||
|
||||
type RenderedStep =
|
||||
| { kind: "user-input"; text: string; done: boolean }
|
||||
| { kind: "thinking"; label: string }
|
||||
| { kind: "assistant-text"; text: string; done: boolean }
|
||||
| { kind: "tool-use"; name: string; args?: string; result?: string }
|
||||
| { kind: "bullet"; text: string; color: "success" | "error" | "dim" }
|
||||
| { kind: "mesh-send"; to: string; message: string }
|
||||
| { kind: "mesh-receive"; from: string; message: string };
|
||||
|
||||
export function Session({
|
||||
sessionId,
|
||||
script,
|
||||
title,
|
||||
cwd = "/Users/agutierrez",
|
||||
width = 760,
|
||||
height = 540,
|
||||
contextPct = 6,
|
||||
loop = true,
|
||||
startDelayMs = 0,
|
||||
onEvent,
|
||||
reactionNonce = 0,
|
||||
reactionKind = "receive",
|
||||
}: SessionProps) {
|
||||
const [rendered, setRendered] = useState<RenderedStep[]>([]);
|
||||
const [liveInput, setLiveInput] = useState("");
|
||||
const [cycle, setCycle] = useState(0);
|
||||
const onEventRef = useRef(onEvent);
|
||||
onEventRef.current = onEvent;
|
||||
|
||||
const scriptKey = useMemo(
|
||||
() => script.map((s) => s.type).join("|") + "::" + sessionId,
|
||||
[script, sessionId],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setRendered([]);
|
||||
setLiveInput("");
|
||||
|
||||
const wait = (ms: number) =>
|
||||
new Promise<void>((res) => {
|
||||
const id = setTimeout(() => {
|
||||
if (!cancelled) res();
|
||||
}, ms);
|
||||
void id;
|
||||
});
|
||||
|
||||
const emit = (e: SessionEvent) => {
|
||||
if (!cancelled) onEventRef.current?.(e);
|
||||
};
|
||||
|
||||
const appendStep = (step: RenderedStep) => {
|
||||
if (cancelled) return;
|
||||
setRendered((prev) => [...prev, step]);
|
||||
};
|
||||
|
||||
const updateLast = (mut: (s: RenderedStep) => RenderedStep) => {
|
||||
if (cancelled) return;
|
||||
setRendered((prev) => {
|
||||
if (prev.length === 0) return prev;
|
||||
const next = prev.slice();
|
||||
next[next.length - 1] = mut(next[next.length - 1]);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const popLast = () => {
|
||||
if (cancelled) return;
|
||||
setRendered((prev) => prev.slice(0, -1));
|
||||
};
|
||||
|
||||
const run = async () => {
|
||||
if (startDelayMs > 0) await wait(startDelayMs);
|
||||
for (let i = 0; i < script.length; i++) {
|
||||
if (cancelled) return;
|
||||
const step = script[i];
|
||||
emit({ kind: "step-start", sessionId, stepIndex: i });
|
||||
|
||||
switch (step.type) {
|
||||
case "pause":
|
||||
await wait(step.durationMs);
|
||||
break;
|
||||
|
||||
case "user-input": {
|
||||
const ms = step.typeMs ?? 35;
|
||||
for (let c = 1; c <= step.text.length; c++) {
|
||||
if (cancelled) return;
|
||||
const slice = step.text.slice(0, c);
|
||||
if (!cancelled) setLiveInput(slice);
|
||||
await wait(ms);
|
||||
}
|
||||
// Submit: clear the input box and push the prompt into scrollback
|
||||
await wait(260);
|
||||
if (cancelled) return;
|
||||
setLiveInput("");
|
||||
appendStep({ kind: "user-input", text: step.text, done: true });
|
||||
await wait(140);
|
||||
break;
|
||||
}
|
||||
|
||||
case "thinking": {
|
||||
appendStep({ kind: "thinking", label: step.label ?? "Thinking" });
|
||||
await wait(step.durationMs);
|
||||
popLast();
|
||||
break;
|
||||
}
|
||||
|
||||
case "assistant-text": {
|
||||
appendStep({ kind: "assistant-text", text: "", done: false });
|
||||
const ms = step.streamMs ?? 18;
|
||||
for (let c = 1; c <= step.text.length; c++) {
|
||||
if (cancelled) return;
|
||||
const slice = step.text.slice(0, c);
|
||||
updateLast((s) =>
|
||||
s.kind === "assistant-text" ? { ...s, text: slice } : s,
|
||||
);
|
||||
await wait(ms);
|
||||
}
|
||||
updateLast((s) =>
|
||||
s.kind === "assistant-text" ? { ...s, done: true } : s,
|
||||
);
|
||||
await wait(250);
|
||||
break;
|
||||
}
|
||||
|
||||
case "tool-use": {
|
||||
appendStep({
|
||||
kind: "tool-use",
|
||||
name: step.name,
|
||||
args: step.args,
|
||||
result: step.result,
|
||||
});
|
||||
await wait(400);
|
||||
break;
|
||||
}
|
||||
|
||||
case "bullet": {
|
||||
appendStep({
|
||||
kind: "bullet",
|
||||
text: step.text,
|
||||
color: step.color ?? "success",
|
||||
});
|
||||
await wait(200);
|
||||
break;
|
||||
}
|
||||
|
||||
case "mesh-send": {
|
||||
appendStep({
|
||||
kind: "mesh-send",
|
||||
to: step.to,
|
||||
message: step.message,
|
||||
});
|
||||
emit({
|
||||
kind: "mesh-send",
|
||||
sessionId,
|
||||
to: step.to,
|
||||
message: step.message,
|
||||
stepIndex: i,
|
||||
});
|
||||
await wait(350);
|
||||
break;
|
||||
}
|
||||
|
||||
case "mesh-receive": {
|
||||
appendStep({
|
||||
kind: "mesh-receive",
|
||||
from: step.from,
|
||||
message: step.message,
|
||||
});
|
||||
emit({
|
||||
kind: "mesh-receive",
|
||||
sessionId,
|
||||
from: step.from,
|
||||
message: step.message,
|
||||
stepIndex: i,
|
||||
});
|
||||
await wait(350);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cancelled) return;
|
||||
emit({ kind: "script-complete", sessionId });
|
||||
|
||||
if (loop) {
|
||||
await wait(2000);
|
||||
if (cancelled) return;
|
||||
setCycle((c) => c + 1);
|
||||
}
|
||||
};
|
||||
|
||||
void run();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [scriptKey, cycle, loop, script, sessionId, startDelayMs]);
|
||||
|
||||
const reactionClass = `fcc-react-${reactionKind}`;
|
||||
const reactionKey = `${reactionKind}-${reactionNonce}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={reactionKey}
|
||||
className={reactionNonce > 0 ? reactionClass : undefined}
|
||||
style={{ willChange: "transform, filter" }}
|
||||
>
|
||||
<style>{`
|
||||
@keyframes fccPulseReceive {
|
||||
0% { transform: scale(1); filter: drop-shadow(0 0 0 rgba(215,119,87,0)); }
|
||||
30% { transform: scale(1.02); filter: drop-shadow(0 0 22px rgba(215,119,87,0.55)); }
|
||||
100% { transform: scale(1); filter: drop-shadow(0 0 0 rgba(215,119,87,0)); }
|
||||
}
|
||||
@keyframes fccPulseArrive {
|
||||
0% { transform: scale(1); filter: brightness(1) drop-shadow(0 0 0 rgba(215,119,87,0)); }
|
||||
25% { transform: scale(1.015); filter: brightness(1.25) drop-shadow(0 0 30px rgba(215,119,87,0.7)); }
|
||||
100% { transform: scale(1); filter: brightness(1) drop-shadow(0 0 0 rgba(215,119,87,0)); }
|
||||
}
|
||||
@keyframes fccPulseSend {
|
||||
0% { transform: scale(1); }
|
||||
35% { transform: scale(0.99); filter: drop-shadow(0 0 12px rgba(215,119,87,0.35)); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
.fcc-react-receive { animation: fccPulseReceive 380ms cubic-bezier(0.22, 0.61, 0.36, 1); }
|
||||
.fcc-react-arrive { animation: fccPulseArrive 520ms cubic-bezier(0.22, 0.61, 0.36, 1); }
|
||||
.fcc-react-send { animation: fccPulseSend 260ms cubic-bezier(0.22, 0.61, 0.36, 1); }
|
||||
`}</style>
|
||||
<TerminalWindow width={width} height={height} title={title}>
|
||||
<div style={{ marginBottom: 6 }}>
|
||||
<span style={{ color: fccTheme.dim }}>[(base) </span>
|
||||
<span style={{ color: fccTheme.text }}>agutierrez@Mac</span>
|
||||
<span style={{ color: fccTheme.dim }}> ~ % </span>
|
||||
<span style={{ color: fccTheme.text }}>claude</span>
|
||||
</div>
|
||||
<Welcome cwd={cwd} />
|
||||
<div
|
||||
aria-hidden
|
||||
style={{
|
||||
marginTop: 8,
|
||||
marginBottom: 4,
|
||||
height: 1,
|
||||
background: `repeating-linear-gradient(90deg, ${fccTheme.subtle} 0 6px, transparent 6px 10px)`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
minHeight: 180,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
{rendered.map((s, i) => {
|
||||
switch (s.kind) {
|
||||
case "user-input":
|
||||
return (
|
||||
<UserPromptRow key={i}>
|
||||
{s.text}
|
||||
{!s.done && <BlinkCursor />}
|
||||
</UserPromptRow>
|
||||
);
|
||||
case "thinking":
|
||||
return (
|
||||
<div key={i} style={{ marginTop: 8 }}>
|
||||
<ThinkingSpinner label={s.label} />
|
||||
</div>
|
||||
);
|
||||
case "assistant-text":
|
||||
return (
|
||||
<AssistantTextRow key={i}>
|
||||
{s.text}
|
||||
{!s.done && <BlinkCursor />}
|
||||
</AssistantTextRow>
|
||||
);
|
||||
case "tool-use":
|
||||
return (
|
||||
<ToolUseRow
|
||||
key={i}
|
||||
name={s.name}
|
||||
args={s.args}
|
||||
result={s.result}
|
||||
/>
|
||||
);
|
||||
case "bullet":
|
||||
return (
|
||||
<BulletRow key={i} color={s.color}>
|
||||
{s.text}
|
||||
</BulletRow>
|
||||
);
|
||||
case "mesh-send":
|
||||
return (
|
||||
<MeshMessageRow
|
||||
key={i}
|
||||
direction="out"
|
||||
peer={s.to}
|
||||
message={s.message}
|
||||
/>
|
||||
);
|
||||
case "mesh-receive":
|
||||
return (
|
||||
<MeshMessageRow
|
||||
key={i}
|
||||
direction="in"
|
||||
peer={s.from}
|
||||
message={s.message}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
|
||||
<PromptInput value={liveInput} />
|
||||
<StatusBar cwd="~" contextPct={contextPct} />
|
||||
</TerminalWindow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BlinkCursor() {
|
||||
return (
|
||||
<span
|
||||
aria-hidden
|
||||
style={{
|
||||
display: "inline-block",
|
||||
width: "0.55ch",
|
||||
height: "1em",
|
||||
marginLeft: "0.15ch",
|
||||
verticalAlign: "text-bottom",
|
||||
backgroundColor: fccTheme.text,
|
||||
animation: "fccCaret 1s steps(1) infinite",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { fccTheme } from "./theme";
|
||||
|
||||
type StatusBarProps = {
|
||||
user?: string;
|
||||
cwd?: string;
|
||||
model?: string;
|
||||
contextPct?: number;
|
||||
errorNote?: string;
|
||||
errorAction?: string;
|
||||
};
|
||||
|
||||
export function StatusBar({
|
||||
user = "agutierrez@Mac",
|
||||
cwd = "~",
|
||||
model = "Opus 4.6 (1M context)",
|
||||
contextPct = 6,
|
||||
errorNote,
|
||||
errorAction,
|
||||
}: StatusBarProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 16,
|
||||
paddingTop: 8,
|
||||
borderTop: `1px solid ${fccTheme.subtle}`,
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
color: fccTheme.dim,
|
||||
fontSize: "inherit",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<span>{user}</span>
|
||||
<span style={{ margin: "0 0.7ch" }}>{"\u007c"}</span>
|
||||
<span>{cwd}</span>
|
||||
<span style={{ margin: "0 0.7ch" }}>{"\u007c"}</span>
|
||||
<span>{model}</span>
|
||||
<span style={{ marginLeft: "0.7ch" }}>{`[ctx:${contextPct}%]`}</span>
|
||||
</div>
|
||||
{errorNote && (
|
||||
<div>
|
||||
<span style={{ color: fccTheme.error }}>{errorNote}</span>
|
||||
{errorAction && (
|
||||
<>
|
||||
<span style={{ color: fccTheme.dim, margin: "0 0.7ch" }}>
|
||||
{"\u00b7"}
|
||||
</span>
|
||||
<span style={{ color: fccTheme.clawdBody }}>{errorAction}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { fccTheme } from "./theme";
|
||||
|
||||
type TerminalWindowProps = {
|
||||
title?: string;
|
||||
width?: number | string;
|
||||
height?: number | string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export function TerminalWindow({
|
||||
title = "agutierrez — \u2728 Initialize new coding project — node · claude — 80\u00d724",
|
||||
width = 760,
|
||||
height = 520,
|
||||
children,
|
||||
}: TerminalWindowProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width,
|
||||
height,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
borderRadius: 10,
|
||||
overflow: "hidden",
|
||||
boxShadow:
|
||||
"0 30px 80px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.05)",
|
||||
backgroundColor: fccTheme.terminalBg,
|
||||
fontFamily: fccTheme.fontMono,
|
||||
}}
|
||||
>
|
||||
<TitleBar title={title} />
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: "14px 18px 16px 18px",
|
||||
fontSize: 13,
|
||||
lineHeight: 1.45,
|
||||
color: fccTheme.text,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TitleBar({ title }: { title: string }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
height: 28,
|
||||
paddingInline: 12,
|
||||
backgroundColor: fccTheme.terminalChrome,
|
||||
borderBottom: "1px solid rgba(0,0,0,0.4)",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
<TrafficLight color="#ff5f57" />
|
||||
<TrafficLight color="#febc2e" />
|
||||
<TrafficLight color="#28c840" />
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
fontSize: 12,
|
||||
color: "rgba(255,255,255,0.85)",
|
||||
fontWeight: 600,
|
||||
pointerEvents: "none",
|
||||
fontFamily:
|
||||
"-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 6,
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<FolderIcon />
|
||||
<span
|
||||
style={{
|
||||
maxWidth: "70%",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TrafficLight({ color }: { color: string }) {
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
display: "inline-block",
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: color,
|
||||
boxShadow: "inset 0 0 0 0.5px rgba(0,0,0,0.25)",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function FolderIcon() {
|
||||
return (
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" aria-hidden>
|
||||
<path
|
||||
d="M1.5 4.5a1 1 0 0 1 1-1h3.3l1.4 1.4h6.3a1 1 0 0 1 1 1v5.6a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1V4.5Z"
|
||||
stroke="rgba(255,255,255,0.75)"
|
||||
strokeWidth="1.2"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export const fccTheme = {
|
||||
clawdBody: "rgb(215,119,87)",
|
||||
clawdBackground: "rgb(0,0,0)",
|
||||
text: "rgb(255,255,255)",
|
||||
dim: "rgb(153,153,153)",
|
||||
subtle: "rgb(80,80,80)",
|
||||
success: "rgb(78,186,101)",
|
||||
error: "rgb(255,107,128)",
|
||||
claudeShimmer: "rgb(235,159,127)",
|
||||
promptBorder: "rgb(136,136,136)",
|
||||
bashBorder: "rgb(253,93,177)",
|
||||
terminalBg: "rgb(0,0,0)",
|
||||
terminalChrome: "rgb(55,55,57)",
|
||||
fontMono:
|
||||
"'Menlo', 'Monaco', 'SF Mono', 'JetBrains Mono', 'Consolas', ui-monospace, monospace",
|
||||
} as const;
|
||||
@@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { fccTheme } from "./theme";
|
||||
|
||||
const FRAMES = ["\u2847", "\u284f", "\u285f", "\u287f", "\u28ff", "\u28f7", "\u28e7", "\u28c7"];
|
||||
|
||||
type ThinkingSpinnerProps = {
|
||||
label?: string;
|
||||
intervalMs?: number;
|
||||
};
|
||||
|
||||
export function ThinkingSpinner({
|
||||
label = "Thinking",
|
||||
intervalMs = 80,
|
||||
}: ThinkingSpinnerProps) {
|
||||
const [i, setI] = useState(0);
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setI((n) => (n + 1) % FRAMES.length), intervalMs);
|
||||
return () => clearInterval(id);
|
||||
}, [intervalMs]);
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", gap: "0.6ch", color: fccTheme.claudeShimmer }}>
|
||||
<span style={{ color: fccTheme.clawdBody }}>{FRAMES[i]}</span>
|
||||
<span style={{ color: fccTheme.dim, fontStyle: "italic" }}>{label}…</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Clawd, type ClawdPose } from "./clawd";
|
||||
import { fccTheme } from "./theme";
|
||||
|
||||
type WelcomeProps = {
|
||||
pose?: ClawdPose;
|
||||
version?: string;
|
||||
model?: string;
|
||||
billing?: string;
|
||||
cwd?: string;
|
||||
};
|
||||
|
||||
export function Welcome({
|
||||
pose = "default",
|
||||
version = "2.1.101",
|
||||
model = "Opus 4.6 (1M context)",
|
||||
billing = "Claude Max",
|
||||
cwd = "/Users/agutierrez",
|
||||
}: WelcomeProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: "1ch",
|
||||
alignItems: "flex-start",
|
||||
fontFamily: fccTheme.fontMono,
|
||||
color: fccTheme.text,
|
||||
lineHeight: 1.15,
|
||||
}}
|
||||
>
|
||||
<div style={{ flexShrink: 0, paddingTop: "0.1em" }}>
|
||||
<Clawd pose={pose} />
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "0.05em",
|
||||
paddingTop: "0.1em",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<span style={{ fontWeight: 700 }}>Claude Code</span>{" "}
|
||||
<span style={{ color: fccTheme.dim }}>v{version}</span>
|
||||
</div>
|
||||
<div style={{ color: fccTheme.dim }}>
|
||||
{model} · {billing}
|
||||
</div>
|
||||
<div style={{ color: fccTheme.dim }}>{cwd}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,33 @@ import { useState } from "react";
|
||||
import { Reveal, SectionIcon } from "./_reveal";
|
||||
|
||||
const FEATURES = [
|
||||
{
|
||||
key: "skills",
|
||||
tab: "Skills",
|
||||
title: "Publish a skill once, every peer invokes it",
|
||||
body: "Write a skill in ~/.claude/skills/review-pr, share it to the mesh, and every teammate's Claude Code has /review-pr. Update the skill on your end → every peer auto-refreshes. No manual CLAUDE.md edits, no git pulls, no copy-paste.",
|
||||
code: `share_skill(name: "review-pr", dir: "./.claude/skills/review-pr")
|
||||
mesh_skill_deploy("review-pr")
|
||||
list_skills() → all skills live on the mesh`,
|
||||
},
|
||||
{
|
||||
key: "mcps",
|
||||
tab: "MCPs",
|
||||
title: "Share an MCP server once, every peer sees its tools",
|
||||
body: "Register an MCP on your machine — Postgres, Stripe, internal API, whatever — then mesh_mcp_deploy it. Every peer's Claude Code auto-discovers the tools, with per-mesh scope and audit logs. Credentials never leave your machine.",
|
||||
code: `mesh_mcp_register("postgres-prod", command: "npx mcp-postgres")
|
||||
mesh_mcp_deploy("postgres-prod")
|
||||
mesh_mcp_catalog() → every MCP live on the mesh`,
|
||||
},
|
||||
{
|
||||
key: "commands",
|
||||
tab: "Commands",
|
||||
title: "Slash commands that travel with the mesh",
|
||||
body: "Any slash command you've defined — /deploy, /audit, /review-pr — can be published to the mesh. Teammates invoke it from their own Claude Code. The command runs with your logic and rules, their context. Shared muscle memory, no copying files between repos.",
|
||||
code: `share_skill(name: "deploy", kind: "command")
|
||||
// Peer B types /deploy in their session
|
||||
// → runs your publisher-side playbook in their repo`,
|
||||
},
|
||||
{
|
||||
key: "groups",
|
||||
tab: "Groups",
|
||||
@@ -90,7 +117,7 @@ export const Features = () => {
|
||||
className="mx-auto mt-4 max-w-xl text-center text-sm text-[var(--cm-fg-tertiary)]"
|
||||
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||
>
|
||||
43 MCP tools. Groups, state, memory, files, databases, vectors, streams — all shipped.
|
||||
Skills, MCPs, slash commands, groups, state, memory, files, databases, vectors, streams — every primitive meshed, end-to-end encrypted.
|
||||
</p>
|
||||
</Reveal>
|
||||
<Reveal delay={3}>
|
||||
|
||||
354
apps/web/src/modules/marketing/home/hero-mesh-animation.tsx
Normal file
354
apps/web/src/modules/marketing/home/hero-mesh-animation.tsx
Normal file
@@ -0,0 +1,354 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { MeshHero } from "./fake-claude-code/mesh-hero";
|
||||
import type { SessionStep } from "./fake-claude-code/session";
|
||||
|
||||
const NATURAL_W = 1600;
|
||||
const NATURAL_H = 860;
|
||||
|
||||
const SCRIPT_A: SessionStep[] = [
|
||||
{ type: "pause", durationMs: 400 },
|
||||
{ type: "user-input", text: "share_skill /review-pr" },
|
||||
{ type: "mesh-send", to: "Lug Nut", message: "share_skill /review-pr" },
|
||||
{ type: "pause", durationMs: 1200 },
|
||||
{ type: "mesh-receive", from: "Mou", message: "postgres-prod MCP live" },
|
||||
{ type: "pause", durationMs: 800 },
|
||||
{
|
||||
type: "tool-use",
|
||||
name: "mesh_tool_call",
|
||||
args: "postgres-prod.query",
|
||||
result: "142 rows",
|
||||
},
|
||||
{ type: "pause", durationMs: 1100 },
|
||||
{ type: "mesh-send", to: "Mou", message: "thanks — skill in use" },
|
||||
{ type: "pause", durationMs: 2200 },
|
||||
];
|
||||
|
||||
const SCRIPT_B: SessionStep[] = [
|
||||
{ type: "pause", durationMs: 700 },
|
||||
{ type: "mesh-receive", from: "Alexis", message: "/review-pr shared" },
|
||||
{ type: "pause", durationMs: 800 },
|
||||
{ type: "user-input", text: "/review-pr PR #142" },
|
||||
{ type: "thinking", durationMs: 700 },
|
||||
{
|
||||
type: "tool-use",
|
||||
name: "Read",
|
||||
args: "auth/middleware.ts",
|
||||
result: "142 lines",
|
||||
},
|
||||
{ type: "pause", durationMs: 800 },
|
||||
{ type: "mesh-send", to: "Mou", message: "found 2 issues in auth flow" },
|
||||
{ type: "pause", durationMs: 1500 },
|
||||
{ type: "mesh-receive", from: "Alexis", message: "thanks — skill in use" },
|
||||
{ type: "pause", durationMs: 1600 },
|
||||
];
|
||||
|
||||
const SCRIPT_C: SessionStep[] = [
|
||||
{ type: "pause", durationMs: 300 },
|
||||
{ type: "user-input", text: "expose postgres to mesh" },
|
||||
{
|
||||
type: "tool-use",
|
||||
name: "mesh_mcp_deploy",
|
||||
args: "postgres-prod",
|
||||
result: "exposed to 6 peers",
|
||||
},
|
||||
{ type: "mesh-send", to: "Alexis", message: "postgres-prod MCP live" },
|
||||
{ type: "pause", durationMs: 1400 },
|
||||
{
|
||||
type: "mesh-receive",
|
||||
from: "Lug Nut",
|
||||
message: "found 2 issues in auth flow",
|
||||
},
|
||||
{ type: "pause", durationMs: 700 },
|
||||
{ type: "assistant-text", text: "Patching issues via mesh." },
|
||||
{ type: "pause", durationMs: 900 },
|
||||
{
|
||||
type: "mesh-send",
|
||||
to: "Lug Nut",
|
||||
message: "fix pushed — rerun /review-pr",
|
||||
},
|
||||
{ type: "pause", durationMs: 1800 },
|
||||
];
|
||||
|
||||
const SCRIPT_PIP: SessionStep[] = [
|
||||
{ type: "pause", durationMs: 1200 },
|
||||
{ type: "mesh-receive", from: "Alexis", message: "share_skill /review-pr" },
|
||||
{ type: "pause", durationMs: 1600 },
|
||||
{ type: "mesh-send", to: "Alexis", message: "cache warm" },
|
||||
{ type: "pause", durationMs: 3200 },
|
||||
];
|
||||
|
||||
const SCRIPT_RIPPLE: SessionStep[] = [
|
||||
{ type: "pause", durationMs: 2100 },
|
||||
{ type: "mesh-receive", from: "Mou", message: "postgres-prod MCP live" },
|
||||
{ type: "pause", durationMs: 1800 },
|
||||
{ type: "mesh-send", to: "Mou", message: "mirror ready" },
|
||||
{ type: "pause", durationMs: 3000 },
|
||||
];
|
||||
|
||||
const SCRIPT_NEBULA: SessionStep[] = [
|
||||
{ type: "pause", durationMs: 2800 },
|
||||
{ type: "mesh-receive", from: "Lug Nut", message: "need security review" },
|
||||
{ type: "pause", durationMs: 1500 },
|
||||
{ type: "mesh-send", to: "Lug Nut", message: "reviewed — LGTM" },
|
||||
{ type: "pause", durationMs: 3000 },
|
||||
];
|
||||
|
||||
const SCRIPT_JET: SessionStep[] = [
|
||||
{ type: "pause", durationMs: 1800 },
|
||||
{ type: "mesh-receive", from: "Alexis", message: "thanks — skill in use" },
|
||||
{ type: "pause", durationMs: 1800 },
|
||||
{ type: "mesh-send", to: "Alexis", message: "heartbeat ok" },
|
||||
{ type: "pause", durationMs: 3200 },
|
||||
];
|
||||
|
||||
const SCRIPT_VELA: SessionStep[] = [
|
||||
{ type: "pause", durationMs: 900 },
|
||||
{ type: "mesh-send", to: "Lug Nut", message: "broker uptime 99.98" },
|
||||
{ type: "pause", durationMs: 2400 },
|
||||
{ type: "mesh-receive", from: "Mou", message: "postgres-prod MCP live" },
|
||||
{ type: "pause", durationMs: 3400 },
|
||||
];
|
||||
|
||||
const SCRIPT_OREL: SessionStep[] = [
|
||||
{ type: "pause", durationMs: 2400 },
|
||||
{ type: "mesh-receive", from: "Alexis", message: "share_skill /review-pr" },
|
||||
{ type: "pause", durationMs: 1600 },
|
||||
{ type: "mesh-send", to: "Alexis", message: "mirrored downstream" },
|
||||
{ type: "pause", durationMs: 3000 },
|
||||
];
|
||||
|
||||
type HeroMeshAnimationProps = {
|
||||
/**
|
||||
* `cover` — fill both width and height of the parent, overflow clipped (for
|
||||
* use as a hero background). `contain` — fit within width, height scales
|
||||
* proportionally (standalone use).
|
||||
*/
|
||||
fit?: "cover" | "contain";
|
||||
};
|
||||
|
||||
export function HeroMeshAnimation({ fit = "contain" }: HeroMeshAnimationProps) {
|
||||
const outerRef = useRef<HTMLDivElement>(null);
|
||||
const [fitScale, setFitScale] = useState(1);
|
||||
|
||||
useEffect(() => {
|
||||
const el = outerRef.current;
|
||||
if (!el) return;
|
||||
const compute = (w: number, h: number) => {
|
||||
if (fit === "cover") {
|
||||
// Pick the larger ratio so the composition fills both dimensions.
|
||||
// Never scale below 1 in cover mode — we want overflow if the parent
|
||||
// is smaller than the natural size.
|
||||
const s = Math.max(w / NATURAL_W, h / NATURAL_H);
|
||||
setFitScale(Math.max(s, 0.001));
|
||||
} else {
|
||||
setFitScale(Math.min(1, w / NATURAL_W));
|
||||
}
|
||||
};
|
||||
const ro = new ResizeObserver((entries) => {
|
||||
const rect = entries[0]?.contentRect;
|
||||
if (!rect) return;
|
||||
compute(rect.width, rect.height);
|
||||
});
|
||||
ro.observe(el);
|
||||
const rect = el.getBoundingClientRect();
|
||||
compute(rect.width, rect.height);
|
||||
return () => ro.disconnect();
|
||||
}, [fit]);
|
||||
|
||||
const isCover = fit === "cover";
|
||||
const scaledW = NATURAL_W * fitScale;
|
||||
const scaledH = NATURAL_H * fitScale;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={outerRef}
|
||||
className={isCover ? "h-full w-full" : "w-full"}
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
...(isCover ? {} : { height: scaledH }),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: scaledW,
|
||||
height: scaledH,
|
||||
...(isCover
|
||||
? {
|
||||
position: "absolute",
|
||||
left: "50%",
|
||||
top: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
}
|
||||
: { margin: "0 auto" }),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: NATURAL_W,
|
||||
height: NATURAL_H,
|
||||
transform: `scale(${fitScale})`,
|
||||
transformOrigin: "top left",
|
||||
}}
|
||||
>
|
||||
<MeshHero
|
||||
width={NATURAL_W}
|
||||
height={NATURAL_H}
|
||||
sessions={[
|
||||
{
|
||||
id: "P_VELA",
|
||||
displayName: "Vela",
|
||||
title: "vela · claude — 80\u00d724",
|
||||
cwd: "~/broker",
|
||||
script: SCRIPT_VELA,
|
||||
position: {
|
||||
xPct: 50,
|
||||
yPct: 10,
|
||||
scale: 0.38,
|
||||
opacity: 0.55,
|
||||
saturate: 0.35,
|
||||
blurPx: 0.6,
|
||||
zIndex: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "P_OREL",
|
||||
displayName: "Orel",
|
||||
title: "orel · claude — 80\u00d724",
|
||||
cwd: "~/registry",
|
||||
script: SCRIPT_OREL,
|
||||
position: {
|
||||
xPct: 50,
|
||||
yPct: 88,
|
||||
scale: 0.38,
|
||||
opacity: 0.55,
|
||||
saturate: 0.35,
|
||||
blurPx: 0.6,
|
||||
zIndex: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "P1",
|
||||
displayName: "Pip",
|
||||
title: "pip · claude — 80\u00d724",
|
||||
cwd: "~/tools",
|
||||
script: SCRIPT_PIP,
|
||||
position: {
|
||||
xPct: 8,
|
||||
yPct: 20,
|
||||
scale: 0.42,
|
||||
rotate: -4,
|
||||
opacity: 0.6,
|
||||
saturate: 0.4,
|
||||
blurPx: 0.5,
|
||||
zIndex: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "P2",
|
||||
displayName: "Ripple",
|
||||
title: "ripple · claude — 80\u00d724",
|
||||
cwd: "~/infra",
|
||||
script: SCRIPT_RIPPLE,
|
||||
position: {
|
||||
xPct: 92,
|
||||
yPct: 20,
|
||||
scale: 0.42,
|
||||
rotate: 4,
|
||||
opacity: 0.6,
|
||||
saturate: 0.4,
|
||||
blurPx: 0.5,
|
||||
zIndex: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "P3",
|
||||
displayName: "Nebula",
|
||||
title: "nebula · claude — 80\u00d724",
|
||||
cwd: "~/ops",
|
||||
script: SCRIPT_NEBULA,
|
||||
position: {
|
||||
xPct: 10,
|
||||
yPct: 82,
|
||||
scale: 0.4,
|
||||
rotate: 3,
|
||||
opacity: 0.58,
|
||||
saturate: 0.38,
|
||||
blurPx: 0.5,
|
||||
zIndex: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "P4",
|
||||
displayName: "Jet",
|
||||
title: "jet · claude — 80\u00d724",
|
||||
cwd: "~/monorepo",
|
||||
script: SCRIPT_JET,
|
||||
position: {
|
||||
xPct: 90,
|
||||
yPct: 82,
|
||||
scale: 0.4,
|
||||
rotate: -3,
|
||||
opacity: 0.58,
|
||||
saturate: 0.38,
|
||||
blurPx: 0.5,
|
||||
zIndex: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "A",
|
||||
displayName: "Alexis",
|
||||
title: "agutierrez — alexis · claude — 80\u00d724",
|
||||
cwd: "~/claudemesh",
|
||||
script: SCRIPT_A,
|
||||
position: {
|
||||
xPct: 20,
|
||||
yPct: 58,
|
||||
scale: 0.65,
|
||||
rotate: -3,
|
||||
saturate: 1,
|
||||
opacity: 1,
|
||||
zIndex: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "B",
|
||||
displayName: "Lug Nut",
|
||||
title: "agutierrez — lug-nut · claude — 80\u00d724",
|
||||
cwd: "~/whyrating",
|
||||
script: SCRIPT_B,
|
||||
position: {
|
||||
xPct: 50,
|
||||
yPct: 40,
|
||||
scale: 0.65,
|
||||
rotate: 0,
|
||||
saturate: 1,
|
||||
opacity: 1,
|
||||
zIndex: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "C",
|
||||
displayName: "Mou",
|
||||
title: "agutierrez — mou · claude — 80\u00d724",
|
||||
cwd: "~/mineryreport",
|
||||
script: SCRIPT_C,
|
||||
position: {
|
||||
xPct: 80,
|
||||
yPct: 58,
|
||||
scale: 0.65,
|
||||
rotate: 3,
|
||||
saturate: 1,
|
||||
opacity: 1,
|
||||
zIndex: 2,
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
118
apps/web/src/modules/marketing/home/hero-with-mesh.tsx
Normal file
118
apps/web/src/modules/marketing/home/hero-with-mesh.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { HeroMeshAnimation } from "./hero-mesh-animation";
|
||||
import { Reveal, SectionIcon } from "./_reveal";
|
||||
|
||||
export const HeroWithMesh = () => {
|
||||
return (
|
||||
<section className="relative overflow-hidden border-b border-[var(--cm-border)] bg-[var(--cm-bg)]">
|
||||
{/* Full-bleed mesh animation as hero background */}
|
||||
<div className="pointer-events-none absolute inset-0 z-0">
|
||||
<div className="absolute inset-0">
|
||||
<HeroMeshAnimation fit="cover" />
|
||||
</div>
|
||||
{/* Radial vignette: dark where text sits, transparent at the edges
|
||||
so the corner peers keep pulsing visibly */}
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(ellipse 900px 540px at 50% 38%, rgba(5,5,5,0.92) 0%, rgba(5,5,5,0.75) 38%, rgba(5,5,5,0.3) 68%, rgba(5,5,5,0) 100%)",
|
||||
}}
|
||||
/>
|
||||
{/* Top/bottom fades so the animation bleeds into surrounding sections */}
|
||||
<div
|
||||
className="absolute inset-x-0 top-0 h-32"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(to bottom, rgba(5,5,5,0.85) 0%, rgba(5,5,5,0) 100%)",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-x-0 bottom-0 h-32"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(to top, rgba(5,5,5,0.95) 0%, rgba(5,5,5,0) 100%)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 mx-auto flex max-w-[var(--cm-max-w)] flex-col items-center px-6 py-24 md:px-12 md:py-32">
|
||||
<Reveal className="mb-8">
|
||||
<SectionIcon glyph="mesh" />
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={1}>
|
||||
<h1
|
||||
className="max-w-4xl text-center text-[clamp(2.75rem,7vw,5.25rem)] font-medium leading-[1.08] tracking-tight text-[var(--cm-fg)]"
|
||||
style={{
|
||||
fontFamily: "var(--cm-font-serif)",
|
||||
textShadow: "0 2px 30px rgba(0,0,0,0.85)",
|
||||
}}
|
||||
>
|
||||
Your Claude Code sessions{" "}
|
||||
<span className="text-[var(--cm-clay)]">work alone.</span>
|
||||
<br />
|
||||
<span className="text-[var(--cm-fg-secondary)]">
|
||||
claudemesh connects them.
|
||||
</span>
|
||||
</h1>
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={2}>
|
||||
<p
|
||||
className="mx-auto mt-8 max-w-2xl text-center text-lg leading-[1.65] text-[var(--cm-fg-secondary)] md:text-xl"
|
||||
style={{
|
||||
fontFamily: "var(--cm-font-serif)",
|
||||
textShadow: "0 2px 20px rgba(0,0,0,0.8)",
|
||||
}}
|
||||
>
|
||||
Share context, files, skills, and MCPs across every Claude Code
|
||||
session — encrypted, with zero setup. The broker routes ciphertext.
|
||||
It never reads your messages.
|
||||
</p>
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={3}>
|
||||
<div className="mt-10 flex flex-col items-stretch gap-3 sm:flex-row sm:items-center">
|
||||
<Link
|
||||
href="/auth/register"
|
||||
className="group inline-flex items-center justify-center gap-2 rounded-[var(--cm-radius-xs)] bg-[var(--cm-clay)] px-5 py-3 text-[15px] font-medium text-[var(--cm-fg)] shadow-[0_10px_40px_rgba(215,119,87,0.35)] transition-colors duration-300 hover:bg-[var(--cm-clay-hover)]"
|
||||
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||
>
|
||||
Start free
|
||||
<span className="transition-transform duration-300 group-hover:translate-x-0.5">
|
||||
→
|
||||
</span>
|
||||
</Link>
|
||||
<div
|
||||
className="flex items-center gap-2 rounded-[var(--cm-radius-xs)] border border-[var(--cm-border)] bg-[var(--cm-bg-elevated)]/85 px-4 py-3 text-[13px] text-[var(--cm-fg-secondary)] backdrop-blur-md"
|
||||
style={{ fontFamily: "var(--cm-font-mono)" }}
|
||||
>
|
||||
<span className="text-[var(--cm-clay)]">$</span>
|
||||
<span>curl -fsSL claudemesh.com/install | bash</span>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={4}>
|
||||
<p
|
||||
className="mt-14 text-center text-sm text-[var(--cm-fg-tertiary)]"
|
||||
style={{
|
||||
fontFamily: "var(--cm-font-sans)",
|
||||
textShadow: "0 2px 16px rgba(0,0,0,0.8)",
|
||||
}}
|
||||
>
|
||||
Open-source CLI · Free during public beta ·{" "}
|
||||
<Link
|
||||
href="https://github.com/alezmad/claudemesh-cli"
|
||||
className="underline decoration-[var(--cm-fg-tertiary)] underline-offset-4 transition-colors hover:text-[var(--cm-fg)] hover:decoration-[var(--cm-clay)]"
|
||||
>
|
||||
View source
|
||||
</Link>
|
||||
</p>
|
||||
</Reveal>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
128
apps/web/src/modules/marketing/home/where-mesh-fits.tsx
Normal file
128
apps/web/src/modules/marketing/home/where-mesh-fits.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import { Reveal, SectionIcon } from "./_reveal";
|
||||
|
||||
type Card = {
|
||||
label: string;
|
||||
title: string;
|
||||
theyDo: string;
|
||||
weDo: string;
|
||||
tone: "compare" | "claim";
|
||||
};
|
||||
|
||||
const CARDS: Card[] = [
|
||||
{
|
||||
label: "vs. MCP",
|
||||
title: "One Claude to its tools",
|
||||
theyDo:
|
||||
"MCP wires one Claude session to external services — GitHub, Postgres, a browser. The tool never knows who called it, never talks back, never sees other sessions.",
|
||||
weDo: "claudemesh ships as an MCP server itself. We extend the model: publish an MCP once, every peer's Claude Code sees its tools. Credentials stay on the publisher's machine.",
|
||||
tone: "compare",
|
||||
},
|
||||
{
|
||||
label: "vs. Subagents",
|
||||
title: "Helpers inside one session",
|
||||
theyDo:
|
||||
"Subagents spawn helper agents within a single Claude Code session. They share one context, one terminal, one machine. When the session closes, they're gone.",
|
||||
weDo: "claudemesh connects full, independent Claude Code sessions across machines, across developers, across continents. Each peer keeps its own repo, its own perspective, its own scrollback.",
|
||||
tone: "compare",
|
||||
},
|
||||
{
|
||||
label: "vs. OpenClaw",
|
||||
title: "Autonomous agents that run while you sleep",
|
||||
theyDo:
|
||||
"OpenClaw runs unattended. One agent brain, many subagents, 200+ LLMs on tap. It triages issues overnight, opens PRs, pokes CI, reacts to webhooks — all without a human in the loop. Different job, and a good one.",
|
||||
weDo: "claudemesh is about the sessions you're actively running. When your Claude Code is open and you're shipping, the mesh wires your session to your teammates'. OpenClaw automates overnight; claudemesh meshes your work hours. They compose — put an OpenClaw instance on the mesh and it joins as just another peer.",
|
||||
tone: "compare",
|
||||
},
|
||||
{
|
||||
label: "What claudemesh is",
|
||||
title: "The wire between Claude Code sessions",
|
||||
theyDo:
|
||||
"Every Claude Code session today is an island. Context dies with the terminal. Skills and MCPs are per-developer. Teammates relay insights through Slack.",
|
||||
weDo: "claudemesh is one thing: a peer network for Claude Code. Share context, files, skills, MCPs, and slash commands across sessions — end-to-end encrypted. The broker routes ciphertext. It never reads your messages.",
|
||||
tone: "claim",
|
||||
},
|
||||
];
|
||||
|
||||
export const WhereMeshFits = () => {
|
||||
return (
|
||||
<section className="border-b border-[var(--cm-border)] bg-[var(--cm-bg)] px-6 py-24 md:px-12 md:py-32">
|
||||
<div className="mx-auto max-w-[var(--cm-max-w)]">
|
||||
<Reveal className="mb-6 flex justify-center">
|
||||
<SectionIcon glyph="arrow" />
|
||||
</Reveal>
|
||||
<Reveal delay={1}>
|
||||
<h2
|
||||
className="mx-auto max-w-4xl text-center text-[clamp(2rem,4.5vw,3.25rem)] font-medium leading-[1.1] text-[var(--cm-fg)]"
|
||||
style={{ fontFamily: "var(--cm-font-serif)" }}
|
||||
>
|
||||
Where claudemesh fits
|
||||
</h2>
|
||||
</Reveal>
|
||||
<Reveal delay={2}>
|
||||
<p
|
||||
className="mx-auto mt-4 max-w-2xl text-center text-sm leading-[1.6] text-[var(--cm-fg-tertiary)]"
|
||||
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||
>
|
||||
A quick tour of what claudemesh is — and what it isn't. We
|
||||
compose with the rest of the Claude Code ecosystem. We don't
|
||||
replace any of it.
|
||||
</p>
|
||||
</Reveal>
|
||||
|
||||
<div className="mt-16 grid gap-5 md:grid-cols-2">
|
||||
{CARDS.map((c) => {
|
||||
const isClaim = c.tone === "claim";
|
||||
return (
|
||||
<Reveal key={c.label} delay={3}>
|
||||
<div
|
||||
className={
|
||||
"flex h-full flex-col rounded-[var(--cm-radius-md)] border p-7 md:p-8 " +
|
||||
(isClaim
|
||||
? "border-[var(--cm-clay)]/60 bg-[var(--cm-clay)]/[0.06]"
|
||||
: "border-[var(--cm-border)] bg-[var(--cm-bg-elevated)]")
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
"mb-3 text-[11px] uppercase tracking-[0.18em] " +
|
||||
(isClaim
|
||||
? "text-[var(--cm-clay)]"
|
||||
: "text-[var(--cm-fg-tertiary)]")
|
||||
}
|
||||
style={{ fontFamily: "var(--cm-font-mono)" }}
|
||||
>
|
||||
{c.label}
|
||||
</div>
|
||||
<h3
|
||||
className="mb-4 text-[22px] font-medium leading-snug text-[var(--cm-fg)]"
|
||||
style={{ fontFamily: "var(--cm-font-serif)" }}
|
||||
>
|
||||
{c.title}
|
||||
</h3>
|
||||
<p
|
||||
className="text-[14px] leading-[1.65] text-[var(--cm-fg-tertiary)]"
|
||||
style={{ fontFamily: "var(--cm-font-serif)" }}
|
||||
>
|
||||
{c.theyDo}
|
||||
</p>
|
||||
<div className="my-4 h-px bg-[var(--cm-border)]" />
|
||||
<p
|
||||
className={
|
||||
"text-[14px] leading-[1.65] " +
|
||||
(isClaim
|
||||
? "text-[var(--cm-fg)]"
|
||||
: "text-[var(--cm-fg-secondary)]")
|
||||
}
|
||||
style={{ fontFamily: "var(--cm-font-serif)" }}
|
||||
>
|
||||
{c.weDo}
|
||||
</p>
|
||||
</div>
|
||||
</Reveal>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user