Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/marketing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ Two deliberate exclusions:

Icons reached through a `:name` binding cannot be found by the icon scanner. When a new icon comes
from `config/content.ts`, add its list to `icon.clientBundle.icons` in `nuxt.config.ts` the way
`featureCards` and `logoCloud` already are, or it will silently render as an empty box.
`logoCloud` already is, or it will silently render as an empty box.

## Adding or editing a legal document

Expand Down
1 change: 0 additions & 1 deletion apps/marketing/app/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
<HomeHero />
<HomeLogoCloud />
<HomeFeatureGrid />
<HomeTestimonials />
<HomePricingTable />
<HomeFaq />
<HomeCallToAction />
Expand Down
32 changes: 9 additions & 23 deletions apps/marketing/config/content.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* Structure of the landing page: which cards exist, in which order, and which translation key and
* icon each one carries.
* Structure of the landing page: which cards exist, in which order, and which translation key
* each one carries.
*
* Copy itself lives in `@code-zero/i18n` (`locales/<locale>/marketing.json`) so translators work
* from one place and Lunaria can report staleness. This file only holds what is not language —
Expand All @@ -11,15 +11,7 @@
export interface FeatureCard {
/** Key under `marketing.features.items`; also the Vue list key. */
readonly id: string;
/** Lucide icon name resolved by `@nuxt/icon` from the locally installed collection. */
readonly icon: string;
}

export interface Testimonial {
/** Key under `marketing.testimonials.items`. */
readonly id: string;
/** Initials rendered in the avatar tile; no remote images on a page that must stay fast. */
readonly initials: string;
readonly layout: 'lead' | 'standard' | 'wide';
}

export interface FaqEntry {
Expand Down Expand Up @@ -48,20 +40,14 @@ export interface PricingPlan {
export const pricingCurrency = 'USD';

export const featureCards = [
{ id: 'observe', icon: 'lucide:eye' },
{ id: 'runner', icon: 'lucide:shield-check' },
{ id: 'verify', icon: 'lucide:circle-check-big' },
{ id: 'models', icon: 'lucide:cpu' },
{ id: 'sandbox', icon: 'lucide:box' },
{ id: 'policy', icon: 'lucide:scale' },
{ id: 'observe', layout: 'lead' },
{ id: 'runner', layout: 'standard' },
{ id: 'verify', layout: 'standard' },
{ id: 'models', layout: 'standard' },
{ id: 'sandbox', layout: 'standard' },
{ id: 'policy', layout: 'wide' },
] as const satisfies readonly FeatureCard[];

export const testimonials = [
{ id: 'one', initials: 'MC' },
{ id: 'two', initials: 'JW' },
{ id: 'three', initials: 'AO' },
] as const satisfies readonly Testimonial[];

export const faqEntries = [
{ id: 'write' },
{ id: 'models' },
Expand Down
2 changes: 1 addition & 1 deletion apps/marketing/content.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const legalSchema = z.object({
/**
* Frontmatter every blog post declares. `date` is the publication date (ISO 8601) the listing
* sorts by; `tag` is a single lowercase category the listing can filter on; `authorInitials`
* feeds the same initials-tile avatar the testimonials use — this site ships no portrait images.
* feeds the initials-tile avatar — this site ships no portrait images.
*/
const blogSchema = z.object({
title: z.string(),
Expand Down
16 changes: 10 additions & 6 deletions apps/marketing/modules/home/components/CallToAction.vue
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
<template>
<section class="section-y">
<div class="shell">
<div class="panel border-accent/35 bg-accent/4 px-6 py-12 text-center sm:px-12">
<h2 class="m-0 text-headline font-750 tracking-tight">{{ $t('marketing.cta.title') }}</h2>
<p class="mx-auto m-0 mt-4 max-w-2xl lede">{{ $t('marketing.cta.subtitle') }}</p>
<div class="mt-8 flex flex-wrap items-center justify-center gap-3">
<a class="btn btn-accent" :href="dashboardUrl">
<div
class="panel grid gap-8 border-accent/35 px-6 py-10 sm:px-10 lg:grid-cols-[1fr_auto] lg:items-center lg:gap-12 lg:py-14"
>
<div>
<h2 class="m-0 text-headline font-750 tracking-tight">{{ $t('marketing.cta.title') }}</h2>
<p class="m-0 mt-4 max-w-xl lede">{{ $t('marketing.cta.subtitle') }}</p>
</div>
<div class="flex flex-col gap-3 sm:flex-row lg:flex-col">
<a class="btn btn-accent h-11 text-base" :href="dashboardUrl">
{{ $t('marketing.cta.primary') }}
<Icon aria-hidden="true" class="h-4 w-4 rtl:scale-x-[-1]" name="lucide:arrow-right" />
</a>
<NuxtLink class="btn btn-subtle" :to="localePath({ path: '/contact' })">
<NuxtLink class="btn btn-subtle h-11 text-base" :to="localePath({ path: '/contact' })">
{{ $t('marketing.cta.secondary') }}
</NuxtLink>
</div>
Expand Down
37 changes: 24 additions & 13 deletions apps/marketing/modules/home/components/FeatureGrid.vue
Original file line number Diff line number Diff line change
@@ -1,25 +1,36 @@
<template>
<section id="features" class="section-y scroll-mt-20">
<section id="features" class="section-y scroll-mt-24">
<div class="shell">
<div class="max-w-2xl">
<h2 class="m-0 text-headline font-750 tracking-tight">
<div class="grid gap-6 lg:grid-cols-2 lg:gap-16">
<h2 class="m-0 max-w-xl text-headline font-750 tracking-tight">
{{ $t('marketing.features.title') }}
</h2>
<p class="m-0 mt-4 lede">{{ $t('marketing.features.subtitle') }}</p>
<p class="m-0 max-w-xl lede lg:self-end">{{ $t('marketing.features.subtitle') }}</p>
</div>

<ul class="m-0 mt-12 grid list-none gap-4 ps-0 sm:grid-cols-2 lg:grid-cols-3">
<li v-for="feature in featureCards" :key="feature.id" class="card">
<span
class="h-9 w-9 grid place-items-center border border-accent/45 bg-accent/8 text-accent"
aria-hidden="true"
<ul class="m-0 mt-12 grid list-none gap-x-10 ps-0 sm:grid-cols-2">
<li
v-for="feature in featureCards"
:key="feature.id"
class="border-t border-line py-8"
:class="
feature.layout === 'lead'
? 'col-span-full grid gap-4 bg-panel px-6 sm:grid-cols-2 sm:gap-10 sm:px-8'
: feature.layout === 'wide'
? 'sm:col-span-full'
: undefined
"
>
<h3
class="m-0 text-lg font-700"
:class="feature.layout === 'lead' ? 'text-accent' : undefined"
>
<Icon class="h-4.5 w-4.5" :name="feature.icon" />
</span>
<h3 class="m-0 mt-4 text-base font-700">
{{ $t(`marketing.features.items.${feature.id}.title`) }}
</h3>
<p class="m-0 mt-2 text-sm text-muted leading-relaxed">
<p
class="m-0 max-w-2xl text-base text-muted leading-relaxed"
:class="feature.layout === 'lead' ? undefined : 'mt-3'"
>
{{ $t(`marketing.features.items.${feature.id}.description`) }}
</p>
</li>
Expand Down
64 changes: 36 additions & 28 deletions apps/marketing/modules/home/components/Hero.vue
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
<template>
<section class="section-y">
<div class="shell grid items-center gap-12 lg:grid-cols-[1.05fr_1fr] lg:gap-16">
<div>
<p class="m-0 eyebrow">{{ $t('marketing.hero.eyebrow') }}</p>
<h1 class="m-0 mt-5 text-display font-750 tracking-tight">
<section class="py-16 sm:py-24 lg:py-28">
<div class="shell grid items-center gap-12 lg:grid-cols-[1.1fr_1fr] lg:gap-12">
<div class="min-w-0">
<p class="m-0 text-sm text-accent font-650">{{ $t('marketing.hero.eyebrow') }}</p>
<h1 class="m-0 mt-6 max-w-2xl text-display font-750 tracking-tight">
{{ $t('marketing.hero.title') }}
</h1>
<p class="m-0 mt-6 max-w-xl lede">{{ $t('marketing.hero.subtitle') }}</p>

<div class="mt-8 flex flex-wrap items-center gap-3">
<NuxtLink class="btn btn-accent" :to="localePath({ path: '/pricing' })">
<div class="mt-8 flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center">
<NuxtLink class="btn btn-accent h-11 text-base" :to="localePath({ path: '/pricing' })">
{{ $t('marketing.hero.primaryCta') }}
<Icon aria-hidden="true" class="h-4 w-4 rtl:scale-x-[-1]" name="lucide:arrow-right" />
</NuxtLink>
<NuxtLink
class="btn btn-subtle"
class="btn btn-subtle h-11 text-base"
:to="links.docs"
external
target="_blank"
Expand All @@ -24,27 +24,35 @@
</NuxtLink>
</div>

<p class="m-0 mt-5 flex items-center gap-2 text-xs text-muted">
<Icon aria-hidden="true" class="h-3.5 w-3.5 text-accent" name="lucide:shield-check" />
<p class="m-0 mt-5 text-sm text-muted">
{{ $t('marketing.hero.note') }}
</p>
</div>

<figure class="m-0">
<div class="panel overflow-hidden">
<div class="h-9 flex items-center gap-2 border-b border-line px-3.5">
<span class="h-2 w-2 rounded-full bg-danger/70" aria-hidden="true" />
<span class="h-2 w-2 rounded-full bg-warning/70" aria-hidden="true" />
<span class="h-2 w-2 rounded-full bg-accent/70" aria-hidden="true" />
<span class="ms-2 mono text-muted">{{ $t('marketing.hero.terminalTitle') }}</span>
<figure class="m-0 min-w-0">
<div class="panel overflow-hidden border-muted/45">
<div class="min-h-12 flex items-center border-b border-line bg-raised px-5">
<span class="text-sm text-muted font-650">{{
$t('marketing.hero.terminalTitle')
}}</span>
</div>
<!-- A transcript rather than a screenshot: it stays legible in both themes, needs no
image request on the critical path, and is readable by a screen reader. -->
<pre
class="m-0 overflow-x-auto p-4 mono text-ink leading-relaxed"
><code>{{ transcript }}</code></pre>
<div
class="focus-ring overflow-x-auto p-5 sm:p-6"
role="region"
:aria-label="$t('marketing.hero.terminalTitle')"
tabindex="0"
>
<p class="m-0 whitespace-nowrap font-mono text-sm text-accent">
$ zero review --proactive
</p>
<pre
class="m-0 mt-6 font-mono text-sm text-ink leading-relaxed"
><code>{{ transcript }}</code></pre>
</div>
</div>
<figcaption class="mt-3 text-xs text-muted">
<figcaption class="mt-4 text-sm text-muted leading-relaxed">
{{ $t('marketing.hero.terminalCaption') }}
</figcaption>
</figure>
Expand All @@ -57,13 +65,13 @@ const localePath = useLocalePath();

// Static sample output, not a live run: the marketing site holds no credentials and never calls
// the runtime. Kept verbatim rather than translated because it is program output, not prose.
const transcript = `$ zero review --mode observe
→ mode observe (read-only)
→ provider anthropic/claude-opus-5
→ checkout /srv/checkouts/acme-web@4f19c2a
const transcript = `| Property | Value |
| --- | --- |
| Mode | \`observe\` |

✔ reproduced src/user.ts:42 possible null dereference
✔ verified pnpm test -- user.spec.ts (14 passed)
### Finding
No finding was produced.

2 findings · 0 writes · run 3a91f0`;
### Checks
No repository-native checks were executed.`;
</script>
14 changes: 7 additions & 7 deletions apps/marketing/modules/home/components/LogoCloud.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<template>
<section class="border-y border-line bg-panel/50 py-10">
<div class="shell">
<h2 class="m-0 text-center eyebrow">{{ $t('marketing.logos.title') }}</h2>
<ul
class="m-0 mt-6 list-none flex flex-wrap items-center justify-center gap-x-10 gap-y-5 ps-0"
>
<section class="border-y border-line bg-panel/70 py-8">
<div class="shell grid gap-6 lg:grid-cols-[1fr_2fr] lg:items-center lg:gap-12">
<h2 class="m-0 text-base text-muted font-500 leading-relaxed">
{{ $t('marketing.logos.title') }}
</h2>
<ul class="m-0 list-none flex flex-wrap items-center gap-x-8 gap-y-4 ps-0 lg:justify-between">
<li
v-for="entry in logoCloud"
:key="entry.id"
class="flex items-center gap-2 text-sm text-muted font-650"
class="flex items-center gap-2 text-base text-ink font-650"
>
<Icon aria-hidden="true" class="h-4.5 w-4.5" :name="entry.icon" />
{{ entry.label }}
Expand Down
40 changes: 0 additions & 40 deletions apps/marketing/modules/home/components/Testimonials.vue

This file was deleted.

17 changes: 9 additions & 8 deletions apps/marketing/modules/shared/components/site/Header.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<template>
<header class="sticky top-0 z-40 border-b border-line bg-canvas/85 backdrop-blur">
<div class="shell h-16 flex items-center gap-4">
<div class="shell h-16 flex items-center gap-2 sm:gap-4">
<NuxtLink
class="focus-ring flex shrink-0 items-center gap-2.5"
class="focus-ring min-h-11 flex shrink-0 items-center gap-2.5"
:to="localePath({ path: '/' })"
:aria-label="site.name"
>
Expand All @@ -14,7 +14,7 @@
<NuxtLink
v-for="link in navigation"
:key="link.id"
class="focus-ring px-2.5 py-2 text-xs text-muted font-600 transition hover:text-ink"
class="focus-ring min-h-11 inline-flex items-center px-2.5 text-sm text-muted font-600 transition hover:text-ink"
:to="link.to"
:external="link.external"
:target="link.external ? '_blank' : undefined"
Expand All @@ -24,15 +24,15 @@
</NuxtLink>
</nav>

<div class="ms-auto flex items-center gap-2">
<div class="ms-auto flex items-center gap-1 sm:gap-2">
<ClientOnly>
<ColorModeToggle />
<template #fallback>
<span class="h-9 w-9" aria-hidden="true" />
<span class="h-11 w-11" aria-hidden="true" />
</template>
</ClientOnly>
<LocaleSwitcher class="hidden sm:block" />
<a class="btn btn-accent btn-sm" :href="dashboardUrl">
<a class="btn btn-accent px-3 sm:px-5" :href="dashboardUrl">
{{ $t('marketing.nav.signIn') }}
</a>
<button
Expand All @@ -54,14 +54,15 @@
:aria-label="$t('marketing.nav.aria')"
class="border-t border-line bg-panel lg:hidden"
>
<ul class="shell m-0 list-none flex flex-col gap-1 py-3 ps-0">
<ul class="shell m-0 list-none flex flex-col gap-1 py-4">
<li v-for="link in navigation" :key="link.id">
<NuxtLink
class="focus-ring block px-1 py-2.5 text-sm text-muted font-600 transition hover:text-ink"
class="focus-ring min-h-11 flex items-center px-2 text-base text-muted font-600 transition hover:text-ink"
:to="link.to"
:external="link.external"
:target="link.external ? '_blank' : undefined"
:rel="link.external ? 'noreferrer' : undefined"
@click="menuOpen = false"
>
{{ $t(`marketing.nav.${link.id}`) }}
</NuxtLink>
Expand Down
4 changes: 2 additions & 2 deletions apps/marketing/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import process from 'node:process';
import { defaultLocale, i18nLocalesFor, localeCookieName } from '@code-zero/i18n';
import { defineNuxtConfig } from 'nuxt/config';

import { featureCards, logoCloud } from './config/content.js';
import { logoCloud } from './config/content.js';

// Only the scopes this app renders: every listed file is deep-merged into the bundle whether or
// not a key from it is read, so the dashboard's auth/organizations copy has no business being
Expand Down Expand Up @@ -75,7 +75,7 @@ export default defineNuxtConfig({
// The scanner cannot see icons that reach `<Icon>` through a `:name` binding, so the ones
// that live in `config/content.ts` are listed from that same source. Deriving the list
// instead of retyping it means adding a card cannot silently ship a missing icon.
icons: [...featureCards.map((card) => card.icon), ...logoCloud.map((entry) => entry.icon)],
icons: logoCloud.map((entry) => entry.icon),
},
},

Expand Down
Loading
Loading