Compare commits
2 Commits
dev
...
214-websit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe0ca70ca2 | ||
|
|
54d604ff1e |
3
.github/agents/copilot-instructions.md
vendored
3
.github/agents/copilot-instructions.md
vendored
@ -208,6 +208,8 @@ ## Active Technologies
|
||||
- Markdown governance artifacts in a PHP 8.4.15 / Laravel 12 / Filament v5 / Livewire v4 repository + `.specify/memory/constitution.md`, `docs/ui/operator-ux-surface-standards.md`, adjacent Specs 196 through 199, existing UI rule IDs `UI-SURF-001`, `ACTSURF-001`, `UI-HARD-001`, `UI-EX-001`, `UI-FIL-001`, `DECIDE-001`, and `UX-001` (200-filament-surface-rules)
|
||||
- Astro 6.0.0 templates + TypeScript 5.x (explicit setup in `apps/website`) + Astro 6, Tailwind CSS v4, custom Astro component primitives (shadcn-inspired), lightweight Playwright browser smoke tests (213-website-foundation-v0)
|
||||
- Static filesystem content, styles, and assets under `apps/website/src` and `apps/website/public`; no database (213-website-foundation-v0)
|
||||
- Astro 6.0.0 templates + TypeScript 5.9 strict + Astro 6, Tailwind CSS v4 via `@tailwindcss/vite`, Astro content collections, local Astro component primitives, Playwright browser smoke tests (214-website-visual-foundation)
|
||||
- Static filesystem content, styles, assets, and content collections under `apps/website/src` and `apps/website/public`; no database (214-website-visual-foundation)
|
||||
- Markdown governance artifacts, JSON Schema plus logical OpenAPI planning contracts, and Bash-backed SpecKit scripts inside a PHP 8.4.15 / Laravel 12 / Filament v5 / Livewire v4 repository + `.specify/memory/constitution.md`, `.specify/templates/spec-template.md`, `.specify/templates/plan-template.md`, `.specify/templates/tasks-template.md`, `.specify/templates/checklist-template.md`, `.specify/README.md`, `docs/ui/operator-ux-surface-standards.md`, and Specs 196 through 200 (201-enforcement-review-guardrails)
|
||||
- Repository-owned markdown and contract artifacts under `.specify/` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/201-enforcement-review-guardrails/`; no product database persistence (201-enforcement-review-guardrails)
|
||||
|
||||
@ -244,6 +246,7 @@ ## Code Style
|
||||
PHP 8.4.15: Follow standard conventions
|
||||
|
||||
## Recent Changes
|
||||
- 214-website-visual-foundation: Added Astro 6.0.0 templates + TypeScript 5.9 strict + Astro 6, Tailwind CSS v4 via `@tailwindcss/vite`, Astro content collections, local Astro component primitives, Playwright browser smoke tests
|
||||
- 201-enforcement-review-guardrails: Added Markdown governance artifacts, JSON Schema plus logical OpenAPI planning contracts, and Bash-backed SpecKit scripts inside a PHP 8.4.15 / Laravel 12 / Filament v5 / Livewire v4 repository + `.specify/memory/constitution.md`, `.specify/templates/spec-template.md`, `.specify/templates/plan-template.md`, `.specify/templates/tasks-template.md`, `.specify/templates/checklist-template.md`, `.specify/README.md`, `docs/ui/operator-ux-surface-standards.md`, and Specs 196 through 200
|
||||
- 213-website-foundation-v0: Added Astro 6.0.0 templates + TypeScript 5.x (explicit setup in `apps/website`) + Astro 6, Tailwind CSS v4, custom Astro component primitives (shadcn-inspired), lightweight Playwright browser smoke tests
|
||||
- 200-filament-surface-rules: Added Markdown governance artifacts in a PHP 8.4.15 / Laravel 12 / Filament v5 / Livewire v4 repository + `.specify/memory/constitution.md`, `docs/ui/operator-ux-surface-standards.md`, adjacent Specs 196 through 199, existing UI rule IDs `UI-SURF-001`, `ACTSURF-001`, `UI-HARD-001`, `UI-EX-001`, `UI-FIL-001`, `DECIDE-001`, and `UX-001`
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
---
|
||||
import Eyebrow from '@/components/content/Eyebrow.astro';
|
||||
import Headline from '@/components/content/Headline.astro';
|
||||
import Lead from '@/components/content/Lead.astro';
|
||||
import SecondaryCTA from '@/components/content/SecondaryCTA.astro';
|
||||
import Card from '@/components/primitives/Card.astro';
|
||||
import type { AudienceRowContent } from '@/types/site';
|
||||
@ -11,17 +14,17 @@ const { item } = Astro.props;
|
||||
---
|
||||
|
||||
<Card class="h-full">
|
||||
<p class="m-0 text-xs font-semibold uppercase tracking-[0.16em] text-[var(--color-brand)]">
|
||||
{item.audience}
|
||||
</p>
|
||||
<h3 class="mt-4 text-3xl font-semibold tracking-[-0.03em] text-[var(--color-ink-900)]">
|
||||
<Eyebrow>{item.audience}</Eyebrow>
|
||||
<Headline as="h3" size="card" class="mt-4">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p class="mt-3 text-base leading-7 text-[var(--color-copy)]">{item.description}</p>
|
||||
</Headline>
|
||||
<Lead class="mt-3" size="body">
|
||||
{item.description}
|
||||
</Lead>
|
||||
<ul class="mt-5 space-y-3 p-0">
|
||||
{
|
||||
item.bullets.map((bullet) => (
|
||||
<li class="list-none rounded-[1rem] bg-white/70 px-4 py-3 text-sm text-[var(--color-ink-800)]">
|
||||
<li class="list-none rounded-[1rem] border border-[color:var(--color-border-subtle)] bg-white/70 px-4 py-3 text-sm text-[var(--color-ink-800)]">
|
||||
{bullet}
|
||||
</li>
|
||||
))
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
---
|
||||
import Card from '@/components/primitives/Card.astro';
|
||||
import Eyebrow from '@/components/content/Eyebrow.astro';
|
||||
import Headline from '@/components/content/Headline.astro';
|
||||
import Lead from '@/components/content/Lead.astro';
|
||||
import type { CalloutContent } from '@/types/site';
|
||||
|
||||
interface Props {
|
||||
@ -11,13 +14,11 @@ const variant = content.tone === 'accent' ? 'accent' : content.tone === 'subtle'
|
||||
---
|
||||
|
||||
<Card variant={variant}>
|
||||
{content.eyebrow && (
|
||||
<p class="m-0 text-xs font-semibold uppercase tracking-[0.16em] text-[var(--color-brand)]">
|
||||
{content.eyebrow}
|
||||
</p>
|
||||
)}
|
||||
<h3 class="mt-4 text-2xl font-semibold tracking-[-0.03em] text-[var(--color-ink-900)]">
|
||||
{content.eyebrow && <Eyebrow>{content.eyebrow}</Eyebrow>}
|
||||
<Headline as="h3" size="card" class="mt-4">
|
||||
{content.title}
|
||||
</h3>
|
||||
<p class="mt-3 text-base leading-7 text-[var(--color-copy)]">{content.description}</p>
|
||||
</Headline>
|
||||
<Lead class="mt-3" size="body">
|
||||
{content.description}
|
||||
</Lead>
|
||||
</Card>
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
---
|
||||
import Button from '@/components/primitives/Button.astro';
|
||||
import Card from '@/components/primitives/Card.astro';
|
||||
import Eyebrow from '@/components/content/Eyebrow.astro';
|
||||
import Headline from '@/components/content/Headline.astro';
|
||||
import type { CtaLink } from '@/types/site';
|
||||
|
||||
interface Props {
|
||||
@ -13,7 +15,10 @@ const { cta, points, title } = Astro.props;
|
||||
---
|
||||
|
||||
<Card variant="accent">
|
||||
<h3 class="m-0 text-3xl font-semibold tracking-[-0.03em] text-[var(--color-ink-900)]">{title}</h3>
|
||||
<Eyebrow>Qualified outreach</Eyebrow>
|
||||
<Headline as="h3" size="card" class="mt-4 text-3xl">
|
||||
{title}
|
||||
</Headline>
|
||||
<ul class="mt-5 space-y-3 p-0">
|
||||
{
|
||||
points.map((point) => (
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
---
|
||||
import Card from '@/components/primitives/Card.astro';
|
||||
import Eyebrow from '@/components/content/Eyebrow.astro';
|
||||
import Headline from '@/components/content/Headline.astro';
|
||||
import Lead from '@/components/content/Lead.astro';
|
||||
|
||||
interface Props {
|
||||
description: string;
|
||||
@ -10,9 +13,11 @@ const { description, title } = Astro.props;
|
||||
---
|
||||
|
||||
<Card>
|
||||
<p class="m-0 text-xs font-semibold uppercase tracking-[0.16em] text-[var(--color-brand)]">
|
||||
Conversation focus
|
||||
</p>
|
||||
<h3 class="mt-4 text-2xl font-semibold tracking-[-0.03em] text-[var(--color-ink-900)]">{title}</h3>
|
||||
<p class="mt-3 text-base leading-7 text-[var(--color-copy)]">{description}</p>
|
||||
<Eyebrow>Conversation focus</Eyebrow>
|
||||
<Headline as="h3" size="card" class="mt-4">
|
||||
{title}
|
||||
</Headline>
|
||||
<Lead class="mt-3" size="body">
|
||||
{description}
|
||||
</Lead>
|
||||
</Card>
|
||||
|
||||
@ -1,11 +1,23 @@
|
||||
---
|
||||
interface Props {
|
||||
class?: string;
|
||||
tone?: 'accent' | 'neutral' | 'signal';
|
||||
}
|
||||
|
||||
const { class: className = '' } = Astro.props;
|
||||
const { class: className = '', tone = 'accent' } = Astro.props;
|
||||
const toneClasses = {
|
||||
accent: 'text-[var(--color-brand)]',
|
||||
neutral: 'text-[var(--color-muted-foreground)]',
|
||||
signal: 'text-[var(--color-signal)]',
|
||||
};
|
||||
---
|
||||
|
||||
<p class:list={['m-0 text-sm font-semibold uppercase tracking-[0.18em] text-[var(--color-brand)]', className]}>
|
||||
<p
|
||||
class:list={[
|
||||
'm-0 text-[var(--type-eyebrow-size)] font-semibold uppercase tracking-[var(--tracking-eyebrow)]',
|
||||
toneClasses[tone],
|
||||
className,
|
||||
]}
|
||||
>
|
||||
<slot />
|
||||
</p>
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
---
|
||||
import Card from '@/components/primitives/Card.astro';
|
||||
import Eyebrow from '@/components/content/Eyebrow.astro';
|
||||
import Headline from '@/components/content/Headline.astro';
|
||||
import Lead from '@/components/content/Lead.astro';
|
||||
import type { FeatureItemContent } from '@/types/site';
|
||||
|
||||
interface Props {
|
||||
@ -10,20 +13,18 @@ const { item } = Astro.props;
|
||||
---
|
||||
|
||||
<Card class="h-full">
|
||||
{item.eyebrow && (
|
||||
<p class="m-0 text-xs font-semibold uppercase tracking-[0.16em] text-[var(--color-brand)]">
|
||||
{item.eyebrow}
|
||||
</p>
|
||||
)}
|
||||
<h3 class="mt-4 text-2xl font-semibold tracking-[-0.03em] text-[var(--color-ink-900)]">
|
||||
{item.eyebrow && <Eyebrow>{item.eyebrow}</Eyebrow>}
|
||||
<Headline as="h3" size="card" class="mt-4">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p class="mt-3 text-base leading-7 text-[var(--color-copy)]">{item.description}</p>
|
||||
</Headline>
|
||||
<Lead class="mt-3" size="body">
|
||||
{item.description}
|
||||
</Lead>
|
||||
{(item.meta || item.href) && (
|
||||
<div class="mt-5 flex flex-wrap items-center gap-3 text-sm">
|
||||
{item.meta && <span class="text-[var(--color-brand)]">{item.meta}</span>}
|
||||
{item.href && (
|
||||
<a class="font-semibold text-[var(--color-ink-900)] underline decoration-[rgba(17,36,58,0.18)] underline-offset-4" href={item.href}>
|
||||
<a class="text-link font-semibold" href={item.href}>
|
||||
Learn more
|
||||
</a>
|
||||
)}
|
||||
|
||||
@ -1,11 +1,22 @@
|
||||
---
|
||||
interface Props {
|
||||
as?: keyof HTMLElementTagNameMap;
|
||||
class?: string;
|
||||
size?: 'card' | 'display' | 'page' | 'section';
|
||||
}
|
||||
|
||||
const { class: className = '' } = Astro.props;
|
||||
const { as = 'h2', class: className = '', size = 'section' } = Astro.props;
|
||||
const Tag = as;
|
||||
const sizeClasses = {
|
||||
display:
|
||||
'font-[var(--font-display)] text-[length:var(--type-display-size)] leading-[var(--line-display)] tracking-[var(--tracking-display)]',
|
||||
page: 'font-[var(--font-display)] text-[length:var(--type-page-size)] leading-[var(--line-heading)] tracking-[var(--tracking-tight)]',
|
||||
section:
|
||||
'font-[var(--font-display)] text-[length:var(--type-section-size)] leading-[var(--line-heading)] tracking-[var(--tracking-tight)]',
|
||||
card: 'font-semibold text-[length:var(--type-card-size)] leading-[1.12] tracking-[var(--tracking-tight)]',
|
||||
};
|
||||
---
|
||||
|
||||
<h2 class:list={['m-0 font-[var(--font-display)] text-4xl leading-[0.98] tracking-[-0.03em] text-[var(--color-ink-900)] sm:text-5xl', className]}>
|
||||
<Tag class:list={['m-0 text-[var(--color-ink-900)]', sizeClasses[size], className]}>
|
||||
<slot />
|
||||
</h2>
|
||||
</Tag>
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
---
|
||||
import Badge from '@/components/primitives/Badge.astro';
|
||||
import Card from '@/components/primitives/Card.astro';
|
||||
import Headline from '@/components/content/Headline.astro';
|
||||
import Lead from '@/components/content/Lead.astro';
|
||||
import type { IntegrationEntry } from '@/types/site';
|
||||
|
||||
interface Props {
|
||||
@ -9,11 +12,15 @@ interface Props {
|
||||
const { item } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="rounded-[1.1rem] border border-[rgba(17,36,58,0.08)] bg-white/78 px-4 py-3 shadow-[var(--shadow-soft)]">
|
||||
<Card class="h-full px-4 py-4" variant="subtle">
|
||||
<div class="flex items-center gap-3">
|
||||
<Badge tone="neutral">{item.category}</Badge>
|
||||
<p class="m-0 text-base font-semibold text-[var(--color-ink-900)]">{item.name}</p>
|
||||
<Headline as="h3" size="card" class="text-base">
|
||||
{item.name}
|
||||
</Headline>
|
||||
</div>
|
||||
<p class="mt-3 max-w-72 text-sm leading-6 text-[var(--color-copy)]">{item.summary}</p>
|
||||
<Lead class="mt-3 max-w-72" size="small">
|
||||
{item.summary}
|
||||
</Lead>
|
||||
{item.note && <p class="mt-2 text-xs font-medium uppercase tracking-[0.14em] text-[var(--color-brand)]">{item.note}</p>}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@ -1,11 +1,19 @@
|
||||
---
|
||||
interface Props {
|
||||
as?: keyof HTMLElementTagNameMap;
|
||||
class?: string;
|
||||
size?: 'body' | 'lead' | 'small';
|
||||
}
|
||||
|
||||
const { class: className = '' } = Astro.props;
|
||||
const { as = 'p', class: className = '', size = 'lead' } = Astro.props;
|
||||
const Tag = as;
|
||||
const sizeClasses = {
|
||||
lead: 'text-[length:var(--type-body-size)] leading-[var(--line-body)] sm:text-lg',
|
||||
body: 'text-[length:var(--type-body-size)] leading-[var(--line-body)]',
|
||||
small: 'text-[length:var(--type-small-size)] leading-[1.65]',
|
||||
};
|
||||
---
|
||||
|
||||
<p class:list={['m-0 text-base leading-8 text-[var(--color-copy)] sm:text-lg', className]}>
|
||||
<Tag class:list={['m-0 text-[var(--color-copy)]', sizeClasses[size], className]}>
|
||||
<slot />
|
||||
</p>
|
||||
</Tag>
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
---
|
||||
import Card from '@/components/primitives/Card.astro';
|
||||
import Eyebrow from '@/components/content/Eyebrow.astro';
|
||||
import Lead from '@/components/content/Lead.astro';
|
||||
import type { MetricItem } from '@/types/site';
|
||||
|
||||
interface Props {
|
||||
@ -11,8 +13,10 @@ const { item } = Astro.props;
|
||||
|
||||
<Card variant="subtle">
|
||||
<p class="m-0 text-3xl font-semibold tracking-[-0.04em] text-[var(--color-ink-900)]">{item.value}</p>
|
||||
<p class="mt-2 text-sm font-semibold uppercase tracking-[0.14em] text-[var(--color-brand)]">
|
||||
<Eyebrow class="mt-2">
|
||||
{item.label}
|
||||
</p>
|
||||
<p class="mt-2 text-sm leading-6 text-[var(--color-copy)]">{item.description}</p>
|
||||
</Eyebrow>
|
||||
<Lead class="mt-2" size="small">
|
||||
{item.description}
|
||||
</Lead>
|
||||
</Card>
|
||||
|
||||
@ -4,9 +4,17 @@ import type { CtaLink } from '@/types/site';
|
||||
|
||||
interface Props {
|
||||
cta: CtaLink;
|
||||
class?: string;
|
||||
showHelper?: boolean;
|
||||
size?: 'lg' | 'md' | 'sm';
|
||||
}
|
||||
|
||||
const { cta } = Astro.props;
|
||||
const { cta, class: className = '', showHelper = false, size = 'md' } = Astro.props;
|
||||
---
|
||||
|
||||
<Button href={cta.href} variant={cta.variant ?? 'primary'}>{cta.label}</Button>
|
||||
<div class:list={['flex flex-col gap-2', className]} data-cta-slot="primary">
|
||||
<Button href={cta.href} variant={cta.variant ?? 'primary'} size={size}>
|
||||
{cta.label}
|
||||
</Button>
|
||||
{showHelper && cta.helper && <p class="m-0 text-sm text-[var(--color-copy)]">{cta.helper}</p>}
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
---
|
||||
import Card from '@/components/primitives/Card.astro';
|
||||
import Headline from '@/components/content/Headline.astro';
|
||||
import Lead from '@/components/content/Lead.astro';
|
||||
import type { LegalSection } from '@/types/site';
|
||||
|
||||
interface Props {
|
||||
@ -11,19 +14,19 @@ const { sections } = Astro.props;
|
||||
<div class="space-y-6">
|
||||
{
|
||||
sections.map((section) => (
|
||||
<section class="rounded-[1.5rem] border border-[rgba(17,36,58,0.08)] bg-white/72 p-6 shadow-[var(--shadow-soft)]">
|
||||
<h2 class="m-0 text-2xl font-semibold tracking-[-0.03em] text-[var(--color-ink-900)]">
|
||||
<Card as="section" class="rounded-[var(--radius-lg)]" variant="subtle">
|
||||
<Headline as="h2" size="card">
|
||||
{section.title}
|
||||
</h2>
|
||||
</Headline>
|
||||
<div class="legal-prose mt-4">
|
||||
{section.body.map((paragraph) => <p>{paragraph}</p>)}
|
||||
{section.body.map((paragraph) => <Lead size="body">{paragraph}</Lead>)}
|
||||
{section.bullets && section.bullets.length > 0 && (
|
||||
<ul>
|
||||
{section.bullets.map((bullet) => <li>{bullet}</li>)}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</Card>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -4,9 +4,17 @@ import type { CtaLink } from '@/types/site';
|
||||
|
||||
interface Props {
|
||||
cta: CtaLink;
|
||||
class?: string;
|
||||
showHelper?: boolean;
|
||||
size?: 'lg' | 'md' | 'sm';
|
||||
}
|
||||
|
||||
const { cta } = Astro.props;
|
||||
const { cta, class: className = '', showHelper = false, size = 'md' } = Astro.props;
|
||||
---
|
||||
|
||||
<Button href={cta.href} variant={cta.variant ?? 'secondary'}>{cta.label}</Button>
|
||||
<div class:list={['flex flex-col gap-2', className]} data-cta-slot="secondary">
|
||||
<Button href={cta.href} variant={cta.variant ?? 'secondary'} size={size}>
|
||||
{cta.label}
|
||||
</Button>
|
||||
{showHelper && cta.helper && <p class="m-0 text-sm text-[var(--color-copy)]">{cta.helper}</p>}
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
---
|
||||
import Card from '@/components/primitives/Card.astro';
|
||||
import Headline from '@/components/content/Headline.astro';
|
||||
import Lead from '@/components/content/Lead.astro';
|
||||
import type { TrustPrincipleContent } from '@/types/site';
|
||||
|
||||
interface Props {
|
||||
@ -10,7 +12,11 @@ const { item } = Astro.props;
|
||||
---
|
||||
|
||||
<Card class="h-full">
|
||||
<h3 class="m-0 text-2xl font-semibold tracking-[-0.03em] text-[var(--color-ink-900)]">{item.title}</h3>
|
||||
<p class="mt-3 text-base leading-7 text-[var(--color-copy)]">{item.description}</p>
|
||||
{item.note && <p class="mt-4 text-sm font-medium text-[var(--color-brand)]">{item.note}</p>}
|
||||
<Headline as="h3" size="card">
|
||||
{item.title}
|
||||
</Headline>
|
||||
<Lead class="mt-3" size="body">
|
||||
{item.description}
|
||||
</Lead>
|
||||
{item.note && <Lead class="mt-4 text-[var(--color-brand)]" size="small">{item.note}</Lead>}
|
||||
</Card>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
---
|
||||
import Button from '@/components/primitives/Button.astro';
|
||||
import PrimaryCTA from '@/components/content/PrimaryCTA.astro';
|
||||
import Container from '@/components/primitives/Container.astro';
|
||||
import { contactCta, footerNavigationGroups, siteMetadata } from '@/lib/site';
|
||||
import { footerNavigationGroups, getFooterLead, siteMetadata } from '@/lib/site';
|
||||
|
||||
interface Props {
|
||||
currentPath: string;
|
||||
@ -9,22 +9,23 @@ interface Props {
|
||||
|
||||
const { currentPath: _currentPath } = Astro.props;
|
||||
const currentYear = new Date().getFullYear();
|
||||
const footerLead = getFooterLead(_currentPath);
|
||||
---
|
||||
|
||||
<footer class="section-divider pt-10 sm:pt-12">
|
||||
<Container wide>
|
||||
<div class="grid gap-8 rounded-[2rem] bg-[rgba(255,255,255,0.58)] p-6 shadow-[var(--shadow-soft)] lg:grid-cols-[1.3fr,1fr] lg:p-8">
|
||||
<footer class="section-divider px-[var(--space-page-x)] pt-10 sm:pt-12" data-footer-intent={footerLead.intent}>
|
||||
<Container width="wide">
|
||||
<div class="surface-card-muted grid gap-8 rounded-[var(--radius-panel)] p-6 lg:grid-cols-[1.3fr,1fr] lg:p-8">
|
||||
<div class="space-y-5">
|
||||
<p class="m-0 text-sm font-semibold uppercase tracking-[0.16em] text-[var(--color-brand)]">
|
||||
{siteMetadata.siteName}
|
||||
{footerLead.eyebrow}
|
||||
</p>
|
||||
<h2 class="m-0 max-w-xl font-[var(--font-display)] text-3xl leading-[0.98] text-[var(--color-ink-900)] sm:text-4xl">
|
||||
A calmer public surface for teams that need governance clarity before they need another dashboard.
|
||||
{footerLead.title}
|
||||
</h2>
|
||||
<p class="m-0 max-w-xl text-base leading-7 text-[var(--color-copy)]">
|
||||
TenantAtlas keeps product explanation, trust framing, and next-step guidance readable without hiding the product model behind hype or placeholders.
|
||||
{footerLead.description}
|
||||
</p>
|
||||
<Button href={contactCta.href} variant="primary" size="sm">{contactCta.label}</Button>
|
||||
<PrimaryCTA cta={footerLead.primaryCta} size="sm" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 sm:grid-cols-3">
|
||||
|
||||
@ -1,23 +1,25 @@
|
||||
---
|
||||
import Button from '@/components/primitives/Button.astro';
|
||||
import SecondaryCTA from '@/components/content/SecondaryCTA.astro';
|
||||
import Container from '@/components/primitives/Container.astro';
|
||||
import { contactCta, isActiveNavigationPath, primaryNavigation, siteMetadata } from '@/lib/site';
|
||||
import { getHeaderCta, isActiveNavigationPath, primaryNavigation, siteMetadata } from '@/lib/site';
|
||||
|
||||
interface Props {
|
||||
currentPath: string;
|
||||
}
|
||||
|
||||
const { currentPath } = Astro.props;
|
||||
const headerCta = getHeaderCta(currentPath);
|
||||
---
|
||||
|
||||
<header class="sticky top-0 z-30 pt-4 sm:pt-6">
|
||||
<Container wide>
|
||||
<header class="sticky top-0 z-30 px-[var(--space-page-x)] pt-4 sm:pt-6">
|
||||
<Container width="wide">
|
||||
<div
|
||||
class="glass-panel flex items-center justify-between gap-4 rounded-[1.75rem] border border-white/70 px-4 py-3 sm:px-5"
|
||||
class="shell-panel flex items-center justify-between gap-4 rounded-[var(--radius-panel)] px-4 py-3 sm:px-5"
|
||||
data-shell-surface="header"
|
||||
>
|
||||
<a href="/" class="flex min-w-0 items-center gap-3 no-underline">
|
||||
<span
|
||||
class="inline-flex h-11 w-11 items-center justify-center rounded-full bg-[linear-gradient(135deg,var(--color-brand),#7fa6cf)] font-[var(--font-display)] text-lg text-white"
|
||||
class="inline-flex h-11 w-11 items-center justify-center rounded-full bg-[linear-gradient(135deg,var(--color-primary),#8eaed1)] font-[var(--font-display)] text-lg text-white shadow-[var(--shadow-inline)]"
|
||||
>
|
||||
TA
|
||||
</span>
|
||||
@ -36,12 +38,14 @@ const { currentPath } = Astro.props;
|
||||
primaryNavigation.map((item) => (
|
||||
<a
|
||||
href={item.href}
|
||||
aria-current={isActiveNavigationPath(currentPath, item.href) ? 'page' : undefined}
|
||||
class:list={[
|
||||
'rounded-full px-4 py-2 text-sm font-medium transition',
|
||||
isActiveNavigationPath(currentPath, item.href)
|
||||
? 'bg-[var(--color-brand-soft)] text-[var(--color-brand)]'
|
||||
? 'bg-[var(--color-accent)] text-[var(--color-accent-foreground)]'
|
||||
: 'text-[var(--color-ink-800)] hover:bg-white/70',
|
||||
]}
|
||||
data-nav-link="primary"
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
@ -50,13 +54,14 @@ const { currentPath } = Astro.props;
|
||||
</nav>
|
||||
|
||||
<div class="hidden lg:block">
|
||||
<Button href={contactCta.href} variant="secondary" size="sm">{contactCta.label}</Button>
|
||||
<SecondaryCTA cta={headerCta} size="sm" />
|
||||
</div>
|
||||
|
||||
<details class="relative lg:hidden">
|
||||
<details class="relative lg:hidden" data-mobile-nav>
|
||||
<summary
|
||||
aria-label="Open navigation menu"
|
||||
class="flex h-11 w-11 cursor-pointer list-none items-center justify-center rounded-full border border-[color:var(--color-line)] bg-white/85 text-[var(--color-ink-900)]"
|
||||
data-mobile-nav-trigger
|
||||
>
|
||||
<span class="sr-only">Open navigation menu</span>
|
||||
<span class="flex flex-col gap-1">
|
||||
@ -73,12 +78,14 @@ const { currentPath } = Astro.props;
|
||||
primaryNavigation.map((item) => (
|
||||
<a
|
||||
href={item.href}
|
||||
aria-current={isActiveNavigationPath(currentPath, item.href) ? 'page' : undefined}
|
||||
class:list={[
|
||||
'rounded-[1rem] px-4 py-3 text-sm',
|
||||
isActiveNavigationPath(currentPath, item.href)
|
||||
? 'bg-[var(--color-brand-soft)] text-[var(--color-brand)]'
|
||||
? 'bg-[var(--color-accent)] text-[var(--color-accent-foreground)]'
|
||||
: 'text-[var(--color-ink-800)] hover:bg-white/75',
|
||||
]}
|
||||
data-nav-link="mobile-primary"
|
||||
>
|
||||
<span class="block font-semibold">{item.label}</span>
|
||||
{item.description && (
|
||||
@ -90,12 +97,7 @@ const { currentPath } = Astro.props;
|
||||
))
|
||||
}
|
||||
<div class="mt-2 rounded-[1rem] bg-[rgba(47,111,183,0.08)] p-3">
|
||||
<p class="m-0 text-sm font-semibold text-[var(--color-ink-900)]">
|
||||
{contactCta.label}
|
||||
</p>
|
||||
{contactCta.helper && (
|
||||
<p class="mt-1 text-sm text-[var(--color-copy)]">{contactCta.helper}</p>
|
||||
)}
|
||||
<SecondaryCTA cta={headerCta} size="sm" showHelper />
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
---
|
||||
import Footer from '@/components/layout/Footer.astro';
|
||||
import Navbar from '@/components/layout/Navbar.astro';
|
||||
import { getPageDefinition } from '@/lib/site';
|
||||
import { resolveSeo } from '@/lib/seo';
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
|
||||
@ -15,6 +16,7 @@ const seo =
|
||||
title && description
|
||||
? resolveSeo({ description, path: currentPath, title })
|
||||
: undefined;
|
||||
const pageDefinition = getPageDefinition(currentPath);
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
@ -25,13 +27,18 @@ const seo =
|
||||
openGraphDescription={seo?.ogDescription}
|
||||
robots={seo?.robots}
|
||||
>
|
||||
<div class="surface-shell min-h-screen">
|
||||
<div
|
||||
class="foundation-page site-shell"
|
||||
data-page-family={pageDefinition.family}
|
||||
data-page-role={pageDefinition.pageRole}
|
||||
data-shell-tone={pageDefinition.shellTone}
|
||||
>
|
||||
<div
|
||||
class="pointer-events-none absolute inset-x-0 top-0 -z-10 h-[30rem] bg-[radial-gradient(circle_at_top,rgba(47,111,183,0.16),transparent_50%),radial-gradient(circle_at_top_right,rgba(59,139,120,0.14),transparent_28%)]"
|
||||
class="pointer-events-none absolute inset-x-0 top-0 -z-10 h-[34rem] bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.68),transparent_28%),radial-gradient(circle_at_top_right,rgba(47,111,183,0.14),transparent_26%)]"
|
||||
>
|
||||
</div>
|
||||
<Navbar currentPath={currentPath} />
|
||||
<main id="content" class="pb-16 sm:pb-20">
|
||||
<main id="content" class="foundation-main pb-20 sm:pb-24">
|
||||
<slot />
|
||||
</main>
|
||||
<Footer currentPath={currentPath} />
|
||||
|
||||
@ -7,19 +7,20 @@ interface Props {
|
||||
const { class: className = '', tone = 'accent' } = Astro.props;
|
||||
|
||||
const toneClasses = {
|
||||
accent: 'bg-[var(--color-brand-soft)] text-[var(--color-brand)]',
|
||||
neutral: 'bg-white/75 text-[var(--color-ink-800)]',
|
||||
signal: 'bg-[rgba(59,139,120,0.14)] text-[var(--color-signal)]',
|
||||
accent: 'bg-[var(--color-accent)] text-[var(--color-accent-foreground)]',
|
||||
neutral: 'bg-white/78 text-[var(--color-ink-800)]',
|
||||
signal: 'bg-[var(--surface-trust)] text-[var(--color-signal)]',
|
||||
warm: 'bg-[rgba(175,109,67,0.14)] text-[var(--color-warm)]',
|
||||
};
|
||||
---
|
||||
|
||||
<span
|
||||
class:list={[
|
||||
'inline-flex w-fit items-center rounded-full px-3 py-1 text-[0.72rem] font-semibold uppercase tracking-[0.18em]',
|
||||
'inline-flex w-fit items-center rounded-[var(--radius-pill)] px-3 py-1 text-[0.72rem] font-semibold uppercase tracking-[var(--tracking-eyebrow)]',
|
||||
toneClasses[tone],
|
||||
className,
|
||||
]}
|
||||
data-badge-tone={tone}
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
|
||||
@ -24,19 +24,19 @@ const {
|
||||
} = Astro.props;
|
||||
|
||||
const baseClass =
|
||||
'inline-flex items-center justify-center rounded-full border font-semibold tracking-[-0.01em] transition duration-150 focus-visible:outline-none';
|
||||
'inline-flex items-center justify-center rounded-[var(--radius-pill)] border font-semibold tracking-[var(--tracking-tight)] transition duration-150';
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'min-h-10 px-4 text-sm',
|
||||
md: 'min-h-11 px-5 text-sm sm:text-[0.95rem]',
|
||||
lg: 'min-h-12 px-6 text-[0.95rem]',
|
||||
lg: 'min-h-12 px-6 text-[0.97rem]',
|
||||
};
|
||||
|
||||
const variantClasses = {
|
||||
primary:
|
||||
'border-transparent bg-[var(--color-ink-900)] text-white shadow-[0_18px_38px_rgba(17,36,58,0.16)] hover:bg-[var(--color-brand)]',
|
||||
'border-transparent bg-[var(--color-primary)] text-[var(--color-primary-foreground)] shadow-[var(--shadow-inline)] hover:bg-[var(--color-brand-700)]',
|
||||
secondary:
|
||||
'border-[color:var(--color-line)] bg-white/85 text-[var(--color-ink-900)] hover:border-[var(--color-ink-900)] hover:bg-white',
|
||||
'border-[color:var(--color-border)] bg-[var(--color-secondary)] text-[var(--color-secondary-foreground)] hover:border-[var(--color-border-strong)] hover:bg-white',
|
||||
ghost: 'border-transparent bg-transparent text-[var(--color-ink-800)] hover:bg-white/70',
|
||||
};
|
||||
|
||||
@ -45,11 +45,27 @@ const classes = [baseClass, sizeClasses[size], variantClasses[variant], classNam
|
||||
|
||||
{
|
||||
href ? (
|
||||
<a href={href} target={target} rel={rel} aria-label={ariaLabel} class:list={classes}>
|
||||
<a
|
||||
href={href}
|
||||
target={target}
|
||||
rel={rel}
|
||||
aria-label={ariaLabel}
|
||||
class:list={classes}
|
||||
data-button-variant={variant}
|
||||
data-cta-weight={variant}
|
||||
data-interaction="button"
|
||||
>
|
||||
<slot />
|
||||
</a>
|
||||
) : (
|
||||
<button type={type} aria-label={ariaLabel} class:list={classes}>
|
||||
<button
|
||||
type={type}
|
||||
aria-label={ariaLabel}
|
||||
class:list={classes}
|
||||
data-button-variant={variant}
|
||||
data-cta-weight={variant}
|
||||
data-interaction="button"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
)
|
||||
|
||||
@ -8,16 +8,17 @@ interface Props {
|
||||
const { as = 'article', class: className = '', variant = 'default' } = Astro.props;
|
||||
|
||||
const variantClasses = {
|
||||
default: 'glass-panel border border-[color:var(--color-line)] bg-[var(--color-panel)]',
|
||||
accent:
|
||||
'border border-[rgba(47,111,183,0.18)] bg-[linear-gradient(180deg,rgba(255,255,255,0.96),rgba(238,245,252,0.94))] shadow-[var(--shadow-soft)]',
|
||||
subtle:
|
||||
'border border-[rgba(17,36,58,0.08)] bg-[linear-gradient(180deg,rgba(255,255,255,0.78),rgba(255,255,255,0.56))]',
|
||||
default: 'surface-card',
|
||||
accent: 'surface-card-accent',
|
||||
subtle: 'surface-card-muted',
|
||||
};
|
||||
|
||||
const Tag = as;
|
||||
---
|
||||
|
||||
<Tag class:list={['rounded-[1.65rem] p-6 sm:p-7', variantClasses[variant], className]}>
|
||||
<Tag
|
||||
class:list={['rounded-[1.65rem] p-6 sm:p-7', variantClasses[variant], className]}
|
||||
data-surface={variant}
|
||||
>
|
||||
<slot />
|
||||
</Tag>
|
||||
|
||||
@ -2,12 +2,29 @@
|
||||
interface Props {
|
||||
as?: keyof HTMLElementTagNameMap;
|
||||
class?: string;
|
||||
gap?: 'lg' | 'md' | 'sm';
|
||||
justify?: 'between' | 'end' | 'start';
|
||||
}
|
||||
|
||||
const { as = 'div', class: className = '' } = Astro.props;
|
||||
const {
|
||||
as = 'div',
|
||||
class: className = '',
|
||||
gap = 'md',
|
||||
justify = 'start',
|
||||
} = Astro.props;
|
||||
const Tag = as;
|
||||
const gapClasses = {
|
||||
sm: 'gap-[var(--space-cluster-sm)]',
|
||||
md: 'gap-[var(--space-cluster)]',
|
||||
lg: 'gap-[var(--space-cluster-lg)]',
|
||||
};
|
||||
const justifyClasses = {
|
||||
start: 'justify-start',
|
||||
between: 'justify-between',
|
||||
end: 'justify-end',
|
||||
};
|
||||
---
|
||||
|
||||
<Tag class:list={['flex flex-wrap items-center gap-3 sm:gap-4', className]}>
|
||||
<Tag class:list={['flex flex-wrap items-center', gapClasses[gap], justifyClasses[justify], className]}>
|
||||
<slot />
|
||||
</Tag>
|
||||
|
||||
@ -2,13 +2,31 @@
|
||||
interface Props {
|
||||
as?: keyof HTMLElementTagNameMap;
|
||||
class?: string;
|
||||
width?: 'content' | 'measure' | 'wide';
|
||||
wide?: boolean;
|
||||
}
|
||||
|
||||
const { as = 'div', class: className = '', wide = false } = Astro.props;
|
||||
const {
|
||||
as = 'div',
|
||||
class: className = '',
|
||||
width,
|
||||
wide = false,
|
||||
} = Astro.props;
|
||||
const Tag = as;
|
||||
const resolvedWidth = width ?? (wide ? 'wide' : 'content');
|
||||
const widthClasses = {
|
||||
content: 'max-w-[var(--content-max-width)]',
|
||||
measure: 'max-w-[var(--reading-max-width)]',
|
||||
wide: 'max-w-[var(--wide-max-width)]',
|
||||
};
|
||||
---
|
||||
|
||||
<Tag class:list={['mx-auto w-full px-5 sm:px-6 lg:px-8', wide ? 'max-w-[80rem]' : 'max-w-6xl', className]}>
|
||||
<Tag
|
||||
class:list={[
|
||||
'mx-auto w-full px-5 sm:px-6 lg:px-8',
|
||||
widthClasses[resolvedWidth],
|
||||
className,
|
||||
]}
|
||||
>
|
||||
<slot />
|
||||
</Tag>
|
||||
|
||||
@ -2,17 +2,22 @@
|
||||
interface Props {
|
||||
class?: string;
|
||||
cols?: '2' | '3' | '4';
|
||||
gap?: 'lg' | 'md';
|
||||
}
|
||||
|
||||
const { class: className = '', cols = '3' } = Astro.props;
|
||||
const { class: className = '', cols = '3', gap = 'md' } = Astro.props;
|
||||
|
||||
const colClasses = {
|
||||
'2': 'grid-cols-1 md:grid-cols-2',
|
||||
'3': 'grid-cols-1 md:grid-cols-2 xl:grid-cols-3',
|
||||
'4': 'grid-cols-1 md:grid-cols-2 xl:grid-cols-4',
|
||||
};
|
||||
const gapClasses = {
|
||||
md: 'gap-[var(--space-grid)] lg:gap-[var(--space-grid-lg)]',
|
||||
lg: 'gap-6 lg:gap-8',
|
||||
};
|
||||
---
|
||||
|
||||
<div class:list={['grid gap-5 lg:gap-6', colClasses[cols], className]}>
|
||||
<div class:list={['grid', gapClasses[gap], colClasses[cols], className]}>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@ -24,8 +24,9 @@ const {
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
readonly={readonly}
|
||||
data-interaction="input"
|
||||
class:list={[
|
||||
'min-h-12 w-full rounded-[1.1rem] border border-[color:var(--color-line)] bg-white/90 px-4 text-[0.97rem] text-[var(--color-ink-900)] shadow-[var(--shadow-soft)] placeholder:text-[var(--color-copy)]/70',
|
||||
'min-h-12 w-full rounded-[var(--radius-md)] border border-[color:var(--color-border)] bg-[var(--color-input)] px-4 text-[0.97rem] text-[var(--color-ink-900)] shadow-[var(--shadow-soft)] placeholder:text-[var(--color-copy)]/70',
|
||||
readonly ? 'cursor-default' : '',
|
||||
className,
|
||||
]}
|
||||
|
||||
@ -2,19 +2,39 @@
|
||||
interface Props {
|
||||
as?: keyof HTMLElementTagNameMap;
|
||||
class?: string;
|
||||
density?: 'base' | 'compact' | 'spacious';
|
||||
id?: string;
|
||||
muted?: boolean;
|
||||
layer?: '1' | '2' | '3';
|
||||
tone?: 'default' | 'emphasis' | 'muted';
|
||||
}
|
||||
|
||||
const { as = 'section', class: className = '', id, muted = false } = Astro.props;
|
||||
const {
|
||||
as = 'section',
|
||||
class: className = '',
|
||||
density = 'base',
|
||||
id,
|
||||
layer = '2',
|
||||
tone = 'default',
|
||||
} = Astro.props;
|
||||
const Tag = as;
|
||||
const densityClasses = {
|
||||
compact: 'section-density-compact',
|
||||
base: 'section-density-base',
|
||||
spacious: 'section-density-spacious',
|
||||
};
|
||||
const toneClasses = {
|
||||
default: '',
|
||||
muted: 'section-shell-muted px-3 sm:px-4',
|
||||
emphasis: 'section-shell-emphasis px-3 sm:px-4',
|
||||
};
|
||||
---
|
||||
|
||||
<Tag
|
||||
id={id}
|
||||
data-disclosure-layer={layer}
|
||||
class:list={[
|
||||
'py-12 sm:py-16 lg:py-20',
|
||||
muted ? 'bg-white/45' : '',
|
||||
densityClasses[density],
|
||||
toneClasses[tone],
|
||||
className,
|
||||
]}
|
||||
>
|
||||
|
||||
@ -9,6 +9,7 @@ interface Props {
|
||||
description?: string;
|
||||
eyebrow?: string;
|
||||
title: string;
|
||||
width?: 'default' | 'measure' | 'wide';
|
||||
}
|
||||
|
||||
const {
|
||||
@ -17,10 +18,16 @@ const {
|
||||
description,
|
||||
eyebrow,
|
||||
title,
|
||||
width = 'default',
|
||||
} = Astro.props;
|
||||
const widthClasses = {
|
||||
default: 'max-w-3xl',
|
||||
measure: 'max-w-[var(--reading-max-width)]',
|
||||
wide: 'max-w-4xl',
|
||||
};
|
||||
---
|
||||
|
||||
<div class:list={['max-w-3xl', align === 'center' ? 'mx-auto text-center' : '', className]}>
|
||||
<div class:list={[widthClasses[width], align === 'center' ? 'mx-auto text-center' : '', className]}>
|
||||
{eyebrow && <Eyebrow>{eyebrow}</Eyebrow>}
|
||||
<Headline>{title}</Headline>
|
||||
{description && <Lead class="mt-4">{description}</Lead>}
|
||||
|
||||
@ -7,9 +7,9 @@ interface Props {
|
||||
|
||||
const { as = 'div', class: className = '', gap = 'md' } = Astro.props;
|
||||
const gapClasses = {
|
||||
sm: 'flex flex-col gap-3',
|
||||
md: 'flex flex-col gap-5',
|
||||
lg: 'flex flex-col gap-7',
|
||||
sm: 'flex flex-col gap-[var(--space-stack-sm)]',
|
||||
md: 'flex flex-col gap-[var(--space-stack)]',
|
||||
lg: 'flex flex-col gap-[var(--space-stack-lg)]',
|
||||
xl: 'flex flex-col gap-10',
|
||||
};
|
||||
const Tag = as;
|
||||
|
||||
@ -23,8 +23,9 @@ const {
|
||||
rows={rows}
|
||||
placeholder={placeholder}
|
||||
readonly={readonly}
|
||||
data-interaction="textarea"
|
||||
class:list={[
|
||||
'min-h-32 w-full rounded-[1.1rem] border border-[color:var(--color-line)] bg-white/90 px-4 py-3 text-[0.97rem] text-[var(--color-ink-900)] shadow-[var(--shadow-soft)] placeholder:text-[var(--color-copy)]/70',
|
||||
'min-h-32 w-full rounded-[var(--radius-md)] border border-[color:var(--color-border)] bg-[var(--color-input)] px-4 py-3 text-[0.97rem] text-[var(--color-ink-900)] shadow-[var(--shadow-soft)] placeholder:text-[var(--color-copy)]/70',
|
||||
readonly ? 'cursor-default' : '',
|
||||
className,
|
||||
]}
|
||||
|
||||
@ -19,8 +19,8 @@ const { description, eyebrow, primary, secondary, title } = Astro.props;
|
||||
---
|
||||
|
||||
<Section>
|
||||
<Container wide>
|
||||
<Card variant="accent" class="overflow-hidden">
|
||||
<Container width="wide">
|
||||
<Card variant="accent" class="overflow-hidden" data-cta-section>
|
||||
<div class="grid gap-6 lg:grid-cols-[1.35fr,0.85fr] lg:items-end">
|
||||
<SectionHeader eyebrow={eyebrow} title={title} description={description} />
|
||||
<div class="flex flex-col gap-3 sm:flex-row lg:justify-end">
|
||||
|
||||
@ -16,8 +16,8 @@ interface Props {
|
||||
const { description, eyebrow, items, title } = Astro.props;
|
||||
---
|
||||
|
||||
<Section>
|
||||
<Container wide>
|
||||
<Section layer="2">
|
||||
<Container width="wide">
|
||||
<div class="space-y-8">
|
||||
<SectionHeader eyebrow={eyebrow} title={title} description={description} />
|
||||
<Grid cols="3">
|
||||
|
||||
@ -14,9 +14,9 @@ interface Props {
|
||||
const { eyebrow, items, title } = Astro.props;
|
||||
---
|
||||
|
||||
<Section class="pt-8 sm:pt-10">
|
||||
<Container wide>
|
||||
<div class="rounded-[1.8rem] border border-[rgba(17,36,58,0.08)] bg-white/55 px-5 py-6 shadow-[var(--shadow-soft)]">
|
||||
<Section class="pt-8 sm:pt-10" density="compact" layer="2">
|
||||
<Container width="wide">
|
||||
<div class="surface-card-muted rounded-[var(--radius-lg)] px-5 py-6">
|
||||
<div class="flex flex-col gap-5 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div class="space-y-3">
|
||||
{eyebrow && <Badge tone="signal">{eyebrow}</Badge>}
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
---
|
||||
import Badge from '@/components/primitives/Badge.astro';
|
||||
import Button from '@/components/primitives/Button.astro';
|
||||
import Card from '@/components/primitives/Card.astro';
|
||||
import Cluster from '@/components/primitives/Cluster.astro';
|
||||
import Container from '@/components/primitives/Container.astro';
|
||||
import Headline from '@/components/content/Headline.astro';
|
||||
import Lead from '@/components/content/Lead.astro';
|
||||
import Metric from '@/components/content/Metric.astro';
|
||||
import PrimaryCTA from '@/components/content/PrimaryCTA.astro';
|
||||
import SecondaryCTA from '@/components/content/SecondaryCTA.astro';
|
||||
import type { HeroContent, MetricItem } from '@/types/site';
|
||||
|
||||
interface Props {
|
||||
@ -17,28 +21,24 @@ const { calloutDescription, calloutTitle, hero, metrics = [] } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="pt-8 sm:pt-10 lg:pt-14">
|
||||
<Container wide>
|
||||
<div class="grid gap-6 lg:grid-cols-[1.35fr,0.85fr]">
|
||||
<Container width="wide">
|
||||
<div class="grid gap-6 lg:grid-cols-[1.35fr,0.85fr]" data-disclosure-layer="1">
|
||||
<Card class="motion-rise overflow-hidden">
|
||||
<div class="space-y-6">
|
||||
<Badge>{hero.eyebrow}</Badge>
|
||||
<div class="space-y-4">
|
||||
<h1 class="max-w-4xl font-[var(--font-display)] text-5xl leading-[0.93] tracking-[-0.04em] text-[var(--color-ink-900)] sm:text-6xl lg:text-7xl">
|
||||
<Headline as="h1" size="display" class="max-w-4xl">
|
||||
{hero.title}
|
||||
</h1>
|
||||
<p class="max-w-3xl text-lg leading-8 text-[var(--color-copy)] sm:text-xl">
|
||||
</Headline>
|
||||
<Lead class="max-w-3xl" size="lead">
|
||||
{hero.description}
|
||||
</p>
|
||||
</Lead>
|
||||
</div>
|
||||
{(hero.primaryCta || hero.secondaryCta) && (
|
||||
<Cluster>
|
||||
<Button href={hero.primaryCta.href} variant={hero.primaryCta.variant ?? 'primary'}>
|
||||
{hero.primaryCta.label}
|
||||
</Button>
|
||||
<Cluster data-cta-cluster gap="md">
|
||||
<PrimaryCTA cta={hero.primaryCta} />
|
||||
{hero.secondaryCta && (
|
||||
<Button href={hero.secondaryCta.href} variant={hero.secondaryCta.variant ?? 'secondary'}>
|
||||
{hero.secondaryCta.label}
|
||||
</Button>
|
||||
<SecondaryCTA cta={hero.secondaryCta} />
|
||||
)}
|
||||
</Cluster>
|
||||
)}
|
||||
@ -75,19 +75,7 @@ const { calloutDescription, calloutTitle, hero, metrics = [] } = Astro.props;
|
||||
|
||||
{metrics.length > 0 && (
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-1">
|
||||
{
|
||||
metrics.map((metric) => (
|
||||
<Card variant="subtle">
|
||||
<p class="m-0 text-3xl font-semibold tracking-[-0.04em] text-[var(--color-ink-900)]">
|
||||
{metric.value}
|
||||
</p>
|
||||
<p class="mt-2 text-sm font-semibold uppercase tracking-[0.14em] text-[var(--color-brand)]">
|
||||
{metric.label}
|
||||
</p>
|
||||
<p class="mt-2 text-sm leading-6 text-[var(--color-copy)]">{metric.description}</p>
|
||||
</Card>
|
||||
))
|
||||
}
|
||||
{metrics.map((metric) => <Metric item={metric} />)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -16,8 +16,8 @@ interface Props {
|
||||
const { description, eyebrow, items, title } = Astro.props;
|
||||
---
|
||||
|
||||
<Section>
|
||||
<Container wide>
|
||||
<Section layer="2">
|
||||
<Container width="wide">
|
||||
<div class="space-y-8">
|
||||
<SectionHeader eyebrow={eyebrow} title={title} description={description} />
|
||||
<Grid cols="3">
|
||||
|
||||
@ -16,7 +16,7 @@ export const homeSeo: PageSeo = {
|
||||
|
||||
export const homeHero: HeroContent = {
|
||||
eyebrow: 'Public website v0',
|
||||
title: 'TenantAtlas is the trust-first public site for Microsoft tenant change history, drift visibility, and safer operations.',
|
||||
title: 'TenantAtlas frames Microsoft tenant governance as a calm, reviewable operating model instead of a loud feature wall.',
|
||||
description:
|
||||
'TenantAtlas gives MSP and enterprise teams one clear operating model for understanding what changed, what drifted, what needs review, and what can be restored without turning governance into a loose collection of disconnected tools.',
|
||||
primaryCta: {
|
||||
@ -31,7 +31,7 @@ export const homeHero: HeroContent = {
|
||||
highlights: [
|
||||
'Static, readable, and separate from app runtime concerns.',
|
||||
'Built for trust conversations before a demo ever starts.',
|
||||
'Designed to scale into docs, changelog, and deeper resources later.',
|
||||
'Structured so landing, trust, and long-form pages still feel like one website.',
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@ -57,7 +57,7 @@ export const securityPrinciples: TrustPrincipleContent[] = [
|
||||
export const securityTrustNotes: CalloutContent[] = [
|
||||
{
|
||||
eyebrow: 'Substantiated public posture',
|
||||
title: 'Substantiated public posture',
|
||||
title: 'Public posture should stay anchored to product safeguards.',
|
||||
description:
|
||||
'The launch story focuses on product shape and operator safeguards: inventory truth, immutable snapshots, safer restore flows, drift visibility, and review support.',
|
||||
tone: 'accent',
|
||||
|
||||
@ -16,7 +16,7 @@ export const solutionsHero: HeroContent = {
|
||||
eyebrow: 'Audience fit',
|
||||
title: 'Show how TenantAtlas fits MSP delivery teams and enterprise operators without collapsing them into one generic story.',
|
||||
description:
|
||||
'The product helps different organizations answer similar governance questions, but the surrounding workflow, accountability, and evidence needs are not identical. The site should acknowledge that directly.',
|
||||
'The product helps different organizations answer similar governance questions, but the surrounding workflow, accountability, and evidence needs are not identical. The site should acknowledge that directly while keeping one calm visual rhythm.',
|
||||
primaryCta: {
|
||||
href: '/integrations',
|
||||
label: 'Review the ecosystem fit',
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
import { coreRoutes, siteMetadata } from '@/lib/site';
|
||||
import type { PageSeo } from '@/types/site';
|
||||
import { coreRoutes, getPageDefinition, siteMetadata } from '@/lib/site';
|
||||
import type { PageFamily, PageRole, PageSeo, ShellTone } from '@/types/site';
|
||||
|
||||
export interface ResolvedSeo extends PageSeo {
|
||||
canonicalUrl: string;
|
||||
family: PageFamily;
|
||||
ogDescription: string;
|
||||
ogTitle: string;
|
||||
pageRole: PageRole;
|
||||
robots: string;
|
||||
shellTone: ShellTone;
|
||||
}
|
||||
|
||||
export function buildCanonicalUrl(path: string): string {
|
||||
@ -13,12 +16,17 @@ export function buildCanonicalUrl(path: string): string {
|
||||
}
|
||||
|
||||
export function resolveSeo(seo: PageSeo): ResolvedSeo {
|
||||
const definition = getPageDefinition(seo.path);
|
||||
|
||||
return {
|
||||
...seo,
|
||||
canonicalUrl: buildCanonicalUrl(seo.path),
|
||||
family: definition.family,
|
||||
ogDescription: seo.ogDescription ?? seo.description,
|
||||
ogTitle: seo.ogTitle ?? seo.title,
|
||||
pageRole: definition.pageRole,
|
||||
robots: 'index,follow',
|
||||
shellTone: definition.shellTone,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,14 @@
|
||||
import type {
|
||||
CtaLink,
|
||||
FooterLead,
|
||||
FooterNavigationGroup,
|
||||
NavigationItem,
|
||||
PageDefinition,
|
||||
PageFamily,
|
||||
ShellTone,
|
||||
SiteMetadata,
|
||||
SitePath,
|
||||
VisualFoundationContract,
|
||||
} from '@/types/site';
|
||||
|
||||
export const siteMetadata: SiteMetadata = {
|
||||
@ -13,6 +19,39 @@ export const siteMetadata: SiteMetadata = {
|
||||
siteUrl: import.meta.env.PUBLIC_SITE_URL ?? 'https://tenantatlas.example',
|
||||
};
|
||||
|
||||
export const visualFoundationContract: VisualFoundationContract = {
|
||||
pageFamilies: ['landing', 'trust', 'content'],
|
||||
ctaHierarchy: ['primary', 'secondary', 'ghost'],
|
||||
requiredColorRoles: [
|
||||
'background',
|
||||
'foreground',
|
||||
'muted',
|
||||
'muted-foreground',
|
||||
'card',
|
||||
'card-foreground',
|
||||
'border',
|
||||
'input',
|
||||
'primary',
|
||||
'primary-foreground',
|
||||
'secondary',
|
||||
'secondary-foreground',
|
||||
'accent',
|
||||
'accent-foreground',
|
||||
'success',
|
||||
'warning',
|
||||
'destructive',
|
||||
'info',
|
||||
],
|
||||
accessibilityBaseline: [
|
||||
'focus-visible',
|
||||
'readable-contrast',
|
||||
'non-color-only-semantics',
|
||||
'navigation-vs-cta-differentiation',
|
||||
'mobile-readable-density',
|
||||
],
|
||||
surfaceLayers: ['page-background', 'default-content', 'card', 'elevated', 'muted-inset', 'highlighted'],
|
||||
};
|
||||
|
||||
export const primaryNavigation: NavigationItem[] = [
|
||||
{ href: '/product', label: 'Product', description: 'Understand the operating model.' },
|
||||
{ href: '/solutions', label: 'Solutions', description: 'See the fit for MSP and enterprise teams.' },
|
||||
@ -55,17 +94,107 @@ export const contactCta: CtaLink = {
|
||||
variant: 'primary',
|
||||
};
|
||||
|
||||
export const coreRoutes = [
|
||||
'/',
|
||||
'/product',
|
||||
'/solutions',
|
||||
'/security-trust',
|
||||
'/integrations',
|
||||
'/contact',
|
||||
'/legal',
|
||||
'/privacy',
|
||||
'/terms',
|
||||
] as const;
|
||||
const footerLeadByFamily: Record<PageFamily, FooterLead> = {
|
||||
landing: {
|
||||
eyebrow: 'Keep the next move obvious',
|
||||
title: 'A calmer public surface should still lead somewhere concrete.',
|
||||
description:
|
||||
'Landing pages should move visitors from orientation into a product, trust, or contact path without competing CTAs or dead ends.',
|
||||
intent: 'conversion',
|
||||
primaryCta: contactCta,
|
||||
},
|
||||
trust: {
|
||||
eyebrow: 'Trust stays actionable',
|
||||
title: 'Security, legal, and contact paths should reinforce one another.',
|
||||
description:
|
||||
'Trust-oriented pages should keep legal context, product posture, and the working-session route connected instead of turning caution into friction.',
|
||||
intent: 'guidance',
|
||||
primaryCta: {
|
||||
href: '/contact',
|
||||
label: 'Discuss trust requirements',
|
||||
helper: 'Bring current review, legal, or rollout questions into one working conversation.',
|
||||
variant: 'primary',
|
||||
},
|
||||
},
|
||||
content: {
|
||||
eyebrow: 'Reading should still progress',
|
||||
title: 'Long-form pages need the same action hierarchy as landing pages.',
|
||||
description:
|
||||
'Contact, legal, privacy, and terms routes should feel deliberate and connected to the evaluation path instead of behaving like detached documents.',
|
||||
intent: 'legal',
|
||||
primaryCta: {
|
||||
href: '/contact',
|
||||
label: 'Continue the evaluation path',
|
||||
helper: 'Move from the reading surface back into a product or trust conversation.',
|
||||
variant: 'primary',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const headerCtaByFamily: Record<PageFamily, CtaLink> = {
|
||||
landing: {
|
||||
href: '/contact',
|
||||
label: 'Request a working session',
|
||||
helper: 'Bring your governance questions, rollout concerns, or evaluation goals.',
|
||||
variant: 'secondary',
|
||||
},
|
||||
trust: {
|
||||
href: '/contact',
|
||||
label: 'Discuss trust requirements',
|
||||
helper: 'Route trust, legal, or rollout questions into one conversation.',
|
||||
variant: 'secondary',
|
||||
},
|
||||
content: {
|
||||
href: '/contact',
|
||||
label: 'Start the working session',
|
||||
helper: 'Turn the reading path into a concrete next step.',
|
||||
variant: 'secondary',
|
||||
},
|
||||
};
|
||||
|
||||
export const pageDefinitions: Record<SitePath, PageDefinition> = {
|
||||
'/': { path: '/', pageRole: 'home', family: 'landing', shellTone: 'brand' },
|
||||
'/product': { path: '/product', pageRole: 'product', family: 'landing', shellTone: 'brand' },
|
||||
'/solutions': { path: '/solutions', pageRole: 'solutions', family: 'landing', shellTone: 'brand' },
|
||||
'/security-trust': { path: '/security-trust', pageRole: 'trust', family: 'trust', shellTone: 'trust' },
|
||||
'/integrations': { path: '/integrations', pageRole: 'integrations', family: 'landing', shellTone: 'neutral' },
|
||||
'/contact': { path: '/contact', pageRole: 'contact', family: 'content', shellTone: 'neutral' },
|
||||
'/legal': { path: '/legal', pageRole: 'legal', family: 'trust', shellTone: 'trust' },
|
||||
'/privacy': { path: '/privacy', pageRole: 'privacy', family: 'content', shellTone: 'neutral' },
|
||||
'/terms': { path: '/terms', pageRole: 'terms', family: 'content', shellTone: 'neutral' },
|
||||
};
|
||||
|
||||
export const coreRoutes = Object.keys(pageDefinitions) as SitePath[];
|
||||
|
||||
export function getPageDefinition(path: string): PageDefinition {
|
||||
return pageDefinitions[path as SitePath] ?? pageDefinitions['/'];
|
||||
}
|
||||
|
||||
export function getPageFamily(path: string): PageFamily {
|
||||
return getPageDefinition(path).family;
|
||||
}
|
||||
|
||||
export function getShellTone(path: string): ShellTone {
|
||||
return getPageDefinition(path).shellTone;
|
||||
}
|
||||
|
||||
export function getHeaderCta(path: string): CtaLink {
|
||||
const definition = getPageDefinition(path);
|
||||
|
||||
return {
|
||||
...headerCtaByFamily[definition.family],
|
||||
...definition.headerCta,
|
||||
};
|
||||
}
|
||||
|
||||
export function getFooterLead(path: string): FooterLead {
|
||||
const definition = getPageDefinition(path);
|
||||
|
||||
return {
|
||||
...footerLeadByFamily[definition.family],
|
||||
...definition.footerLead,
|
||||
};
|
||||
}
|
||||
|
||||
export function isActiveNavigationPath(currentPath: string, href: string): boolean {
|
||||
if (href === '/') {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
---
|
||||
import ContactPanel from '@/components/content/ContactPanel.astro';
|
||||
import DemoPrompt from '@/components/content/DemoPrompt.astro';
|
||||
import SecondaryCTA from '@/components/content/SecondaryCTA.astro';
|
||||
import RichText from '@/components/content/RichText.astro';
|
||||
import PageShell from '@/components/layout/PageShell.astro';
|
||||
import Card from '@/components/primitives/Card.astro';
|
||||
@ -30,8 +31,8 @@ import {
|
||||
calloutDescription="The page should make it obvious who should reach out, why, and what a useful first exchange looks like."
|
||||
/>
|
||||
|
||||
<Section>
|
||||
<Container wide>
|
||||
<Section tone="muted" density="base" layer="2">
|
||||
<Container width="wide">
|
||||
<Grid cols="3">
|
||||
<ContactPanel cta={contactHero.primaryCta} points={contactTopics} title="Who should get in touch" />
|
||||
{contactPrompts.map((prompt) => <DemoPrompt title={prompt.title} description={prompt.description} />)}
|
||||
@ -39,13 +40,13 @@ import {
|
||||
</Container>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<Container wide>
|
||||
<Section density="base" layer="3">
|
||||
<Container width="wide">
|
||||
<div class="grid gap-6 lg:grid-cols-[1fr,0.8fr]">
|
||||
<Card>
|
||||
<SectionHeader
|
||||
eyebrow="Suggested note"
|
||||
title="Give the team enough context to make the first reply useful."
|
||||
title="Structure the first conversation before anyone shares sensitive context."
|
||||
description="The public path stays static in v0, but it can still help a serious buyer structure the first message."
|
||||
/>
|
||||
<div class="mt-6 space-y-4">
|
||||
@ -61,24 +62,9 @@ import {
|
||||
description="Visitors should be able to inspect privacy and terms before they continue."
|
||||
/>
|
||||
<Cluster class="mt-6">
|
||||
<a
|
||||
href="/privacy"
|
||||
class="inline-flex min-h-11 items-center justify-center rounded-full border border-[color:var(--color-line)] bg-white/85 px-5 text-sm font-semibold text-[var(--color-ink-900)]"
|
||||
>
|
||||
Privacy
|
||||
</a>
|
||||
<a
|
||||
href="/terms"
|
||||
class="inline-flex min-h-11 items-center justify-center rounded-full border border-[color:var(--color-line)] bg-white/85 px-5 text-sm font-semibold text-[var(--color-ink-900)]"
|
||||
>
|
||||
Terms
|
||||
</a>
|
||||
<a
|
||||
href="/legal"
|
||||
class="inline-flex min-h-11 items-center justify-center rounded-full border border-[color:var(--color-line)] bg-white/85 px-5 text-sm font-semibold text-[var(--color-ink-900)]"
|
||||
>
|
||||
Legal
|
||||
</a>
|
||||
<SecondaryCTA cta={{ href: '/privacy', label: 'Privacy', variant: 'secondary' }} size="sm" />
|
||||
<SecondaryCTA cta={{ href: '/terms', label: 'Terms', variant: 'secondary' }} size="sm" />
|
||||
<SecondaryCTA cta={{ href: '/legal', label: 'Legal', variant: 'secondary' }} size="sm" />
|
||||
</Cluster>
|
||||
<div class="mt-6">
|
||||
<RichText sections={contactLegalSections} />
|
||||
|
||||
@ -35,18 +35,18 @@ import {
|
||||
|
||||
<FeatureGrid
|
||||
eyebrow="Product pillars"
|
||||
title="Explain the product in connected pillars, not isolated promises."
|
||||
description="Each section of the site should help a first-time visitor understand why backup, restore, findings, evidence, and reviews belong together."
|
||||
title="Carry one visual language from product orientation into proof."
|
||||
description="Each section should make the operating model easier to follow without switching into a louder or more decorative visual mode."
|
||||
items={homePillars}
|
||||
/>
|
||||
|
||||
<Section>
|
||||
<Container wide>
|
||||
<Section tone="muted" density="base" layer="3">
|
||||
<Container width="wide">
|
||||
<div class="space-y-8">
|
||||
<SectionHeader
|
||||
eyebrow="Public proof"
|
||||
title="A credible first reading should answer the buyer’s next two questions before they ask them."
|
||||
description="Why is this product category needed now, and why should anyone trust the story enough to continue?"
|
||||
title="A credible first read should answer the next question without another layout mode."
|
||||
description="Why is this category needed, why is the story believable, and why should a serious buyer keep reading?"
|
||||
/>
|
||||
<Grid cols="3">
|
||||
{homeProofBlocks.map((block) => <Callout content={block} />)}
|
||||
@ -57,7 +57,7 @@ import {
|
||||
|
||||
<CTASection
|
||||
eyebrow="Next step"
|
||||
title="Move from first-glance clarity into the deeper product story."
|
||||
title="Move from category clarity into the deeper product and trust path."
|
||||
description="From the Home page, visitors should be able to inspect the product model, review trust framing, or reach the contact path without guessing where to go next."
|
||||
primary={{ href: '/product', label: 'See the product model' }}
|
||||
secondary={{ href: '/contact', label: 'Start the working session', variant: 'secondary' }}
|
||||
|
||||
@ -23,12 +23,12 @@ import {
|
||||
calloutDescription="The integrations page should reinforce where the product actually fits today and why those boundaries improve trust rather than limit it."
|
||||
/>
|
||||
|
||||
<Section>
|
||||
<Container wide>
|
||||
<Section tone="muted" density="base" layer="2">
|
||||
<Container width="wide">
|
||||
<div class="space-y-8">
|
||||
<SectionHeader
|
||||
eyebrow="Current direction"
|
||||
title="Show the systems that shape the product workflow today."
|
||||
title="Keep ecosystem direction sharp, current, and bounded."
|
||||
description="This page should stay focused on the contracts and ecosystems that matter to Microsoft tenant governance work now."
|
||||
/>
|
||||
<Grid cols="2">
|
||||
|
||||
@ -18,12 +18,12 @@ import { legalHero, legalNoticeSections, legalSeo } from '@/content/pages/legal'
|
||||
calloutDescription="The legal hub keeps the conversion path honest by making privacy, terms, and notice routing easy to find before or during evaluation conversations."
|
||||
/>
|
||||
|
||||
<Section>
|
||||
<Container wide>
|
||||
<Section tone="muted" density="base" layer="2">
|
||||
<Container width="wide">
|
||||
<div class="space-y-8">
|
||||
<SectionHeader
|
||||
eyebrow="Available now"
|
||||
title="Privacy and terms are published as standalone routes."
|
||||
title="Use one legal surface for privacy, terms, and notice routing."
|
||||
description="The legal hub should work as an index and as the public home for launch-required legal notices."
|
||||
/>
|
||||
<Grid cols="3">
|
||||
@ -59,8 +59,8 @@ import { legalHero, legalNoticeSections, legalSeo } from '@/content/pages/legal'
|
||||
</Container>
|
||||
</Section>
|
||||
|
||||
<Section id="public-legal-notice">
|
||||
<Container wide>
|
||||
<Section id="public-legal-notice" density="compact" layer="3">
|
||||
<Container width="measure">
|
||||
<RichText sections={legalNoticeSections} />
|
||||
</Container>
|
||||
</Section>
|
||||
|
||||
@ -15,8 +15,8 @@ import { privacyHero, privacySections, privacySeo } from '@/content/pages/privac
|
||||
calloutDescription="The page explains the website and inquiry path clearly without pretending to be the product’s full data-processing documentation."
|
||||
/>
|
||||
|
||||
<Section>
|
||||
<Container wide>
|
||||
<Section tone="muted" density="base" layer="2">
|
||||
<Container width="measure">
|
||||
<RichText sections={privacySections} />
|
||||
</Container>
|
||||
</Section>
|
||||
|
||||
@ -27,17 +27,17 @@ import {
|
||||
|
||||
<FeatureGrid
|
||||
eyebrow="Connected governance model"
|
||||
title="Treat the product as one operating system for safer tenant change management."
|
||||
description="This page explains how the pieces fit together so visitors do not mistake the product for a loose collection of backup, reporting, and restore features."
|
||||
title="Make the operating model legible before the feature list."
|
||||
description="This page should explain how the pieces fit together so visitors do not mistake the product for a loose collection of backup, reporting, and restore features."
|
||||
items={productModelBlocks}
|
||||
/>
|
||||
|
||||
<Section>
|
||||
<Container wide>
|
||||
<Section tone="muted" density="base" layer="3">
|
||||
<Container width="wide">
|
||||
<div class="space-y-8">
|
||||
<SectionHeader
|
||||
eyebrow="Narrative"
|
||||
title="Explain the operator journey, not just the capabilities."
|
||||
title="Keep the path from observation to action readable."
|
||||
description="The public product page should make it obvious how the product helps a team move from current-state understanding into reviewable action."
|
||||
/>
|
||||
<Grid cols="3">
|
||||
|
||||
@ -29,17 +29,17 @@ import {
|
||||
|
||||
<TrustGrid
|
||||
eyebrow="Product posture"
|
||||
title="Operational trust starts with the way the product handles risky decisions."
|
||||
title="Show operator safeguards with restrained public claims."
|
||||
description="The trust page should explain the guardrails that matter to a serious buyer: previewability, attributable change history, evidence linkage, and restrained public claims."
|
||||
items={securityPrinciples}
|
||||
/>
|
||||
|
||||
<Section>
|
||||
<Container wide>
|
||||
<Section tone="muted" density="base" layer="3">
|
||||
<Container width="wide">
|
||||
<div class="space-y-8">
|
||||
<SectionHeader
|
||||
eyebrow="Public messaging"
|
||||
title="Substantiated public posture"
|
||||
title="Substantiated posture should stay narrower than internal ambition."
|
||||
description="Keep the public trust story within the set of claims the team can support at launch."
|
||||
/>
|
||||
<Grid cols="3">
|
||||
|
||||
@ -23,12 +23,12 @@ import {
|
||||
calloutDescription="The public site can speak differently to MSP and enterprise visitors while staying anchored to the same product truth."
|
||||
/>
|
||||
|
||||
<Section>
|
||||
<Container wide>
|
||||
<Section tone="muted" density="base" layer="2">
|
||||
<Container width="wide">
|
||||
<div class="space-y-8">
|
||||
<SectionHeader
|
||||
eyebrow="Operating models"
|
||||
title="Separate the delivery context clearly."
|
||||
title="Review MSP and enterprise fit without changing the product story."
|
||||
description="Visitors should be able to recognize themselves in the page quickly, without translating a generic story into their own workflow."
|
||||
/>
|
||||
<Grid cols="2">
|
||||
|
||||
@ -15,8 +15,8 @@ import { termsHero, termsSections, termsSeo } from '@/content/pages/terms';
|
||||
calloutDescription="The page keeps the public website honest about what it can explain and what still belongs in later commercial/legal paperwork."
|
||||
/>
|
||||
|
||||
<Section>
|
||||
<Container wide>
|
||||
<Section tone="muted" density="base" layer="2">
|
||||
<Container width="measure">
|
||||
<RichText sections={termsSections} />
|
||||
</Container>
|
||||
</Section>
|
||||
|
||||
@ -1,28 +1,8 @@
|
||||
@import "tailwindcss";
|
||||
@import "./tokens.css";
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--color-ink-900: #11243a;
|
||||
--color-ink-800: #233a53;
|
||||
--color-copy: #42556a;
|
||||
--color-line: rgba(17, 36, 58, 0.14);
|
||||
--color-panel: rgba(255, 255, 255, 0.82);
|
||||
--color-panel-strong: rgba(255, 255, 255, 0.95);
|
||||
--color-panel-soft: rgba(243, 247, 251, 0.86);
|
||||
--color-brand: #2f6fb7;
|
||||
--color-brand-soft: rgba(47, 111, 183, 0.12);
|
||||
--color-signal: #3b8b78;
|
||||
--color-warm: #af6d43;
|
||||
--shadow-panel: 0 24px 80px rgba(17, 36, 58, 0.12);
|
||||
--shadow-soft: 0 18px 48px rgba(17, 36, 58, 0.08);
|
||||
}
|
||||
|
||||
html {
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(255, 255, 255, 0.92), transparent 32%),
|
||||
radial-gradient(circle at top right, rgba(92, 149, 215, 0.18), transparent 28%),
|
||||
linear-gradient(180deg, #f6f3ee 0%, #edf2f7 56%, #f3f7fb 100%);
|
||||
background: var(--color-background);
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
@ -30,7 +10,10 @@ body {
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
font-family: var(--font-sans);
|
||||
color: var(--color-ink-900);
|
||||
color: var(--color-foreground);
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(255, 255, 255, 0.92), transparent 30%),
|
||||
linear-gradient(180deg, var(--color-background) 0%, var(--color-background-elevated) 48%, var(--color-muted) 100%);
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
@ -55,10 +38,10 @@ code {
|
||||
|
||||
::selection {
|
||||
background: rgba(47, 111, 183, 0.18);
|
||||
color: var(--color-ink-900);
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
:where(a, button, input, textarea, summary):focus-visible {
|
||||
outline: 3px solid rgba(47, 111, 183, 0.32);
|
||||
outline-offset: 4px;
|
||||
}
|
||||
@ -67,28 +50,46 @@ main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.surface-shell {
|
||||
.foundation-page {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.surface-shell::before {
|
||||
.foundation-page::before {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -2;
|
||||
content: "";
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.65), transparent 16%),
|
||||
radial-gradient(circle at 20% 20%, rgba(255, 255, 255, 0.7), transparent 28%);
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.68), transparent 16%),
|
||||
radial-gradient(circle at 20% 18%, rgba(255, 255, 255, 0.72), transparent 28%);
|
||||
}
|
||||
|
||||
.surface-shell::after {
|
||||
.foundation-page[data-shell-tone='brand']::before {
|
||||
background:
|
||||
radial-gradient(circle at 8% 0%, rgba(255, 255, 255, 0.82), transparent 30%),
|
||||
radial-gradient(circle at 86% 0%, rgba(47, 111, 183, 0.18), transparent 28%),
|
||||
radial-gradient(circle at 72% 22%, rgba(59, 139, 120, 0.12), transparent 26%),
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.62), transparent 18%);
|
||||
}
|
||||
|
||||
.foundation-page[data-shell-tone='trust']::before {
|
||||
background:
|
||||
radial-gradient(circle at 10% 0%, rgba(255, 255, 255, 0.8), transparent 28%),
|
||||
radial-gradient(circle at 82% 8%, rgba(59, 139, 120, 0.16), transparent 30%),
|
||||
radial-gradient(circle at 74% 30%, rgba(175, 109, 67, 0.08), transparent 26%),
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.62), transparent 18%);
|
||||
}
|
||||
|
||||
.foundation-page::after {
|
||||
position: absolute;
|
||||
inset: 1rem;
|
||||
inset: clamp(0.7rem, 1vw, 1.2rem);
|
||||
z-index: -1;
|
||||
content: "";
|
||||
border: 1px solid rgba(17, 36, 58, 0.04);
|
||||
border-radius: 2rem;
|
||||
border: 1px solid var(--color-frame);
|
||||
border-radius: calc(var(--radius-panel) + 0.45rem);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.skip-link {
|
||||
@ -97,8 +98,8 @@ .skip-link {
|
||||
left: 1rem;
|
||||
z-index: 40;
|
||||
transform: translateY(-200%);
|
||||
border-radius: 999px;
|
||||
background: var(--color-ink-900);
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--color-foreground);
|
||||
padding: 0.75rem 1rem;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
@ -109,14 +110,83 @@ .skip-link:focus {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.glass-panel {
|
||||
background: linear-gradient(180deg, var(--color-panel-strong), var(--color-panel));
|
||||
box-shadow: var(--shadow-panel);
|
||||
.section-divider {
|
||||
border-top: 1px solid var(--color-border-subtle);
|
||||
}
|
||||
|
||||
.site-shell {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.shell-panel {
|
||||
border: 1px solid rgba(255, 255, 255, 0.72);
|
||||
background: linear-gradient(180deg, var(--surface-shell-strong), var(--surface-shell));
|
||||
box-shadow: var(--shadow-panel-strong);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.section-divider {
|
||||
border-top: 1px solid rgba(17, 36, 58, 0.08);
|
||||
.glass-panel {
|
||||
border: 1px solid rgba(255, 255, 255, 0.72);
|
||||
background: linear-gradient(180deg, var(--surface-shell-strong), var(--surface-shell));
|
||||
box-shadow: var(--shadow-panel-strong);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.surface-card {
|
||||
border: 1px solid var(--color-border);
|
||||
background: linear-gradient(180deg, var(--color-card), var(--surface-card-soft));
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.surface-card-muted {
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
background: linear-gradient(180deg, var(--surface-muted-strong), var(--surface-muted));
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.surface-card-accent {
|
||||
border: 1px solid var(--color-border-strong);
|
||||
background: linear-gradient(180deg, var(--surface-accent-strong), var(--surface-accent));
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.section-density-compact {
|
||||
padding-block: var(--space-section-compact);
|
||||
}
|
||||
|
||||
.section-density-base {
|
||||
padding-block: var(--space-section);
|
||||
}
|
||||
|
||||
.section-density-spacious {
|
||||
padding-block: var(--space-section-spacious);
|
||||
}
|
||||
|
||||
.section-shell-muted {
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.52), rgba(243, 247, 251, 0.4));
|
||||
border-radius: calc(var(--radius-lg) + 0.15rem);
|
||||
}
|
||||
|
||||
.section-shell-emphasis {
|
||||
border: 1px solid var(--color-border-strong);
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.56), rgba(47, 111, 183, 0.05));
|
||||
border-radius: calc(var(--radius-lg) + 0.15rem);
|
||||
}
|
||||
|
||||
.text-link {
|
||||
color: var(--color-foreground);
|
||||
text-decoration: underline;
|
||||
text-decoration-color: rgba(17, 36, 58, 0.2);
|
||||
text-underline-offset: 0.3em;
|
||||
transition:
|
||||
color 140ms ease,
|
||||
text-decoration-color 140ms ease;
|
||||
}
|
||||
|
||||
.text-link:hover {
|
||||
color: var(--color-primary);
|
||||
text-decoration-color: rgba(47, 111, 183, 0.35);
|
||||
}
|
||||
|
||||
.legal-prose p {
|
||||
@ -140,6 +210,21 @@ .legal-prose li + li {
|
||||
margin-top: 0.6rem;
|
||||
}
|
||||
|
||||
.foundation-page [data-disclosure-layer='1'] {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.foundation-page [data-disclosure-layer='2'] {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.foundation-page [data-disclosure-layer='3'] {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.motion-rise {
|
||||
animation: rise-in 520ms ease both;
|
||||
}
|
||||
|
||||
@ -3,17 +3,117 @@ @theme {
|
||||
--font-display: "Iowan Old Style", "Palatino Linotype", serif;
|
||||
--font-mono: "IBM Plex Mono", "SFMono-Regular", monospace;
|
||||
|
||||
--color-shell-50: oklch(0.985 0.01 86);
|
||||
--color-shell-100: oklch(0.97 0.015 85);
|
||||
--color-shell-200: oklch(0.935 0.025 84);
|
||||
--color-shell-300: oklch(0.88 0.04 78);
|
||||
--color-shell-900: oklch(0.19 0.03 65);
|
||||
--color-shell-950: oklch(0.14 0.025 62);
|
||||
--color-brand-400: oklch(0.76 0.09 210);
|
||||
--color-brand-500: oklch(0.68 0.11 218);
|
||||
--color-brand-700: oklch(0.49 0.11 228);
|
||||
--color-signal-400: oklch(0.78 0.1 162);
|
||||
--color-signal-700: oklch(0.52 0.08 170);
|
||||
--color-warm-300: oklch(0.87 0.05 53);
|
||||
--color-warm-500: oklch(0.69 0.09 48);
|
||||
--color-stone-50: oklch(0.986 0.008 86);
|
||||
--color-stone-100: oklch(0.974 0.012 84);
|
||||
--color-stone-150: oklch(0.958 0.016 82);
|
||||
--color-stone-200: oklch(0.936 0.018 79);
|
||||
--color-stone-300: oklch(0.896 0.025 76);
|
||||
--color-ink-700: oklch(0.4 0.04 244);
|
||||
--color-ink-800: oklch(0.31 0.04 248);
|
||||
--color-ink-900: oklch(0.23 0.038 252);
|
||||
--color-brand-300: oklch(0.84 0.05 228);
|
||||
--color-brand-400: oklch(0.76 0.09 214);
|
||||
--color-brand-500: oklch(0.67 0.12 226);
|
||||
--color-brand-700: oklch(0.48 0.1 232);
|
||||
--color-mint-300: oklch(0.85 0.05 182);
|
||||
--color-mint-500: oklch(0.72 0.07 186);
|
||||
--color-mint-700: oklch(0.54 0.07 184);
|
||||
--color-amber-300: oklch(0.88 0.045 73);
|
||||
--color-amber-500: oklch(0.75 0.085 62);
|
||||
--color-amber-700: oklch(0.56 0.09 54);
|
||||
--color-red-500: oklch(0.62 0.16 28);
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
|
||||
--color-background: var(--color-stone-50);
|
||||
--color-background-elevated: var(--color-stone-100);
|
||||
--color-foreground: var(--color-ink-900);
|
||||
--color-muted: var(--color-stone-150);
|
||||
--color-muted-foreground: var(--color-ink-700);
|
||||
--color-card: rgba(255, 255, 255, 0.9);
|
||||
--color-card-foreground: var(--color-ink-900);
|
||||
--color-border: rgba(17, 36, 58, 0.12);
|
||||
--color-border-strong: rgba(47, 111, 183, 0.22);
|
||||
--color-border-subtle: rgba(17, 36, 58, 0.07);
|
||||
--color-frame: rgba(17, 36, 58, 0.06);
|
||||
--color-input: rgba(255, 255, 255, 0.94);
|
||||
--color-primary: var(--color-brand-500);
|
||||
--color-primary-foreground: #f9fbff;
|
||||
--color-secondary: rgba(255, 255, 255, 0.82);
|
||||
--color-secondary-foreground: var(--color-ink-900);
|
||||
--color-accent: rgba(47, 111, 183, 0.1);
|
||||
--color-accent-foreground: var(--color-brand-700);
|
||||
--color-success: var(--color-mint-700);
|
||||
--color-warning: var(--color-amber-700);
|
||||
--color-destructive: var(--color-red-500);
|
||||
--color-info: var(--color-brand-700);
|
||||
|
||||
--surface-page: rgba(255, 255, 255, 0.34);
|
||||
--surface-shell: rgba(255, 255, 255, 0.78);
|
||||
--surface-shell-strong: rgba(255, 255, 255, 0.94);
|
||||
--surface-card-soft: rgba(246, 248, 251, 0.82);
|
||||
--surface-muted: rgba(243, 247, 251, 0.88);
|
||||
--surface-muted-strong: rgba(247, 249, 252, 0.94);
|
||||
--surface-accent: rgba(47, 111, 183, 0.1);
|
||||
--surface-accent-strong: rgba(241, 246, 253, 0.98);
|
||||
--surface-trust: rgba(59, 139, 120, 0.09);
|
||||
|
||||
--radius-sm: 1rem;
|
||||
--radius-md: 1.35rem;
|
||||
--radius-lg: 1.75rem;
|
||||
--radius-panel: 2rem;
|
||||
--radius-pill: 999px;
|
||||
|
||||
--shadow-panel-strong: 0 28px 90px rgba(17, 36, 58, 0.14);
|
||||
--shadow-card: 0 20px 56px rgba(17, 36, 58, 0.1);
|
||||
--shadow-soft: 0 12px 36px rgba(17, 36, 58, 0.08);
|
||||
--shadow-inline: 0 10px 22px rgba(17, 36, 58, 0.08);
|
||||
|
||||
--space-page-x: clamp(1.25rem, 2vw, 2.5rem);
|
||||
--space-page-y: clamp(4rem, 6vw, 6rem);
|
||||
--space-section-compact: clamp(3rem, 4vw, 4.25rem);
|
||||
--space-section: clamp(4rem, 6vw, 5.75rem);
|
||||
--space-section-spacious: clamp(5rem, 7vw, 7rem);
|
||||
--space-cluster-sm: 0.75rem;
|
||||
--space-cluster: 1rem;
|
||||
--space-cluster-lg: 1.5rem;
|
||||
--space-stack-sm: 0.75rem;
|
||||
--space-stack: 1.25rem;
|
||||
--space-stack-lg: 1.75rem;
|
||||
--space-grid: 1.25rem;
|
||||
--space-grid-lg: 1.75rem;
|
||||
|
||||
--content-max-width: 76rem;
|
||||
--wide-max-width: 84rem;
|
||||
--reading-max-width: 68rem;
|
||||
|
||||
--type-display-size: clamp(3.3rem, 6vw, 5rem);
|
||||
--type-page-size: clamp(2.75rem, 4.2vw, 3.75rem);
|
||||
--type-section-size: clamp(2rem, 3.5vw, 3rem);
|
||||
--type-card-size: clamp(1.35rem, 2vw, 1.85rem);
|
||||
--type-body-size: 1.02rem;
|
||||
--type-small-size: 0.94rem;
|
||||
--type-eyebrow-size: 0.74rem;
|
||||
--type-helper-size: 0.82rem;
|
||||
--tracking-display: -0.055em;
|
||||
--tracking-tight: -0.03em;
|
||||
--tracking-eyebrow: 0.18em;
|
||||
--line-display: 0.95;
|
||||
--line-heading: 1.02;
|
||||
--line-body: 1.75;
|
||||
--line-tight: 1.45;
|
||||
|
||||
/* Legacy aliases used by existing primitives and content blocks. */
|
||||
--color-copy: var(--color-muted-foreground);
|
||||
--color-line: var(--color-border);
|
||||
--color-panel: var(--surface-shell);
|
||||
--color-panel-strong: var(--surface-shell-strong);
|
||||
--color-panel-soft: var(--surface-muted);
|
||||
--color-brand: var(--color-primary);
|
||||
--color-brand-soft: var(--surface-accent);
|
||||
--color-signal: var(--color-success);
|
||||
--color-warm: var(--color-warning);
|
||||
--shadow-panel: var(--shadow-panel-strong);
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
export type ButtonVariant = 'primary' | 'secondary' | 'ghost';
|
||||
export type PageFamily = 'landing' | 'trust' | 'content';
|
||||
export type PageRole =
|
||||
| 'home'
|
||||
| 'product'
|
||||
@ -6,7 +7,21 @@ export type PageRole =
|
||||
| 'trust'
|
||||
| 'integrations'
|
||||
| 'contact'
|
||||
| 'legal';
|
||||
| 'legal'
|
||||
| 'privacy'
|
||||
| 'terms';
|
||||
export type SitePath =
|
||||
| '/'
|
||||
| '/product'
|
||||
| '/solutions'
|
||||
| '/security-trust'
|
||||
| '/integrations'
|
||||
| '/contact'
|
||||
| '/legal'
|
||||
| '/privacy'
|
||||
| '/terms';
|
||||
export type ShellTone = 'brand' | 'neutral' | 'trust';
|
||||
export type FooterIntent = 'conversion' | 'guidance' | 'legal';
|
||||
|
||||
export interface CtaLink {
|
||||
href: string;
|
||||
@ -27,6 +42,23 @@ export interface FooterNavigationGroup {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface FooterLead {
|
||||
description: string;
|
||||
eyebrow: string;
|
||||
intent: FooterIntent;
|
||||
primaryCta: CtaLink;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface PageDefinition {
|
||||
family: PageFamily;
|
||||
footerLead?: Partial<FooterLead>;
|
||||
headerCta?: Partial<CtaLink>;
|
||||
pageRole: PageRole;
|
||||
path: SitePath;
|
||||
shellTone: ShellTone;
|
||||
}
|
||||
|
||||
export interface SiteMetadata {
|
||||
siteDescription: string;
|
||||
siteName: string;
|
||||
@ -103,3 +135,11 @@ export interface LegalSection {
|
||||
bullets?: string[];
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface VisualFoundationContract {
|
||||
accessibilityBaseline: readonly string[];
|
||||
ctaHierarchy: readonly ButtonVariant[];
|
||||
pageFamilies: readonly PageFamily[];
|
||||
requiredColorRoles: readonly string[];
|
||||
surfaceLayers: readonly string[];
|
||||
}
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import {
|
||||
expectDisclosureLayer,
|
||||
expectFooterLinks,
|
||||
expectNavigationVsCtaDifferentiation,
|
||||
expectPageFamily,
|
||||
expectPrimaryNavigation,
|
||||
expectShell,
|
||||
openMobileNavigation,
|
||||
@ -13,8 +16,17 @@ const coreRoutes = ['/', '/product', '/solutions', '/security-trust', '/integrat
|
||||
test('contact page qualifies the conversation and keeps legal links reachable', async ({ page }) => {
|
||||
await visitPage(page, '/contact');
|
||||
await expectShell(page, 'Start a qualified working session instead of a generic demo request.');
|
||||
await expectPageFamily(page, 'content');
|
||||
await expectDisclosureLayer(page, '1');
|
||||
await expectDisclosureLayer(page, '2');
|
||||
await expectPrimaryNavigation(page);
|
||||
await expectNavigationVsCtaDifferentiation(page);
|
||||
await expectFooterLinks(page);
|
||||
await expect(
|
||||
page.getByRole('heading', {
|
||||
name: 'Structure the first conversation before anyone shares sensitive context.',
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole('main').getByRole('link', { name: 'Email the TenantAtlas team' }).first()).toBeVisible();
|
||||
await expect(page.getByRole('main').getByRole('link', { name: 'Privacy' }).first()).toBeVisible();
|
||||
await expect(page.getByRole('main').getByRole('link', { name: 'Terms' }).first()).toBeVisible();
|
||||
@ -23,14 +35,22 @@ test('contact page qualifies the conversation and keeps legal links reachable',
|
||||
test('legal, privacy, and terms routes are published and linked', async ({ page }) => {
|
||||
await visitPage(page, '/legal');
|
||||
await expectShell(page, 'Legal access should stay one click away from the contact path.');
|
||||
await expectPageFamily(page, 'trust');
|
||||
await expectDisclosureLayer(page, '1');
|
||||
await expectDisclosureLayer(page, '2');
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Use one legal surface for privacy, terms, and notice routing.' }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole('main').getByRole('link', { name: 'Privacy' }).first()).toBeVisible();
|
||||
await expect(page.getByRole('main').getByRole('link', { name: 'Terms' }).first()).toBeVisible();
|
||||
|
||||
await visitPage(page, '/privacy');
|
||||
await expectShell(page, 'Public-site privacy overview for TenantAtlas inquiries.');
|
||||
await expectPageFamily(page, 'content');
|
||||
|
||||
await visitPage(page, '/terms');
|
||||
await expectShell(page, 'Website terms for the public TenantAtlas surface.');
|
||||
await expectPageFamily(page, 'content');
|
||||
});
|
||||
|
||||
test('core pages keep contact and legal paths within reach', async ({ page }) => {
|
||||
@ -50,6 +70,7 @@ test.describe('mobile navigation', () => {
|
||||
test('mobile menu exposes the published contact and legal paths', async ({ page }) => {
|
||||
await visitPage(page, '/');
|
||||
await openMobileNavigation(page);
|
||||
await expect(page.locator('[data-mobile-nav]').first()).toBeVisible();
|
||||
await expect(page.getByRole('banner').getByRole('link', { name: /Contact/ }).first()).toBeVisible();
|
||||
await expect(page.getByRole('contentinfo').getByRole('link', { name: 'Privacy' })).toBeVisible();
|
||||
await expect(page.getByRole('contentinfo').getByRole('link', { name: 'Terms' })).toBeVisible();
|
||||
|
||||
@ -1,39 +1,53 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import {
|
||||
expectCtaHierarchy,
|
||||
expectDisclosureLayer,
|
||||
expectFooterLinks,
|
||||
expectNavigationVsCtaDifferentiation,
|
||||
expectPageFamily,
|
||||
expectPrimaryNavigation,
|
||||
expectShell,
|
||||
visitPage,
|
||||
} from './smoke-helpers';
|
||||
|
||||
test('home explains the product category and exposes the next step', async ({ page }) => {
|
||||
test('home uses the landing foundation to explain the product category with one clear action hierarchy', async ({
|
||||
page,
|
||||
}) => {
|
||||
await visitPage(page, '/');
|
||||
await expectShell(page, /TenantAtlas/);
|
||||
await expectPageFamily(page, 'landing');
|
||||
await expectDisclosureLayer(page, '1');
|
||||
await expectDisclosureLayer(page, '2');
|
||||
await expectPrimaryNavigation(page);
|
||||
await expectNavigationVsCtaDifferentiation(page);
|
||||
await expectFooterLinks(page);
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Governance of record for Microsoft tenant operations.' }).first(),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole('main').getByRole('link', { name: 'See the product model' }).first()).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('main').getByRole('link', { name: 'Review the trust posture' }).first(),
|
||||
page.getByRole('heading', {
|
||||
name: 'Carry one visual language from product orientation into proof.',
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expectCtaHierarchy(page, 'See the product model', 'Review the trust posture');
|
||||
|
||||
const skipLink = page.getByRole('link', { name: 'Skip to content' });
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
await expect(skipLink).toBeFocused();
|
||||
});
|
||||
|
||||
test('product explains the connected operating model instead of a loose feature list', async ({
|
||||
test('product keeps the connected operating model readable without collapsing into a feature list', async ({
|
||||
page,
|
||||
}) => {
|
||||
await visitPage(page, '/product');
|
||||
await expectShell(page, 'One operating model for change history, drift visibility, and review readiness.');
|
||||
await expectPageFamily(page, 'landing');
|
||||
await expectDisclosureLayer(page, '1');
|
||||
await expectDisclosureLayer(page, '2');
|
||||
await expectPrimaryNavigation(page);
|
||||
await expectNavigationVsCtaDifferentiation(page);
|
||||
await expectFooterLinks(page);
|
||||
await expect(page.getByRole('heading', { name: 'Connected governance model' }).first()).toBeVisible();
|
||||
await expect(page.getByRole('main').getByRole('link', { name: 'See audience fit' }).first()).toBeVisible();
|
||||
await expect(
|
||||
page
|
||||
.getByRole('main')
|
||||
.getByRole('link', { name: 'Talk through your current operating model' })
|
||||
.first(),
|
||||
page.getByRole('heading', { name: 'Make the operating model legible before the feature list.' }),
|
||||
).toBeVisible();
|
||||
await expectCtaHierarchy(page, 'See audience fit', 'Talk through your current operating model');
|
||||
});
|
||||
|
||||
@ -22,11 +22,18 @@ export async function expectShell(page: Page, heading: string | RegExp): Promise
|
||||
await expect(page.getByRole('heading', { level: 1, name: heading })).toBeVisible();
|
||||
}
|
||||
|
||||
export async function expectPageFamily(page: Page, family: 'content' | 'landing' | 'trust'): Promise<void> {
|
||||
await expect(page.locator(`[data-page-family="${family}"]`).first()).toBeVisible();
|
||||
}
|
||||
|
||||
export async function expectPrimaryNavigation(page: Page): Promise<void> {
|
||||
const header = page.getByRole('banner');
|
||||
|
||||
for (const label of primaryNavigationLabels) {
|
||||
await expect(header.getByRole('link', { name: label })).toBeVisible();
|
||||
const link = header.getByRole('link', { name: label }).first();
|
||||
|
||||
await expect(link).toBeVisible();
|
||||
await expect(link).toHaveAttribute('data-nav-link');
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,3 +50,27 @@ export async function openMobileNavigation(page: Page): Promise<void> {
|
||||
await menuTrigger.click();
|
||||
}
|
||||
}
|
||||
|
||||
export async function expectDisclosureLayer(page: Page, layer: '1' | '2' | '3'): Promise<void> {
|
||||
await expect(page.locator(`[data-disclosure-layer="${layer}"]`).first()).toBeVisible();
|
||||
}
|
||||
|
||||
export async function expectCtaHierarchy(
|
||||
page: Page,
|
||||
primaryLabel: string | RegExp,
|
||||
secondaryLabel: string | RegExp,
|
||||
): Promise<void> {
|
||||
const main = page.getByRole('main');
|
||||
|
||||
await expect(main.locator('[data-cta-weight="primary"]').filter({ hasText: primaryLabel }).first()).toBeVisible();
|
||||
await expect(
|
||||
main.locator('[data-cta-weight="secondary"]').filter({ hasText: secondaryLabel }).first(),
|
||||
).toBeVisible();
|
||||
}
|
||||
|
||||
export async function expectNavigationVsCtaDifferentiation(page: Page): Promise<void> {
|
||||
const header = page.getByRole('banner');
|
||||
|
||||
await expect(header.locator('[data-nav-link]').first()).toBeVisible();
|
||||
await expect(header.locator('[data-cta-weight="secondary"]').first()).toBeVisible();
|
||||
}
|
||||
|
||||
@ -1,36 +1,61 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import {
|
||||
expectDisclosureLayer,
|
||||
expectFooterLinks,
|
||||
expectNavigationVsCtaDifferentiation,
|
||||
expectPageFamily,
|
||||
expectPrimaryNavigation,
|
||||
expectShell,
|
||||
visitPage,
|
||||
} from './smoke-helpers';
|
||||
|
||||
test('solutions separates MSP and enterprise fit clearly', async ({ page }) => {
|
||||
test('solutions keeps MSP and enterprise audience fit inside one landing-page rhythm', async ({ page }) => {
|
||||
await visitPage(page, '/solutions');
|
||||
await expectShell(page, /MSP|enterprise/i);
|
||||
await expectPageFamily(page, 'landing');
|
||||
await expectDisclosureLayer(page, '1');
|
||||
await expectDisclosureLayer(page, '2');
|
||||
await expectPrimaryNavigation(page);
|
||||
await expectNavigationVsCtaDifferentiation(page);
|
||||
await expectFooterLinks(page);
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Review MSP and enterprise fit without changing the product story.' }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'MSP operating model' })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Enterprise IT operating model' })).toBeVisible();
|
||||
await expect(page.getByRole('main').getByRole('link', { name: 'Review the ecosystem fit' }).first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('security and trust stays grounded in substantiated product posture', async ({ page }) => {
|
||||
test('security and trust stays grounded in substantiated product posture and layered disclosure', async ({
|
||||
page,
|
||||
}) => {
|
||||
await visitPage(page, '/security-trust');
|
||||
await expectShell(page, /trust posture|trust-first/i);
|
||||
await expectPageFamily(page, 'trust');
|
||||
await expectDisclosureLayer(page, '1');
|
||||
await expectDisclosureLayer(page, '2');
|
||||
await expectPrimaryNavigation(page);
|
||||
await expectNavigationVsCtaDifferentiation(page);
|
||||
await expectFooterLinks(page);
|
||||
await expect(page.getByRole('heading', { name: 'Substantiated public posture' }).first()).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Show operator safeguards with restrained public claims.' }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole('main').getByRole('link', { name: 'Read the legal surface' }).first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('integrations shows real ecosystem direction without wishlist claims', async ({ page }) => {
|
||||
test('integrations shows real ecosystem direction without wishlist claims or shell drift', async ({ page }) => {
|
||||
await visitPage(page, '/integrations');
|
||||
await expectShell(page, /ecosystem fit|integrations/i);
|
||||
await expectPageFamily(page, 'landing');
|
||||
await expectDisclosureLayer(page, '1');
|
||||
await expectDisclosureLayer(page, '2');
|
||||
await expectPrimaryNavigation(page);
|
||||
await expectNavigationVsCtaDifferentiation(page);
|
||||
await expectFooterLinks(page);
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Keep ecosystem direction sharp, current, and bounded.' }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText('Microsoft Graph')).toBeVisible();
|
||||
await expect(page.getByText('Entra ID')).toBeVisible();
|
||||
await expect(page.getByRole('main').getByRole('link', { name: 'Plan the working session' }).first()).toBeVisible();
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import {
|
||||
expectCtaHierarchy,
|
||||
expectNavigationVsCtaDifferentiation,
|
||||
expectPageFamily,
|
||||
expectPrimaryNavigation,
|
||||
expectShell,
|
||||
visitPage,
|
||||
} from './smoke-helpers';
|
||||
|
||||
test('representative pages route CTA, badge, surface, and input semantics through shared primitives', async ({
|
||||
page,
|
||||
}) => {
|
||||
await visitPage(page, '/');
|
||||
await expectShell(page, /TenantAtlas/);
|
||||
await expectPageFamily(page, 'landing');
|
||||
await expectPrimaryNavigation(page);
|
||||
await expectNavigationVsCtaDifferentiation(page);
|
||||
await expectCtaHierarchy(page, 'See the product model', 'Review the trust posture');
|
||||
await expect(page.locator('[data-interaction="button"]').filter({ hasText: 'See the product model' }).first()).toBeVisible();
|
||||
await expect(page.locator('[data-badge-tone]').first()).toBeVisible();
|
||||
|
||||
await visitPage(page, '/security-trust');
|
||||
await expectShell(page, /trust posture|trust-first/i);
|
||||
await expect(page.locator('[data-surface="accent"]').first()).toBeVisible();
|
||||
await expect(page.locator('[data-badge-tone]').first()).toBeVisible();
|
||||
|
||||
await visitPage(page, '/contact');
|
||||
await expectShell(page, 'Start a qualified working session instead of a generic demo request.');
|
||||
await expect(page.locator('[data-interaction="input"]').first()).toBeVisible();
|
||||
await expect(page.locator('[data-interaction="textarea"]').first()).toBeVisible();
|
||||
await expect(
|
||||
page.locator('[data-button-variant="secondary"]').filter({ hasText: 'Privacy' }).first(),
|
||||
).toBeVisible();
|
||||
});
|
||||
@ -0,0 +1,35 @@
|
||||
# Specification Quality Checklist: Website Visual Foundation
|
||||
|
||||
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
||||
**Created**: 2026-04-18
|
||||
**Feature**: [spec.md](../spec.md)
|
||||
|
||||
## Content Quality
|
||||
|
||||
- [x] No implementation details (languages, frameworks, APIs)
|
||||
- [x] Focused on user value and business needs
|
||||
- [x] Written for non-technical stakeholders
|
||||
- [x] All mandatory sections completed
|
||||
|
||||
## Requirement Completeness
|
||||
|
||||
- [x] No [NEEDS CLARIFICATION] markers remain
|
||||
- [x] Requirements are testable and unambiguous
|
||||
- [x] Success criteria are measurable
|
||||
- [x] Success criteria are technology-agnostic (no implementation details)
|
||||
- [x] All acceptance scenarios are defined
|
||||
- [x] Edge cases are identified
|
||||
- [x] Scope is clearly bounded
|
||||
- [x] Dependencies and assumptions identified
|
||||
|
||||
## Feature Readiness
|
||||
|
||||
- [x] All functional requirements have clear acceptance criteria
|
||||
- [x] User scenarios cover primary flows
|
||||
- [x] Feature meets measurable outcomes defined in Success Criteria
|
||||
- [x] No implementation details leak into specification
|
||||
|
||||
## Notes
|
||||
|
||||
- Validation passed after refining the success criteria to keep the measurable outcomes technology-agnostic.
|
||||
- The specification remains strictly local to `apps/website` and introduces no obligations for `apps/platform`.
|
||||
@ -0,0 +1,178 @@
|
||||
version: 0.1.0
|
||||
type: design-foundation
|
||||
feature: 214-website-visual-foundation
|
||||
title: Website Visual Foundation Contract
|
||||
summary: >-
|
||||
Website-local visual contract for apps/website. This feature does not add or
|
||||
change HTTP/API surfaces; the contract defines the semantic design rules that
|
||||
implementation must satisfy.
|
||||
scope:
|
||||
app: apps/website
|
||||
includes:
|
||||
- visual direction
|
||||
- design tokens
|
||||
- typography roles
|
||||
- spacing rules
|
||||
- surface rules
|
||||
- interaction semantics
|
||||
- semantic website primitives
|
||||
- shadcn usage constraints
|
||||
- page consistency rules
|
||||
excludes:
|
||||
- apps/platform
|
||||
- Filament theming
|
||||
- shared cross-surface design system
|
||||
- API contracts
|
||||
- logo or brand redesign
|
||||
visual_direction:
|
||||
target_tone:
|
||||
- clear
|
||||
- calm
|
||||
- precise
|
||||
- trustworthy
|
||||
- modern
|
||||
- high-quality
|
||||
- operationally serious
|
||||
disallowed_traits:
|
||||
- playful
|
||||
- neon-heavy
|
||||
- glass-heavy-by-default
|
||||
- over-animated
|
||||
- loud
|
||||
- generic-startup-template
|
||||
token_roles:
|
||||
color:
|
||||
required:
|
||||
- background
|
||||
- foreground
|
||||
- muted
|
||||
- muted-foreground
|
||||
- card
|
||||
- card-foreground
|
||||
- border
|
||||
- input
|
||||
- primary
|
||||
- primary-foreground
|
||||
- secondary
|
||||
- secondary-foreground
|
||||
- accent
|
||||
- accent-foreground
|
||||
- success
|
||||
- warning
|
||||
- destructive
|
||||
- info
|
||||
rules:
|
||||
- neutrals-carry-most-of-the-surface
|
||||
- primary-used-sparingly
|
||||
- status-colors-semantic-only
|
||||
typography:
|
||||
required:
|
||||
- display
|
||||
- page-heading
|
||||
- section-heading
|
||||
- card-heading
|
||||
- body
|
||||
- small
|
||||
- eyebrow
|
||||
- helper
|
||||
spacing:
|
||||
required_levels:
|
||||
- page
|
||||
- section
|
||||
- component
|
||||
surface:
|
||||
required:
|
||||
- page-background
|
||||
- default-content
|
||||
- card
|
||||
- elevated
|
||||
- muted-inset
|
||||
- highlighted
|
||||
radius:
|
||||
preferred_scale:
|
||||
- small
|
||||
- medium
|
||||
- large
|
||||
interaction:
|
||||
buttons:
|
||||
required:
|
||||
- primary
|
||||
- secondary
|
||||
- ghost
|
||||
rules:
|
||||
- no-multiple-equally-loud-ctas-by-default
|
||||
- primary-not-inflationary
|
||||
links:
|
||||
rules:
|
||||
- navigation-vs-inline-intent-must-be-clear
|
||||
inputs:
|
||||
rules:
|
||||
- shared-height-logic
|
||||
- shared-border-logic
|
||||
- shared-focus-logic
|
||||
- clear-error-states
|
||||
primitives:
|
||||
navigation:
|
||||
- header
|
||||
- nav-links
|
||||
- cta-slot
|
||||
- mobile-nav
|
||||
hero:
|
||||
- eyebrow
|
||||
- main-headline
|
||||
- supporting-copy
|
||||
- primary-cta
|
||||
- secondary-cta
|
||||
- trust-supporting-area
|
||||
section:
|
||||
- section-wrapper
|
||||
- section-intro
|
||||
- section-title
|
||||
- section-body
|
||||
- optional-aside
|
||||
grouping:
|
||||
- card
|
||||
- feature-row
|
||||
- comparison-block
|
||||
- callout
|
||||
- quote-frame
|
||||
- logos-bar
|
||||
- stat-block
|
||||
trust:
|
||||
- compliance-callout
|
||||
- evidence-block
|
||||
- changelog-news-block
|
||||
- contact-request-demo-block
|
||||
shadcn_usage:
|
||||
mode: adapted-locally
|
||||
allowed:
|
||||
- use-as-pattern-reference
|
||||
- adapt-into-local-astro-primitives
|
||||
- reduce-or-normalize-default-behavior
|
||||
forbidden:
|
||||
- unreviewed-default-styling-adoption
|
||||
- component-library-driven-page-design
|
||||
- local-token-bypass-overrides
|
||||
- implicit-react-runtime-requirement
|
||||
page_consistency:
|
||||
applies_to:
|
||||
- landing-product
|
||||
- trust-legal
|
||||
- content-heavy
|
||||
rules:
|
||||
- shared-content-width-strategy
|
||||
- shared-heading-hierarchy
|
||||
- shared-section-rhythm
|
||||
- shared-cta-placement-logic
|
||||
- shared-card-and-callout-behavior
|
||||
- shared-footer-structure
|
||||
- shared-link-emphasis-rules
|
||||
validation:
|
||||
required_commands:
|
||||
- corepack pnpm build:website
|
||||
- cd apps/website && corepack pnpm exec playwright test
|
||||
review_checks:
|
||||
- representative-pages-use-shared-foundation
|
||||
- no-platform-coupling-introduced
|
||||
- no-react-or-radix-required-by-default
|
||||
- navigation-and-cta-hierarchy-remain-clear
|
||||
193
specs/214-website-visual-foundation/data-model.md
Normal file
193
specs/214-website-visual-foundation/data-model.md
Normal file
@ -0,0 +1,193 @@
|
||||
# Data Model: Website Visual Foundation
|
||||
|
||||
## Overview
|
||||
|
||||
This feature introduces no database schema. The model is a website-local design contract expressed through tokens, primitive semantics, and page-family consistency rules inside `apps/website`.
|
||||
|
||||
## Entities
|
||||
|
||||
### Website Visual Foundation
|
||||
|
||||
- **Purpose**: The canonical visual contract for `apps/website`.
|
||||
- **Key fields**:
|
||||
- `scope` (`apps/website` only)
|
||||
- `toneKeywords`
|
||||
- `disallowedTraits`
|
||||
- `pageFamilies`
|
||||
- `accessibilityBaseline`
|
||||
- `shadcnUsageMode`
|
||||
- **Relationships**:
|
||||
- Owns many `Token Role` entries
|
||||
- Owns many `Typography Role` entries
|
||||
- Owns many `Spacing Rule` entries
|
||||
- Owns many `Surface Rule` entries
|
||||
- Owns many `Interaction Contract` entries
|
||||
- Owns many `Primitive Contract` entries
|
||||
- Owns many `Page Consistency Rule` entries
|
||||
- **Validation rules**:
|
||||
- Must remain explicitly local to `apps/website`
|
||||
- Must explicitly exclude `apps/platform`, Filament theming, and shared cross-surface obligations
|
||||
- Must define both desired tone and disallowed visual patterns
|
||||
|
||||
### Token Role
|
||||
|
||||
- **Purpose**: A semantic token used to express color, surface, border, radius, or shadow meaning.
|
||||
- **Key fields**:
|
||||
- `name`
|
||||
- `category` (`color`, `surface`, `border`, `radius`, `shadow`)
|
||||
- `semanticPurpose`
|
||||
- `defaultValueSource`
|
||||
- `allowedContexts`
|
||||
- **Relationships**:
|
||||
- Belongs to `Website Visual Foundation`
|
||||
- May be referenced by many `Primitive Contract` entries
|
||||
- May be referenced by many `Page Consistency Rule` entries
|
||||
- **Validation rules**:
|
||||
- Required color roles include `background`, `foreground`, `muted`, `muted-foreground`, `card`, `card-foreground`, `border`, `input`, `primary`, `primary-foreground`, `secondary`, `secondary-foreground`, `accent`, `accent-foreground`, `success`, `warning`, `destructive`, and `info`
|
||||
- Primaries must be used intentionally and not as the dominant surface color
|
||||
- Status roles must remain semantic rather than decorative
|
||||
|
||||
### Typography Role
|
||||
|
||||
- **Purpose**: A named text role that governs hierarchy and readability.
|
||||
- **Key fields**:
|
||||
- `name`
|
||||
- `intent` (`display`, `page`, `section`, `card`, `body`, `small`, `eyebrow`, `helper`)
|
||||
- `fontFamily`
|
||||
- `weight`
|
||||
- `tracking`
|
||||
- `lineHeight`
|
||||
- **Relationships**:
|
||||
- Belongs to `Website Visual Foundation`
|
||||
- May be referenced by many `Primitive Contract` entries
|
||||
- May be referenced by many `Page Consistency Rule` entries
|
||||
- **Validation rules**:
|
||||
- Required roles include display or hero, page, section, card, body, small, eyebrow or label, and UI label or helper text
|
||||
- Body and small text must stay readable on long-form pages
|
||||
- Headlines must read as calm and precise rather than aggressive or novelty-first
|
||||
|
||||
### Spacing Rule
|
||||
|
||||
- **Purpose**: A repeatable spacing decision at page, section, or component level.
|
||||
- **Key fields**:
|
||||
- `level` (`page`, `section`, `component`)
|
||||
- `density`
|
||||
- `gapStrategy`
|
||||
- `maxWidthBehavior`
|
||||
- `rhythmIntent`
|
||||
- **Relationships**:
|
||||
- Belongs to `Website Visual Foundation`
|
||||
- May constrain many `Primitive Contract` entries
|
||||
- May constrain many `Page Consistency Rule` entries
|
||||
- **Validation rules**:
|
||||
- Must define page spacing, section spacing, and component spacing
|
||||
- Must support density shifts between hero-led and text-heavy layouts without abandoning the system
|
||||
- Must avoid page-by-page optical tweaking as the default mechanism
|
||||
|
||||
### Surface Rule
|
||||
|
||||
- **Purpose**: Defines one level of content containment or emphasis.
|
||||
- **Key fields**:
|
||||
- `name` (`page-background`, `default-content`, `card`, `elevated`, `muted-inset`, `highlighted`)
|
||||
- `emphasisLevel`
|
||||
- `borderBehavior`
|
||||
- `shadowBehavior`
|
||||
- `intendedUse`
|
||||
- **Relationships**:
|
||||
- Belongs to `Website Visual Foundation`
|
||||
- May be used by many `Primitive Contract` entries
|
||||
- May be referenced by many `Page Consistency Rule` entries
|
||||
- **Validation rules**:
|
||||
- Surface differences must communicate structure rather than decoration alone
|
||||
- Cards must group meaningful content, not appear arbitrarily as style wrappers
|
||||
- Shadows must remain restrained and border-led clarity must stay primary
|
||||
|
||||
### Interaction Contract
|
||||
|
||||
- **Purpose**: Governs semantic behavior for interactive elements.
|
||||
- **Key fields**:
|
||||
- `elementType` (`button`, `link`, `input`)
|
||||
- `variantSet`
|
||||
- `focusBehavior`
|
||||
- `errorBehavior`
|
||||
- `emphasisRules`
|
||||
- **Relationships**:
|
||||
- Belongs to `Website Visual Foundation`
|
||||
- May be used by many `Primitive Contract` entries
|
||||
- **Validation rules**:
|
||||
- Button hierarchy must at least cover `primary`, `secondary`, and low-emphasis or `ghost`
|
||||
- Links must distinguish navigation intent from inline/supportive CTA behavior
|
||||
- Inputs must share height, border, focus, and error-state logic across the website
|
||||
|
||||
### Primitive Contract
|
||||
|
||||
- **Purpose**: A reusable website building block that carries semantic and visual rules.
|
||||
- **Key fields**:
|
||||
- `name`
|
||||
- `category` (`navigation`, `hero`, `section`, `grouping`, `cta`, `trust`)
|
||||
- `backingComponent`
|
||||
- `allowedVariants`
|
||||
- `requiredTokenRoles`
|
||||
- `defaultSpacingRule`
|
||||
- **Relationships**:
|
||||
- Belongs to `Website Visual Foundation`
|
||||
- May reference many `Token Role`, `Typography Role`, `Spacing Rule`, and `Surface Rule` entries
|
||||
- May be evaluated by many `Page Consistency Rule` entries
|
||||
- **Validation rules**:
|
||||
- Required primitive groups include navigation, hero, section, content grouping, CTA, and trust primitives
|
||||
- Primitive APIs must reinforce the foundation instead of allowing unrestricted local overrides
|
||||
- Existing Astro primitives remain the primary implementation targets
|
||||
|
||||
### shadcn Usage Policy
|
||||
|
||||
- **Purpose**: Defines how `shadcn/ui` concepts may enter the website codebase.
|
||||
- **Key fields**:
|
||||
- `mode` (`adapted-locally`)
|
||||
- `allowedUses`
|
||||
- `forbiddenUses`
|
||||
- `reviewExpectations`
|
||||
- **Relationships**:
|
||||
- Belongs to `Website Visual Foundation`
|
||||
- Constrains many `Primitive Contract` entries
|
||||
- **Validation rules**:
|
||||
- Library patterns must be adapted to website-owned tokens and primitives
|
||||
- Uncontrolled default styling must not become the website’s visual language
|
||||
- The policy must not implicitly require React or a second component runtime
|
||||
|
||||
### Page Consistency Rule
|
||||
|
||||
- **Purpose**: A cross-page rule that keeps the website coherent across route types.
|
||||
- **Key fields**:
|
||||
- `pageFamily`
|
||||
- `contentWidthStrategy`
|
||||
- `headingHierarchy`
|
||||
- `sectionRhythm`
|
||||
- `ctaPlacementLogic`
|
||||
- `footerExpectation`
|
||||
- **Relationships**:
|
||||
- Belongs to `Website Visual Foundation`
|
||||
- Applies to many routes in `apps/website/src/pages`
|
||||
- References many `Primitive Contract` entries
|
||||
- **Validation rules**:
|
||||
- Must cover at least landing/product, trust/legal, and content-heavy page families
|
||||
- Must enforce one recognizable website rhythm across those page families
|
||||
- Must prevent pages from inventing competing CTA or surface hierarchies
|
||||
|
||||
## Relationship Summary
|
||||
|
||||
- `Website Visual Foundation` owns many `Token Role`
|
||||
- `Website Visual Foundation` owns many `Typography Role`
|
||||
- `Website Visual Foundation` owns many `Spacing Rule`
|
||||
- `Website Visual Foundation` owns many `Surface Rule`
|
||||
- `Website Visual Foundation` owns many `Interaction Contract`
|
||||
- `Website Visual Foundation` owns many `Primitive Contract`
|
||||
- `Website Visual Foundation` owns many `Page Consistency Rule`
|
||||
- `Primitive Contract` references `Token Role`, `Typography Role`, `Spacing Rule`, and `Surface Rule`
|
||||
- `Page Consistency Rule` evaluates routes through the required `Primitive Contract` set
|
||||
- `shadcn Usage Policy` constrains how future primitives are authored or adapted
|
||||
|
||||
## State / Lifecycle Notes
|
||||
|
||||
- No persisted runtime states are introduced.
|
||||
- The foundation is repo-owned truth: it is expressed through shared styles, primitive APIs, and representative page composition rather than a database or application state machine.
|
||||
- A primitive or page is compliant when it maps to the defined foundation entities without inventing a competing local visual rule.
|
||||
183
specs/214-website-visual-foundation/plan.md
Normal file
183
specs/214-website-visual-foundation/plan.md
Normal file
@ -0,0 +1,183 @@
|
||||
# Implementation Plan: Website Visual Foundation
|
||||
|
||||
**Branch**: `214-website-visual-foundation` | **Date**: 2026-04-18 | **Spec**: `specs/214-website-visual-foundation/spec.md`
|
||||
**Input**: Feature specification from `specs/214-website-visual-foundation/spec.md`
|
||||
|
||||
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/scripts/` for helper scripts.
|
||||
|
||||
## Summary
|
||||
|
||||
- Keep `apps/website` fully local to the website track and preserve the existing workspace contracts defined by the website working contract.
|
||||
- Build the visual foundation on the current Astro 6 + Tailwind CSS v4 stack by formalizing semantic token roles, typography hierarchy, spacing rules, surface logic, interaction semantics, and page-level consistency rules.
|
||||
- Keep Astro-native primitives as the canonical implementation surface and treat `shadcn/ui` as an adaptation/reference contract rather than introducing React plus official `shadcn/ui` as a new required runtime layer.
|
||||
- Apply the foundation across representative page families already present in `apps/website` so landing, trust/legal, and content-heavy surfaces read as one controlled enterprise website instead of locally styled variants.
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: Astro 6.0.0 templates + TypeScript 5.9 strict
|
||||
**Primary Dependencies**: Astro 6, Tailwind CSS v4 via `@tailwindcss/vite`, Astro content collections, local Astro component primitives, Playwright browser smoke tests
|
||||
**Storage**: Static filesystem content, styles, assets, and content collections under `apps/website/src` and `apps/website/public`; no database
|
||||
**Testing**: Root build proof via `corepack pnpm build:website` plus Playwright browser smoke coverage in `apps/website/tests/smoke`
|
||||
**Validation Lanes**: fast-feedback
|
||||
**Target Platform**: Static public website for modern desktop and mobile browsers
|
||||
**Project Type**: Web (standalone Astro app inside the monorepo)
|
||||
**Performance Goals**: Preserve static HTML output for public routes, keep reading/navigation flows zero-hydration by default, and keep UI refinements within a low-JS, accessibility-first posture
|
||||
**Constraints**: Preserve `@tenantatlas/website`, `WEBSITE_PORT`, and root `dev:website` / `build:website` workflows; do not introduce platform runtime coupling; do not promote website semantics into a shared cross-surface design system; keep any `shadcn/ui` usage adapted to website-owned tokens and primitives
|
||||
**Scale/Scope**: 9 published public routes, existing layout/primitives/sections/content directories, and one website-local visual foundation spanning landing, trust/legal, and content-heavy page families
|
||||
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||
|
||||
- Inventory-first / Graph contract / deterministic capabilities / RBAC-UX / Filament surface rules: N/A for this feature because all planned work stays inside `apps/website` and introduces no `/admin`, `/admin/t/{tenant}/...`, or `/system` runtime behavior.
|
||||
- Read/write separation: Pass. The feature changes public presentation and component composition only; it introduces no website write workflow, backend form handler, queue, or remote call path.
|
||||
- Workspace isolation: Pass. The website remains runtime-independent from `apps/platform`, and the plan preserves the explicit website working contract boundaries.
|
||||
- Data minimization: Pass. The feature only reorganizes or extends public styles, content composition, and component semantics; it introduces no tenant data, secrets, auth state, or operational records.
|
||||
- Test governance (TEST-GOV-001): Pass. Validation remains in `fast-feedback` using the existing website build and local Playwright smoke suite, with no database, auth, provider, or heavy-suite defaults.
|
||||
- Proportionality / no premature abstraction: Pass. The plan extends the current Astro/Tailwind foundation with a narrow website-local semantic layer instead of introducing React, a CMS, or a shared website-platform design system.
|
||||
- Persisted truth / new state: Pass. No database schema, persisted entity, queue lifecycle, or new application state family is introduced.
|
||||
- UI semantics / few layers: Pass. The added semantics stay limited to design tokens, primitive contracts, and page-composition rules for the website, not a reusable cross-app interpretation framework.
|
||||
- Website working contract: Pass. The plan keeps changes local to `apps/website` except for preserving existing root workflow compatibility, and it introduces no new API, auth, DTO, or shared-package coupling to `apps/platform`.
|
||||
|
||||
Status: ✅ No constitution violations for this feature. The implementation remains website-only, static-first, and scoped to the existing Astro website track.
|
||||
|
||||
## Test Governance Check
|
||||
|
||||
> **Fill for any runtime-changing or test-affecting feature. Docs-only or template-only work may state concise `N/A` or `none`.**
|
||||
|
||||
- **Test purpose / classification by changed surface**: Browser smoke coverage for representative public page families plus root static build proof
|
||||
- **Affected validation lanes**: fast-feedback
|
||||
- **Why this lane mix is the narrowest sufficient proof**: The feature changes runtime website rendering, styling, and composition but stays within a static Astro site. Build proof catches asset and route breakage; a focused Playwright smoke suite catches browser-visible regressions in navigation, CTA visibility, representative content surfaces, and mobile usability without introducing backend or heavy end-to-end cost.
|
||||
- **Narrowest proving command(s)**: `corepack pnpm build:website` and `cd apps/website && corepack pnpm exec playwright test`
|
||||
- **Fixture / helper / factory / seed / context cost risks**: none; public website pages require no database, auth, tenant, or provider setup
|
||||
- **Expensive defaults or shared helper growth introduced?**: no; any browser assertions remain local to `apps/website/tests/smoke`
|
||||
- **Heavy-family additions, promotions, or visibility changes**: none
|
||||
- **Closing validation and reviewer handoff**: Re-run the website build and smoke suite after token, primitive, or page-family changes. Reviewers should verify that Home, trust/legal, and another content-heavy route still load cleanly, navigation and footer links remain reachable, CTA hierarchy stays visible, and no unnecessary client framework hydration is introduced.
|
||||
- **Budget / baseline / trend follow-up**: none beyond the existing small website smoke-suite runtime
|
||||
- **Review-stop questions**: Does validation remain fast-feedback only? Did any change introduce hidden React/runtime coupling, broaden the smoke suite beyond representative website flows, or create new platform-facing contracts?
|
||||
- **Escalation path**: document-in-feature
|
||||
- **Why no dedicated follow-up spec is needed**: This feature’s validation scope is tightly bounded to the website’s local rendering and navigation behavior. A separate test-governance spec is only needed if the website later gains interactive workflows, API-backed forms, or broader browser coverage.
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
specs/214-website-visual-foundation/
|
||||
├── plan.md
|
||||
├── research.md
|
||||
├── data-model.md
|
||||
├── quickstart.md
|
||||
├── contracts/
|
||||
│ └── website-visual-foundation.contract.yaml
|
||||
└── tasks.md
|
||||
```
|
||||
|
||||
### Source Code (repository root)
|
||||
|
||||
```text
|
||||
apps/website/
|
||||
├── astro.config.mjs
|
||||
├── package.json
|
||||
├── playwright.config.ts
|
||||
├── public/
|
||||
├── src/
|
||||
│ ├── components/
|
||||
│ │ ├── layout/ # Navbar, Footer, PageShell
|
||||
│ │ ├── primitives/ # Container, Section, Button, Badge, Card, Input, Textarea, Grid, Stack
|
||||
│ │ ├── sections/ # PageHero, FeatureGrid, TrustGrid, CTASection, LogoStrip
|
||||
│ │ └── content/ # Eyebrow, Headline, Lead, Callout, Metric, audience/trust helpers
|
||||
│ ├── content/ # Route content and future content collections
|
||||
│ ├── layouts/
|
||||
│ │ └── BaseLayout.astro
|
||||
│ ├── lib/ # Site metadata, SEO, and helper config
|
||||
│ ├── pages/ # Published public routes
|
||||
│ ├── styles/
|
||||
│ │ ├── global.css
|
||||
│ │ └── tokens.css
|
||||
│ └── types/
|
||||
└── tests/
|
||||
└── smoke/
|
||||
```
|
||||
|
||||
**Structure Decision**: Keep the existing website structure and formalize the design foundation inside the current `styles`, `primitives`, `sections`, `content`, and `layout` layers. The implementation should encode semantic design rules in the Astro-native foundation already present instead of introducing a second component stack.
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
None.
|
||||
|
||||
## Proportionality Review
|
||||
|
||||
- **Current operator problem**: Website contributors and reviewers lack a canonical, website-local visual contract, so styling decisions drift across pages and primitives and dilute enterprise trust.
|
||||
- **Existing structure is insufficient because**: The current site already has tokens, gradients, rounded cards, and reusable primitives, but those choices are only partially codified. They do not yet define the semantic token roles, page-family rules, or `shadcn/ui` usage boundaries needed to prevent future drift.
|
||||
- **Narrowest correct implementation**: Extend the current Astro/Tailwind foundation with explicit semantic token mapping, primitive contracts, and page-consistency rules, then apply those rules to representative routes. Do not add a new client framework, platform coupling, or shared website-platform design system.
|
||||
- **Ownership cost created**: Ongoing maintenance of token semantics, shared primitives, and a small browser smoke suite that protects representative public routes.
|
||||
- **Alternative intentionally rejected**: Continuing page-local styling was rejected because it preserves drift; full React plus official `shadcn/ui` was rejected because it adds unnecessary runtime and maintenance cost; a shared cross-surface design system was rejected because the feature is intentionally website-only.
|
||||
- **Release truth**: Current-release truth for `apps/website`
|
||||
|
||||
## Phase 0 — Outline & Research (complete)
|
||||
|
||||
- Output: `specs/214-website-visual-foundation/research.md`
|
||||
- Key decisions captured:
|
||||
- Preserve the website working contract and keep all visual-language work local to `apps/website`.
|
||||
- Build the foundation on semantic token layering in Tailwind CSS v4 and CSS custom properties rather than page-local utility drift.
|
||||
- Keep Astro-native primitives as the canonical implementation surface and adapt any `shadcn/ui` usage to that layer instead of adding React as a new runtime dependency.
|
||||
- Use representative page families already in the repo to drive the foundation: landing, trust/legal, and content-heavy surfaces.
|
||||
- Validate with root build proof plus the existing Playwright smoke suite for representative public routes.
|
||||
|
||||
## Phase 1 — Design & Contracts (complete)
|
||||
|
||||
### Data model
|
||||
|
||||
- Output: `specs/214-website-visual-foundation/data-model.md`
|
||||
- No database schema changes are required; the model is a website-local design contract expressed through token roles, primitive contracts, and page-family consistency rules.
|
||||
|
||||
### Design contract
|
||||
|
||||
- Output: `specs/214-website-visual-foundation/contracts/website-visual-foundation.contract.yaml`
|
||||
- This feature has no HTTP or API contract changes. The contract artifact captures the website-local visual foundation rules that implementation must satisfy.
|
||||
|
||||
### Quickstart
|
||||
|
||||
- Output: `specs/214-website-visual-foundation/quickstart.md`
|
||||
- Quickstart covers local development, representative implementation order, and the required validation commands.
|
||||
|
||||
### Agent context update
|
||||
|
||||
- Completed via `.specify/scripts/bash/update-agent-context.sh copilot` so the Copilot context reflects the current website stack and this feature’s planning artifacts.
|
||||
|
||||
### Constitution re-check (post-design)
|
||||
|
||||
- ✅ The design remains fully local to `apps/website` and preserves the website working contract.
|
||||
- ✅ No database truth, backend form handling, queueing, or platform-side runtime concern is introduced.
|
||||
- ✅ The semantic layer remains narrow: tokens, primitives, and page-family rules only.
|
||||
- ✅ Validation remains cheap, representative, and website-specific.
|
||||
|
||||
## Phase 2 — Implementation Plan (next)
|
||||
|
||||
### Story 1 (P1): Semantic token and rule foundation
|
||||
|
||||
- Normalize the existing palette and CSS variables into an explicit semantic role model for colors, surfaces, borders, shadows, radius, and typography.
|
||||
- Encode the spacing model and section rhythm into the current Astro foundation so `Container`, `Section`, `SectionHeader`, `Card`, `Button`, `Input`, and `Textarea` expose one canonical visual language.
|
||||
- Keep the site static and light-first, with focus states, contrast, and mobile readability treated as foundation rules rather than page-local cleanup.
|
||||
- Tests / validation:
|
||||
- Confirm the website still builds via `corepack pnpm build:website`.
|
||||
- Re-run smoke coverage for Home and Product, explicitly proving focus visibility, readable contrast, non-color-only semantics, and clear navigation-vs-CTA differentiation.
|
||||
|
||||
### Story 2 (P1): Representative page-family alignment
|
||||
|
||||
- Apply the foundation consistently across representative page families already present in the app: landing (`/` or `/product`), trust/legal (`/security-trust`, `/privacy`, `/terms`), and other content-heavy routes.
|
||||
- Reduce style drift in hero, section intro, callout, stat, and card usage so CTA emphasis, section spacing, surface elevation, and progressive-disclosure layering behave consistently across routes.
|
||||
- Ensure the footer, navigation shell, and CTA placement logic match the foundation’s page-level rules.
|
||||
- Tests / validation:
|
||||
- Extend or adjust browser smoke coverage to assert representative headings, CTA visibility, shell/navigation stability, and progressive-disclosure layering across at least three page families.
|
||||
- Verify mobile navigation remains usable.
|
||||
|
||||
### Story 3 (P2): `shadcn/ui` usage contract in code
|
||||
|
||||
- Encode `shadcn/ui` usage constraints through local primitives and variant APIs so future component work is forced back through website-owned tokens, surfaces, and interaction semantics.
|
||||
- Keep Astro-native wrappers as the primary authoring path; any borrowed `shadcn/ui` pattern must be translated into local primitives instead of exposing uncontrolled library defaults.
|
||||
- Avoid introducing React, Radix, or extra framework/runtime weight unless a later explicit spec proves a concrete need.
|
||||
- Tests / validation:
|
||||
- Re-run build and smoke coverage after primitive API changes.
|
||||
- Review representative components to ensure no page-local style override bypasses the shared token and primitive model.
|
||||
76
specs/214-website-visual-foundation/quickstart.md
Normal file
76
specs/214-website-visual-foundation/quickstart.md
Normal file
@ -0,0 +1,76 @@
|
||||
# Quickstart: Website Visual Foundation
|
||||
|
||||
## Purpose
|
||||
|
||||
This quickstart describes the local workflow for implementing the website-only visual foundation in `apps/website`.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node 20+ available through the repo workspace tooling
|
||||
- Corepack-enabled pnpm
|
||||
- Repo root at `wt-website`
|
||||
|
||||
## Local Development
|
||||
|
||||
Start the website from the repo root:
|
||||
|
||||
```bash
|
||||
corepack pnpm dev:website
|
||||
```
|
||||
|
||||
The website must continue to honor the existing `WEBSITE_PORT` contract.
|
||||
|
||||
## Expected Implementation Order
|
||||
|
||||
Implement the feature in this order:
|
||||
|
||||
1. Audit the current token and foundation layer in `apps/website/src/styles/tokens.css` and `apps/website/src/styles/global.css`.
|
||||
2. Formalize semantic token roles for color, typography, spacing, surfaces, radius, borders, and shadows.
|
||||
3. Update the current Astro primitives so shared rules are enforced through component APIs rather than page-local class decisions.
|
||||
4. Apply the foundation across representative page families: landing/product, trust/legal, and content-heavy routes.
|
||||
5. Encode `shadcn/ui` usage constraints through local primitives and implementation review rules, without introducing a new required React runtime.
|
||||
6. Re-run build and smoke validation.
|
||||
|
||||
## Working Constraints
|
||||
|
||||
The implementation must preserve all existing website working-contract guarantees:
|
||||
|
||||
- keep `@tenantatlas/website` unchanged
|
||||
- keep `WEBSITE_PORT` unchanged
|
||||
- keep root `dev:website` and `build:website` workflows working
|
||||
- keep all changes local to `apps/website` unless a deliberate new contract is explicitly introduced
|
||||
- do not introduce platform runtime coupling, shared auth, or shared DTO/API assumptions
|
||||
|
||||
## Required Validation
|
||||
|
||||
Run the website build proof from the repo root:
|
||||
|
||||
```bash
|
||||
corepack pnpm build:website
|
||||
```
|
||||
|
||||
Run the browser smoke suite from the website app:
|
||||
|
||||
```bash
|
||||
cd apps/website
|
||||
corepack pnpm exec playwright test
|
||||
```
|
||||
|
||||
## What Reviewers Should Verify
|
||||
|
||||
Reviewers should confirm that:
|
||||
|
||||
- representative landing, trust/legal, and content-heavy routes still load correctly
|
||||
- heading hierarchy, section rhythm, and CTA weight are visibly consistent across those page families
|
||||
- navigation and footer links remain reachable
|
||||
- focus states and mobile readability remain intact
|
||||
- no uncontrolled local override bypasses the shared token and primitive model
|
||||
- no React, Radix, or platform coupling is introduced without a separate explicit decision
|
||||
|
||||
## Out of Scope for This Feature
|
||||
|
||||
- Filament theming or `apps/platform` styling
|
||||
- shared website-platform design system work
|
||||
- API-backed contact handling or a website backend
|
||||
- CMS introduction
|
||||
- visual regression infrastructure beyond the current lightweight smoke path
|
||||
58
specs/214-website-visual-foundation/research.md
Normal file
58
specs/214-website-visual-foundation/research.md
Normal file
@ -0,0 +1,58 @@
|
||||
# Research: Website Visual Foundation
|
||||
|
||||
## Decision 1: Preserve the website working contract and keep the feature local to `apps/website`
|
||||
|
||||
- **Decision**: Treat the visual foundation as a Class A/B website-local change under the website working contract and keep all runtime behavior inside `apps/website`.
|
||||
- **Rationale**: The current repo truth explicitly allows website-internal evolution while forbidding silent new coupling to `apps/platform`, shared APIs, shared auth, shared DTOs, or shared packages. This spec is about visual language, so the narrowest correct plan is to preserve that separation completely.
|
||||
- **Alternatives considered**:
|
||||
- Introduce a shared website-platform design system: rejected because the spec explicitly excludes cross-surface contracts and the repo does not currently verify such a coupling.
|
||||
- Solve visual consistency inside platform and website together: rejected because it would violate the website-only boundary and import unnecessary coordination cost.
|
||||
|
||||
## Decision 2: Build the foundation on semantic token layering, not raw palette reuse
|
||||
|
||||
- **Decision**: Keep the current Tailwind CSS v4 CSS-first setup and formalize a two-layer token model: primitive palette and font tokens in `src/styles/tokens.css`, semantic role mapping and surface behavior in the website’s shared foundation styles.
|
||||
- **Rationale**: The repo already uses Tailwind v4 and CSS variables, but the current token layer is only partially semantic. A formal role model is the smallest change that can stop visual drift while preserving the current stack and keeping styling readable in Astro templates.
|
||||
- **Alternatives considered**:
|
||||
- Continue with page-local utilities and one-off CSS variables: rejected because it would preserve the drift problem the spec is meant to eliminate.
|
||||
- Replace the current CSS-first setup with a JavaScript token system or external design-token pipeline: rejected because the website does not need that extra build or ownership complexity.
|
||||
|
||||
## Decision 3: Keep Astro-native primitives canonical and adapt `shadcn/ui` concepts locally
|
||||
|
||||
- **Decision**: Keep the existing Astro primitive layer as the canonical implementation surface. `shadcn/ui` will be treated as a design/build reference whose patterns are adapted into local primitives instead of installed as React plus official `shadcn/ui`.
|
||||
- **Rationale**: `apps/website` currently has no React, Radix, or `shadcn/ui` dependency. The public site is static-first and already composed from Astro primitives. Adapting `shadcn/ui` ideas into that layer satisfies the spec’s requirement for controlled usage without adding a second component runtime or framework abstraction.
|
||||
- **Alternatives considered**:
|
||||
- Add React and official `shadcn/ui` now: rejected because it adds unnecessary runtime and dependency cost for a content-driven static site.
|
||||
- Ignore `shadcn/ui` entirely: rejected because the spec explicitly requires a usage contract; the right answer is controlled adaptation, not omission.
|
||||
|
||||
## Decision 4: Use representative page families already in the repo to drive the foundation
|
||||
|
||||
- **Decision**: Drive the implementation and validation of the visual language through three website page families already present in `apps/website`: landing/product-marketing surfaces, trust/legal surfaces, and content-heavy long-form surfaces.
|
||||
- **Rationale**: The visual foundation only proves itself if it survives more than one hero screen. The existing route set already provides the right coverage: Home or Product for landing behavior, Security & Trust or Privacy/Terms for trust/legal behavior, and Legal/Privacy/Terms for long-form text density and callout rhythm.
|
||||
- **Alternatives considered**:
|
||||
- Optimize only the Home page: rejected because the spec explicitly requires consistency across more than one page type.
|
||||
- Wait for blog or changelog routes before defining the foundation: rejected because the existing legal and trust pages are sufficient to validate long-form behavior now.
|
||||
|
||||
## Decision 5: Encode interaction and surface semantics in shared primitives, not page-local classes
|
||||
|
||||
- **Decision**: Consolidate button hierarchy, link semantics, input behavior, card/surface treatments, and section rhythm through the current primitives and layout helpers instead of letting each page set those rules locally.
|
||||
- **Rationale**: The repo already contains `Button`, `Card`, `Input`, `Textarea`, `Section`, `SectionHeader`, and `PageHero`. Those are the narrowest existing seams where the design foundation can be enforced consistently without inventing a new framework.
|
||||
- **Alternatives considered**:
|
||||
- Document the rules but leave existing primitives unchanged: rejected because the drift would persist in code even if the rules existed in prose.
|
||||
- Introduce many new micro-primitives for every visual nuance: rejected because the foundation should reduce variety, not create a larger component taxonomy.
|
||||
|
||||
## Decision 6: Validate through build proof plus focused Playwright smoke coverage
|
||||
|
||||
- **Decision**: Keep validation in `fast-feedback` using `corepack pnpm build:website` and the existing Playwright smoke suite in `apps/website/tests/smoke`.
|
||||
- **Rationale**: This feature changes runtime rendering, navigation shell, and CTA hierarchy, so build proof alone is not enough. The current local Playwright setup is already the narrowest realistic browser-level proof for representative pages and mobile navigation behavior.
|
||||
- **Alternatives considered**:
|
||||
- Build-only validation: rejected because it would miss browser-visible regressions in hierarchy, navigation, and CTA reachability.
|
||||
- Visual regression or a heavier multi-browser lane: rejected because the current change does not justify that added test-governance cost yet.
|
||||
|
||||
## Baseline Findings
|
||||
|
||||
- `apps/website` already runs as an Astro 6 static app with strict TypeScript, Tailwind CSS v4, and local Playwright smoke tests.
|
||||
- The site already has reusable Astro primitives (`Button`, `Card`, `Section`, `SectionHeader`, `Input`, `Textarea`, `Container`, `Grid`, `Stack`) and section components (`PageHero`, `FeatureGrid`, `TrustGrid`, `CTASection`, `LogoStrip`).
|
||||
- `src/styles/tokens.css` currently defines palette and font tokens, while `src/styles/global.css` still carries important semantic choices such as focus behavior, panel backgrounds, shadows, and decorative shell treatments.
|
||||
- No `shadcn/ui`, Radix, or React dependency is present in `apps/website` today.
|
||||
- Existing published routes already cover the page-family range needed for foundation validation: landing/product, trust/legal, and content-heavy reading surfaces.
|
||||
- Root workspace contracts already expose `corepack pnpm dev:website` and `corepack pnpm build:website`, and those must remain intact.
|
||||
176
specs/214-website-visual-foundation/spec.md
Normal file
176
specs/214-website-visual-foundation/spec.md
Normal file
@ -0,0 +1,176 @@
|
||||
# Feature Specification: Website Visual Foundation
|
||||
|
||||
**Feature Branch**: `214-website-visual-foundation`
|
||||
**Created**: 2026-04-18
|
||||
**Status**: Draft
|
||||
**Input**: User description: "Create a website-only design foundation and visual language for apps/website, covering visual direction, design tokens, typography, spacing, surface rules, shadcn/ui usage, semantic website components, and explicit boundaries that exclude apps/platform."
|
||||
|
||||
## Spec Candidate Check *(mandatory — SPEC-GATE-001)*
|
||||
|
||||
- **Problem**: `apps/website` risks drifting into page-by-page styling decisions that weaken enterprise trust, reduce consistency, and make later page work slower and harder to review.
|
||||
- **Today's failure**: New website sections currently invite ad hoc choices for typography, spacing, surfaces, CTA weight, and default shadcn/ui styling, which can produce a polished page in isolation but an inconsistent website overall.
|
||||
- **User-visible improvement**: Website pages present a calmer, more consistent, and more trustworthy enterprise SaaS visual language across landing, trust, legal, and content-heavy surfaces.
|
||||
- **Smallest enterprise-capable version**: Define a website-local visual foundation that covers tone, token roles, typography, spacing, surface behavior, interaction semantics, semantic page primitives, and shadcn/ui usage constraints.
|
||||
- **Explicit non-goals**: Filament theming, any visual contract for `apps/platform`, a shared cross-surface design system, detailed page IA, final website copy, full implementation of every component, and logo or brand redesign.
|
||||
- **Permanent complexity imported**: A website-local token vocabulary, typography hierarchy, spacing model, surface taxonomy, interaction semantics, component primitive set, and review rules for shadcn/ui usage.
|
||||
- **Why now**: The website is early enough that a foundation can still prevent drift before more pages and components normalize inconsistent patterns.
|
||||
- **Why not local**: Local page fixes or isolated component tweaks cannot solve cross-page consistency, cannot stop template-driven styling drift, and do not give reviewers a stable baseline for future work.
|
||||
- **Approval class**: Core Enterprise
|
||||
- **Red flags triggered**: New semantic UI layer; risk of overspecifying aesthetics; risk of accidental bleed into platform visual decisions.
|
||||
- **Score**: Nutzen: 3 | Dringlichkeit: 2 | Scope: 2 | Komplexitaet: 1 | Produktnaehe: 1 | Wiederverwendung: 2 | **Gesamt: 11/12**
|
||||
- **Decision**: approve
|
||||
|
||||
## Spec Scope Fields *(mandatory)*
|
||||
|
||||
- **Scope**: workspace
|
||||
- **Primary Routes**: All public-facing pages in `apps/website`, with immediate relevance to landing, trust, legal/privacy, content-heavy, and CTA/contact surfaces.
|
||||
- **Data Ownership**: Website-owned visual rules, semantic primitives, and review criteria only; no tenant-owned records, platform data, or shared persistence.
|
||||
- **RBAC**: None. This feature applies to public website surfaces and introduces no authorization model.
|
||||
|
||||
## Proportionality Review *(mandatory when structural complexity is introduced)*
|
||||
|
||||
- **New source of truth?**: yes
|
||||
- **New persisted entity/table/artifact?**: no
|
||||
- **New abstraction?**: yes
|
||||
- **New enum/state/reason family?**: no
|
||||
- **New cross-domain UI framework/taxonomy?**: yes, but only within `apps/website`
|
||||
- **Current operator problem**: Website builders and reviewers do not have a shared enterprise-grade baseline for visual decisions, so every new page risks re-litigating foundational styling choices.
|
||||
- **Existing structure is insufficient because**: Default component libraries and page-local styling do not encode the intended product tone, do not constrain cross-page consistency, and do not protect `apps/platform` from accidental visual coupling.
|
||||
- **Narrowest correct implementation**: A website-only foundation spec that defines rules and semantics without creating a shared design system, platform obligations, or a full component implementation backlog.
|
||||
- **Ownership cost**: Future website work must stay aligned with the foundation, and reviewers must enforce the token and primitive model instead of allowing page-local exceptions to accumulate.
|
||||
- **Alternative intentionally rejected**: Styling pages one by one or accepting default shadcn/ui aesthetics was rejected because both approaches optimize local speed at the cost of long-term coherence and trust.
|
||||
- **Release truth**: Current-release truth for `apps/website`; this is not future-platform preparation and must not be used to imply a cross-surface contract.
|
||||
|
||||
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
|
||||
|
||||
- **Test purpose / classification**: Runtime website rendering, styling, and composition change inside `apps/website` with browser smoke coverage and static build proof.
|
||||
- **Validation lane(s)**: fast-feedback
|
||||
- **Why this classification and these lanes are sufficient**: This feature changes executable website presentation, shared primitives, and route composition, but it remains a static Astro surface with no backend writes, no auth, no database setup, and no platform runtime coupling. Root build proof plus focused Playwright smoke coverage is the narrowest honest proof.
|
||||
- **New or expanded test families**: Focused Playwright browser smoke coverage in `apps/website/tests/smoke` for representative landing, trust/legal, content-heavy, and component-guardrail flows.
|
||||
- **Fixture / helper cost impact**: low; smoke helpers may expand slightly, but no database, auth, tenant, or provider setup is required.
|
||||
- **Heavy-family visibility / justification**: none; validation remains in fast-feedback only.
|
||||
- **Reviewer handoff**: Reviewers should confirm that representative public routes still load cleanly, shell/navigation/footer behavior remains stable, CTA hierarchy and progressive disclosure stay readable, accessibility-baseline rules remain visible, and no unnecessary client-framework or platform coupling is introduced.
|
||||
- **Budget / baseline / trend impact**: none beyond the small existing website smoke-suite cost.
|
||||
- **Escalation needed**: document-in-feature
|
||||
- **Planned validation commands**: `corepack pnpm build:website` and `cd apps/website && corepack pnpm exec playwright test`
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### User Story 1 - Build new website pages from a stable foundation (Priority: P1)
|
||||
|
||||
A website contributor can create a new public page without inventing baseline rules for hierarchy, spacing, surfaces, and CTA emphasis.
|
||||
|
||||
**Why this priority**: Preventing visual drift at the point of creation is the core value of the feature and the fastest way to improve long-term quality.
|
||||
|
||||
**Independent Test**: Review a proposed new page and confirm that its headings, section rhythm, surfaces, CTAs, and content groupings can all be mapped to named foundation rules without adding bespoke foundational decisions.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a contributor starts a new page in `apps/website`, **When** they choose typography, spacing, surface treatment, and CTA hierarchy, **Then** each choice can be traced to a defined token role or semantic primitive.
|
||||
2. **Given** a contributor proposes a new section or card treatment, **When** an existing foundation primitive already covers the need, **Then** no new foundational visual rule is required.
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 - Review multiple page types against one visual language (Priority: P1)
|
||||
|
||||
A product or design reviewer can judge consistency across multiple website page types using one shared website-only standard.
|
||||
|
||||
**Why this priority**: The foundation only creates leverage if it works across more than one hero page and remains stable on trust, legal, and content-heavy surfaces.
|
||||
|
||||
**Independent Test**: Compare representative landing, trust/legal, and content-heavy page concepts and verify that they share the same heading logic, spacing rhythm, surface model, and CTA weighting rules.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a landing page and a trust-oriented page, **When** they are reviewed together, **Then** both can be evaluated against the same typography, surface, and CTA rules without page-specific exceptions.
|
||||
2. **Given** a content-heavy page such as privacy, terms, or integrations, **When** it is reviewed against the foundation, **Then** the page still reads as part of the same website rather than as a separate visual mode.
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 - Use shadcn/ui without inheriting a template look (Priority: P2)
|
||||
|
||||
A website component builder can use shadcn/ui as a building block while keeping the website's own visual language and semantic constraints in control.
|
||||
|
||||
**Why this priority**: shadcn/ui is useful for speed, but uncontrolled defaults would quickly reintroduce inconsistency and generic template aesthetics.
|
||||
|
||||
**Independent Test**: Evaluate a proposed shadcn/ui-based component and confirm that its tokens, emphasis, borders, radius, and interaction states are governed by the website foundation rather than by library defaults.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a builder selects a shadcn/ui primitive for website use, **When** they adapt it, **Then** the component must inherit website-defined tokens and semantics instead of preserving its default look unmodified.
|
||||
2. **Given** a local override bypasses shared token or primitive rules, **When** the component is reviewed, **Then** the override is rejected as non-compliant with the foundation.
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- A page is text-heavy rather than hero-led and still needs the same visual language to feel deliberate and trustworthy.
|
||||
- A page contains multiple potential CTAs and must still maintain one clear action hierarchy rather than several equally loud buttons.
|
||||
- Status or accent colors are available but must not become decorative shortcuts that dilute semantic clarity.
|
||||
- Cards and elevated surfaces must stay useful on long-form content pages instead of turning every block into a visually noisy container.
|
||||
- Future website work must not interpret this foundation as approval to theme Filament or create a shared design contract with `apps/platform`.
|
||||
- Early website forms, if introduced, must use the same input height, border, focus, and error-state logic instead of one-off marketing styling.
|
||||
|
||||
## Requirements *(mandatory)*
|
||||
|
||||
**Constitution alignment (required):** This feature introduces no Microsoft Graph calls, no queueing, no long-running operations, no authorization changes, and no Filament surface changes. Its contract is explicitly local to `apps/website` and must not create obligations for `apps/platform`.
|
||||
|
||||
**Constitution alignment (UI-SEM-001 / LAYER-001 / BLOAT-001):** This feature intentionally introduces a website-local semantic visual layer because page-local styling and default library decisions are insufficient to create a coherent enterprise website. The layer is scoped narrowly to public website semantics, replaces ad hoc page-by-page foundation decisions, and must not expand into a platform-wide design system without a separate spec.
|
||||
|
||||
**Implementation boundary:** Any implementation under this specification MUST preserve the existing website working contract by keeping `@tenantatlas/website`, `WEBSITE_PORT`, and the root `dev:website` / `build:website` workflows intact, and it MUST NOT introduce runtime or package coupling to `apps/platform`.
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- **FR-001**: The specification MUST define a visual direction for `apps/website` that is explicitly local to the website and explicitly excludes `apps/platform`, Filament theming, and any shared cross-surface design contract.
|
||||
- **FR-002**: The visual direction MUST describe the website as clear, calm, precise, trustworthy, modern, and high-quality, and MUST explicitly reject playful, neon-heavy, glass-heavy, over-animated, loud, or generic startup-template aesthetics.
|
||||
- **FR-003**: The foundation MUST define a color-role model that includes, at minimum, `background`, `foreground`, `muted`, `muted-foreground`, `card`, `card-foreground`, `border`, `input`, `primary`, `primary-foreground`, `secondary`, `secondary-foreground`, `accent`, `accent-foreground`, `success`, `warning`, `destructive`, and `info`.
|
||||
- **FR-004**: The color-role rules MUST state that neutrals carry most of the interface, that primary color is used sparingly, that status colors remain semantic rather than decorative, and that contrast supports readability on both short hero screens and longer content pages.
|
||||
- **FR-005**: The foundation MUST define a typography hierarchy covering display or hero heading, page heading, section heading, card heading, body text, small text, eyebrow or label text, and UI label or helper text.
|
||||
- **FR-006**: Typography rules MUST require calm, precise headings, readable body copy for long-form pages, and sufficient contrast for small UI text across landing, trust, and content-oriented page types.
|
||||
- **FR-007**: The foundation MUST define a repeatable spacing model for page spacing, section spacing, and component spacing, including rules for section rhythm, content density shifts, and avoidance of page-by-page optical tweaking without a system reference.
|
||||
- **FR-008**: The foundation MUST define a surface language covering page background, default content surface, card surface, elevated surface, muted or inset surface, and highlighted or emphasis surface, with rules that make surface levels structurally useful rather than decorative.
|
||||
- **FR-009**: The foundation MUST define a small radius scale and clear border and shadow rules, including border-first clarity for cards and inputs, restrained shadow usage, and a rejection of decorative float effects.
|
||||
- **FR-010**: The foundation MUST define semantic interaction rules for buttons, links, and inputs, including button hierarchy, CTA weighting, text-vs-button link behavior, shared focus logic, shared border logic, and clear error states for any website form surfaces.
|
||||
- **FR-011**: The foundation MUST define a website semantic primitive model covering navigation primitives, hero primitives, section primitives, content-grouping primitives, CTA primitives, and trust primitives.
|
||||
- **FR-012**: The foundation MUST define usage rules for shadcn/ui that allow it as an implementation building block, require adaptation to website tokens and semantics, and forbid uncontrolled use of default component styling or local overrides disconnected from shared tokens.
|
||||
- **FR-013**: The foundation MUST define page-level consistency rules for content width, heading hierarchy, section rhythm, CTA placement logic, card behavior, callout behavior, footer structure, and link-emphasis rules across `apps/website`.
|
||||
- **FR-014**: The foundation MUST include a tone-to-UI alignment statement that anchors the website in an enterprise and MSP-credible posture: precise instead of hype-driven, believable instead of flashy, structured instead of loud, and modern but controlled.
|
||||
- **FR-015**: The foundation MUST include an accessibility baseline covering readable text sizing, sufficient contrast, distinct focus states, non-color-only semantics, durable mobile readability, and clear differentiation between navigation and CTA elements.
|
||||
- **FR-016**: The foundation MUST define progressive-disclosure behavior so that information density, CTA emphasis, and surface emphasis reveal complexity in layers instead of presenting all signals at once.
|
||||
- **FR-017**: The specification MUST state its required deliverables: design token set, typography hierarchy, color-role mapping, spacing principles, surface/border/radius/shadow rules, button/link/input semantics, section/card/callout primitives, shadcn/ui usage constraints, and page consistency rules.
|
||||
- **FR-018**: The foundation MUST be reviewable against at least three page families: landing pages, trust or legal pages, and content-heavy pages.
|
||||
- **FR-019**: No requirement in this specification MAY be interpreted as mandatory visual alignment work for `apps/platform`, Filament, or a shared website-platform design system.
|
||||
- **FR-020**: Implementation work under this specification MUST preserve the website working contract by retaining `@tenantatlas/website`, `WEBSITE_PORT`, and the root `dev:website` / `build:website` workflows, and it MUST NOT introduce runtime coupling or shared-package obligations for `apps/platform`.
|
||||
|
||||
#### Out of Scope
|
||||
|
||||
- Filament theming
|
||||
- `apps/platform`
|
||||
- Cross-surface visual contracts
|
||||
- A shared design system for website and platform
|
||||
- Final page IA or page-by-page content architecture beyond route-level composition changes strictly required to apply and prove the foundation
|
||||
- Final website copy or content inventory beyond semantic, hierarchy, disclosure, or CTA-weighting adjustments strictly required to apply and prove the foundation
|
||||
- Full implementation of every component described by the foundation
|
||||
- Logo or brand redesign
|
||||
|
||||
#### Assumptions
|
||||
|
||||
- `apps/website` continues to serve public marketing, trust, legal, and content-oriented page types rather than operator-facing product UI.
|
||||
- The first implementation phase should prefer a small number of repeatable primitives over a wide component catalog.
|
||||
- Enterprise trust and clarity matter more than maximal visual novelty for this website surface.
|
||||
|
||||
### Key Entities *(include if feature involves data)*
|
||||
|
||||
- **Visual Direction**: The high-level statement of intended tone, trust posture, and disallowed aesthetic patterns for the website.
|
||||
- **Design Token Set**: The named color, spacing, radius, border, and shadow roles that anchor repeatable visual decisions.
|
||||
- **Typography Hierarchy**: The ordered set of text roles that governs hero, page, section, card, body, and supporting text usage.
|
||||
- **Surface Model**: The set of page and component surface levels used to organize content and emphasis without decorative overload.
|
||||
- **Semantic Primitive Set**: The named website building blocks for navigation, hero, sections, groupings, CTAs, and trust-oriented content.
|
||||
- **shadcn/ui Usage Contract**: The rules that determine when and how shadcn/ui primitives can be adapted for website use.
|
||||
|
||||
## Success Criteria *(mandatory)*
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-001**: In review of representative landing, trust/legal, and content-heavy page exemplars, 100% of headings, CTAs, cards, callouts, and input states can be mapped to named foundation rules without inventing new foundational styling rules.
|
||||
- **SC-002**: Reviewers can assess the same three representative page families without raising unresolved foundational questions about typography hierarchy, spacing rhythm, surface usage, or CTA weighting.
|
||||
- **SC-003**: 100% of library-derived website components proposed for the initial implementation phase can be justified as adaptations of the defined token and primitive model rather than uncontrolled default styles.
|
||||
- **SC-004**: The specification contains zero requirements that obligate visual changes to `apps/platform`, Filament, or a shared cross-surface design system.
|
||||
- **SC-005**: At least one primary CTA hierarchy and one low-emphasis CTA pattern are defined clearly enough that reviewers can classify CTA weight consistently across representative website pages.
|
||||
197
specs/214-website-visual-foundation/tasks.md
Normal file
197
specs/214-website-visual-foundation/tasks.md
Normal file
@ -0,0 +1,197 @@
|
||||
# Tasks: Website Visual Foundation
|
||||
|
||||
**Input**: Design documents from `/specs/214-website-visual-foundation/`
|
||||
**Prerequisites**: `plan.md`, `spec.md`, `research.md`, `data-model.md`, `quickstart.md`, `contracts/website-visual-foundation.contract.yaml`
|
||||
|
||||
**Tests**: Browser smoke coverage and the root website build proof are required for this runtime-changing website feature.
|
||||
|
||||
## Test Governance Checklist
|
||||
|
||||
- [X] Lane assignment is named and is the narrowest sufficient proof for the changed behavior.
|
||||
- [X] New or changed tests stay in the smallest honest family, and the browser coverage remains explicit.
|
||||
- [X] Shared helpers and context defaults stay cheap by default; no backend, auth, or fixture-heavy setup is introduced.
|
||||
- [X] Planned validation commands cover the website change without pulling in unrelated platform lane cost.
|
||||
- [X] Any runtime-cost or escalation note stays documented in this feature rather than being deferred implicitly.
|
||||
- [X] Explicit governance outcome is recorded as `document-in-feature` for this feature-local fast-feedback validation scope.
|
||||
|
||||
## Phase 1: Setup (Shared Infrastructure)
|
||||
|
||||
**Purpose**: Prepare the shared types, metadata hooks, and smoke-test helpers that the visual-foundation refactor will rely on.
|
||||
|
||||
- [X] T001 Add page-family and visual-foundation contract types in `apps/website/src/types/site.ts`
|
||||
- [X] T002 [P] Add foundation-aware site metadata and CTA/footer configuration hooks in `apps/website/src/lib/site.ts` and `apps/website/src/lib/seo.ts`
|
||||
- [X] T003 [P] Extend shell, CTA-hierarchy, accessibility-baseline, navigation-vs-CTA differentiation, and mobile-navigation smoke helpers in `apps/website/tests/smoke/smoke-helpers.ts`
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Foundational (Blocking Prerequisites)
|
||||
|
||||
**Purpose**: Establish the semantic token layer, shared shell, and reusable layout scaffolds that every user story depends on.
|
||||
|
||||
**⚠️ CRITICAL**: No user story work should begin until this phase is complete.
|
||||
|
||||
- [X] T004 Implement semantic color, typography, spacing, surface, border, radius, shadow, and accessibility-baseline roles in `apps/website/src/styles/tokens.css` and `apps/website/src/styles/global.css`
|
||||
- [X] T005 [P] Refactor the shared document and navigation shell to use foundation surfaces in `apps/website/src/layouts/BaseLayout.astro`, `apps/website/src/components/layout/PageShell.astro`, `apps/website/src/components/layout/Navbar.astro`, and `apps/website/src/components/layout/Footer.astro`
|
||||
- [X] T006 [P] Refactor shared width and spacing primitives in `apps/website/src/components/primitives/Container.astro`, `apps/website/src/components/primitives/Section.astro`, `apps/website/src/components/primitives/SectionHeader.astro`, `apps/website/src/components/primitives/Stack.astro`, `apps/website/src/components/primitives/Cluster.astro`, and `apps/website/src/components/primitives/Grid.astro`
|
||||
- [X] T007 [P] Align reusable section scaffolds to the foundation contract, including progressive-disclosure and CTA-emphasis layering, in `apps/website/src/components/sections/PageHero.astro`, `apps/website/src/components/sections/CTASection.astro`, `apps/website/src/components/sections/FeatureGrid.astro`, `apps/website/src/components/sections/TrustGrid.astro`, and `apps/website/src/components/sections/LogoStrip.astro`
|
||||
|
||||
**Checkpoint**: Semantic tokens, shared shell, and section scaffolds are in place. User-story work can now proceed.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: User Story 1 - Build New Pages From a Stable Foundation (Priority: P1) 🎯 MVP
|
||||
|
||||
**Goal**: Make Home and Product the proof that new website pages can be assembled from the shared foundation instead of page-local design decisions.
|
||||
|
||||
**Independent Test**: Visit Home and Product and verify the smoke suite proves a shared heading hierarchy, consistent section rhythm, clear CTA weighting, focus visibility, readable contrast, non-color-only semantics, and navigation-vs-CTA differentiation without bespoke foundational styling.
|
||||
|
||||
### Tests for User Story 1
|
||||
|
||||
> **NOTE**: Write this test first and confirm it fails before implementing the story.
|
||||
|
||||
- [X] T008 [US1] Update landing/product smoke coverage for heading hierarchy, CTA emphasis, focus visibility, readable contrast, non-color-only semantics, and navigation-vs-CTA differentiation in `apps/website/tests/smoke/home-product.spec.ts`
|
||||
|
||||
### Implementation for User Story 1
|
||||
|
||||
- [X] T009 [P] [US1] Align core content primitives to the shared foundation in `apps/website/src/components/content/Eyebrow.astro`, `apps/website/src/components/content/Headline.astro`, `apps/website/src/components/content/Lead.astro`, `apps/website/src/components/content/FeatureItem.astro`, `apps/website/src/components/content/Callout.astro`, and `apps/website/src/components/content/Metric.astro`
|
||||
- [X] T010 [P] [US1] Update landing and product content modules for semantic sections and CTA rhythm in `apps/website/src/content/pages/home.ts` and `apps/website/src/content/pages/product.ts`
|
||||
- [X] T011 [US1] Apply the stable foundation to `apps/website/src/pages/index.astro` and `apps/website/src/pages/product.astro`
|
||||
|
||||
**Checkpoint**: Home and Product work as the MVP proof that new pages can be built from the shared foundation.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: User Story 2 - Review Multiple Page Types Against One Visual Language (Priority: P1)
|
||||
|
||||
**Goal**: Make trust, legal, audience-fit, and long-form routes read as the same website rather than as separate visual modes.
|
||||
|
||||
**Independent Test**: Visit representative landing, trust/legal, and content-heavy routes and verify the smoke suite proves consistent shell behavior, section rhythm, progressive-disclosure layering, and CTA placement across those page families.
|
||||
|
||||
### Tests for User Story 2
|
||||
|
||||
> **NOTE**: Write this test first and confirm it fails before implementing the story.
|
||||
|
||||
- [X] T012 [US2] Update landing, trust/legal, and content-heavy smoke coverage for heading hierarchy, section rhythm, surface consistency, shell/navigation stability, progressive-disclosure layering, CTA placement, and mobile-navigation usability in `apps/website/tests/smoke/home-product.spec.ts`, `apps/website/tests/smoke/solutions-trust-integrations.spec.ts`, and `apps/website/tests/smoke/contact-legal.spec.ts`
|
||||
|
||||
### Implementation for User Story 2
|
||||
|
||||
- [X] T013 [P] [US2] Align audience, trust, and long-form content primitives in `apps/website/src/components/content/AudienceRow.astro`, `apps/website/src/components/content/TrustPrincipleCard.astro`, `apps/website/src/components/content/IntegrationBadge.astro`, `apps/website/src/components/content/ContactPanel.astro`, `apps/website/src/components/content/DemoPrompt.astro`, and `apps/website/src/components/content/RichText.astro`
|
||||
- [X] T014 [P] [US2] Normalize trust/legal/content-heavy page content modules for semantic structure, disclosure layers, and CTA rhythm without changing final copy or IA in `apps/website/src/content/pages/solutions.ts`, `apps/website/src/content/pages/security-trust.ts`, `apps/website/src/content/pages/integrations.ts`, `apps/website/src/content/pages/contact.ts`, `apps/website/src/content/pages/legal.ts`, `apps/website/src/content/pages/privacy.ts`, and `apps/website/src/content/pages/terms.ts`
|
||||
- [X] T015 [US2] Apply page-family consistency rules and route-composition changes needed to prove the foundation, without redefining final IA or copy, in `apps/website/src/pages/solutions.astro`, `apps/website/src/pages/security-trust.astro`, `apps/website/src/pages/integrations.astro`, `apps/website/src/pages/contact.astro`, `apps/website/src/pages/legal.astro`, `apps/website/src/pages/privacy.astro`, and `apps/website/src/pages/terms.astro`
|
||||
|
||||
**Checkpoint**: Landing, trust/legal, and long-form routes can be reviewed against the same visual language.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: User Story 3 - Use shadcn/ui Without Inheriting a Template Look (Priority: P2)
|
||||
|
||||
**Goal**: Encode the website-local component contract so future `shadcn/ui`-inspired work routes through local primitives instead of uncontrolled library defaults.
|
||||
|
||||
**Independent Test**: Run a dedicated smoke check that proves representative pages still expose consistent CTA hierarchy, shared input/button behavior, and mobile navigation while relying on local foundation-backed primitives.
|
||||
|
||||
### Tests for User Story 3
|
||||
|
||||
> **NOTE**: Write this test first and confirm it fails before implementing the story.
|
||||
|
||||
- [X] T016 [US3] Write component-guardrail smoke coverage for focus states, input semantics, non-color-only semantics, navigation-vs-CTA differentiation, CTA hierarchy, and consistent rendered component behavior in `apps/website/tests/smoke/visual-foundation-guardrails.spec.ts`
|
||||
|
||||
### Implementation for User Story 3
|
||||
|
||||
- [X] T017 [P] [US3] Encode local foundation-backed component variants in `apps/website/src/components/primitives/Button.astro`, `apps/website/src/components/primitives/Badge.astro`, `apps/website/src/components/primitives/Card.astro`, `apps/website/src/components/primitives/Input.astro`, and `apps/website/src/components/primitives/Textarea.astro`
|
||||
- [X] T018 [P] [US3] Route CTA and low-emphasis action patterns through shared wrappers in `apps/website/src/components/content/PrimaryCTA.astro`, `apps/website/src/components/content/SecondaryCTA.astro`, `apps/website/src/components/layout/Navbar.astro`, and `apps/website/src/components/layout/Footer.astro`
|
||||
- [X] T019 [US3] Remove remaining page-local template-style escapes from `apps/website/src/components/sections/PageHero.astro`, `apps/website/src/components/sections/CTASection.astro`, `apps/website/src/pages/index.astro`, `apps/website/src/pages/product.astro`, `apps/website/src/pages/security-trust.astro`, and `apps/website/src/pages/contact.astro`
|
||||
|
||||
**Checkpoint**: `shadcn/ui`-style patterns are now constrained by the local website foundation instead of shaping the website by default.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Polish & Cross-Cutting Concerns
|
||||
|
||||
**Purpose**: Run the narrowest validation proof and confirm website-working-contract compatibility after all stories land.
|
||||
|
||||
- [X] T020 [P] Record fast-feedback lane validation, including contrast, focus visibility, non-color-only semantics, navigation-vs-CTA differentiation, and progressive-disclosure proof, and run the required proof commands from `specs/214-website-visual-foundation/plan.md` and `specs/214-website-visual-foundation/quickstart.md`
|
||||
- [X] T021 Verify website working-contract compatibility and static-first runtime constraints, including no new React/runtime coupling or unnecessary client hydration, in `package.json`, `apps/website/package.json`, `apps/website/astro.config.mjs`, and `apps/website/playwright.config.ts`
|
||||
|
||||
---
|
||||
|
||||
## Dependencies & Execution Order
|
||||
|
||||
### Phase Dependencies
|
||||
|
||||
- **Setup (Phase 1)**: Starts immediately.
|
||||
- **Foundational (Phase 2)**: Depends on Setup and blocks all user stories.
|
||||
- **User Story 1 (Phase 3)**: Depends on Foundational only.
|
||||
- **User Story 2 (Phase 4)**: Depends on Foundational only.
|
||||
- **User Story 3 (Phase 5)**: Depends on Foundational only and can proceed in parallel with US1 or US2 once the shared shell and primitives are stable.
|
||||
- **Polish (Phase 6)**: Depends on all targeted user stories being complete.
|
||||
|
||||
### User Story Dependencies
|
||||
|
||||
- **US1**: No dependency on other user stories; this is the MVP slice.
|
||||
- **US2**: No dependency on US1, but it reuses the same shared shell, token, and primitive foundation.
|
||||
- **US3**: No dependency on US1 or US2 for implementation, but its guardrail checks should run against representative routes after the shared foundation is stable.
|
||||
|
||||
### Within Each User Story
|
||||
|
||||
- Write the browser smoke test first and verify it fails.
|
||||
- Align shared content or primitive components before changing page content modules.
|
||||
- Update page content modules before completing route composition.
|
||||
- Finish route-level integration before moving to the next story’s polish or validation work.
|
||||
|
||||
---
|
||||
|
||||
## Parallel Opportunities
|
||||
|
||||
- `T002` and `T003` can run in parallel after `T001` is scoped.
|
||||
- `T005`, `T006`, and `T007` can run in parallel after `T004` starts.
|
||||
- In US1, `T009` and `T010` can run in parallel before `T011`.
|
||||
- In US2, `T013` and `T014` can run in parallel before `T015`.
|
||||
- In US3, `T017` and `T018` can run in parallel before `T019`.
|
||||
- `T020` and `T021` can run in parallel during final polish.
|
||||
|
||||
---
|
||||
|
||||
## Parallel Example: User Story 2
|
||||
|
||||
```bash
|
||||
# Launch trust/legal/content-heavy preparation together:
|
||||
Task: "T013 [US2] Align audience, trust, and long-form content primitives"
|
||||
Task: "T014 [US2] Update trust/legal/content-heavy page content modules"
|
||||
|
||||
# Then complete route integration:
|
||||
Task: "T015 [US2] Apply page-family consistency rules to the affected routes"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
### MVP First
|
||||
|
||||
1. Complete Phase 1: Setup.
|
||||
2. Complete Phase 2: Foundational.
|
||||
3. Complete Phase 3: User Story 1.
|
||||
4. Run `corepack pnpm build:website` and the updated `home-product` smoke proof.
|
||||
5. Demo the MVP on Home and Product.
|
||||
|
||||
### Incremental Delivery
|
||||
|
||||
1. Setup + Foundational establish the reusable website-local foundation.
|
||||
2. US1 proves new page construction from the shared rules.
|
||||
3. US2 aligns representative page families under one visual language.
|
||||
4. US3 hardens the local primitive contract so future `shadcn/ui`-inspired work cannot drift back to template defaults.
|
||||
5. Polish runs the narrowest proof commands and verifies website-working-contract compatibility.
|
||||
|
||||
### Suggested MVP Scope
|
||||
|
||||
- Deliver through **User Story 1** if the smallest initial release is needed.
|
||||
- Add **User Story 2** next for page-family consistency across trust/legal/content-heavy surfaces.
|
||||
- Finish with **User Story 3** to lock future component work behind the local foundation contract.
|
||||
|
||||
## Validation Notes
|
||||
|
||||
- `2026-04-19`: `corepack pnpm build:website` passed from repo root.
|
||||
- `2026-04-19`: `corepack pnpm --filter @tenantatlas/website exec playwright test` passed with 10 smoke tests.
|
||||
- Focus visibility proof is covered by the updated landing-page smoke flow (`Skip to content` keyboard focus plus CTA hierarchy checks).
|
||||
- Non-color-only semantics, navigation-vs-CTA differentiation, progressive-disclosure layers, badge/button/input hooks, and content-heavy route consistency are exercised through the updated smoke helpers and representative route specs.
|
||||
- Website working-contract review confirmed `@tenantatlas/website`, `WEBSITE_PORT`, Astro static output, and Playwright web-server flow remain intact with no React, Radix, or platform runtime coupling introduced.
|
||||
Loading…
Reference in New Issue
Block a user