feat(web): Product, How it works, Trust, Pricing and Docs pages, with a marketing e2e check

Pricing shows no plans, prices or free trial until decided. Docs search and articles without pages are shown but not linked. Disables the agent file that next dev generates.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014SdXCbrN5PtGExkpvMaMHx
This commit is contained in:
Claude
2026-10-12 15:34:50 +00:00
parent 6947113027
commit 0cff3902b2
44 changed files with 1187 additions and 76 deletions
+4 -2
View File
@@ -1,6 +1,7 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useEffect, useState } from 'react';
import { BrandMark } from './BrandMark.tsx';
import { Button } from './Button.tsx';
@@ -9,6 +10,7 @@ import { Icon } from '../ui/Icon.tsx';
export function Nav() {
const [open, setOpen] = useState(false);
const pathname = usePathname();
useEffect(() => {
if (!open) return;
@@ -29,7 +31,7 @@ export function Nav() {
<nav aria-label="Primary" className="nav-links t-body-m">
{NAV_LINKS.map((l) => (
<Link key={l.href} href={l.href}>
<Link key={l.href} href={l.href} aria-current={pathname === l.href ? 'page' : undefined}>
{l.label}
</Link>
))}
@@ -58,7 +60,7 @@ export function Nav() {
{open && (
<nav id="mobile-menu" aria-label="Mobile" className="nav-menu">
{NAV_LINKS.map((l) => (
<Link key={l.href} href={l.href} className="t-title-m" onClick={() => setOpen(false)}>
<Link key={l.href} href={l.href} className="t-title-m" aria-current={pathname === l.href ? 'page' : undefined} onClick={() => setOpen(false)}>
{l.mobileLabel ?? l.label}
</Link>
))}
+14
View File
@@ -0,0 +1,14 @@
import type { ReactNode } from 'react';
import { Footer } from './Footer.tsx';
import { Nav } from './Nav.tsx';
/** Navigation, the page, and the footer. Every marketing page is wrapped in this. */
export function Shell({ children, page = false }: { children: ReactNode; /** Inner pages use a tighter rhythm than the home page. */ page?: boolean }) {
return (
<>
<Nav />
<main className={page ? 'm-page' : undefined}>{children}</main>
<Footer />
</>
);
}
+22
View File
@@ -110,3 +110,25 @@ export const FAQ: ReadonlyArray<{ question: string; answer: string; open?: boole
'Changes are made on a copy, a backup is taken before anything is applied, and each approval is tied to the exact checks you saw. You can disconnect Wursor from your site at any time.',
},
];
/** Rows of the plan comparison (draft numbers from the design; shown only when PRICING_CONFIRMED). */
export const COMPARE_ROWS: ReadonlyArray<{ label: string; values: readonly [string, string, string] }> = [
{ label: 'Sites', values: ['1', '5', '25'] },
{ label: 'Changes per month', values: ['50', 'Unlimited', 'Unlimited'] },
{ label: 'Safe copy before every publish', values: ['Included', 'Included', 'Included'] },
{ label: 'Proof and nine checks', values: ['Included', 'Included', 'Included'] },
{ label: 'Undo history', values: ['30 days', '90 days', '1 year'] },
{ label: 'Team members', values: ['1', '5', 'Unlimited'] },
{ label: 'Client sharing', values: ['—', '—', 'Included'] },
{ label: 'Support', values: ['Email', 'Priority email', 'Priority and onboarding'] },
];
/** Docs articles. An article without an href has no page yet. */
export const ARTICLES: ReadonlyArray<{ title: string; href?: string }> = [
{ title: 'Start or connect a site' },
{ title: 'What happens when I ask for a change' },
{ title: 'How approval works', href: '/trust' },
{ title: 'Undo a published change' },
{ title: 'Invite your team' },
{ title: 'Change or cancel your plan' },
];
+33 -26
View File
@@ -3,6 +3,38 @@ import { Button } from '../Button.tsx';
import { PLAN_FEATURES, PLANS, PRICING_CONFIRMED, ROUTES } from '../content.ts';
import { SectionHeader } from './SectionHeader.tsx';
/** The three plan cards. Draft numbers: render only when pricing is confirmed. */
export function PlanCards() {
return (
<div className="m-plans">
{PLANS.map((p) => (
<div key={p.name} className={`m-plan${p.featured ? ' m-plan-featured' : ''}`}>
<div className="m-plan-head">
<h3 className="t-title-s">{p.name}</h3>
{p.featured && <span className="m-plan-tag t-label-m">Most popular</span>}
</div>
<p className="m-plan-price">
<span className="t-title-l">{p.price}</span>
<span className="t-caption">per month</span>
</p>
<p className="t-body-m m-section-sub">{p.allowance}</p>
<ul className="m-plan-features">
{PLAN_FEATURES.map((f) => (
<li key={f} className="t-body-m">
<Icon name="check" size={20} />
{f}
</li>
))}
</ul>
<Button href={ROUTES.signUp} variant={p.featured ? 'primary' : 'secondary'} className="m-plan-cta">
Choose plan
</Button>
</div>
))}
</div>
);
}
/**
* Plan names, prices and allowances show only once they are decided (PRD D-HL-8, PX-13). Until then
* this section says so and sends people to sign up or to the pricing page.
@@ -30,32 +62,7 @@ export function PricingTeaser({ confirmed = PRICING_CONFIRMED }: { confirmed?: b
title="Simple plans. No surprises."
subtitle="Hosting on Wursor Cloud is included. Change or cancel any time."
/>
<div className="m-plans">
{PLANS.map((p) => (
<div key={p.name} className={`m-plan${p.featured ? ' m-plan-featured' : ''}`}>
<div className="m-plan-head">
<h3 className="t-title-s">{p.name}</h3>
{p.featured && <span className="m-plan-tag t-label-m">Most popular</span>}
</div>
<p className="m-plan-price">
<span className="t-title-l">{p.price}</span>
<span className="t-caption">per month</span>
</p>
<p className="t-body-m m-section-sub">{p.allowance}</p>
<ul className="m-plan-features">
{PLAN_FEATURES.map((f) => (
<li key={f} className="t-body-m">
<Icon name="check" size={20} />
{f}
</li>
))}
</ul>
<Button href={ROUTES.signUp} variant={p.featured ? 'primary' : 'secondary'} className="m-plan-cta">
Choose plan
</Button>
</div>
))}
</div>
<PlanCards />
</section>
);
}
+18 -11
View File
@@ -24,6 +24,23 @@ const RULES: ReadonlyArray<{ icon: IconName; title: string; body: string }> = [
},
];
/** The four rules as cards. Used on the home page and on the Trust page. */
export function SafetyRules() {
return (
<div className="m-rules">
{RULES.map((r) => (
<div key={r.title} className="m-rule">
<span className="m-rule-icon">
<Icon name={r.icon} size={20} />
</span>
<h3 className="t-title-s">{r.title}</h3>
<p className="t-body-m m-section-sub">{r.body}</p>
</div>
))}
</div>
);
}
export function SafeByDesign() {
return (
<section className="m-section">
@@ -33,17 +50,7 @@ export function SafeByDesign() {
subtitle="Four rules Wursor can’t break, however you set it up."
mobileSubtitle="Four rules Wursor can’t break."
/>
<div className="m-rules">
{RULES.map((r) => (
<div key={r.title} className="m-rule">
<span className="m-rule-icon">
<Icon name={r.icon} size={20} />
</span>
<h3 className="t-title-s">{r.title}</h3>
<p className="t-body-m m-section-sub">{r.body}</p>
</div>
))}
</div>
<SafetyRules />
</section>
);
}
+7 -5
View File
@@ -12,22 +12,24 @@ export function Responsive({ desktop, mobile }: { desktop: ReactNode; mobile?: R
}
type Props = {
eyebrow: string;
eyebrow?: string;
title: string;
subtitle?: string;
mobileTitle?: string;
mobileSubtitle?: string;
align?: 'center' | 'left';
/** Use h1 for the page's first heading only. */
as?: 'h1' | 'h2';
};
/** Eyebrow, headline and one supporting sentence. Centred by default. */
export function SectionHeader({ eyebrow, title, subtitle, mobileTitle, mobileSubtitle, align = 'center' }: Props) {
export function SectionHeader({ eyebrow, title, subtitle, mobileTitle, mobileSubtitle, align = 'center', as: Heading = 'h2' }: Props) {
return (
<div className={`m-section-header m-align-${align}`}>
<p className="t-overline m-eyebrow">{eyebrow}</p>
<h2 className="t-display-l m-section-title">
{eyebrow && <p className="t-overline m-eyebrow">{eyebrow}</p>}
<Heading className="t-display-l m-section-title">
<Responsive desktop={title} mobile={mobileTitle} />
</h2>
</Heading>
{subtitle && (
<p className="t-body-l m-section-sub">
<Responsive desktop={subtitle} mobile={mobileSubtitle} />
+46
View File
@@ -0,0 +1,46 @@
import { Icon } from '../../ui/Icon.tsx';
import { SectionHeader } from '../home/SectionHeader.tsx';
const ALWAYS = [
'Works on a safe copy first',
'Shows you the result before it goes live',
'Runs checks on every change',
'Asks before anything is published',
'Records every step',
] as const;
const NEVER = [
'Publishes without your approval',
'Changes your live site while you are reviewing',
'Deletes content without a way back',
'Hides what it did or why',
'Keeps working after you pause it',
] as const;
function Column({ title, items, kind }: { title: string; items: readonly string[]; kind: 'always' | 'never' }) {
return (
<div className="m-boundary">
<h3 className="t-title-m">{title}</h3>
<ul aria-label={title} className="m-points">
{items.map((i) => (
<li key={i} className="t-body-l" data-kind={kind}>
<Icon name={kind === 'always' ? 'check-circle' : 'x'} size={20} />
{i}
</li>
))}
</ul>
</div>
);
}
export function AlwaysNever() {
return (
<section className="m-grid-section">
<SectionHeader eyebrow="Boundaries" title="What Wursor does, and what it never does." />
<div className="m-boundaries">
<Column title="Always" items={ALWAYS} kind="always" />
<Column title="Never" items={NEVER} kind="never" />
</div>
</section>
);
}
+41
View File
@@ -0,0 +1,41 @@
import { COMPARE_ROWS, PLANS } from '../content.ts';
import { SectionHeader } from '../home/SectionHeader.tsx';
/** Plans side by side. Shown only once pricing is decided (PRD D-HL-8, PX-13). */
export function ComparePlans() {
return (
<section className="m-grid-section">
<SectionHeader eyebrow="Compare plans" title="Everything, side by side." />
<div className="m-compare">
<table aria-label="Compare plans">
<thead>
<tr>
<th scope="col" className="t-label-m">
<span className="sr-only">Feature</span>
</th>
{PLANS.map((p) => (
<th key={p.name} scope="col" className="t-label-m">
{p.name}
</th>
))}
</tr>
</thead>
<tbody>
{COMPARE_ROWS.map((r) => (
<tr key={r.label}>
<th scope="row" className="t-body-m">
{r.label}
</th>
{r.values.map((v, i) => (
<td key={i} className="t-body-m">
{v}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</section>
);
}
+26
View File
@@ -0,0 +1,26 @@
import { Icon, type IconName } from '../../ui/Icon.tsx';
import { SectionHeader } from '../home/SectionHeader.tsx';
export type FeatureCardData = { icon: IconName; title: string; body: string };
type Props = { eyebrow?: string; title: string; subtitle?: string; cards: readonly FeatureCardData[] };
/** A heading and a grid of icon cards, three across. */
export function FeatureGrid({ eyebrow, title, subtitle, cards }: Props) {
return (
<section className="m-grid-section">
<SectionHeader {...(eyebrow ? { eyebrow } : {})} title={title} {...(subtitle ? { subtitle } : {})} />
<div className="m-featuregrid">
{cards.map((c) => (
<div key={c.title} className="m-rule">
<span className="m-rule-icon">
<Icon name={c.icon} size={20} />
</span>
<h3 className="t-title-s">{c.title}</h3>
<p className="t-body-m m-section-sub">{c.body}</p>
</div>
))}
</div>
</section>
);
}
+38
View File
@@ -0,0 +1,38 @@
import type { ReactNode } from 'react';
import { Icon } from '../../ui/Icon.tsx';
import { SectionHeader } from '../home/SectionHeader.tsx';
import { RowVisual } from './RowVisual.tsx';
type Props = {
eyebrow: string;
title: string;
subtitle: string;
bullets: readonly string[];
/** The card shown on the backdrop. */
children: ReactNode;
card?: { left: number; top: number; width: number; height?: number };
/** Put the picture before the text. */
reverse?: boolean;
};
/** Text with a short list on one side and a picture on the other. */
export function FeatureRow({ eyebrow, title, subtitle, bullets, children, card, reverse = false }: Props) {
return (
<section className={`m-row${reverse ? ' m-row-reverse' : ''}`}>
<div className="m-row-text">
<SectionHeader align="left" eyebrow={eyebrow} title={title} subtitle={subtitle} />
{bullets.length > 0 && (
<ul className="m-points">
{bullets.map((b) => (
<li key={b} className="t-body-l">
<Icon name="check-circle" size={20} />
{b}
</li>
))}
</ul>
)}
</div>
<RowVisual {...(card ? { card } : {})}>{children}</RowVisual>
</section>
);
}
+14
View File
@@ -0,0 +1,14 @@
import type { ReactNode } from 'react';
import { SectionHeader } from '../home/SectionHeader.tsx';
type Props = { eyebrow: string; title: string; subtitle?: string; children?: ReactNode };
/** The top of a marketing page: eyebrow, the page heading, one supporting sentence and the page's actions. */
export function PageHero({ eyebrow, title, subtitle, children }: Props) {
return (
<section className="m-pagehero">
<SectionHeader as="h1" eyebrow={eyebrow} title={title} {...(subtitle ? { subtitle } : {})} />
{children && <div className="m-hero-actions">{children}</div>}
</section>
);
}
@@ -0,0 +1,21 @@
import Link from 'next/link';
import { Icon } from '../../ui/Icon.tsx';
import { ARTICLES } from '../content.ts';
import { SectionHeader } from '../home/SectionHeader.tsx';
/** Docs articles. Only those that have a page are links; the others are shown as plain rows until they are written. */
export function PopularArticles() {
return (
<section className="m-grid-section">
<SectionHeader title="Popular articles" />
<ul className="m-articles">
{ARTICLES.map((a) => (
<li key={a.title} className="t-body-m">
{a.href ? <Link href={a.href}>{a.title}</Link> : <span>{a.title}</span>}
<Icon name="chevron-right" size={20} />
</li>
))}
</ul>
</section>
);
}
@@ -0,0 +1,44 @@
import { Button } from '../Button.tsx';
import { PRICING_CONFIRMED, ROUTES } from '../content.ts';
import { Faq } from '../home/Faq.tsx';
import { ClosingCta } from '../home/ClosingCta.tsx';
import { PlanCards } from '../home/PricingTeaser.tsx';
import { ComparePlans } from './ComparePlans.tsx';
import { PageHero } from './PageHero.tsx';
/**
* The pricing page. Plans, prices, allowances and the free trial are shown only once they are decided
* (PRD D-HL-8, PX-13). Until then the page says so and still leads to sign-up.
*/
export function PricingContent({ confirmed = PRICING_CONFIRMED }: { confirmed?: boolean }) {
return (
<>
<PageHero
eyebrow="Pricing"
title="Simple plans. No surprises."
subtitle={
confirmed
? 'Start free for 14 days. Hosting on Wursor Cloud is included. Change or cancel any time.'
: 'Plans and prices will be published before launch.'
}
>
{!confirmed && (
<Button href={ROUTES.signUp} size="lg">
Get started
</Button>
)}
</PageHero>
{confirmed && (
<>
<section className="m-grid-section">
<PlanCards />
<p className="t-caption m-draft-note">Draft pricing. Final plans and prices will be confirmed before launch.</p>
</section>
<ComparePlans />
</>
)}
<Faq />
<ClosingCta />
</>
);
}
+26
View File
@@ -0,0 +1,26 @@
import type { CSSProperties, ReactNode } from 'react';
import { Fit } from '../../ui/Fit.tsx';
type Props = {
children: ReactNode;
/** Where the card sits in the 600 × 480 picture. */
card?: { left: number; top: number; width: number; height?: number };
};
/** A 600 × 480 backdrop with one card on it, scaled to the column. Decorative: hidden from assistive technology. */
export function RowVisual({ children, card = { left: 70, top: 95, width: 460 } }: Props) {
const style: CSSProperties = { left: card.left, top: card.top, width: card.width, ...(card.height ? { height: card.height } : {}) };
return (
<div className="m-rowvisual" aria-hidden="true" inert>
<Fit designWidth={600} designHeight={480}>
<div className="m-rowscene">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img className="m-rowbackdrop" src="/marketing/backdrop.svg" alt="" />
<div className="m-rowcard" style={style}>
{children}
</div>
</div>
</Fit>
</div>
);
}
+16
View File
@@ -0,0 +1,16 @@
import type { SetupStepData } from '../../product/SetupChecklist.tsx';
/** The example first-run checklist shown on the marketing pages (Harbor Bakery). */
export const SETUP_STEPS: readonly SetupStepData[] = [
{ state: 'done', title: 'Site address added', detail: 'harborbakery.com' },
{ state: 'done', title: 'Connection verified', detail: 'Secure, read and write' },
{ state: 'next', title: 'Safe copy ready', detail: 'Wursor is creating it' },
{ state: 'todo', title: 'Make your first change', detail: 'Pick a suggestion' },
];
export const ACTIVITY_ROWS = [
{ kind: 'published', text: 'Published the Contact page', time: 'Just now' },
{ kind: 'check', text: 'Checked 9 things, all passed', time: '2 min ago' },
{ kind: 'change', text: 'Changed the Contact page', time: '3 min ago' },
{ kind: 'safe-copy', text: 'Made a safe copy of your site', time: '5 min ago' },
] as const;
+25
View File
@@ -0,0 +1,25 @@
import { Icon, type IconName } from '../ui/Icon.tsx';
export type ActivityKind = 'published' | 'check' | 'change' | 'safe-copy';
export type ActivityRowData = { kind: ActivityKind; text: string; time: string };
const ICON: Record<ActivityKind, IconName> = { published: 'rocket', check: 'shield-check', change: 'pencil', 'safe-copy': 'camera' };
/** A short, plain-words record of what happened to a site, newest first. */
export function ActivityCard({ rows }: { rows: readonly ActivityRowData[] }) {
return (
<div className="p-card">
<ul aria-label="Activity" className="p-activity">
{rows.map((r) => (
<li key={r.text} className="p-activityrow" data-kind={r.kind}>
<span className="p-activity-icon">
<Icon name={ICON[r.kind]} size={20} />
</span>
<span className="p-activity-text">{r.text}</span>
<span className="p-activity-time">{r.time}</span>
</li>
))}
</ul>
</div>
);
}
+6
View File
@@ -0,0 +1,6 @@
import type { ReactNode } from 'react';
/** Keyboard key cap for shortcuts. */
export function Kbd({ children }: { children: ReactNode }) {
return <kbd className="p-kbd">{children}</kbd>;
}
+21
View File
@@ -0,0 +1,21 @@
import { Icon } from '../ui/Icon.tsx';
import { Kbd } from './Kbd.tsx';
type Props = {
/** The accessible name. The placeholder is not a label. */
label: string;
shortcut?: string;
placeholder?: string;
disabled?: boolean;
};
/** Pill search field with a shortcut hint. Used in the top bar, the command palette and the docs. */
export function SearchField({ label, shortcut, placeholder = 'Search or ask…', disabled = false }: Props) {
return (
<div className={`p-search${disabled ? ' p-search-disabled' : ''}`}>
<Icon name="search" size={16} />
<input type="search" aria-label={label} placeholder={placeholder} disabled={disabled} className="p-search-input" />
{shortcut && <Kbd>{shortcut}</Kbd>}
</div>
);
}
+35
View File
@@ -0,0 +1,35 @@
import { Icon } from '../ui/Icon.tsx';
import { Button } from './Button.tsx';
export type SetupStepData = { state: 'done' | 'next' | 'todo'; title: string; detail: string };
const STATE_LABEL = { done: 'Done', next: 'Next', todo: 'To do' } as const;
type Props = { steps: readonly SetupStepData[]; onContinue?: (() => void) | undefined };
/** The first-run checklist: what is done, what is next (with Continue), and what is still to do. */
export function SetupChecklist({ steps, onContinue }: Props) {
return (
<div className="p-card">
<ol aria-label="Setup steps" className="p-setup">
{steps.map((s) => (
<li key={s.title} className="p-setupstep" data-state={s.state} aria-current={s.state === 'next' ? 'step' : undefined}>
<span className="p-setup-marker" aria-hidden="true">
{s.state === 'done' && <Icon name="check" size={20} />}
</span>
<span className="p-setup-text">
<span className="sr-only">{STATE_LABEL[s.state]}</span>
<span className="p-setup-title">{s.title}</span>
<span className="p-setup-detail">{s.detail}</span>
</span>
{s.state === 'next' && (
<Button variant="primary" onClick={onContinue}>
Continue
</Button>
)}
</li>
))}
</ol>
</div>
);
}
+4
View File
@@ -1,4 +1,5 @@
export { AgentCursor } from './AgentCursor.tsx';
export { ActivityCard, type ActivityRowData } from './ActivityCard.tsx';
export { ApprovalBar } from './ApprovalBar.tsx';
export { ApprovalCard } from './ApprovalCard.tsx';
export { Avatar } from './Avatar.tsx';
@@ -15,6 +16,7 @@ export { DeviceFrame } from './DeviceFrame.tsx';
export { HARBOR } from './harbor-bakery.ts';
export { HarborStream } from './HarborStream.tsx';
export { IconButton } from './IconButton.tsx';
export { Kbd } from './Kbd.tsx';
export { Message } from './Message.tsx';
export { Milestone } from './Milestone.tsx';
export { HarborConversation } from './HarborConversation.tsx';
@@ -23,6 +25,8 @@ export { PreviewColumn } from './PreviewColumn.tsx';
export { PreviewToolbar, type PreviewView } from './PreviewToolbar.tsx';
export { RiskChip } from './RiskChip.tsx';
export { SegmentedControl } from './SegmentedControl.tsx';
export { SearchField } from './SearchField.tsx';
export { SetupChecklist, type SetupStepData } from './SetupChecklist.tsx';
export { Sidebar } from './Sidebar.tsx';
export { SiteMock } from './SiteMock.tsx';
export { StatusPill, type Status } from './StatusPill.tsx';
+53
View File
@@ -298,3 +298,56 @@ main { display: block; }
.m-cta-sub { display: none; }
.footer { margin-top: 72px; }
}
/* ---------- Inner pages ---------- */
.m-pagehero { display: flex; flex-direction: column; align-items: center; gap: 28px; max-width: 1200px; margin: 0 auto; padding: 72px var(--space-6) 0; text-align: center; }
.m-pagehero .m-hero-actions { align-items: center; }
.m-page .m-section, .m-page .m-cta, .m-page .m-row, .m-page .m-grid-section { margin-top: 140px; }
.m-page .m-grid-first { margin-top: 64px; }
.m-page .m-grid-tight { margin-top: 72px; }
.m-page .footer { margin-top: 140px; }
.m-grid-section { display: flex; flex-direction: column; align-items: center; gap: 48px; width: 100%; max-width: 1248px; margin: 0 auto; padding: 0 var(--space-6); }
.m-featuregrid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-6); width: 100%; }
.m-featuregrid .m-rule h3 { color: var(--color-text-primary); }
.m-docs-search { width: 560px; max-width: 100%; }
.m-row { display: flex; align-items: center; gap: 96px; max-width: 1248px; margin: 0 auto; padding: 0 var(--space-6); }
.m-row-reverse { flex-direction: row-reverse; }
.m-row-text { display: flex; flex: 1 0 0; min-width: 0; flex-direction: column; align-items: flex-start; gap: 20px; }
.m-rowvisual { position: relative; flex: none; width: 600px; max-width: 100%; overflow: hidden; border-radius: var(--radius-xl); text-align: left; }
.m-rowscene { position: relative; width: 600px; height: 480px; overflow: hidden; }
.m-rowbackdrop { position: absolute; top: -120px; left: -372px; display: block; width: 1344px; height: 720px; max-width: none; }
.m-rowcard { position: absolute; }
.m-convowindow { width: 448px; height: 424px; overflow: hidden; border-radius: 12.8px; background: var(--color-bg-surface); border: 1px solid var(--color-border-subtle); box-shadow: var(--shadow-modal); }
.m-boundaries { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-6); width: 100%; }
.m-boundary { display: flex; flex-direction: column; gap: 20px; padding: var(--space-8); border-radius: 20px; background: var(--color-bg-surface); border: 1px solid var(--color-border-subtle); }
.m-boundary h3 { margin: 0; color: var(--color-text-primary); }
.m-points li[data-kind="never"] .icon { color: var(--color-diff-remove-fg); }
.m-compare { width: 100%; overflow-x: auto; border-radius: 20px; background: var(--color-bg-surface); border: 1px solid var(--color-border-subtle); }
.m-compare table { width: 100%; min-width: 720px; border-collapse: collapse; }
.m-compare th, .m-compare td { padding: var(--space-4) var(--space-6); text-align: center; border-top: 1px solid var(--color-border-subtle); }
.m-compare thead th { border-top: 0; color: var(--color-text-primary); }
.m-compare tbody th { text-align: left; font-weight: 400; color: var(--color-text-primary); }
.m-compare td { width: 190px; color: var(--color-text-secondary); }
.m-draft-note { margin: 0; color: var(--color-text-tertiary); }
.m-articles { width: 760px; max-width: 100%; margin: 0; padding: 0; overflow: hidden; list-style: none; border-radius: 20px; background: var(--color-bg-surface); border: 1px solid var(--color-border-subtle); }
.m-articles li { display: flex; align-items: center; gap: var(--space-4); padding: 18px var(--space-6); color: var(--color-text-primary); }
.m-articles li + li { border-top: 1px solid var(--color-border-subtle); }
.m-articles li > :first-child { flex: 1 0 0; }
.m-articles .icon { color: var(--color-text-tertiary); }
@media (max-width: 900px) {
.m-pagehero { padding-top: var(--space-8); gap: var(--space-5); }
.m-page .m-section, .m-page .m-cta, .m-page .m-row, .m-page .m-grid-section { margin-top: 72px; }
.m-page .m-grid-first, .m-page .m-grid-tight { margin-top: 48px; }
.m-page .footer { margin-top: 72px; }
.m-grid-section { gap: var(--space-8); padding: 0 var(--space-4); }
.m-featuregrid { grid-template-columns: 1fr; }
.m-row, .m-row-reverse { flex-direction: column; align-items: stretch; gap: var(--space-8); padding: 0 var(--space-4); }
.m-rowvisual { width: 100%; border-radius: 28px; }
.m-boundaries { grid-template-columns: 1fr; }
.m-boundary { padding: var(--space-6); }
}
+25
View File
@@ -241,3 +241,28 @@ button:where([class^="p-"]):focus-visible { outline: 2px solid var(--color-accen
.p-approval-text { display: flex; flex: 1 0 0; min-width: 0; flex-direction: column; }
.p-approval-title { font: 500 14px/20px var(--font-sans); color: var(--color-text-primary); }
.p-approval-meta { font: 400 12px/16px var(--font-sans); color: var(--color-text-tertiary); }
/* ---------- Cards, setup checklist, activity, search ---------- */
.sr-only { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
.p-card { display: flex; flex-direction: column; padding: var(--space-2); overflow: hidden; border-radius: 20px; background: var(--color-bg-surface); border: 1px solid var(--color-border-subtle); text-align: left; font-family: var(--font-sans); }
.p-setup, .p-activity { display: flex; flex-direction: column; margin: 0; padding: 0; list-style: none; }
.p-setupstep { display: flex; align-items: flex-start; gap: var(--space-3); padding: var(--space-3) var(--space-4); }
.p-setup-marker { display: inline-flex; flex: none; align-items: center; justify-content: center; width: 24px; height: 24px; box-sizing: border-box; border-radius: var(--radius-full); }
.p-setupstep[data-state="done"] .p-setup-marker { background: var(--color-status-success-dot); color: var(--color-text-on-accent); }
.p-setupstep[data-state="next"] .p-setup-marker { border: 2px solid var(--color-accent-default); }
.p-setupstep[data-state="todo"] .p-setup-marker { border: 1.5px solid var(--color-border-default); }
.p-setup-text { display: flex; flex: 1 0 0; min-width: 0; flex-direction: column; gap: var(--space-1); font: 400 14px/20px var(--font-sans); }
.p-setup-title { font-weight: 500; color: var(--color-text-primary); }
.p-setup-detail { color: var(--color-text-secondary); }
.p-setupstep[data-state="todo"] .p-setup-title, .p-setupstep[data-state="todo"] .p-setup-detail { color: var(--color-text-secondary); }
.p-activityrow { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4); }
.p-activity-icon { display: inline-flex; flex: none; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: var(--radius-full); background: var(--color-bg-sunken); color: var(--color-text-secondary); }
.p-activityrow[data-kind="published"] .p-activity-icon, .p-activityrow[data-kind="check"] .p-activity-icon { color: var(--color-status-success-fg); }
.p-activity-text { flex: 1 0 0; min-width: 0; font: 400 14px/20px var(--font-sans); color: var(--color-text-primary); }
.p-activity-time { flex: none; font: 400 12px/16px var(--font-sans); color: var(--color-text-tertiary); white-space: nowrap; }
.p-kbd { display: inline-flex; flex: none; align-items: center; justify-content: center; height: 20px; padding: 0 var(--space-1); border-radius: var(--radius-xs); background: var(--color-bg-sunken); border: 1px solid var(--color-border-default); font: 400 12px/16px var(--font-mono); color: var(--color-text-secondary); }
.p-search { display: flex; align-items: center; gap: var(--space-2); width: 100%; height: var(--size-control-md); padding: 0 var(--space-2) 0 var(--space-3); border-radius: var(--radius-full); background: var(--color-bg-sunken); border: 1px solid var(--color-border-subtle); color: var(--color-text-secondary); }
.p-search-input { flex: 1 0 0; min-width: 0; padding: 0; border: 0; outline: 0; background: transparent; color: var(--color-text-primary); font: 400 14px/20px var(--font-sans); }
.p-search-input::placeholder { color: var(--color-text-secondary); opacity: 1; }
.p-search:focus-within { outline: 2px solid var(--color-accent-default); outline-offset: 2px; }
.p-search-disabled { opacity: 0.7; }
+4 -4
View File
@@ -1,10 +1,10 @@
import type { CSSProperties } from 'react';
export type IconName =
| 'activity' | 'arrow-right' | 'bell' | 'board' | 'check' | 'check-circle' | 'chevron-down' | 'chevron-right' | 'chevron-up' | 'clock' | 'close' | 'dots'
| 'eye' | 'globe' | 'home' | 'list' | 'loader' | 'lock' | 'menu' | 'mic' | 'monitor' | 'paperclip'
| 'pencil' | 'plus' | 'pointer' | 'refresh' | 'search' | 'shield-check' | 'sidebar' | 'sliders'
| 'shield' | 'smartphone' | 'sparkles' | 'split' | 'stop' | 'undo';
| 'activity' | 'arrow-right' | 'bell' | 'board' | 'check' | 'camera' | 'check-circle' | 'chevron-down' | 'chevron-right' | 'chevron-up' | 'clock' | 'close' | 'credit-card' | 'dots'
| 'eye' | 'globe' | 'help' | 'home' | 'key' | 'list' | 'loader' | 'lock' | 'menu' | 'mic' | 'monitor' | 'paperclip'
| 'pencil' | 'plug' | 'plus' | 'pointer' | 'refresh' | 'rocket' | 'search' | 'shield-check' | 'sidebar' | 'sliders'
| 'shield' | 'smartphone' | 'sparkles' | 'split' | 'stop' | 'undo' | 'user' | 'x';
type Props = { name: IconName; size?: number; className?: string };