From 6c5474d3a9ce055caf105f27b987bfe600458ae4 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Sat, 29 Aug 2026 15:06:02 -0700 Subject: [PATCH 1/4] docs(plans): website suite now has an nx target and a green baseline (#851) --- .../2026-08-29-inline-style-substrate-migration.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/superpowers/plans/2026-08-29-inline-style-substrate-migration.md b/docs/superpowers/plans/2026-08-29-inline-style-substrate-migration.md index e588f57f1..8565cfd77 100644 --- a/docs/superpowers/plans/2026-08-29-inline-style-substrate-migration.md +++ b/docs/superpowers/plans/2026-08-29-inline-style-substrate-migration.md @@ -59,11 +59,12 @@ and this never bites. - Dev server: Browser pane `preview_start` with the `website-dev` launch entry. Never `npm run dev` via Bash. -- Website tests: `cd apps/website && npx vitest run --config vite.config.mts` - — there is **no `nx test website` target**. Baseline: `5 failed | 341 passed` - (pre-existing content-assertion drift in `PostCard`, `Differentiator`, - `thanks/page`). Exactly those 5 must still fail after every batch; a 6th is - yours. +- Website tests: `npx nx test website` — **SUPERSEDED mid-arc (2026-08-29): + #851 gave the suite an nx target and repaired the three rotted spec files.** + The "5 pre-existing failures" baseline that batches 1-4 asserted against is + retired; the suite is fully green (347+ tests) and every remaining batch + must keep it that way. Any failure is yours. (The direct + `cd apps/website && npx vitest run --config vite.config.mts` still works.) - Lint errors (CI fails on errors, tolerates warnings; strip ANSI first): `npx nx lint website 2>&1 | sed -r 's/\x1b\[[0-9;]*m//g' | grep -cE ' error '` - Prod build gate before every PR: `npx nx build website --configuration=production` From b0318e57ee1f7eaad1df8805316fb86cde1c8e6a Mon Sep 17 00:00:00 2001 From: Brian Love Date: Sat, 29 Aug 2026 15:18:51 -0700 Subject: [PATCH 2/4] refactor(website): move pricing components to marketing.css CompareTable, LeadForm, CompatibilityMatrix, and PricingFAQ move their static presentation (backgrounds, borders, type scale, table cell shapes) from inline style props into src/styles/marketing.css, with zero visual change. - CompareTable's repeated cell/header style objects (shape D) collapse into a handful of `.pricing-compare-*` classes reused across tiers and rows; bounded tier/row states (highlighted tier, active billing cycle, last row) become `data-highlight`/`data-active`/`data-last` modifiers instead of inline ternaries. - LeadForm's 6 focus/blur handler pairs (border-color + box-shadow) are replaced by a single `.lead-form-input:focus` rule; the handlers are deleted. - CompatibilityMatrix's TONE_COLORS lookup becomes a `data-tone` modifier on `.compat-matrix-td-label`. - Values computed from external config (CtaStrip's `gridTemplateColumns` keyed off `TIERS.length`) stay inline per the migration's "unbounded/computed" exception. Migration: docs/superpowers/plans/2026-08-29-inline-style-substrate-migration.md --- .../src/components/pricing/CompareTable.tsx | 227 ++------- .../pricing/CompatibilityMatrix.tsx | 62 +-- .../src/components/pricing/LeadForm.tsx | 212 ++------- .../src/components/pricing/PricingFAQ.tsx | 20 +- apps/website/src/styles/marketing.css | 433 ++++++++++++++++++ 5 files changed, 513 insertions(+), 441 deletions(-) diff --git a/apps/website/src/components/pricing/CompareTable.tsx b/apps/website/src/components/pricing/CompareTable.tsx index 7f3609653..48db280a1 100644 --- a/apps/website/src/components/pricing/CompareTable.tsx +++ b/apps/website/src/components/pricing/CompareTable.tsx @@ -1,7 +1,6 @@ 'use client'; import { useState } from 'react'; -import { tokens } from '@threadplane/design-tokens'; import { Button } from '../ui/Button'; import { trackCtaClick } from '../../lib/analytics/client'; import type { CtaId } from '../../lib/analytics/events'; @@ -100,16 +99,16 @@ function allInclusive(): Record { } const Check = () => ( - + ); const Dash = () => ( - + ); function renderCell(value: CellValue): React.ReactNode { if (typeof value === 'boolean') return value ? : ; if (value === '—') return ; - return {value}; + return {value}; } function PlanButton({ tier, cycle }: { tier: TierConfig; cycle: BillingCycle }) { @@ -118,7 +117,7 @@ function PlanButton({ tier, cycle }: { tier: TierConfig; cycle: BillingCycle }) const common = { variant, size: 'md' as const, - style: { width: '100%' }, + className: 'pricing-plan-btn', }; if (cta.stripeBuyable) { return ( @@ -172,42 +171,15 @@ function BillingToggle({ setCycle: (c: BillingCycle) => void; discountPct: number; }) { - const baseBtn = { - fontFamily: tokens.typography.fontSans, - fontSize: 13, - fontWeight: 600, - padding: '8px 16px', - border: 'none', - cursor: 'pointer', - transition: 'background 150ms ease, color 150ms ease', - background: 'transparent', - color: tokens.colors.textSecondary, - borderRadius: 999, - } as const; - const activeBtn = { - ...baseBtn, - background: tokens.colors.accent, - color: '#fff', - } as const; return ( -
-
+
+
@@ -215,7 +187,8 @@ function BillingToggle({ role="tab" aria-selected={cycle === 'annual'} onClick={() => setCycle('annual')} - style={cycle === 'annual' ? activeBtn : baseBtn} + className="pricing-billing-tab" + data-active={cycle === 'annual' || undefined} > Annual{discountPct > 0 ? ` — save ${discountPct}%` : ''} @@ -236,83 +209,28 @@ function SectionTable({ showPrice: boolean; }) { return ( -
-
- +
+
+
- {TIERS.map((tier) => ( {showPrice ? ( - {TIERS.map((tier) => { @@ -341,33 +247,15 @@ function SectionTable({ return ( ) : ( - @@ -402,31 +281,17 @@ function SectionTable({ {rows.map((row, i) => ( - {TIERS.map((tier) => ( @@ -454,15 +319,8 @@ function CtaStrip({ cycle }: { cycle: BillingCycle }) { >
{TIERS.map((tier) => ( -
-
+
+
@@ -476,25 +334,18 @@ export function CompareTable() { const discountPct = annualDiscountPercent(); return ( -
+
-
+
-
+
-
+
diff --git a/apps/website/src/components/pricing/CompatibilityMatrix.tsx b/apps/website/src/components/pricing/CompatibilityMatrix.tsx index 589263c0d..43dd46874 100644 --- a/apps/website/src/components/pricing/CompatibilityMatrix.tsx +++ b/apps/website/src/components/pricing/CompatibilityMatrix.tsx @@ -1,6 +1,5 @@ // SPDX-License-Identifier: MIT import React from 'react'; -import { tokens } from '@threadplane/design-tokens'; interface Row { label: string; @@ -15,47 +14,16 @@ const ROWS: ReadonlyArray = [ { label: 'Unsupported', versions: 'Angular ≤19', tone: 'muted' }, ]; -const TONE_COLORS: Record = { - success: '#16a34a', - warn: '#d97706', - info: tokens.colors.accent, - muted: tokens.colors.textMuted, -}; - export function CompatibilityMatrix() { return ( -
-
+ {title} {tier.highlight && ( -
+
MOST POPULAR
)}
{tier.name}
@@ -321,19 +239,7 @@ function SectionTable({
+ Price -
- +
+ {p.display} {p.period && ( - + {p.period} )} @@ -378,21 +266,12 @@ function SectionTable({
+ {TIERS.map((tier) => ( ))}
+ {row.feature} {renderCell(row.cells[tier.slug])}
+
+
- - + - @@ -63,26 +31,10 @@ export function CompatibilityMatrix() { {ROWS.map((row) => ( - - diff --git a/apps/website/src/components/pricing/LeadForm.tsx b/apps/website/src/components/pricing/LeadForm.tsx index c3d816720..287793845 100644 --- a/apps/website/src/components/pricing/LeadForm.tsx +++ b/apps/website/src/components/pricing/LeadForm.tsx @@ -1,6 +1,5 @@ 'use client'; import { useState } from 'react'; -import { tokens } from '@threadplane/design-tokens'; import { analyticsEvents } from '../../lib/analytics/events'; import { track } from '../../lib/analytics/client'; import { Container } from '../ui/Container'; @@ -73,131 +72,41 @@ export function LeadForm() { } }; - const inputStyle: React.CSSProperties = { - background: tokens.surfaces.surface, - border: `1px solid ${tokens.surfaces.border}`, - color: tokens.colors.textPrimary, - borderRadius: tokens.radius.md, - padding: '10px 14px', - width: '100%', - fontFamily: tokens.typography.body.family, - fontSize: 14, - outline: 'none', - }; - - const handleFocus = (e: React.FocusEvent) => { - e.target.style.borderColor = tokens.colors.accent; - e.target.style.boxShadow = tokens.shadows.focus; - }; - const handleBlur = (e: React.FocusEvent) => { - e.target.style.borderColor = tokens.surfaces.border; - e.target.style.boxShadow = 'none'; - }; - return (
-
-
- Enterprise -

+
+
+ Enterprise +

Built for procurement.
Backed by delivery.

-

+

Volume licensing, custom contract, and optional concierge delivery — so your first Angular agent ships, not just compiles.

-
+
{/* Value props column */} -
    +
      {VALUE_PROPS.map((vp) => (
    • -

+
Status + Angular versions
+ {row.label} + {row.versions}