diff --git a/apps/web/public/fonts/AnthropicMono-Italic.woff2 b/apps/web/public/fonts/AnthropicMono-Italic.woff2 new file mode 100644 index 0000000..526fad9 Binary files /dev/null and b/apps/web/public/fonts/AnthropicMono-Italic.woff2 differ diff --git a/apps/web/public/fonts/AnthropicMono-Roman.woff2 b/apps/web/public/fonts/AnthropicMono-Roman.woff2 new file mode 100644 index 0000000..f9f73ea Binary files /dev/null and b/apps/web/public/fonts/AnthropicMono-Roman.woff2 differ diff --git a/apps/web/public/fonts/AnthropicSans-Italic.woff2 b/apps/web/public/fonts/AnthropicSans-Italic.woff2 new file mode 100644 index 0000000..84e7ed1 Binary files /dev/null and b/apps/web/public/fonts/AnthropicSans-Italic.woff2 differ diff --git a/apps/web/public/fonts/AnthropicSans-Roman.woff2 b/apps/web/public/fonts/AnthropicSans-Roman.woff2 new file mode 100644 index 0000000..57824da Binary files /dev/null and b/apps/web/public/fonts/AnthropicSans-Roman.woff2 differ diff --git a/apps/web/public/fonts/AnthropicSerif-Italic.woff2 b/apps/web/public/fonts/AnthropicSerif-Italic.woff2 new file mode 100644 index 0000000..4b0d098 Binary files /dev/null and b/apps/web/public/fonts/AnthropicSerif-Italic.woff2 differ diff --git a/apps/web/public/fonts/AnthropicSerif-Roman.woff2 b/apps/web/public/fonts/AnthropicSerif-Roman.woff2 new file mode 100644 index 0000000..ef2960a Binary files /dev/null and b/apps/web/public/fonts/AnthropicSerif-Roman.woff2 differ diff --git a/apps/web/public/images/hero-mesh.png b/apps/web/public/images/hero-mesh.png new file mode 100644 index 0000000..d351e58 Binary files /dev/null and b/apps/web/public/images/hero-mesh.png differ diff --git a/apps/web/src/app/[locale]/(marketing)/legal/[slug]/page.tsx b/apps/web/src/app/[locale]/(marketing)/legal/[slug]/page.tsx deleted file mode 100644 index af8884e..0000000 --- a/apps/web/src/app/[locale]/(marketing)/legal/[slug]/page.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { notFound } from "next/navigation"; - -import { - CollectionType, - getContentItemBySlug, - getContentItems, -} from "@turbostarter/cms"; -import { getTranslation } from "@turbostarter/i18n/server"; - -import { getMetadata } from "~/lib/metadata"; -import { Mdx } from "~/modules/common/mdx"; -import { - Section, - SectionBadge, - SectionDescription, - SectionHeader, - SectionTitle, -} from "~/modules/marketing/layout/section"; - -interface PageParams { - params: Promise<{ - slug: string; - locale: string; - }>; -} - -export default async function Page({ params }: PageParams) { - const item = getContentItemBySlug({ - collection: CollectionType.LEGAL, - slug: (await params).slug, - locale: (await params).locale, - }); - - if (!item) { - return notFound(); - } - - const { t } = await getTranslation({ ns: "common" }); - - return ( -
- - {t("legal.label")} - {item.title} - {item.description} - - -
- ); -} - -export function generateStaticParams() { - return getContentItems({ collection: CollectionType.LEGAL }).items.map( - ({ slug, locale }) => ({ - slug, - locale, - }), - ); -} - -export async function generateMetadata({ params }: PageParams) { - const item = getContentItemBySlug({ - collection: CollectionType.LEGAL, - slug: (await params).slug, - locale: (await params).locale, - }); - - if (!item) { - return notFound(); - } - - return getMetadata({ - title: item.title, - description: item.description, - })({ params }); -} diff --git a/apps/web/src/app/[locale]/(marketing)/page.tsx b/apps/web/src/app/[locale]/(marketing)/page.tsx index c859939..af3898b 100644 --- a/apps/web/src/app/[locale]/(marketing)/page.tsx +++ b/apps/web/src/app/[locale]/(marketing)/page.tsx @@ -1,42 +1,29 @@ -"use client"; - -import { useTranslation } from "@turbostarter/i18n"; -import { buttonVariants } from "@turbostarter/ui-web/button"; -import { Icons } from "@turbostarter/ui-web/icons"; - -import { pathsConfig } from "~/config/paths"; -import { TurboLink } from "~/modules/common/turbo-link"; +import { Hero } from "~/modules/marketing/home/hero"; +import { Surfaces } from "~/modules/marketing/home/surfaces"; +import { Pricing } from "~/modules/marketing/home/pricing"; +import { LaptopToLaptop } from "~/modules/marketing/home/laptop-to-laptop"; +import { Features } from "~/modules/marketing/home/features"; +import { MeetsYou } from "~/modules/marketing/home/meets-you"; +import { FAQ } from "~/modules/marketing/home/faq"; +import { CallToAction } from "~/modules/marketing/home/cta"; +import { LatestNewsToaster } from "~/modules/marketing/home/toaster"; const HomePage = () => { - const { t } = useTranslation("common"); - return ( -
-
-

- {t("home.title", { defaultValue: "Welcome to TurboStarter" })} -

-

- {t("home.description", { defaultValue: "The fastest way to build your next SaaS. Authentication, billing, database, and UI components — all pre-configured and ready to go." })} -

-
- - {t("home.getStarted", { defaultValue: "Get Started" })} - - - - {t("home.documentation", { defaultValue: "Documentation" })} - -
-
-
+
+ + + + + + + + + +
); }; diff --git a/apps/web/src/app/sitemap.ts b/apps/web/src/app/sitemap.ts index 88493e6..8b85475 100644 --- a/apps/web/src/app/sitemap.ts +++ b/apps/web/src/app/sitemap.ts @@ -1,4 +1,3 @@ -import { CollectionType, getContentItems } from "@turbostarter/cms"; import { getPathname, config } from "@turbostarter/i18n"; import { appConfig } from "~/config/app"; @@ -52,29 +51,5 @@ export default function sitemap(): MetadataRoute.Sitemap { changeFrequency: "monthly", priority: 0.8, }, - { - ...getEntry(pathsConfig.marketing.blog.index), - lastModified: new Date(), - changeFrequency: "monthly", - priority: 0.8, - }, - ...getContentItems({ - collection: CollectionType.BLOG, - locale: appConfig.locale, - }).items.map((post) => ({ - ...getEntry(pathsConfig.marketing.blog.post(post.slug)), - lastModified: new Date(post.lastModifiedAt), - changeFrequency: "monthly", - priority: 0.7, - })), - ...getContentItems({ - collection: CollectionType.LEGAL, - locale: appConfig.locale, - }).items.map((post) => ({ - ...getEntry(pathsConfig.marketing.legal(post.slug)), - lastModified: new Date(post.lastModifiedAt), - changeFrequency: "yearly", - priority: 0.5, - })), ]; } diff --git a/apps/web/src/assets/styles/globals.css b/apps/web/src/assets/styles/globals.css index 9c4b4e3..e3f3146 100644 --- a/apps/web/src/assets/styles/globals.css +++ b/apps/web/src/assets/styles/globals.css @@ -2,3 +2,102 @@ @import "@turbostarter/ui-web/globals.css"; @source "../../../../../packages/ui"; + +/* ============================================================ + claudemesh — Anthropic design system + Fonts, tokens, and primitives extracted from claude.com + ============================================================ */ + +@font-face { + font-family: "Anthropic Sans"; + src: url("/fonts/AnthropicSans-Roman.woff2") format("woff2"); + font-weight: 300 800; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: "Anthropic Sans"; + src: url("/fonts/AnthropicSans-Italic.woff2") format("woff2"); + font-weight: 300 800; + font-style: italic; + font-display: swap; +} +@font-face { + font-family: "Anthropic Serif"; + src: url("/fonts/AnthropicSerif-Roman.woff2") format("woff2"); + font-weight: 300 800; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: "Anthropic Serif"; + src: url("/fonts/AnthropicSerif-Italic.woff2") format("woff2"); + font-weight: 300 800; + font-style: italic; + font-display: swap; +} +@font-face { + font-family: "Anthropic Mono"; + src: url("/fonts/AnthropicMono-Roman.woff2") format("woff2"); + font-weight: 300 800; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: "Anthropic Mono"; + src: url("/fonts/AnthropicMono-Italic.woff2") format("woff2"); + font-weight: 300 800; + font-style: italic; + font-display: swap; +} + +:root { + /* --- Anthropic swatch palette (extracted from claude.com) --- */ + --cm-clay: #d97757; + --cm-clay-hover: #c96442; + --cm-fig: #c46686; + --cm-oat: #e3dacc; + --cm-cactus: #bcd1ca; + + --cm-gray-000: #ffffff; + --cm-gray-050: #faf9f5; + --cm-gray-150: #f0eee6; + --cm-gray-350: #c2c0b6; + --cm-gray-450: #9c9a92; + --cm-gray-800: #262624; + --cm-gray-850: #1f1e1d; + --cm-gray-900: #141413; + + --cm-bg: var(--cm-gray-900); + --cm-bg-elevated: var(--cm-gray-850); + --cm-bg-hover: var(--cm-gray-800); + --cm-fg: var(--cm-gray-050); + --cm-fg-secondary: #c2c0b6; + --cm-fg-tertiary: #87867f; + --cm-border: rgba(217, 119, 87, 0.2); + --cm-border-hover: rgba(217, 119, 87, 0.5); + + /* --- Type families --- */ + --cm-font-sans: "Anthropic Sans", -apple-system, system-ui, Arial, sans-serif; + --cm-font-serif: "Anthropic Serif", Georgia, "Times New Roman", serif; + --cm-font-mono: "Anthropic Mono", "JetBrains Mono", ui-monospace, monospace; + + /* --- Type scale (fluid, from Anthropic clamps) --- */ + --cm-text-h1: clamp(2.125rem, 1.8rem + 2.6vw, 3.25rem); + --cm-text-h2: clamp(1.875rem, 1.625rem + 1.95vw, 2.75rem); + --cm-text-h3: clamp(1.75rem, 1.607rem + 1.12vw, 2.25rem); + --cm-text-body-lg: clamp(1.1875rem, 1.17rem + 0.14vw, 1.25rem); + + /* --- Spacing --- */ + --cm-gutter: 2rem; + --cm-max-w: 90rem; + + /* --- Radii --- */ + --cm-radius-xs: 0.25rem; + --cm-radius-md: 0.5rem; + --cm-radius-lg: 1rem; + + /* --- Motion --- */ + --cm-ease: cubic-bezier(0.22, 0.61, 0.36, 1); + --cm-dur: 300ms; +} diff --git a/apps/web/src/modules/common/layout/credits/api.ts b/apps/web/src/modules/common/layout/credits/api.ts index fc03f2d..c36324c 100644 --- a/apps/web/src/modules/common/layout/credits/api.ts +++ b/apps/web/src/modules/common/layout/credits/api.ts @@ -1,11 +1,9 @@ -import { handle } from "@turbostarter/api/utils"; - -import { api } from "~/lib/api/client"; - +// AI credits were backed by the removed @turbostarter/ai package. +// claudemesh does not meter AI credits, so this stubs the query to return null. export const queries = { get: (params: { id: string }) => ({ queryKey: ["credits", params.id], - queryFn: () => handle(api.ai.credits.$get)(), + queryFn: () => Promise.resolve(null as number | null), }), }; diff --git a/apps/web/src/modules/common/layout/credits/index.tsx b/apps/web/src/modules/common/layout/credits/index.tsx index 2a1d83f..58454d5 100644 --- a/apps/web/src/modules/common/layout/credits/index.tsx +++ b/apps/web/src/modules/common/layout/credits/index.tsx @@ -2,10 +2,6 @@ import NumberFlow from "@number-flow/react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { motion } from "motion/react"; -import { - getCreditsLevel, - getCreditsProgress, -} from "@turbostarter/ai/credits/utils"; import { useTranslation } from "@turbostarter/i18n"; import { cn } from "@turbostarter/ui"; @@ -13,6 +9,15 @@ import { authClient } from "~/lib/auth/client"; import { credits } from "./api"; +// Local replacements — @turbostarter/ai package removed in claudemesh fork. +// claudemesh does not meter AI credits (not an AI consumption product), but +// the surrounding UI still calls these with a number. +type CreditsLevel = "high" | "medium" | "low"; +const getCreditsLevel = (n: number): CreditsLevel => + n > 500 ? "high" : n > 100 ? "medium" : "low"; +const getCreditsProgress = (n: number): number => + Math.max(0, Math.min(1, n / 1000)); + export const useCredits = () => { const queryClient = useQueryClient(); const { data } = authClient.useSession(); diff --git a/apps/web/src/modules/common/layout/credits/server.ts b/apps/web/src/modules/common/layout/credits/server.ts index ee6e5bd..1fb30e2 100644 --- a/apps/web/src/modules/common/layout/credits/server.ts +++ b/apps/web/src/modules/common/layout/credits/server.ts @@ -1,17 +1,9 @@ -import { handle } from "@turbostarter/api/utils"; - -import { api } from "~/lib/api/server"; import { getQueryClient } from "~/lib/query/server"; import { credits } from "./api"; export const prefetchCredits = async (id: string) => { const queryClient = getQueryClient(); - - await queryClient.prefetchQuery({ - ...credits.queries.get({ id }), - queryFn: handle(api.ai.credits.$get), - }); - + await queryClient.prefetchQuery(credits.queries.get({ id })); return queryClient; }; diff --git a/apps/web/src/modules/common/mdx.tsx b/apps/web/src/modules/common/mdx.tsx deleted file mode 100644 index abc500c..0000000 --- a/apps/web/src/modules/common/mdx.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { MDXContent } from "@content-collections/mdx/react"; - -interface MdxProps { - readonly mdx: string; -} - -export const Mdx = ({ mdx }: MdxProps) => { - return ( -
- -
- ); -}; diff --git a/apps/web/src/modules/marketing/home/_reveal.tsx b/apps/web/src/modules/marketing/home/_reveal.tsx new file mode 100644 index 0000000..be0b6a0 --- /dev/null +++ b/apps/web/src/modules/marketing/home/_reveal.tsx @@ -0,0 +1,108 @@ +"use client"; +import { motion, type Variants } from "motion/react"; +import type { ReactNode } from "react"; + +const fade: Variants = { + hidden: { opacity: 0, y: 32 }, + visible: (i: number = 0) => ({ + opacity: 1, + y: 0, + transition: { + duration: 0.7, + ease: [0.22, 0.61, 0.36, 1], + delay: i * 0.08, + }, + }), +}; + +export const Reveal = ({ + children, + delay = 0, + as: Tag = "div", + className, +}: { + children: ReactNode; + delay?: number; + as?: keyof typeof motion; + className?: string; +}) => { + const M = motion[Tag] as typeof motion.div; + return ( + + {children} + + ); +}; + +export const RevealStagger = ({ + children, + className, +}: { + children: ReactNode; + className?: string; +}) => ( + + {children} + +); + +export const StaggerItem = ({ + children, + className, +}: { + children: ReactNode; + className?: string; +}) => ( + + {children} + +); + +const leafPath = + "M12 2c-2 4-5 6-5 10a5 5 0 0010 0c0-4-3-6-5-10z"; + +export const SectionIcon = ({ + glyph = "leaf", +}: { + glyph?: "leaf" | "arrow" | "grid" | "phone" | "terminal" | "mesh"; +}) => { + const paths: Record = { + leaf: leafPath, + arrow: "M5 12h14m-6-6l6 6-6 6", + grid: "M4 4h6v6H4zM14 4h6v6h-6zM4 14h6v6H4zM14 14h6v6h-6z", + phone: "M7 3h10a1 1 0 011 1v16a1 1 0 01-1 1H7a1 1 0 01-1-1V4a1 1 0 011-1zm5 15v.01", + terminal: "M4 6l4 4-4 4M12 16h8", + mesh: "M12 3l9 5-9 5-9-5 9-5zm-9 12l9 5 9-5M3 10l9 5 9-5", + }; + return ( + + + + ); +}; diff --git a/apps/web/src/modules/marketing/home/cta.tsx b/apps/web/src/modules/marketing/home/cta.tsx new file mode 100644 index 0000000..ff7413c --- /dev/null +++ b/apps/web/src/modules/marketing/home/cta.tsx @@ -0,0 +1,64 @@ +import Link from "next/link"; +import { Reveal, SectionIcon } from "./_reveal"; + +export const CallToAction = () => { + return ( +
+
+
+ + + + +

+ Connect what's scattered. +
+ + Ship what ships together. + +

+
+ +

+ Anthropic built Claude Code per developer. The next unlock is + between developers. Build the layer with us. +

+
+ +
+ + Star on GitHub + + → + + + + Read the docs + +
+
+
+
+ ); +}; diff --git a/apps/web/src/modules/marketing/home/faq.tsx b/apps/web/src/modules/marketing/home/faq.tsx index 786b8f1..511b98d 100644 --- a/apps/web/src/modules/marketing/home/faq.tsx +++ b/apps/web/src/modules/marketing/home/faq.tsx @@ -1,89 +1,103 @@ -import { getTranslation } from "@turbostarter/i18n/server"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@turbostarter/ui-web/accordion"; -import { buttonVariants } from "@turbostarter/ui-web/button"; -import { Icons } from "@turbostarter/ui-web/icons"; +"use client"; +import { useState } from "react"; +import { Reveal } from "./_reveal"; -import { pathsConfig } from "~/config/paths"; -import { TurboLink } from "~/modules/common/turbo-link"; -import { - Section, - SectionHeader, - SectionBadge, - SectionTitle, - SectionDescription, -} from "~/modules/marketing/layout/section"; - -const questions = [ - { - question: "faq.question.whatDoesOurPlatformDo.question", - answer: "faq.question.whatDoesOurPlatformDo.answer", - }, - { - question: "faq.question.howWillThisBenefitMyBusiness.question", - answer: "faq.question.howWillThisBenefitMyBusiness.answer", - }, - { - question: "faq.question.isMyDataSafe.question", - answer: "faq.question.isMyDataSafe.answer", - }, - { - question: "faq.question.whatKindOfIntegrationsAreAvailable.question", - answer: "faq.question.whatKindOfIntegrationsAreAvailable.answer", - }, - { - question: "faq.question.howEasyIsItToOnboardMyTeam.question", - answer: "faq.question.howEasyIsItToOnboardMyTeam.answer", - }, - { - question: "faq.question.whatTypesOfBusinessesCanUseThis.question", - answer: "faq.question.whatTypesOfBusinessesCanUseThis.answer", - }, - { - question: "faq.question.canICustomizeThisToFitMyBusinessNeeds.question", - answer: "faq.question.canICustomizeThisToFitMyBusinessNeeds.answer", - }, -] as const; - -export const Faq = async () => { - const { t } = await getTranslation({ ns: "marketing" }); +const ITEMS = [ + { + q: "Is claudemesh free?", + a: "Yes — the broker, CLI, dashboard, and SDK are MIT-licensed and free forever. Solo developers and small teams can self-host at no cost. Paid tiers add hosted brokers, SSO, audit retention, and support.", + }, + { + q: "How do I get started?", + a: "Install the broker with one curl command. Add one env var to your Claude Code config. Your session joins the mesh. `npx claudemesh init` does both in 60 seconds.", + }, + { + q: "Does claudemesh send my code or prompts to the cloud?", + a: "No. The broker is a local WebSocket server. Messages stay on your network. The only data that leaves your machines is what your Claude Code already sends to Anthropic — we don't touch it.", + }, + { + q: "Do I need to run a server?", + a: "Yes — one machine on your network runs the broker. That can be your laptop, a shared dev box, a Raspberry Pi, or a container in your cluster. It's one binary, SQLite-backed, ~15 MB.", + }, + { + q: "Does it work across offices / continents?", + a: "Yes. Put the broker on a VPS, or expose it through Tailscale / WireGuard. Every Claude Code session that can reach the broker joins the mesh.", + }, + { + q: "How does it route messages?", + a: "By peer name (alex, jordan), by repo, or by priority level (now / next / low). Messages are queued until the target peer is idle, then delivered. You can also set a peer to DND to block all but priority:now.", + }, + { + q: "Which Claude Code versions work with claudemesh?", + a: "Claude Code 2.0 and above. The mesh hooks in via a PreToolUse hook + a small MCP server — both ship in your Claude Code config after running `claudemesh init`.", + }, +]; +export const FAQ = () => { + const [open, setOpen] = useState(0); return ( -
- - {t("faq.label")} - {t("faq.title")} - - {t("faq.description")} - - - +
+ +

+ FAQ +

+
+
+ {ITEMS.map((item, i) => { + const isOpen = open === i; + return ( + + +
+
+

+ {item.a} +

+
+
+
+ ); })} - > - {t("faq.cta")} - - - - - - {questions.map((question) => ( - - - {t(question.question)} - - - {t(question.answer)} - - - ))} - -
+ + + ); }; diff --git a/apps/web/src/modules/marketing/home/features.tsx b/apps/web/src/modules/marketing/home/features.tsx index fa35b98..de73421 100644 --- a/apps/web/src/modules/marketing/home/features.tsx +++ b/apps/web/src/modules/marketing/home/features.tsx @@ -1,1500 +1,107 @@ -import React from "react"; +"use client"; +import { useState } from "react"; +import { Reveal, SectionIcon } from "./_reveal"; -import { getTranslation } from "@turbostarter/i18n/server"; -import { cn } from "@turbostarter/ui"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@turbostarter/ui-web/card"; +const FEATURES = [ + { + key: "onboard", + tab: "Onboarding", + title: "Bootstrap any teammate", + body: "New hire's Claude inherits the team's context library on day one. No hand-holding, no week-long repo tour.", + }, + { + key: "handoff", + tab: "Hand-offs", + title: "Work travels with context", + body: "Pass an investigation to your teammate's session with full history — hypotheses, logs, files touched, commands run.", + }, + { + key: "refactor", + tab: "Refactors", + title: "Coordinate cross-cutting changes", + body: "Rename a type, rotate a secret, bump a schema — once. Every other agent picks up the change from its own repo.", + }, +]; -import { ThemedImage } from "~/modules/common/themed-image"; -import { - Section, - SectionBadge, - SectionDescription, - SectionHeader, - SectionTitle, -} from "~/modules/marketing/layout/section"; - -const GooglePlayButton = ({ - size = "md", - ...props -}: React.AnchorHTMLAttributes & { size?: "md" | "lg" }) => { +export const Features = () => { + const [active, setActive] = useState(0); return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+ + + + +

- - - - - - - - - - - - - - + + +
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; - -const AppStoreButton = ({ - size = "md", - ...props -}: React.AnchorHTMLAttributes & { size?: "md" | "lg" }) => { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; - -const ChromeWebStoreButton = ({ - size = "md", - ...props -}: React.AnchorHTMLAttributes & { size?: "md" | "lg" }) => { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - $ + curl -fsSL claudemesh.sh/install | bash +
-

- - - ); -}; - -const Extension = async () => { - const { t } = await getTranslation({ ns: "marketing" }); - - return ( - - - {t("features.feature.extension.title")} - - {t("features.feature.extension.description")} - - -
- - - -
-
- - -
- -
-
-
- ); -}; - -const Core = async () => { - const { t } = await getTranslation({ ns: "marketing" }); - return ( - - - - {t("features.feature.core.title")} - - {t("features.feature.core.description")} - - - - - - - - - ); -}; - -const AI = async () => { - const { t } = await getTranslation({ ns: "marketing" }); - return ( - - - - {t("features.feature.ai.title")} - - {t("features.feature.ai.description")} - - - - - - - - - ); -}; - -export const Features = async () => { - const { t } = await getTranslation({ ns: "marketing" }); - return ( -
- - {t("features.label")} - {t("features.title")} - {t("features.description")} - - -
- - - - + + +

+ Free forever for solo developers · Or read the{" "} + + documentation + +

+
+ +
+ {FEATURES.map((f, i) => ( + + ))} +
+
+

+ {FEATURES[active]?.title} +

+

+ {FEATURES[active]?.body} +

+
+
-
+ ); }; diff --git a/apps/web/src/modules/marketing/home/hero.tsx b/apps/web/src/modules/marketing/home/hero.tsx index 7f2795a..a5d8912 100644 --- a/apps/web/src/modules/marketing/home/hero.tsx +++ b/apps/web/src/modules/marketing/home/hero.tsx @@ -1,231 +1,120 @@ -import { getTranslation } from "@turbostarter/i18n/server"; -import { buttonVariants } from "@turbostarter/ui-web/button"; -import { Icons } from "@turbostarter/ui-web/icons"; -import { Marquee } from "@turbostarter/ui-web/marquee"; +import Link from "next/link"; +import { Reveal, SectionIcon } from "./_reveal"; -import { pathsConfig } from "~/config/paths"; -import { ThemedImage } from "~/modules/common/themed-image"; -import { TurboLink } from "~/modules/common/turbo-link"; -import { CtaButton } from "~/modules/marketing/layout/cta-button"; -import { Section, SectionBadge } from "~/modules/marketing/layout/section"; - -export const Hero = async () => { - const { t } = await getTranslation(); +const LOGOS = [ + "Vercel", + "Linear", + "Stripe", + "Supabase", + "Shopify", + "Figma", +]; +export const Hero = () => { return ( -
- - -
🎉
+
+ {/* faint mesh backdrop */} +
+
+ + + + +
- {t("announcement")} - - - - -

- {t("product.title")} -

-

- {t("product.description")} -

-
- - - - {t("contact.cta")} - -
-
-
- -
-
-
- - {t("shippedWith")} - - -
- - - - + + — meshing + + - - - + +

+ Built for{" "} + + {"<"} + swarms + {">"} + +

+
- - - - - - - - - - + +

+ Connect every Claude Code session on your team into one live mesh. + Ship context, not screenshots. Self-host the broker. Own the wire. + + Free and open-source. Forever. + +

+
- +
+ - - - - - - + → + + +
- - - - - - - - - - - - - - - - - - + $ + curl -fsSL claudemesh.sh/install | bash +
+
+ - +

+ Or{" "} + - - - - - -

+ read the documentation + +

+ -
-
-
+ +
+ {LOGOS.map((logo) => ( +
+ {logo} +
+ ))} +
+
-
+
); }; diff --git a/apps/web/src/modules/marketing/home/laptop-to-laptop.tsx b/apps/web/src/modules/marketing/home/laptop-to-laptop.tsx new file mode 100644 index 0000000..e7a9aa4 --- /dev/null +++ b/apps/web/src/modules/marketing/home/laptop-to-laptop.tsx @@ -0,0 +1,92 @@ +import Link from "next/link"; +import { Reveal, SectionIcon } from "./_reveal"; + +const STEPS = [ + { + id: "01", + title: "Start a task on your laptop", + body: "Open Claude Code. Work normally. Your session announces itself to the mesh — what repo, what branch, what you're on.", + }, + { + id: "02", + title: "Hand it off without typing it up", + body: "Message a teammate's Claude by name, by repo, by priority. The broker routes. The other session picks it up when its human goes idle.", + }, + { + id: "03", + title: "Come back to a finished PR", + body: "While you were in a meeting, the other agent ran its typecheck, made the fix, and filed the diff. You review. You merge. You ship.", + }, +]; + +export const LaptopToLaptop = () => { + return ( +
+
+ + + + +

+ Start a task on one laptop, +
+ + come back to a finished PR. + +

+
+ +

+ Route work between Claude Code sessions on different machines. The + broker handles presence, priority, and queueing. Your humans handle + the interesting parts. +

+
+ + + Pair your machines + + + +
+ {STEPS.map((s) => ( +
+
+ [{s.id}] +
+

+ {s.title} +

+

+ {s.body} +

+
+ ))} +
+
+
+
+ ); +}; diff --git a/apps/web/src/modules/marketing/home/meets-you.tsx b/apps/web/src/modules/marketing/home/meets-you.tsx new file mode 100644 index 0000000..2e53b8f --- /dev/null +++ b/apps/web/src/modules/marketing/home/meets-you.tsx @@ -0,0 +1,142 @@ +import Link from "next/link"; +import { Reveal, SectionIcon } from "./_reveal"; + +const CARDS = [ + { + accent: "clay", + title: "Start in your terminal", + body: "Drop the broker next to Claude Code. One env var. Your session joins the mesh.", + cta: { label: "Install", href: "#" }, + mock: ( +
+
$ claudemesh join
+
✓ connected to mesh.team.local
+
✓ announced: alex · api-gateway · working
+
✓ 5 peers online
+
+ → ready. messages route to your Claude Code. +
+
+ ), + }, + { + accent: "oat", + title: "Bridge to your editor", + body: "VS Code, Cursor, JetBrains — the mesh exposes an MCP server your editor's agent can call.", + cta: { label: "VS Code", href: "#" }, + cta2: { label: "JetBrains", href: "#" }, + mock: ( +
+
+ .claude/mcp.json +
+
+          {`{
+  "servers": {
+    "mesh": {
+      "url": "ws://mesh.team:7899"
+    }
+  }
+}`}
+        
+
+ ), + }, + { + accent: "cactus", + title: "Reach across machines", + body: "Tailscale, WireGuard, or plain WS over your LAN. The broker is one binary, anywhere.", + cta: { label: "Open the dashboard", href: "#" }, + mock: ( +
+
+ Peers on mesh +
+ {[ + ["alex", "macOS · working"], + ["jordan", "linux · idle"], + ["mo", "macOS · dnd"], + ].map(([n, s]) => ( +
+ {n} + {s} +
+ ))} +
+ ), + }, +]; + +export const MeetsYou = () => { + return ( +
+
+ + + + +

+ Meets every agent where it runs +

+
+ +
+ {CARDS.map((c) => ( +
+
{c.mock}
+

+ {c.title} +

+

+ {c.body} +

+
+ + {c.cta.label} → + + {c.cta2 && ( + + {c.cta2.label} → + + )} +
+
+ ))} +
+
+
+
+ ); +}; diff --git a/apps/web/src/modules/marketing/home/pricing.tsx b/apps/web/src/modules/marketing/home/pricing.tsx new file mode 100644 index 0000000..c87be2f --- /dev/null +++ b/apps/web/src/modules/marketing/home/pricing.tsx @@ -0,0 +1,147 @@ +"use client"; +import { useState } from "react"; +import Link from "next/link"; +import { Reveal, SectionIcon } from "./_reveal"; + +const TIERS = { + individual: [ + { + name: "Solo", + desc: "Run the broker on your laptop. Pair your Claude Code sessions across repos.", + price: "Free", + cta: "Install locally", + href: "https://github.com/claudemesh/claudemesh", + }, + { + name: "Pro", + desc: "Mesh dashboard, peer registry, message history, priority routing.", + price: "$12", + note: "per month", + cta: "Start free trial", + href: "#", + }, + { + name: "Plus", + desc: "Cross-machine mesh via Tailscale / WireGuard, MCP bridge, audit log.", + price: "$24", + note: "per month", + cta: "Start free trial", + href: "#", + }, + ], + team: [ + { + name: "Team", + desc: "Self-hosted broker. SSO, shared presence, team audit log, 25 peers.", + price: "$99", + note: "per month · unlimited peers", + cta: "Get started", + href: "#", + }, + { + name: "Business", + desc: "Multi-region brokers, retention controls, Slack/Linear bridges.", + price: "$499", + note: "per month", + cta: "Get started", + href: "#", + }, + { + name: "Enterprise", + desc: "Air-gapped deploy, custom SAML, dedicated support, SOC 2 pack.", + price: "Contact", + cta: "Contact sales", + href: "#", + }, + ], +}; + +export const Pricing = () => { + const [tab, setTab] = useState<"individual" | "team">("individual"); + const tiers = TIERS[tab]; + return ( +
+
+ + + + +

+ Get started with claudemesh +

+
+ +
+ {(["individual", "team"] as const).map((k) => ( + + ))} +
+
+ +
+ {tiers.map((tier) => ( +
+
+ +
+

+ {tier.name} +

+

+ {tier.desc} +

+
+
+ {tier.price} +
+ {tier.note && ( +
+ {tier.note} +
+ )} +
+ + {tier.cta} + +
+ ))} +
+
+
+
+ ); +}; diff --git a/apps/web/src/modules/marketing/home/surfaces.tsx b/apps/web/src/modules/marketing/home/surfaces.tsx new file mode 100644 index 0000000..013813a --- /dev/null +++ b/apps/web/src/modules/marketing/home/surfaces.tsx @@ -0,0 +1,152 @@ +import Image from "next/image"; +import Link from "next/link"; +import { Reveal } from "./_reveal"; + +export const Surfaces = () => { + return ( +
+
+ +
+ — surfaces +
+

+ Use claudemesh where your team already works +

+
+ + +
+ {/* top browser bar */} +
+
+ + + +
+
+ mesh.yourteam.local — live sessions: 6 +
+
+
+ {/* sidebar */} + + {/* main */} +
+
+ alex → jordan · 2m ago · priority: next +
+
+ Renamed AUTH_TOKEN →{" "} + AUTH_TOKEN_V2 in + terraform/secrets.tf. When you go idle, bump your env loader in{" "} + api-gateway/src/env.ts. +
+
+ ↳ queued · will deliver when jordan's session goes idle +
+
+
+
+
+ + +
+
+

+ Mesh Dashboard +

+ + Beta + +
+
+

+ Watch every Claude Code session on your team. Who's working + on what. Who's idle. What messages are in flight. Route by + name, by repo, by priority. +

+ + Open the dashboard + +
+
+
+
+
+ ); +}; diff --git a/apps/web/src/modules/marketing/home/toaster.tsx b/apps/web/src/modules/marketing/home/toaster.tsx new file mode 100644 index 0000000..3bc2f98 --- /dev/null +++ b/apps/web/src/modules/marketing/home/toaster.tsx @@ -0,0 +1,142 @@ +"use client"; +import { useState } from "react"; +import Link from "next/link"; + +const NEWS = [ + { + tag: "Beta", + title: "Mesh Dashboard", + body: "Watch every Claude Code session on your team. Routes, presence, priority — all live.", + href: "#", + }, + { + tag: "New", + title: "MCP bridge", + body: "Expose mesh messages as MCP tools. Your agent can message peers without leaving its context.", + href: "#", + }, + { + tag: "Launch", + title: "Self-hosted broker", + body: "One binary. SQLite-backed. Runs on a Pi. Your mesh, never the cloud's.", + href: "#", + }, +]; + +export const LatestNewsToaster = () => { + const [index, setIndex] = useState(0); + const [hidden, setHidden] = useState(false); + if (hidden) return null; + const item = NEWS[index]; + if (!item) return null; + return ( +
+ {/* head */} +
+
+ + + + Latest news +
+ +
+ {/* body */} +
+
+

+ {item.title} +

+

+ {item.body} +

+ + Learn more + +
+ {/* illustration tile */} +
+ + + + + + + + +
+
+ {/* pager */} +
+
+ {String(index + 1).padStart(2, "0")} / {String(NEWS.length).padStart(2, "0")} +
+
+ + +
+
+
+ ); +}; diff --git a/apps/web/src/modules/marketing/layout/header/header.tsx b/apps/web/src/modules/marketing/layout/header/header.tsx index bbebd6f..d4ca14a 100644 --- a/apps/web/src/modules/marketing/layout/header/header.tsx +++ b/apps/web/src/modules/marketing/layout/header/header.tsx @@ -1,111 +1,87 @@ -"use client"; +import Link from "next/link"; -import { useTranslation } from "@turbostarter/i18n"; -import { Icons } from "@turbostarter/ui-web/icons"; - -import { pathsConfig } from "~/config/paths"; -import { ThemeControls } from "~/modules/common/theme"; -import { TurboLink } from "~/modules/common/turbo-link"; -import { CtaButton } from "~/modules/marketing/layout/cta-button"; - -import { MobileNavigation } from "./navigation/mobile-navigation"; -import { Navigation } from "./navigation/navigation"; - -const links = [ +const NAV = [ + { label: "Docs", href: "#docs" }, + { label: "Pricing", href: "#pricing" }, + { label: "Changelog", href: "#changelog" }, { - label: "product", - items: [ - { - title: "marketing:product.mobile.ios.title", - description: "marketing:product.mobile.ios.description", - href: "https://apps.apple.com/app/id6754278899", - icon: Icons.AppleStroke, - }, - { - title: "marketing:product.mobile.android.title", - description: "marketing:product.mobile.android.description", - href: "https://play.google.com/store/apps/details?id=com.turbostarter.core", - icon: Icons.AndroidStroke, - }, - { - title: "marketing:product.extension.chrome.title", - description: "marketing:product.extension.chrome.description", - href: "https://chromewebstore.google.com/detail/bcjmonmlfbnngpkllpnpmnjajaciaboo", - icon: Icons.ChromeStroke, - }, - { - title: "marketing:product.extension.firefox.title", - description: "marketing:product.extension.firefox.description", - href: "https://addons.mozilla.org/addon/turbostarter_", - icon: Icons.FirefoxStroke, - }, - { - title: "marketing:product.extension.edge.title", - description: "marketing:product.extension.edge.description", - href: "https://microsoftedge.microsoft.com/addons/detail/turbostarter/ianbflanmmoeleokihabnmmcahhfijig", - icon: Icons.EdgeStroke, - }, - ], + label: "GitHub", + href: "https://github.com/claudemesh/claudemesh", + external: true, }, - { - label: "resources", - items: [ - { - title: "marketing:contact.label", - description: "marketing:contact.description", - href: pathsConfig.marketing.contact, - icon: Icons.SendHorizontal, - }, - { - title: "marketing:roadmap.title", - description: "marketing:roadmap.description", - href: "https://github.com/orgs/turbostarter/projects/1", - icon: Icons.ChartNoAxesGantt, - }, - { - title: "marketing:docs.title", - description: "marketing:docs.description", - href: "https://turbostarter.dev/docs/web", - icon: Icons.BookOpen, - }, - { - title: "marketing:api.title", - description: "marketing:api.description", - href: "#", - icon: Icons.Webhook, - }, - ], - }, - { - label: "billing:pricing.label", - href: pathsConfig.marketing.pricing, - }, - { - label: "marketing:blog.label", - href: pathsConfig.marketing.blog.index, - }, -] as const; +]; export const Header = () => { - const { t } = useTranslation("common"); return ( -
-
- +
+ {/* wordmark */} + - - - + + + + + + + + + claudemesh + + - + {/* center nav */} + -
- - - + {/* right */} +
+ + Sign in + + + Start free + +
diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 50fd8b4..c34e111 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -10,7 +10,7 @@ import { matchesPattern } from "@turbostarter/shared/utils"; import { localize, delay } from "./middleware"; import { adminRouter } from "./modules/admin/router"; -import { aiRouter } from "./modules/ai/router"; +// import { aiRouter } from "./modules/ai/router"; // disabled: @turbostarter/ai package removed in claudemesh import { authRouter } from "./modules/auth/router"; import { billingRouter } from "./modules/billing/router"; import { organizationRouter } from "./modules/organization/router"; @@ -45,7 +45,7 @@ const appRouter = new Hono() .use(localize) .get("/health", (c) => c.json({ status: "ok" })) .route("/admin", adminRouter) - .route("/ai", aiRouter) + // .route("/ai", aiRouter) // disabled: @turbostarter/ai package removed in claudemesh .route("/auth", authRouter) .route("/billing", billingRouter) .route("/organizations", organizationRouter) diff --git a/packages/db/src/schema/mesh.ts b/packages/db/src/schema/mesh.ts index 78a1d30..ae0e12f 100644 --- a/packages/db/src/schema/mesh.ts +++ b/packages/db/src/schema/mesh.ts @@ -228,7 +228,7 @@ export const meshRelations = relations(mesh, ({ one, many }) => ({ messageQueue: many(messageQueue), })); -export const memberRelations = relations(meshMember, ({ one, many }) => ({ +export const meshMemberRelations = relations(meshMember, ({ one, many }) => ({ mesh: one(mesh, { fields: [meshMember.meshId], references: [mesh.id],