From f884b160611b20c803c0b0ab7adec0836e9d0df0 Mon Sep 17 00:00:00 2001 From: ahmido Date: Sun, 19 Apr 2026 07:19:58 +0000 Subject: [PATCH 1/2] feat: implement website visual foundation (#251) ## Summary - implement the website-only visual foundation for apps/website - formalize semantic tokens, typography, spacing, surfaces, and shared CTA/navigation primitives - align landing, trust/legal, and content-heavy routes plus Playwright smoke coverage with the new foundation ## Validation - corepack pnpm build:website - corepack pnpm --filter @tenantatlas/website exec playwright test ## Scope - website-only change set for spec 214 - no apps/platform runtime coupling introduced Co-authored-by: Ahmed Darrazi Reviewed-on: https://git.cloudarix.de/ahmido/TenantAtlas/pulls/251 --- .github/agents/copilot-instructions.md | 3 + .../src/components/content/AudienceRow.astro | 17 +- .../src/components/content/Callout.astro | 17 +- .../src/components/content/ContactPanel.astro | 7 +- .../src/components/content/DemoPrompt.astro | 15 +- .../src/components/content/Eyebrow.astro | 16 +- .../src/components/content/FeatureItem.astro | 19 +- .../src/components/content/Headline.astro | 17 +- .../components/content/IntegrationBadge.astro | 15 +- .../website/src/components/content/Lead.astro | 14 +- .../src/components/content/Metric.astro | 10 +- .../src/components/content/PrimaryCTA.astro | 12 +- .../src/components/content/RichText.astro | 13 +- .../src/components/content/SecondaryCTA.astro | 12 +- .../content/TrustPrincipleCard.astro | 12 +- .../src/components/layout/Footer.astro | 19 +- .../src/components/layout/Navbar.astro | 34 +-- .../src/components/layout/PageShell.astro | 13 +- .../src/components/primitives/Badge.astro | 9 +- .../src/components/primitives/Button.astro | 28 ++- .../src/components/primitives/Card.astro | 13 +- .../src/components/primitives/Cluster.astro | 21 +- .../src/components/primitives/Container.astro | 22 +- .../src/components/primitives/Grid.astro | 9 +- .../src/components/primitives/Input.astro | 3 +- .../src/components/primitives/Section.astro | 28 ++- .../components/primitives/SectionHeader.astro | 9 +- .../src/components/primitives/Stack.astro | 6 +- .../src/components/primitives/Textarea.astro | 3 +- .../src/components/sections/CTASection.astro | 4 +- .../src/components/sections/FeatureGrid.astro | 4 +- .../src/components/sections/LogoStrip.astro | 6 +- .../src/components/sections/PageHero.astro | 42 ++-- .../src/components/sections/TrustGrid.astro | 4 +- apps/website/src/content/pages/home.ts | 4 +- .../src/content/pages/security-trust.ts | 2 +- apps/website/src/content/pages/solutions.ts | 2 +- apps/website/src/lib/seo.ts | 12 +- apps/website/src/lib/site.ts | 151 +++++++++++++- apps/website/src/pages/contact.astro | 32 +-- apps/website/src/pages/index.astro | 14 +- apps/website/src/pages/integrations.astro | 6 +- apps/website/src/pages/legal.astro | 10 +- apps/website/src/pages/privacy.astro | 4 +- apps/website/src/pages/product.astro | 10 +- apps/website/src/pages/security-trust.astro | 8 +- apps/website/src/pages/solutions.astro | 6 +- apps/website/src/pages/terms.astro | 4 +- apps/website/src/styles/global.css | 163 +++++++++++---- apps/website/src/styles/tokens.css | 126 +++++++++-- apps/website/src/types/site.ts | 42 +++- .../website/tests/smoke/contact-legal.spec.ts | 21 ++ apps/website/tests/smoke/home-product.spec.ts | 40 ++-- apps/website/tests/smoke/smoke-helpers.ts | 33 ++- .../solutions-trust-integrations.spec.ts | 33 ++- .../visual-foundation-guardrails.spec.ts | 36 ++++ .../checklists/requirements.md | 35 ++++ .../website-visual-foundation.contract.yaml | 178 ++++++++++++++++ .../data-model.md | 193 +++++++++++++++++ specs/214-website-visual-foundation/plan.md | 183 ++++++++++++++++ .../quickstart.md | 76 +++++++ .../214-website-visual-foundation/research.md | 58 ++++++ specs/214-website-visual-foundation/spec.md | 176 ++++++++++++++++ specs/214-website-visual-foundation/tasks.md | 197 ++++++++++++++++++ 64 files changed, 2036 insertions(+), 295 deletions(-) create mode 100644 apps/website/tests/smoke/visual-foundation-guardrails.spec.ts create mode 100644 specs/214-website-visual-foundation/checklists/requirements.md create mode 100644 specs/214-website-visual-foundation/contracts/website-visual-foundation.contract.yaml create mode 100644 specs/214-website-visual-foundation/data-model.md create mode 100644 specs/214-website-visual-foundation/plan.md create mode 100644 specs/214-website-visual-foundation/quickstart.md create mode 100644 specs/214-website-visual-foundation/research.md create mode 100644 specs/214-website-visual-foundation/spec.md create mode 100644 specs/214-website-visual-foundation/tasks.md diff --git a/.github/agents/copilot-instructions.md b/.github/agents/copilot-instructions.md index 3d53c2a6..1e3674f0 100644 --- a/.github/agents/copilot-instructions.md +++ b/.github/agents/copilot-instructions.md @@ -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` diff --git a/apps/website/src/components/content/AudienceRow.astro b/apps/website/src/components/content/AudienceRow.astro index bc41ecff..7534da18 100644 --- a/apps/website/src/components/content/AudienceRow.astro +++ b/apps/website/src/components/content/AudienceRow.astro @@ -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; --- -

- {item.audience} -

-

+ {item.audience} + {item.title} -

-

{item.description}

+ + + {item.description} +
    { item.bullets.map((bullet) => ( -
  • +
  • {bullet}
  • )) diff --git a/apps/website/src/components/content/Callout.astro b/apps/website/src/components/content/Callout.astro index b80f48fd..6db7a82b 100644 --- a/apps/website/src/components/content/Callout.astro +++ b/apps/website/src/components/content/Callout.astro @@ -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' --- - {content.eyebrow && ( -

    - {content.eyebrow} -

    - )} -

    + {content.eyebrow && {content.eyebrow}} + {content.title} -

    -

    {content.description}

    + + + {content.description} +
    diff --git a/apps/website/src/components/content/ContactPanel.astro b/apps/website/src/components/content/ContactPanel.astro index 60f67c38..d75dff3a 100644 --- a/apps/website/src/components/content/ContactPanel.astro +++ b/apps/website/src/components/content/ContactPanel.astro @@ -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; --- -

    {title}

    + Qualified outreach + + {title} +
      { points.map((point) => ( diff --git a/apps/website/src/components/content/DemoPrompt.astro b/apps/website/src/components/content/DemoPrompt.astro index 4b5c72b2..8dacf568 100644 --- a/apps/website/src/components/content/DemoPrompt.astro +++ b/apps/website/src/components/content/DemoPrompt.astro @@ -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; --- -

      - Conversation focus -

      -

      {title}

      -

      {description}

      + Conversation focus + + {title} + + + {description} +
      diff --git a/apps/website/src/components/content/Eyebrow.astro b/apps/website/src/components/content/Eyebrow.astro index 93795711..a990b6fc 100644 --- a/apps/website/src/components/content/Eyebrow.astro +++ b/apps/website/src/components/content/Eyebrow.astro @@ -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)]', +}; --- -

      +

      diff --git a/apps/website/src/components/content/FeatureItem.astro b/apps/website/src/components/content/FeatureItem.astro index 02c8b273..cee88cb5 100644 --- a/apps/website/src/components/content/FeatureItem.astro +++ b/apps/website/src/components/content/FeatureItem.astro @@ -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; --- - {item.eyebrow && ( -

      - {item.eyebrow} -

      - )} -

      + {item.eyebrow && {item.eyebrow}} + {item.title} -

      -

      {item.description}

      + + + {item.description} + {(item.meta || item.href) && (
      {item.meta && {item.meta}} {item.href && ( - + Learn more )} diff --git a/apps/website/src/components/content/Headline.astro b/apps/website/src/components/content/Headline.astro index b167c7b2..9a83448a 100644 --- a/apps/website/src/components/content/Headline.astro +++ b/apps/website/src/components/content/Headline.astro @@ -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)]', +}; --- -

      + -

      + diff --git a/apps/website/src/components/content/IntegrationBadge.astro b/apps/website/src/components/content/IntegrationBadge.astro index aeeafb34..0d9b1325 100644 --- a/apps/website/src/components/content/IntegrationBadge.astro +++ b/apps/website/src/components/content/IntegrationBadge.astro @@ -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; --- -
      +
      {item.category} -

      {item.name}

      + + {item.name} +
      -

      {item.summary}

      + + {item.summary} + {item.note &&

      {item.note}

      } -
      + diff --git a/apps/website/src/components/content/Lead.astro b/apps/website/src/components/content/Lead.astro index 3f0f3027..847dbffc 100644 --- a/apps/website/src/components/content/Lead.astro +++ b/apps/website/src/components/content/Lead.astro @@ -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]', +}; --- -

      + -

      + diff --git a/apps/website/src/components/content/Metric.astro b/apps/website/src/components/content/Metric.astro index f1b00255..3fa32d70 100644 --- a/apps/website/src/components/content/Metric.astro +++ b/apps/website/src/components/content/Metric.astro @@ -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;

      {item.value}

      -

      + {item.label} -

      -

      {item.description}

      + + + {item.description} +
      diff --git a/apps/website/src/components/content/PrimaryCTA.astro b/apps/website/src/components/content/PrimaryCTA.astro index d2043771..edf7800e 100644 --- a/apps/website/src/components/content/PrimaryCTA.astro +++ b/apps/website/src/components/content/PrimaryCTA.astro @@ -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; --- - +
      + + {showHelper && cta.helper &&

      {cta.helper}

      } +
      diff --git a/apps/website/src/components/content/RichText.astro b/apps/website/src/components/content/RichText.astro index fa813d45..50b53d10 100644 --- a/apps/website/src/components/content/RichText.astro +++ b/apps/website/src/components/content/RichText.astro @@ -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;
      { sections.map((section) => ( -
      -

      + + {section.title} -

      + -
      + )) }
      diff --git a/apps/website/src/components/content/SecondaryCTA.astro b/apps/website/src/components/content/SecondaryCTA.astro index 78f8c40a..3372f0b5 100644 --- a/apps/website/src/components/content/SecondaryCTA.astro +++ b/apps/website/src/components/content/SecondaryCTA.astro @@ -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; --- - +
      + + {showHelper && cta.helper &&

      {cta.helper}

      } +
      diff --git a/apps/website/src/components/content/TrustPrincipleCard.astro b/apps/website/src/components/content/TrustPrincipleCard.astro index 043fea74..fbb0c26a 100644 --- a/apps/website/src/components/content/TrustPrincipleCard.astro +++ b/apps/website/src/components/content/TrustPrincipleCard.astro @@ -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; --- -

      {item.title}

      -

      {item.description}

      - {item.note &&

      {item.note}

      } + + {item.title} + + + {item.description} + + {item.note && {item.note}}
      diff --git a/apps/website/src/components/layout/Footer.astro b/apps/website/src/components/layout/Footer.astro index 9b81361a..28835b21 100644 --- a/apps/website/src/components/layout/Footer.astro +++ b/apps/website/src/components/layout/Footer.astro @@ -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); --- -