feat(web): Submatter marketing redesign with Signal Editorial system
Introduce public /landing and /pricing pages for the SBMTR brand with bento features, convoy product demo, motion reveals, and scoped marketing tokens. Unauthenticated visitors route to the landing page. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
b513b3b04f
commit
2f6adf4382
22 changed files with 1215 additions and 22 deletions
29
apps/web/app/(marketing)/landing/page.tsx
Normal file
29
apps/web/app/(marketing)/landing/page.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import type { Metadata } from "next";
|
||||
|
||||
import { ComparisonSection } from "@/components/marketing/comparison-section";
|
||||
import { CtaSection } from "@/components/marketing/cta-section";
|
||||
import { FeaturesSection } from "@/components/marketing/features-section";
|
||||
import { HeroSection } from "@/components/marketing/hero-section";
|
||||
import { HowItWorksSection } from "@/components/marketing/how-it-works-section";
|
||||
import { SiteFooter } from "@/components/marketing/site-footer";
|
||||
import { SiteHeader } from "@/components/marketing/site-header";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Subagents. Frontmatter. One workspace.",
|
||||
};
|
||||
|
||||
export default function LandingPage() {
|
||||
return (
|
||||
<>
|
||||
<SiteHeader />
|
||||
<main>
|
||||
<HeroSection />
|
||||
<FeaturesSection />
|
||||
<HowItWorksSection />
|
||||
<ComparisonSection />
|
||||
<CtaSection />
|
||||
</main>
|
||||
<SiteFooter />
|
||||
</>
|
||||
);
|
||||
}
|
||||
125
apps/web/app/(marketing)/landing/pricing/page.tsx
Normal file
125
apps/web/app/(marketing)/landing/pricing/page.tsx
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
import { BorderGlowCard } from "@/components/marketing/border-glow-card";
|
||||
import { Reveal, Stagger, StaggerItem } from "@/components/marketing/reveal";
|
||||
import { SiteFooter } from "@/components/marketing/site-footer";
|
||||
import { SiteHeader } from "@/components/marketing/site-header";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Pricing",
|
||||
};
|
||||
|
||||
const tiers = [
|
||||
{
|
||||
name: "Solo",
|
||||
price: "Free",
|
||||
period: "during beta",
|
||||
description: "Personal workspace for testing agent workflows and markdown sync.",
|
||||
features: ["1 workspace", "Markdown backlog sync", "MCP claim / complete", "Docs & planner"],
|
||||
cta: "Start free",
|
||||
href: "/sign-up",
|
||||
highlighted: false,
|
||||
},
|
||||
{
|
||||
name: "Team",
|
||||
price: "$12",
|
||||
period: "per seat / mo",
|
||||
description: "Shared convoys, run history, and model routing for small builder teams.",
|
||||
features: [
|
||||
"Unlimited workspaces",
|
||||
"Agent run history",
|
||||
"Role-based routing",
|
||||
"Audit log",
|
||||
"Priority sync",
|
||||
],
|
||||
cta: "Start trial",
|
||||
href: "/sign-up",
|
||||
highlighted: true,
|
||||
},
|
||||
{
|
||||
name: "Fleet",
|
||||
price: "Custom",
|
||||
period: "contact us",
|
||||
description: "Mission control for multiple repos, tenants, and orchestrated pipelines.",
|
||||
features: ["Connected repos", "Fleet dashboard", "Webhook gates", "SSO", "Self-hosted option"],
|
||||
cta: "Contact",
|
||||
href: "mailto:hello@submatter.dev",
|
||||
highlighted: false,
|
||||
},
|
||||
] as const;
|
||||
|
||||
export default function PricingPage() {
|
||||
return (
|
||||
<>
|
||||
<SiteHeader />
|
||||
<main className="mx-auto max-w-6xl px-4 py-16 sm:px-6 sm:py-24">
|
||||
<Reveal className="mx-auto max-w-2xl text-center">
|
||||
<p className="font-mono-brand text-[11px] uppercase tracking-[0.2em] text-muted-foreground">Pricing</p>
|
||||
<h1 className="mt-4 font-marketing text-4xl font-semibold tracking-[-0.02em] text-foreground sm:text-5xl">
|
||||
Predictable seats.
|
||||
<br />
|
||||
No per-token surprise.
|
||||
</h1>
|
||||
<p className="mt-4 font-marketing text-muted-foreground">
|
||||
Beta pricing for feedback. BYOK for model providers always.
|
||||
</p>
|
||||
</Reveal>
|
||||
|
||||
<Stagger className="mt-16 grid gap-4 lg:grid-cols-3">
|
||||
{tiers.map((tier) => (
|
||||
<StaggerItem key={tier.name}>
|
||||
<BorderGlowCard
|
||||
as="article"
|
||||
className={cn(
|
||||
"flex h-full flex-col p-6",
|
||||
tier.highlighted && "ring-1 ring-matter/30",
|
||||
)}
|
||||
>
|
||||
{tier.highlighted ? (
|
||||
<span className="mb-4 inline-flex w-fit font-mono-brand text-[10px] uppercase tracking-widest text-matter">
|
||||
popular
|
||||
</span>
|
||||
) : (
|
||||
<span className="mb-4 h-4" aria-hidden />
|
||||
)}
|
||||
<h2 className="font-marketing text-lg font-semibold text-foreground">{tier.name}</h2>
|
||||
<p className="mt-2 flex items-baseline gap-2">
|
||||
<span className="font-marketing text-3xl font-semibold text-foreground">{tier.price}</span>
|
||||
<span className="font-marketing text-sm text-muted-foreground">{tier.period}</span>
|
||||
</p>
|
||||
<p className="mt-3 font-marketing text-sm text-muted-foreground">{tier.description}</p>
|
||||
<ul className="mt-6 flex-1 space-y-2.5">
|
||||
{tier.features.map((feature) => (
|
||||
<li key={feature} className="flex gap-2 font-marketing text-sm text-foreground">
|
||||
<Check className="mt-0.5 size-4 shrink-0 text-agent" aria-hidden />
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Link
|
||||
href={tier.href}
|
||||
className={cn(
|
||||
"mt-8 inline-flex h-10 items-center justify-center font-marketing text-sm font-medium transition-colors",
|
||||
tier.highlighted
|
||||
? "bg-foreground text-background hover:bg-foreground/90"
|
||||
: "border border-border hover:bg-muted",
|
||||
)}
|
||||
>
|
||||
{tier.cta}
|
||||
</Link>
|
||||
</BorderGlowCard>
|
||||
</StaggerItem>
|
||||
))}
|
||||
</Stagger>
|
||||
|
||||
<p className="mt-14 text-center font-mono-brand text-[11px] text-muted-foreground">
|
||||
kind: pricing · status: draft · priority: P2
|
||||
</p>
|
||||
</main>
|
||||
<SiteFooter />
|
||||
</>
|
||||
);
|
||||
}
|
||||
47
apps/web/app/(marketing)/layout.tsx
Normal file
47
apps/web/app/(marketing)/layout.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import type { Metadata } from "next";
|
||||
import type { ReactNode } from "react";
|
||||
import { Barlow_Condensed, IBM_Plex_Sans, JetBrains_Mono } from "next/font/google";
|
||||
|
||||
import "@/styles/marketing.css";
|
||||
|
||||
const barlowCondensed = Barlow_Condensed({
|
||||
subsets: ["latin"],
|
||||
weight: ["700", "800"],
|
||||
variable: "--font-display",
|
||||
});
|
||||
|
||||
const ibmPlexSans = IBM_Plex_Sans({
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "500", "600"],
|
||||
variable: "--font-marketing-sans",
|
||||
});
|
||||
|
||||
const jetbrainsMono = JetBrains_Mono({
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "500"],
|
||||
variable: "--font-mono-brand",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: "Submatter — Subagents. Frontmatter. One workspace.",
|
||||
template: "%s · Submatter",
|
||||
},
|
||||
description:
|
||||
"Agent-native workspace with markdown backlog sync, MCP task lifecycle, and convoy pipelines for Cursor subagents.",
|
||||
openGraph: {
|
||||
title: "SBMTR — Submatter",
|
||||
description: "Subagents. Frontmatter. One workspace.",
|
||||
siteName: "Submatter",
|
||||
},
|
||||
};
|
||||
|
||||
export default function MarketingLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
className={`marketing-theme min-h-screen bg-background font-marketing text-foreground antialiased ${barlowCondensed.variable} ${ibmPlexSans.variable} ${jetbrainsMono.variable}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -6,27 +6,14 @@ import { db } from "@tasks/database/client";
|
|||
import { workspaceMembers, workspaces } from "@tasks/database/schema";
|
||||
|
||||
/**
|
||||
* Root landing route. Decides where an authenticated user goes when they
|
||||
* hit `/`. The post-sign-in `callbackUrl` defaults here, so this is the
|
||||
* single source of truth for "where does a user actually land?"
|
||||
*
|
||||
* Decision: land on the user's oldest workspace home (`/{slug}/`). Why oldest:
|
||||
* we don't have a "last visited" column yet (deliberately deferred — see
|
||||
* Task-pick-workspace-landing-route) and oldest-membership is stable across
|
||||
* page loads. The workspace home is no longer a mockup; it now shows real
|
||||
* stats and recent activity via `objects.stats` / `objects.listRecent`.
|
||||
*
|
||||
* The auth callback (`ensureUserHasWorkspace` in `apps/web/lib/auth.ts`)
|
||||
* provisions a personal workspace on first sign-in, so the zero-workspace
|
||||
* branch below should be effectively unreachable for fresh sign-ins. It
|
||||
* survives only as a defensive fallback for sessions that were minted
|
||||
* before that provisioning logic existed.
|
||||
* Root landing route. Unauthenticated visitors go to the public marketing site;
|
||||
* signed-in users land on their oldest workspace home (`/{slug}/`).
|
||||
*/
|
||||
export default async function HomePage() {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user?.id) {
|
||||
redirect("/sign-in");
|
||||
redirect("/landing");
|
||||
}
|
||||
|
||||
const [membership] = await db
|
||||
|
|
@ -41,11 +28,5 @@ export default async function HomePage() {
|
|||
redirect(`/${membership.slug}`);
|
||||
}
|
||||
|
||||
// Zero-workspace fallback. See header doc — this should be unreachable
|
||||
// post-`ensureUserHasWorkspace`. If we hit it anyway, bounce through
|
||||
// sign-in so the auth callback re-runs and provisions a workspace.
|
||||
// Track follow-up: a proper onboarding flow for users in this state
|
||||
// is captured in Plan-daily-driver-finish/Epic-shipping-the-shell/
|
||||
// Task-onboarding-zero-workspace-flow.md.
|
||||
redirect("/sign-in?error=no_workspace");
|
||||
}
|
||||
|
|
|
|||
22
apps/web/components/marketing/border-glow-card.tsx
Normal file
22
apps/web/components/marketing/border-glow-card.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import type { ReactNode } from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type BorderGlowCardProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
as?: "div" | "article" | "li";
|
||||
};
|
||||
|
||||
export function BorderGlowCard({ children, className, as: Tag = "div" }: BorderGlowCardProps) {
|
||||
return (
|
||||
<Tag
|
||||
className={cn(
|
||||
"marketing-glow-card rounded-md border border-border",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
78
apps/web/components/marketing/comparison-section.tsx
Normal file
78
apps/web/components/marketing/comparison-section.tsx
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import { Reveal } from "@/components/marketing/reveal";
|
||||
|
||||
const rows = [
|
||||
{ label: "Markdown backlog in git", sbmtr: true, linear: false, notion: "partial", convoy: false },
|
||||
{ label: "MCP claim / complete", sbmtr: true, linear: false, notion: false, convoy: true },
|
||||
{ label: "Full workspace surface", sbmtr: true, linear: "partial", notion: true, convoy: false },
|
||||
{ label: "Role-based convoys", sbmtr: true, linear: false, notion: false, convoy: true },
|
||||
{ label: "Human approval gates", sbmtr: true, linear: false, notion: false, convoy: "partial" },
|
||||
] as const;
|
||||
|
||||
function Cell({ value }: { value: boolean | string }) {
|
||||
if (value === true) return <span className="font-mono-brand text-agent">yes</span>;
|
||||
if (value === false) return <span className="text-muted-foreground/50">—</span>;
|
||||
return <span className="font-mono-brand text-matter">{value}</span>;
|
||||
}
|
||||
|
||||
export function ComparisonSection() {
|
||||
return (
|
||||
<section className="border-b border-border bg-background py-20 sm:py-28">
|
||||
<div className="mx-auto max-w-6xl px-4 sm:px-6">
|
||||
<Reveal className="mb-10 max-w-2xl">
|
||||
<p className="font-mono-brand text-[11px] uppercase tracking-[0.2em] text-muted-foreground">Compare</p>
|
||||
<h2 className="mt-4 font-marketing text-3xl font-semibold tracking-[-0.02em] text-foreground">
|
||||
The agent-native lane
|
||||
</h2>
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={0.08}>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full min-w-[640px] border-collapse text-left text-sm">
|
||||
<thead>
|
||||
<tr className="border-b-2 border-foreground">
|
||||
<th className="py-3 pr-4 font-marketing font-medium text-muted-foreground" scope="col">
|
||||
Capability
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-3 font-display text-xs font-extrabold tracking-[0.2em] text-foreground"
|
||||
scope="col"
|
||||
>
|
||||
SBMTR
|
||||
</th>
|
||||
<th className="px-4 py-3 font-marketing font-medium text-muted-foreground" scope="col">
|
||||
Linear
|
||||
</th>
|
||||
<th className="px-4 py-3 font-marketing font-medium text-muted-foreground" scope="col">
|
||||
Notion
|
||||
</th>
|
||||
<th className="px-4 py-3 font-marketing font-medium text-muted-foreground" scope="col">
|
||||
Convoy tools
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((row) => (
|
||||
<tr key={row.label} className="border-b border-border">
|
||||
<td className="py-4 pr-4 font-marketing text-foreground">{row.label}</td>
|
||||
<td className="px-4 py-4">
|
||||
<Cell value={row.sbmtr} />
|
||||
</td>
|
||||
<td className="px-4 py-4">
|
||||
<Cell value={row.linear} />
|
||||
</td>
|
||||
<td className="px-4 py-4">
|
||||
<Cell value={row.notion} />
|
||||
</td>
|
||||
<td className="px-4 py-4">
|
||||
<Cell value={row.convoy} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
35
apps/web/components/marketing/cta-section.tsx
Normal file
35
apps/web/components/marketing/cta-section.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import Link from "next/link";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
|
||||
import { Reveal } from "@/components/marketing/reveal";
|
||||
|
||||
export function CtaSection() {
|
||||
return (
|
||||
<section className="border-b border-border bg-[hsl(var(--hero))] py-20 text-[hsl(var(--hero-foreground))] sm:py-28">
|
||||
<div className="mx-auto max-w-6xl px-4 sm:px-6">
|
||||
<Reveal className="mx-auto max-w-3xl text-center">
|
||||
<p className="font-mono-brand text-[11px] uppercase tracking-[0.25em] text-[hsl(var(--hero-muted))]">---</p>
|
||||
<h2 className="mt-6 font-display text-5xl font-extrabold tracking-[0.12em] sm:text-7xl">SBMTR</h2>
|
||||
<p className="mx-auto mt-6 max-w-lg font-marketing text-lg text-[hsl(var(--hero-muted))]">
|
||||
Subagents. Frontmatter. One workspace. Open Monday morning in Submatter — not a demo shell.
|
||||
</p>
|
||||
<div className="mt-10 flex flex-wrap items-center justify-center gap-6">
|
||||
<Link
|
||||
href="/sign-up"
|
||||
className="group inline-flex items-center gap-2 border-b-2 border-[hsl(var(--matter))] pb-1 font-marketing text-sm font-medium transition-colors hover:text-[hsl(var(--matter))]"
|
||||
>
|
||||
Start free
|
||||
<ArrowRight className="size-4 transition-transform group-hover:translate-x-0.5" aria-hidden />
|
||||
</Link>
|
||||
<Link
|
||||
href="/landing/pricing"
|
||||
className="font-marketing text-sm text-[hsl(var(--hero-muted))] transition-colors hover:text-[hsl(var(--hero-foreground))]"
|
||||
>
|
||||
View pricing
|
||||
</Link>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
21
apps/web/components/marketing/dot-field.tsx
Normal file
21
apps/web/components/marketing/dot-field.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { cn } from "@/lib/utils";
|
||||
|
||||
type DotFieldProps = {
|
||||
className?: string;
|
||||
variant?: "hero" | "light";
|
||||
};
|
||||
|
||||
export function DotField({ className, variant = "hero" }: DotFieldProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-0",
|
||||
variant === "hero" ? "marketing-dot-field marketing-scanline" : "opacity-30",
|
||||
variant === "light" &&
|
||||
"bg-[radial-gradient(circle_at_center,hsl(var(--foreground)/0.07)_1px,transparent_1px)] [background-size:24px_24px]",
|
||||
className,
|
||||
)}
|
||||
aria-hidden
|
||||
/>
|
||||
);
|
||||
}
|
||||
110
apps/web/components/marketing/features-section.tsx
Normal file
110
apps/web/components/marketing/features-section.tsx
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import { Bot, FileText, GitBranch, Layers, Route, Shield } from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
import { BorderGlowCard } from "@/components/marketing/border-glow-card";
|
||||
import { DotField } from "@/components/marketing/dot-field";
|
||||
import { Reveal, Stagger, StaggerItem } from "@/components/marketing/reveal";
|
||||
|
||||
type Feature = {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
description: string;
|
||||
span: string;
|
||||
tag: string;
|
||||
};
|
||||
|
||||
const features: Feature[] = [
|
||||
{
|
||||
icon: FileText,
|
||||
title: "Markdown source of truth",
|
||||
description: "Plan / Epic / Task files in git. Frontmatter fields drive status, priority, and agent routing.",
|
||||
span: "lg:col-span-7",
|
||||
tag: "sync",
|
||||
},
|
||||
{
|
||||
icon: Bot,
|
||||
title: "MCP lifecycle",
|
||||
description: "claim_task → work → complete_task. Auditable runs, not ad-hoc object patches.",
|
||||
span: "lg:col-span-5",
|
||||
tag: "mcp",
|
||||
},
|
||||
{
|
||||
icon: GitBranch,
|
||||
title: "Convoy pipelines",
|
||||
description: "Architect, implementer, reviewer roles with human gates between stages.",
|
||||
span: "lg:col-span-4",
|
||||
tag: "convoy",
|
||||
},
|
||||
{
|
||||
icon: Layers,
|
||||
title: "Full workspace",
|
||||
description: "Docs, boards, forms, whiteboards — same object model humans and agents share.",
|
||||
span: "lg:col-span-4",
|
||||
tag: "workspace",
|
||||
},
|
||||
{
|
||||
icon: Shield,
|
||||
title: "Multitenant",
|
||||
description: "workspace_id on every row. Built for teams, not a single homelab tenant.",
|
||||
span: "lg:col-span-4",
|
||||
tag: "tenant",
|
||||
},
|
||||
];
|
||||
|
||||
export function FeaturesSection() {
|
||||
return (
|
||||
<section id="features" className="relative border-b border-border bg-background py-20 sm:py-28">
|
||||
<DotField variant="light" className="opacity-40" />
|
||||
|
||||
<div className="relative mx-auto max-w-6xl px-4 sm:px-6">
|
||||
<Reveal className="mb-14 max-w-2xl">
|
||||
<p className="font-mono-brand text-[11px] uppercase tracking-[0.2em] text-muted-foreground">Capabilities</p>
|
||||
<h2 className="mt-4 font-marketing text-3xl font-semibold tracking-[-0.02em] text-foreground sm:text-4xl">
|
||||
Not a chat wrapper.
|
||||
<br />
|
||||
A coordination plane.
|
||||
</h2>
|
||||
</Reveal>
|
||||
|
||||
<Stagger className="grid gap-4 lg:grid-cols-12">
|
||||
{features.map((feature) => (
|
||||
<StaggerItem key={feature.title} className={feature.span}>
|
||||
<BorderGlowCard className="flex h-full flex-col p-6">
|
||||
<div className="mb-5 flex items-center justify-between">
|
||||
<feature.icon className="size-5 text-primary" strokeWidth={1.75} aria-hidden />
|
||||
<span className="font-mono-brand text-[10px] uppercase tracking-widest text-muted-foreground">
|
||||
{feature.tag}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="font-marketing text-lg font-semibold text-foreground">{feature.title}</h3>
|
||||
<p className="mt-2 flex-1 font-marketing text-sm leading-relaxed text-muted-foreground">
|
||||
{feature.description}
|
||||
</p>
|
||||
</BorderGlowCard>
|
||||
</StaggerItem>
|
||||
))}
|
||||
|
||||
<StaggerItem className="lg:col-span-12">
|
||||
<BorderGlowCard className="flex flex-col gap-4 p-6 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-start gap-4">
|
||||
<Route className="mt-0.5 size-5 shrink-0 text-matter" strokeWidth={1.75} aria-hidden />
|
||||
<div>
|
||||
<h3 className="font-marketing text-lg font-semibold text-foreground">Per-role model routing</h3>
|
||||
<p className="mt-1 max-w-2xl font-marketing text-sm text-muted-foreground">
|
||||
Fast implementers, premium architects — declared in convoy frontmatter and enforced by your
|
||||
agent pipeline.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="font-mono-brand text-xs text-muted-foreground sm:text-right">
|
||||
model_policy: composer-fast
|
||||
<br />
|
||||
tier: fast | premium
|
||||
</p>
|
||||
</BorderGlowCard>
|
||||
</StaggerItem>
|
||||
</Stagger>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
75
apps/web/components/marketing/hero-section.tsx
Normal file
75
apps/web/components/marketing/hero-section.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import Link from "next/link";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
|
||||
import { DotField } from "@/components/marketing/dot-field";
|
||||
import { ProductDemo } from "@/components/marketing/product-demo";
|
||||
import { Reveal } from "@/components/marketing/reveal";
|
||||
|
||||
export function HeroSection() {
|
||||
return (
|
||||
<section className="relative overflow-hidden bg-[hsl(var(--hero))] text-[hsl(var(--hero-foreground))]">
|
||||
<DotField />
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_60%_50%_at_70%_20%,hsl(var(--matter)/0.08),transparent_60%)]"
|
||||
aria-hidden
|
||||
/>
|
||||
|
||||
<div className="relative mx-auto grid max-w-6xl gap-12 px-4 pb-20 pt-12 sm:px-6 lg:grid-cols-2 lg:items-center lg:gap-16 lg:pb-28 lg:pt-16">
|
||||
<div className="space-y-8">
|
||||
<Reveal>
|
||||
<p className="font-mono-brand text-[11px] uppercase tracking-[0.2em] text-[hsl(var(--hero-muted))]">
|
||||
Agent-native workspace
|
||||
</p>
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={0.05}>
|
||||
<h1 className="max-w-xl font-marketing text-[2.75rem] font-semibold leading-[1.02] tracking-[-0.03em] sm:text-6xl lg:text-[4.25rem]">
|
||||
The matter beneath{" "}
|
||||
<span className="font-display font-extrabold uppercase tracking-[0.08em] text-[hsl(var(--matter))]">
|
||||
SBMTR
|
||||
</span>
|
||||
</h1>
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={0.1}>
|
||||
<p className="max-w-lg font-marketing text-lg leading-relaxed text-[hsl(var(--hero-muted))]">
|
||||
Submatter syncs your <span className="font-mono-brand text-[hsl(var(--hero-foreground))]">plans/</span>{" "}
|
||||
backlog to a real workspace, then lets Cursor subagents claim tasks through MCP — with convoy gates
|
||||
humans actually approve.
|
||||
</p>
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={0.15}>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Link
|
||||
href="/sign-up"
|
||||
className="group inline-flex h-12 items-center gap-2 border-b-2 border-[hsl(var(--matter))] bg-transparent pb-0.5 font-marketing text-sm font-medium text-[hsl(var(--hero-foreground))] transition-colors hover:text-[hsl(var(--matter))]"
|
||||
>
|
||||
Start free
|
||||
<ArrowRight className="size-4 transition-transform group-hover:translate-x-0.5" aria-hidden />
|
||||
</Link>
|
||||
<Link
|
||||
href="/landing#pipeline"
|
||||
className="inline-flex h-12 items-center px-1 font-marketing text-sm text-[hsl(var(--hero-muted))] transition-colors hover:text-[hsl(var(--hero-foreground))]"
|
||||
>
|
||||
See the pipeline
|
||||
</Link>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={0.2}>
|
||||
<p className="font-mono-brand text-[11px] text-[hsl(var(--hero-muted))]">
|
||||
subagents · frontmatter · one workspace
|
||||
</p>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
<Reveal delay={0.12} y={32} className="lg:translate-y-4">
|
||||
<ProductDemo />
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
<div className="h-1 bg-[hsl(var(--matter))]" aria-hidden />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
72
apps/web/components/marketing/how-it-works-section.tsx
Normal file
72
apps/web/components/marketing/how-it-works-section.tsx
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { Reveal, Stagger, StaggerItem } from "@/components/marketing/reveal";
|
||||
|
||||
const steps = [
|
||||
{
|
||||
n: "01",
|
||||
title: "Write in plans/",
|
||||
body: "YAML frontmatter on every Plan, Epic, and Task. Versioned in git like the rest of your codebase.",
|
||||
code: "plans/Plan-*/Epic-*/Task-*.md",
|
||||
},
|
||||
{
|
||||
n: "02",
|
||||
title: "Sync to Submatter",
|
||||
body: "Importer mirrors frontmatter into Postgres. Cursor mappings keep agent ids aligned with slugs.",
|
||||
code: "watch:markdown-backlog",
|
||||
},
|
||||
{
|
||||
n: "03",
|
||||
title: "Agents claim work",
|
||||
body: "Subagents call MCP tools against a scoped workspace_id. Runs appear in fleet history.",
|
||||
code: "claim_task → complete_task",
|
||||
},
|
||||
{
|
||||
n: "04",
|
||||
title: "Humans gate convoys",
|
||||
body: "Reviewer and architect roles pause for approval. Status flows back to markdown on merge.",
|
||||
code: "ready → in_progress → done",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export function HowItWorksSection() {
|
||||
return (
|
||||
<section id="pipeline" className="border-b border-border bg-card py-20 sm:py-28">
|
||||
<div className="mx-auto max-w-6xl px-4 sm:px-6">
|
||||
<Reveal className="mb-14 flex flex-col gap-6 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div className="max-w-xl">
|
||||
<p className="font-mono-brand text-[11px] uppercase tracking-[0.2em] text-muted-foreground">
|
||||
How it works
|
||||
</p>
|
||||
<h2 className="mt-4 font-marketing text-3xl font-semibold tracking-[-0.02em] text-foreground sm:text-4xl">
|
||||
From frontmatter to shipped
|
||||
</h2>
|
||||
</div>
|
||||
<p className="max-w-sm font-marketing text-sm text-muted-foreground lg:text-right">
|
||||
Interactive product demo pattern — show the file, show the pipeline, show the gate.
|
||||
</p>
|
||||
</Reveal>
|
||||
|
||||
<Stagger className="relative grid gap-0 md:grid-cols-4">
|
||||
<div
|
||||
className="absolute left-0 right-0 top-8 hidden h-px bg-border md:block"
|
||||
aria-hidden
|
||||
/>
|
||||
{steps.map((step) => (
|
||||
<StaggerItem key={step.n} className="relative border-t border-border pt-8 md:border-t-0 md:pt-0">
|
||||
<div className="md:px-4 md:first:pl-0 md:last:pr-0">
|
||||
<div className="mb-6 flex items-center gap-3">
|
||||
<span className="flex size-8 items-center justify-center rounded-full border border-border bg-background font-mono-brand text-xs text-foreground">
|
||||
{step.n}
|
||||
</span>
|
||||
<span className="hidden h-px flex-1 bg-border md:block" aria-hidden />
|
||||
</div>
|
||||
<h3 className="font-marketing text-base font-semibold text-foreground">{step.title}</h3>
|
||||
<p className="mt-2 font-marketing text-sm leading-relaxed text-muted-foreground">{step.body}</p>
|
||||
<p className="mt-4 font-mono-brand text-[11px] text-primary">{step.code}</p>
|
||||
</div>
|
||||
</StaggerItem>
|
||||
))}
|
||||
</Stagger>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
61
apps/web/components/marketing/logo.tsx
Normal file
61
apps/web/components/marketing/logo.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import Link from "next/link";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type LogoProps = {
|
||||
className?: string;
|
||||
showTagline?: boolean;
|
||||
size?: "sm" | "md" | "lg";
|
||||
variant?: "dark" | "light";
|
||||
};
|
||||
|
||||
const sizeClasses = {
|
||||
sm: { rule: "w-8", word: "text-xl tracking-[0.22em]", tag: "text-[10px]" },
|
||||
md: { rule: "w-10", word: "text-2xl tracking-[0.24em]", tag: "text-xs" },
|
||||
lg: { rule: "w-14", word: "text-4xl tracking-[0.26em]", tag: "text-sm" },
|
||||
};
|
||||
|
||||
export function Logo({
|
||||
className,
|
||||
showTagline = false,
|
||||
size = "md",
|
||||
variant = "light",
|
||||
}: LogoProps) {
|
||||
const s = sizeClasses[size];
|
||||
const isDark = variant === "dark";
|
||||
|
||||
return (
|
||||
<Link href="/landing" className={cn("group inline-flex flex-col gap-1", className)}>
|
||||
<span
|
||||
className={cn(
|
||||
"h-0.5 rounded-full transition-colors",
|
||||
s.rule,
|
||||
isDark
|
||||
? "bg-[hsl(var(--hero-foreground))] group-hover:bg-[hsl(var(--matter))]"
|
||||
: "bg-foreground group-hover:bg-primary",
|
||||
)}
|
||||
aria-hidden
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
"font-display font-extrabold leading-none",
|
||||
s.word,
|
||||
isDark ? "text-[hsl(var(--hero-foreground))]" : "text-foreground",
|
||||
)}
|
||||
>
|
||||
SBMTR
|
||||
</span>
|
||||
{showTagline ? (
|
||||
<span
|
||||
className={cn(
|
||||
"font-marketing",
|
||||
s.tag,
|
||||
isDark ? "text-[hsl(var(--hero-muted))]" : "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
Submatter
|
||||
</span>
|
||||
) : null}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
116
apps/web/components/marketing/product-demo.tsx
Normal file
116
apps/web/components/marketing/product-demo.tsx
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
"use client";
|
||||
|
||||
import { motion, useReducedMotion } from "motion/react";
|
||||
import { Bot, CheckCircle2, GitBranch, Terminal } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const pipelineSteps = [
|
||||
{ role: "architect", model: "opus", status: "done" as const },
|
||||
{ role: "implementer", model: "fast", status: "active" as const },
|
||||
{ role: "reviewer", model: "fast", status: "queued" as const },
|
||||
];
|
||||
|
||||
export function ProductDemo({ className }: { className?: string }) {
|
||||
const reduceMotion = useReducedMotion();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative overflow-hidden rounded-md border border-[hsl(var(--hero-border))] bg-[hsl(240_6%_6%)] shadow-2xl shadow-black/40",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2 border-b border-[hsl(var(--hero-border))] px-4 py-3">
|
||||
<div className="flex gap-1.5" aria-hidden>
|
||||
<span className="size-2.5 rounded-full bg-[#FF5F57]" />
|
||||
<span className="size-2.5 rounded-full bg-[#FEBC2E]" />
|
||||
<span className="size-2.5 rounded-full bg-[#28C840]" />
|
||||
</div>
|
||||
<p className="flex-1 text-center font-mono-brand text-[11px] text-[hsl(var(--hero-muted))]">
|
||||
submatter — convoy · mcp-claim-task-tool
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-0 lg:grid-cols-5">
|
||||
<div className="border-b border-[hsl(var(--hero-border))] p-4 lg:col-span-2 lg:border-b-0 lg:border-r">
|
||||
<div className="mb-3 flex items-center gap-2 text-[hsl(var(--hero-muted))]">
|
||||
<Terminal className="size-3.5" aria-hidden />
|
||||
<span className="font-mono-brand text-[10px] uppercase tracking-widest">plans/</span>
|
||||
</div>
|
||||
<pre className="font-mono-brand text-[11px] leading-relaxed text-[hsl(var(--hero-foreground))]">
|
||||
<span className="text-[hsl(var(--hero-muted))]">---{"\n"}</span>
|
||||
<span className="text-[hsl(var(--agent))]">kind</span>
|
||||
<span className="text-[hsl(var(--hero-muted))]">: </span>task{"\n"}
|
||||
<span className="text-[hsl(var(--agent))]">slug</span>
|
||||
<span className="text-[hsl(var(--hero-muted))]">: </span>mcp-claim-task-tool{"\n"}
|
||||
<span className="text-[hsl(var(--agent))]">status</span>
|
||||
<span className="text-[hsl(var(--hero-muted))]">: </span>
|
||||
<span className="text-[hsl(var(--matter))]">in_progress</span>
|
||||
{"\n"}
|
||||
<span className="text-[hsl(var(--agent))]">priority</span>
|
||||
<span className="text-[hsl(var(--hero-muted))]">: </span>P1{"\n"}
|
||||
<span className="text-[hsl(var(--agent))]">model_policy</span>
|
||||
<span className="text-[hsl(var(--hero-muted))]">: </span>composer-fast{"\n"}
|
||||
<span className="text-[hsl(var(--hero-muted))]">---</span>
|
||||
<span className="marketing-cursor-blink text-[hsl(var(--matter))]">▍</span>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="p-4 lg:col-span-3">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<GitBranch className="size-3.5 text-[hsl(var(--hero-muted))]" aria-hidden />
|
||||
<span className="font-mono-brand text-[10px] uppercase tracking-widest text-[hsl(var(--hero-muted))]">
|
||||
Convoy pipeline
|
||||
</span>
|
||||
</div>
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full border border-[hsl(var(--hero-border))] px-2 py-0.5 font-mono-brand text-[10px] text-[hsl(var(--agent))]">
|
||||
<span className="marketing-pulse-dot size-1.5 rounded-full bg-[hsl(var(--agent))]" />
|
||||
MCP live
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ol className="space-y-2">
|
||||
{pipelineSteps.map((step, index) => (
|
||||
<motion.li
|
||||
key={step.role}
|
||||
className={cn(
|
||||
"flex items-center justify-between rounded border px-3 py-2.5",
|
||||
step.status === "active"
|
||||
? "border-[hsl(var(--matter)/0.45)] bg-[hsl(var(--matter)/0.08)]"
|
||||
: "border-[hsl(var(--hero-border))] bg-[hsl(240_5%_8%)]",
|
||||
)}
|
||||
initial={reduceMotion ? false : { opacity: 0, x: 12 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.15 + index * 0.12, duration: 0.45 }}
|
||||
>
|
||||
<div className="flex items-center gap-2.5">
|
||||
{step.status === "done" ? (
|
||||
<CheckCircle2 className="size-3.5 text-[hsl(var(--agent))]" aria-hidden />
|
||||
) : (
|
||||
<Bot
|
||||
className={cn(
|
||||
"size-3.5",
|
||||
step.status === "active" ? "text-[hsl(var(--matter))]" : "text-[hsl(var(--hero-muted))]",
|
||||
)}
|
||||
aria-hidden
|
||||
/>
|
||||
)}
|
||||
<span className="font-mono-brand text-xs text-[hsl(var(--hero-foreground))]">
|
||||
role-{step.role}
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-mono-brand text-[10px] text-[hsl(var(--hero-muted))]">{step.model}</span>
|
||||
</motion.li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
<div className="mt-4 rounded border border-dashed border-[hsl(var(--hero-border))] px-3 py-2 font-mono-brand text-[10px] text-[hsl(var(--hero-muted))]">
|
||||
claim_task · workspace_id scoped · run ar_8f2a
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
90
apps/web/components/marketing/reveal.tsx
Normal file
90
apps/web/components/marketing/reveal.tsx
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { motion, useReducedMotion } from "motion/react";
|
||||
|
||||
type RevealProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
delay?: number;
|
||||
y?: number;
|
||||
};
|
||||
|
||||
export function Reveal({ children, className, delay = 0, y = 24 }: RevealProps) {
|
||||
const reduceMotion = useReducedMotion();
|
||||
|
||||
if (reduceMotion) {
|
||||
return <div className={className}>{children}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={className}
|
||||
initial={{ opacity: 0, y }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-60px" }}
|
||||
transition={{ duration: 0.55, delay, ease: [0.22, 1, 0.36, 1] }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
type StaggerProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
stagger?: number;
|
||||
};
|
||||
|
||||
export function Stagger({ children, className, stagger = 0.08 }: StaggerProps) {
|
||||
const reduceMotion = useReducedMotion();
|
||||
|
||||
if (reduceMotion) {
|
||||
return <div className={className}>{children}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={className}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, margin: "-40px" }}
|
||||
variants={{
|
||||
hidden: {},
|
||||
visible: { transition: { staggerChildren: stagger } },
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export function StaggerItem({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
const reduceMotion = useReducedMotion();
|
||||
|
||||
if (reduceMotion) {
|
||||
return <div className={className}>{children}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={className}
|
||||
variants={{
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { duration: 0.5, ease: [0.22, 1, 0.36, 1] },
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
44
apps/web/components/marketing/site-footer.tsx
Normal file
44
apps/web/components/marketing/site-footer.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import Link from "next/link";
|
||||
|
||||
import { Logo } from "@/components/marketing/logo";
|
||||
|
||||
const footerLinks = [
|
||||
{ href: "/landing#features", label: "Features" },
|
||||
{ href: "/landing#pipeline", label: "Pipeline" },
|
||||
{ href: "/landing/pricing", label: "Pricing" },
|
||||
{ href: "/sign-in", label: "Sign in" },
|
||||
] as const;
|
||||
|
||||
export function SiteFooter() {
|
||||
return (
|
||||
<footer className="bg-card">
|
||||
<div className="mx-auto flex max-w-6xl flex-col gap-10 px-4 py-14 sm:px-6 md:flex-row md:items-start md:justify-between">
|
||||
<div className="space-y-3">
|
||||
<Logo size="sm" showTagline variant="light" />
|
||||
<p className="max-w-xs font-marketing text-sm text-muted-foreground">
|
||||
Subagents. Frontmatter. One workspace.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<nav className="flex flex-wrap gap-x-10 gap-y-3" aria-label="Footer">
|
||||
{footerLinks.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="font-marketing text-sm text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className="border-t-2 border-foreground">
|
||||
<div className="mx-auto flex max-w-6xl flex-col gap-2 px-4 py-5 sm:flex-row sm:items-center sm:justify-between sm:px-6">
|
||||
<p className="font-mono-brand text-[11px] text-muted-foreground">© {new Date().getFullYear()} Submatter</p>
|
||||
<p className="font-mono-brand text-[11px] text-muted-foreground">sbmtr.io · built for agent pipelines</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
52
apps/web/components/marketing/site-header.tsx
Normal file
52
apps/web/components/marketing/site-header.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import Link from "next/link";
|
||||
|
||||
import { Logo } from "@/components/marketing/logo";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const nav = [
|
||||
{ href: "/landing#features", label: "Features" },
|
||||
{ href: "/landing#pipeline", label: "Pipeline" },
|
||||
{ href: "/landing/pricing", label: "Pricing" },
|
||||
] as const;
|
||||
|
||||
export function SiteHeader({ className }: { className?: string }) {
|
||||
return (
|
||||
<header
|
||||
className={cn(
|
||||
"sticky top-0 z-50 border-b border-[hsl(var(--hero-border))] bg-[hsl(var(--hero)/0.85)] text-[hsl(var(--hero-foreground))] backdrop-blur-md",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="mx-auto flex h-14 max-w-6xl items-center justify-between gap-6 px-4 sm:px-6">
|
||||
<Logo size="sm" variant="dark" />
|
||||
|
||||
<nav className="hidden items-center gap-8 md:flex" aria-label="Main">
|
||||
{nav.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="font-marketing text-sm text-[hsl(var(--hero-muted))] transition-colors hover:text-[hsl(var(--hero-foreground))]"
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<Link
|
||||
href="/sign-in"
|
||||
className="hidden font-marketing text-sm text-[hsl(var(--hero-muted))] transition-colors hover:text-[hsl(var(--hero-foreground))] sm:inline"
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
<Link
|
||||
href="/sign-up"
|
||||
className="inline-flex h-9 items-center border border-[hsl(var(--matter))] px-4 font-marketing text-sm font-medium text-[hsl(var(--matter))] transition-colors hover:bg-[hsl(var(--matter)/0.1)]"
|
||||
>
|
||||
Start free
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ export default auth((req) => {
|
|||
|
||||
const isPublic =
|
||||
pathname === "/" ||
|
||||
pathname.startsWith("/landing") ||
|
||||
pathname.startsWith("/sign-in") ||
|
||||
pathname.startsWith("/sign-up") ||
|
||||
pathname.startsWith("/api/auth");
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@
|
|||
"clsx": "^2.1.1",
|
||||
"drizzle-orm": "^0.38.0",
|
||||
"lucide-react": "^0.468.0",
|
||||
"motion": "^12.42.2",
|
||||
"next": "^15.1.0",
|
||||
"next-auth": "^5.0.0-beta.25",
|
||||
"next-themes": "^0.4.4",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,11 @@
|
|||
--teal: 168 76% 42%;
|
||||
--teal-foreground: 0 0% 100%;
|
||||
|
||||
--matter: 32 92% 50%;
|
||||
--matter-foreground: 220 25% 12%;
|
||||
--agent: 168 55% 36%;
|
||||
--agent-foreground: 0 0% 100%;
|
||||
|
||||
--secondary: 210 40% 96%;
|
||||
--secondary-foreground: 222 47% 11%;
|
||||
|
||||
|
|
|
|||
160
apps/web/styles/marketing.css
Normal file
160
apps/web/styles/marketing.css
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
/* Submatter marketing — Signal Editorial (ui-ux-pro-max: Swiss + interactive demo + bold type) */
|
||||
|
||||
.marketing-theme {
|
||||
--background: 40 18% 96%;
|
||||
--foreground: 220 20% 8%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 220 20% 8%;
|
||||
|
||||
--primary: 228 45% 22%;
|
||||
--primary-foreground: 40 20% 98%;
|
||||
|
||||
--matter: 32 95% 52%;
|
||||
--matter-foreground: 220 20% 8%;
|
||||
|
||||
--agent: 162 72% 38%;
|
||||
--agent-foreground: 0 0% 100%;
|
||||
|
||||
--secondary: 40 12% 92%;
|
||||
--secondary-foreground: 220 20% 8%;
|
||||
|
||||
--muted: 40 10% 93%;
|
||||
--muted-foreground: 220 8% 42%;
|
||||
|
||||
--accent: 228 30% 94%;
|
||||
--accent-foreground: 228 45% 22%;
|
||||
|
||||
--border: 40 8% 86%;
|
||||
--input: 40 8% 86%;
|
||||
--ring: 32 95% 52%;
|
||||
|
||||
--hero: 240 6% 4%;
|
||||
--hero-foreground: 40 15% 96%;
|
||||
--hero-muted: 240 4% 55%;
|
||||
--hero-border: 240 4% 14%;
|
||||
|
||||
--radius: 0.375rem;
|
||||
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
.marketing-theme .font-display {
|
||||
font-family: var(--font-display), system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.marketing-theme .font-marketing {
|
||||
font-family: var(--font-marketing-sans), system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.marketing-theme .font-mono-brand {
|
||||
font-family: var(--font-mono-brand), ui-monospace, monospace;
|
||||
}
|
||||
|
||||
/* Dot field (React Bits–style ambient grid) */
|
||||
.marketing-dot-field {
|
||||
background-image: radial-gradient(
|
||||
circle at center,
|
||||
hsl(var(--hero-foreground) / 0.12) 1px,
|
||||
transparent 1px
|
||||
);
|
||||
background-size: 28px 28px;
|
||||
mask-image: radial-gradient(ellipse 80% 70% at 50% 40%, black 20%, transparent 75%);
|
||||
}
|
||||
|
||||
.marketing-scanline {
|
||||
background: repeating-linear-gradient(
|
||||
0deg,
|
||||
transparent,
|
||||
transparent 2px,
|
||||
hsl(var(--hero-foreground) / 0.02) 2px,
|
||||
hsl(var(--hero-foreground) / 0.02) 4px
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Border glow card (React Bits Border Glow–inspired) */
|
||||
.marketing-glow-card {
|
||||
position: relative;
|
||||
background: hsl(var(--card));
|
||||
transition: box-shadow 200ms ease, border-color 200ms ease;
|
||||
}
|
||||
|
||||
.marketing-glow-card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -1px;
|
||||
border-radius: inherit;
|
||||
padding: 1px;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
hsl(var(--matter) / 0),
|
||||
hsl(var(--matter) / 0.35),
|
||||
hsl(var(--agent) / 0.2),
|
||||
hsl(var(--matter) / 0)
|
||||
);
|
||||
-webkit-mask:
|
||||
linear-gradient(#fff 0 0) content-box,
|
||||
linear-gradient(#fff 0 0);
|
||||
mask:
|
||||
linear-gradient(#fff 0 0) content-box,
|
||||
linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
opacity: 0;
|
||||
transition: opacity 200ms ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.marketing-glow-card:hover::before,
|
||||
.marketing-glow-card:focus-within::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.marketing-glow-card:hover {
|
||||
box-shadow:
|
||||
0 0 0 1px hsl(var(--border)),
|
||||
0 12px 40px -12px hsl(var(--foreground) / 0.12);
|
||||
}
|
||||
|
||||
@keyframes marketing-blink {
|
||||
0%,
|
||||
49% {
|
||||
opacity: 1;
|
||||
}
|
||||
50%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.marketing-cursor-blink {
|
||||
animation: marketing-blink 1.1s step-end infinite;
|
||||
}
|
||||
|
||||
@keyframes marketing-pulse-dot {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.55;
|
||||
transform: scale(0.92);
|
||||
}
|
||||
}
|
||||
|
||||
.marketing-pulse-dot {
|
||||
animation: marketing-pulse-dot 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.marketing-cursor-blink,
|
||||
.marketing-pulse-dot {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.marketing-glow-card::before {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
|
@ -55,6 +55,14 @@ const config: Config = {
|
|||
DEFAULT: "hsl(var(--teal))",
|
||||
foreground: "hsl(var(--teal-foreground))",
|
||||
},
|
||||
matter: {
|
||||
DEFAULT: "hsl(var(--matter))",
|
||||
foreground: "hsl(var(--matter-foreground, 0 0% 100%))",
|
||||
},
|
||||
agent: {
|
||||
DEFAULT: "hsl(var(--agent))",
|
||||
foreground: "hsl(var(--agent-foreground, 0 0% 100%))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
|
|
|
|||
|
|
@ -290,6 +290,9 @@ importers:
|
|||
lucide-react:
|
||||
specifier: ^0.468.0
|
||||
version: 0.468.0(react@19.2.4)
|
||||
motion:
|
||||
specifier: ^12.42.2
|
||||
version: 12.42.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
next:
|
||||
specifier: ^15.1.0
|
||||
version: 15.5.14(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
|
|
@ -3835,6 +3838,20 @@ packages:
|
|||
resolution: {integrity: sha512-PcOxmqwYCW7O2ovKRU8OoQQj2yqTfEB/yeTYk4gPid6dN5ODRfU1hXd9tTVZzax/0NkO7AxpHykvZnT1aYp/BQ==}
|
||||
engines: {node: ^14.13.1 || >=16.0.0}
|
||||
|
||||
framer-motion@12.42.2:
|
||||
resolution: {integrity: sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw==}
|
||||
peerDependencies:
|
||||
'@emotion/is-prop-valid': '*'
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
react-dom: ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@emotion/is-prop-valid':
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
|
||||
fresh@2.0.0:
|
||||
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
|
@ -4362,6 +4379,26 @@ packages:
|
|||
mlly@1.8.2:
|
||||
resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==}
|
||||
|
||||
motion-dom@12.42.2:
|
||||
resolution: {integrity: sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA==}
|
||||
|
||||
motion-utils@12.39.0:
|
||||
resolution: {integrity: sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==}
|
||||
|
||||
motion@12.42.2:
|
||||
resolution: {integrity: sha512-Atvv11yUKIid41cVrRBDVX5m8tF8kNpExRSlbpt6APClhDjtwQssgFHhQzejxw7/7YYbjHSPKBVbHo05BuJT5Q==}
|
||||
peerDependencies:
|
||||
'@emotion/is-prop-valid': '*'
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
react-dom: ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@emotion/is-prop-valid':
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
|
|
@ -8719,6 +8756,15 @@ snapshots:
|
|||
|
||||
fractional-indexing@3.2.0: {}
|
||||
|
||||
framer-motion@12.42.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||
dependencies:
|
||||
motion-dom: 12.42.2
|
||||
motion-utils: 12.39.0
|
||||
tslib: 2.8.1
|
||||
optionalDependencies:
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
|
||||
fresh@2.0.0: {}
|
||||
|
||||
fsevents@2.3.3:
|
||||
|
|
@ -9250,6 +9296,20 @@ snapshots:
|
|||
pkg-types: 1.3.1
|
||||
ufo: 1.6.3
|
||||
|
||||
motion-dom@12.42.2:
|
||||
dependencies:
|
||||
motion-utils: 12.39.0
|
||||
|
||||
motion-utils@12.39.0: {}
|
||||
|
||||
motion@12.42.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||
dependencies:
|
||||
framer-motion: 12.42.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
tslib: 2.8.1
|
||||
optionalDependencies:
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
mz@2.7.0:
|
||||
|
|
|
|||
Loading…
Reference in a new issue