diff --git a/apps/website/index.html b/apps/website/index.html index 343fa2a..ae7902e 100644 --- a/apps/website/index.html +++ b/apps/website/index.html @@ -3,10 +3,14 @@ - reDeploy — declarative, idempotent, resumable smart-contract deployments + reDeploy · The Solid Chain + diff --git a/apps/website/src/App.tsx b/apps/website/src/App.tsx index 648b5eb..05079dc 100644 --- a/apps/website/src/App.tsx +++ b/apps/website/src/App.tsx @@ -1,17 +1,25 @@ import Hero from "./components/Hero.js"; -import Benefits from "./components/Benefits.js"; -import Features from "./components/Features.js"; import Footer from "./components/Footer.js"; +import Packages from "./components/Packages.js"; +import Pipeline from "./components/Pipeline.js"; +import SpecGraphSection from "./components/SpecGraphSection.js"; +import Studio from "./components/Studio.js"; +import Topbar from "./components/Topbar.js"; export default function App() { return (
-
- - - -
-
); } diff --git a/apps/website/src/assets/studio-deploy-flow.png b/apps/website/src/assets/studio-deploy-flow.png deleted file mode 100644 index 4fdd2ac..0000000 Binary files a/apps/website/src/assets/studio-deploy-flow.png and /dev/null differ diff --git a/apps/website/src/assets/studio-inspector.png b/apps/website/src/assets/studio-inspector.png deleted file mode 100644 index e671d67..0000000 Binary files a/apps/website/src/assets/studio-inspector.png and /dev/null differ diff --git a/apps/website/src/assets/studio-templates.png b/apps/website/src/assets/studio-templates.png deleted file mode 100644 index 60a3e3c..0000000 Binary files a/apps/website/src/assets/studio-templates.png and /dev/null differ diff --git a/apps/website/src/components/Benefits.tsx b/apps/website/src/components/Benefits.tsx deleted file mode 100644 index 4e289bf..0000000 --- a/apps/website/src/components/Benefits.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { BENEFITS } from "../content.js"; - -export default function Benefits() { - return ( -
-
-

Why teams use reDeploy

-

- For smart-contract teams deploying multi-contract systems across chains who need reproducible, - resumable, verifiable deployments — and a visual way to wire and inspect them. -

- -
-
- ); -} diff --git a/apps/website/src/components/DependencyGraph.tsx b/apps/website/src/components/DependencyGraph.tsx new file mode 100644 index 0000000..ce61e12 --- /dev/null +++ b/apps/website/src/components/DependencyGraph.tsx @@ -0,0 +1,78 @@ +import { GRAPH_PANE } from "../content.js"; + +/** The dependency graph the example spec (SPEC_JSON) compiles to. */ +export default function DependencyGraph() { + return ( + + + + + + + + + + {/* edges */} + + + + {/* nodes */} + + + + Token + + + 0x9f1a…c44e ✓ + + + + Registry + + + 0x27b3…08d1 ✓ + + + + Vault ^^ + + + CREATE · pending + + {/* legend */} + + {GRAPH_PANE.legendTop} + + + {GRAPH_PANE.legendBottom} + + + + ); +} diff --git a/apps/website/src/components/Features.tsx b/apps/website/src/components/Features.tsx deleted file mode 100644 index 6edcc77..0000000 --- a/apps/website/src/components/Features.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { FEATURES } from "../content.js"; -import studioCanvas from "../assets/studio-canvas.png"; -import studioInspector from "../assets/studio-inspector.png"; -import studioTemplates from "../assets/studio-templates.png"; -import studioDeployFlow from "../assets/studio-deploy-flow.png"; - -// Real Studio screenshots (captured from the running @redeploy/studio app), -// keyed by feature.id. See apps/website/src/assets/ for the source PNGs. -const SCREENSHOTS: Record = { - canvas: studioCanvas, - inspector: studioInspector, - templates: studioTemplates, - "deploy-flow": studioDeployFlow, -}; - -export default function Features() { - return ( -
-

Studio, at a glance

-

- The visual studio (@redeploy/studio) authors and inspects your deployment graph. The screenshots - below are real captures of the running app. -

- -
- ); -} diff --git a/apps/website/src/components/Footer.tsx b/apps/website/src/components/Footer.tsx index 906d69e..95d0a64 100644 --- a/apps/website/src/components/Footer.tsx +++ b/apps/website/src/components/Footer.tsx @@ -1,24 +1,20 @@ -import { FOOTER_LINKS } from "../content.js"; - -const currentYear = new Date().getFullYear(); +import { FOOTER } from "../content.js"; export default function Footer() { return ( - ); } diff --git a/apps/website/src/components/Hero.tsx b/apps/website/src/components/Hero.tsx index ea84f23..3bbac31 100644 --- a/apps/website/src/components/Hero.tsx +++ b/apps/website/src/components/Hero.tsx @@ -1,16 +1,15 @@ import { HERO } from "../content.js"; +import RichText from "./RichText.js"; export default function Hero() { return ( -
-

{HERO.eyebrow}

-

- {HERO.headline} +
+

+

-

{HERO.subhead}

- - {HERO.ctaLabel} - -

+

+ +

+ ); } diff --git a/apps/website/src/components/Packages.tsx b/apps/website/src/components/Packages.tsx new file mode 100644 index 0000000..9c00a82 --- /dev/null +++ b/apps/website/src/components/Packages.tsx @@ -0,0 +1,20 @@ +import { PACKAGES, PACKAGES_HEADING } from "../content.js"; + +export default function Packages() { + return ( +
+
+ {PACKAGES_HEADING.label} + {PACKAGES_HEADING.rest} +
+
+ {PACKAGES.map((pkg) => ( +
+
{pkg.name}
+
{pkg.description}
+
+ ))} +
+
+ ); +} diff --git a/apps/website/src/components/Pipeline.tsx b/apps/website/src/components/Pipeline.tsx new file mode 100644 index 0000000..13b1bf8 --- /dev/null +++ b/apps/website/src/components/Pipeline.tsx @@ -0,0 +1,29 @@ +import { PIPELINE_HEADING, PIPELINE_NOTE, PIPELINE_STAGES } from "../content.js"; +import RichText from "./RichText.js"; + +export default function Pipeline() { + return ( +
+
+ {PIPELINE_HEADING.label} + {PIPELINE_HEADING.rest} +
+
+ {PIPELINE_STAGES.map((stage) => ( +
+ {stage.num} +

+ +

+

+ +

+
+ ))} +
+

+ +

+
+ ); +} diff --git a/apps/website/src/components/RichText.tsx b/apps/website/src/components/RichText.tsx new file mode 100644 index 0000000..300d526 --- /dev/null +++ b/apps/website/src/components/RichText.tsx @@ -0,0 +1,28 @@ +import { Fragment } from "react"; +import type { RichSegment } from "../content.js"; + +/** Renders an array of RichSegment as inline nodes: code/em/b elements or a lime ``. */ +export default function RichText({ segments }: { segments: RichSegment[] }) { + return ( + <> + {segments.map((seg, i) => { + switch (seg.as) { + case "code": + return {seg.text}; + case "em": + return {seg.text}; + case "b": + return {seg.text}; + case "hl": + return ( + + {seg.text} + + ); + default: + return {seg.text}; + } + })} + + ); +} diff --git a/apps/website/src/components/SpecCode.tsx b/apps/website/src/components/SpecCode.tsx new file mode 100644 index 0000000..66b08d7 --- /dev/null +++ b/apps/website/src/components/SpecCode.tsx @@ -0,0 +1,69 @@ +// Lightweight syntax highlighter for the tiny spec-JSON dialect shown in the +// hero split pane. Rather than hand-authoring a tree of colored s (easy +// to typo and hard to keep in sync with content.ts), we tokenize the raw +// SPEC_JSON text at render time and color it the same way the approved design +// does: keys dim, plain string values ink, {"ref": …} lime, {"param"/"read": …} +// amber, and JSON punctuation a muted grey. Everything else (whitespace, +// newlines, indentation) is left uncolored. + +type TokenClass = "k" | "s" | "ref" | "par" | "pun"; + +interface Token { + text: string; + cls?: TokenClass; +} + +const TOKEN_RE = /\{"ref":\s*"[^"]*"\}|\{"(?:param|read)":\s*"[^"]*"\}|"[^"]*"|[{}[\]:,]/g; + +export function tokenizeSpec(source: string): Token[] { + const tokens: Token[] = []; + let lastIndex = 0; + + for (const match of source.matchAll(TOKEN_RE)) { + const text = match[0]; + const index = match.index ?? 0; + + if (index > lastIndex) { + tokens.push({ text: source.slice(lastIndex, index) }); + } + + let cls: TokenClass; + if (text.startsWith('{"ref"')) { + cls = "ref"; + } else if (text.startsWith('{"param"') || text.startsWith('{"read"')) { + cls = "par"; + } else if (text.startsWith('"')) { + const after = source.slice(index + text.length); + cls = /^\s*:/.test(after) ? "k" : "s"; + } else { + cls = "pun"; + } + + tokens.push({ text, cls }); + lastIndex = index + text.length; + } + + if (lastIndex < source.length) { + tokens.push({ text: source.slice(lastIndex) }); + } + + return tokens; +} + +export default function SpecCode({ source }: { source: string }) { + const tokens = tokenizeSpec(source); + + return ( +
+ {tokens.map((token, i) => + token.cls ? ( + + {token.text} + + ) : ( + {token.text} + ), + )} +
+ ); +} diff --git a/apps/website/src/components/SpecGraphSection.tsx b/apps/website/src/components/SpecGraphSection.tsx new file mode 100644 index 0000000..b0254d0 --- /dev/null +++ b/apps/website/src/components/SpecGraphSection.tsx @@ -0,0 +1,33 @@ +import { GRAPH_PANE, SPEC_JSON, SPEC_PANE, SPLIT_CAPTION } from "../content.js"; +import DependencyGraph from "./DependencyGraph.js"; +import RichText from "./RichText.js"; +import SpecCode from "./SpecCode.js"; + +/** Split panel: the spec you write (left) ⇄ the dependency graph it compiles to (right). */ +export default function SpecGraphSection() { + return ( + <> +
+
+
+ {SPEC_PANE.command} + {SPEC_PANE.note} +
+ +
+
+
+ {GRAPH_PANE.command} + {GRAPH_PANE.note} +
+
+ +
+
+
+

+ +

+ + ); +} diff --git a/apps/website/src/components/Studio.tsx b/apps/website/src/components/Studio.tsx new file mode 100644 index 0000000..558fc9a --- /dev/null +++ b/apps/website/src/components/Studio.tsx @@ -0,0 +1,25 @@ +import studioCanvas from "../assets/studio-canvas.png"; +import { STUDIO_SECTION } from "../content.js"; +import RichText from "./RichText.js"; + +export default function Studio() { + return ( +
+
+

+ +

+

{STUDIO_SECTION.body}

+ + {STUDIO_SECTION.ctaPrimary.label} + + + {STUDIO_SECTION.ctaSecondary.label} + +
+
+ {STUDIO_SECTION.screenshotAlt} +
+
+ ); +} diff --git a/apps/website/src/components/Topbar.tsx b/apps/website/src/components/Topbar.tsx new file mode 100644 index 0000000..bc65eae --- /dev/null +++ b/apps/website/src/components/Topbar.tsx @@ -0,0 +1,30 @@ +import { NAV_LINKS } from "../content.js"; + +export default function Topbar() { + return ( +
+ + + reDeploy + {" "} + ^^ + +
+ ); +} diff --git a/apps/website/src/content.ts b/apps/website/src/content.ts index 2327f48..aaead59 100644 --- a/apps/website/src/content.ts +++ b/apps/website/src/content.ts @@ -1,119 +1,195 @@ -// Marketing copy for the reDeploy site. +// Marketing copy & structured content for the reDeploy site (spec ⇄ graph redesign). // -// Source of truth: repo-root CLAUDE.md ("What this project is" + package -// descriptions under "Key directories"). Keep this file in sync with that — +// Source of truth for narrative claims: repo-root CLAUDE.md ("What this project is" + +// package descriptions under "Key directories"). Keep this file in sync with that — // do not claim anything reDeploy does not do today. export const REPO_URL = "https://github.com/robercano/reDeploy"; -// Placeholder — no docs site exists yet. Swap for a real URL once published. -export const DOCS_URL = "https://github.com/robercano/reDeploy#readme"; +// No public Studio instance yet — link to the source instead of a live deploy. +export const STUDIO_URL = "https://github.com/robercano/reDeploy/tree/main/apps/studio"; + +/** + * A run of rich text. Plain by default; `as` wraps it in an inline element: + * "code" -> , "em" -> , "b" -> , "hl" -> a lime-highlighted . + */ +export interface RichSegment { + text: string; + as?: "code" | "em" | "b" | "hl"; +} + +export interface NavLink { + label: string; + href: string; +} + +export const NAV_LINKS: NavLink[] = [ + { label: "pipeline", href: "#pipeline" }, + { label: "studio", href: "#studio" }, + { label: "packages", href: "#packages" }, + { label: "github", href: REPO_URL }, +]; export const HERO = { - eyebrow: "Built on Hardhat Ignition", - headline: "Declarative, idempotent, resumable smart-contract deployments.", - subhead: - "reDeploy lets smart-contract teams declaratively define multi-contract deployments — constructor " + - "args, inter-contract links, ordering — deploy them idempotently and resumably, apply resumable " + - "post-deployment configuration, verify source and on-chain state, and read deployment state from " + - "external systems. A visual studio authors and inspects it all.", - ctaLabel: "View on GitHub", - ctaHref: REPO_URL, + headline: [{ text: "One spec. One graph. " }, { text: "One truth.", as: "hl" }] as RichSegment[], + subhead: [ + { + text: "Declare your multi-contract system — args, links, ordering — and reDeploy keeps chain and spec in agreement: ", + }, + { text: "idempotent deploys", as: "b" }, + { text: ", " }, + { text: "resumable configuration", as: "b" }, + { text: ", " }, + { text: "verified state", as: "b" }, + { text: ". Built on Hardhat Ignition." }, + ] as RichSegment[], +}; + +export const SPEC_PANE = { + command: "cat protocol.spec.json", + note: "what you write", +}; + +export const GRAPH_PANE = { + command: "redeploy simulate", + note: "what it means", + svgLabel: "Dependency graph: Token and Registry feed Vault; config steps wire Registry to Vault", + legendTop: "— ref arg - - after · · · config wire", + legendBottom: "2 deployed · 1 planned · 2 config steps pending", }; -export interface Benefit { +// The example spec rendered (and syntax-highlighted) in the left-hand pane. Kept as +// plain text — SpecCode.tsx tokenizes it for display rather than hand-authoring spans. +export const SPEC_JSON = `{ + "contracts": [ + { "id": "Token", "args": ["Solid", "SLD"] }, + { "id": "Registry" }, + { "id": "Vault", + "args": [{"ref": "Token"}, {"param": "feeBps"}], + "after": ["Registry"] } + ], + "config": [ + { "wire": "Registry.register", "with": {"ref": "Vault"} }, + { "grantRole": "KEEPER", "on": "Vault", + "to": {"read": "Registry.opsAddress"} } + ] +}`; + +export const SPLIT_CAPTION: RichSegment[] = [ + { text: "the spec compiles to a dependency graph; the graph deploys in order; " }, + { text: "re-running changes only what's missing.", as: "hl" }, +]; + +export interface SectionHeading { + label: string; + rest: string; +} + +export const PIPELINE_HEADING: SectionHeading = { + label: "THE PIPELINE", + rest: " — one bar through every link, like the mark says", +}; + +export const PACKAGES_HEADING: SectionHeading = { + label: "PACKAGES", + rest: " — take only what you need", +}; + +export interface PipelineStage { id: string; - packageName: string; - title: string; - description: string; + num: string; + title: RichSegment[]; + description: RichSegment[]; } -export const BENEFITS: Benefit[] = [ +export const PIPELINE_STAGES: PipelineStage[] = [ { - id: "idempotent-resumable", - packageName: "@redeploy/core", - title: "Idempotent, resumable deployments", - description: - "A contract that's already deployed is never re-deployed. Journal-based resume means an " + - "interrupted multi-contract deployment picks up exactly where it left off.", + id: "deploy", + num: "01 deploy", + title: [{ text: "Deploy " }, { text: "^^", as: "hl" }], + description: [ + { text: "Topologically ordered from your " }, + { text: "ref", as: "code" }, + { text: "s and " }, + { text: "after", as: "code" }, + { text: "s. Idempotent by journal: deployed contracts are skipped, interrupted runs resume." }, + ], }, { - id: "resumable-config", - packageName: "@redeploy/config", - title: "Resumable post-deployment configuration", - description: - "Declarative configuration steps applied after deployment, with config-state idempotency — " + - "partial configuration runs resume cleanly instead of re-running steps that already succeeded.", + id: "configure", + num: "02 configure", + title: [{ text: "Configure" }], + description: [ + { text: "Roles, wiring, setters as declarative steps — " }, + { text: "set", as: "code" }, + { text: ", " }, + { text: "grantRole", as: "code" }, + { text: ", " }, + { text: "wire", as: "code" }, + { text: " — journaled and resumable, with args " }, + { text: "read", as: "code" }, + { text: " live from deployed contracts." }, + ], }, { - id: "verification", - packageName: "@redeploy/verify", - title: "Source and on-chain verification", - description: - "Verify contract source and bytecode via Etherscan/Sourcify, and assert that live on-chain " + - "configuration actually matches your declared spec.", + id: "verify", + num: "03 verify", + title: [{ text: "Verify" }], + description: [ + { text: "Source on Etherscan & Sourcify, then the rarer half: assert the " }, + { text: "live configuration", as: "em" }, + { text: " still matches the spec. Drift is a diff, not an incident." }, + ], }, { - id: "typed-reader", - packageName: "@redeploy/reader", - title: "Typed read-only access for external systems", - description: - "A read-only library exposing deployment and configuration state through a typed API, so other " + - "systems can query what's deployed and how it's configured without touching the deploy path.", - }, - { - id: "visual-studio", - packageName: "@redeploy/studio", - title: "Visual Studio", - description: - "Drag-and-drop authoring of the connection and configuration graph, emitting spec files, plus a " + - "live deployment inspector — all in one visual tool.", + id: "read", + num: "04 read", + title: [{ text: "Read" }], + description: [ + { + text: "A typed, read-only view of deployment + config state, with snapshots — one truth for frontends, subgraphs and ops.", + }, + ], }, ]; -export interface Feature { - id: string; - title: string; +export const PIPELINE_NOTE: RichSegment[] = [ + { text: "every stage is re-runnable at any time — " }, + { text: "the pipeline converges on the spec instead of executing scripts.", as: "hl" }, +]; + +export const STUDIO_SECTION = { + title: [{ text: "Or skip the JSON. " }, { text: "Draw it.", as: "hl" }] as RichSegment[], + body: "The reDeploy Studio is a drag-and-drop canvas over the same engine: author contracts and config visually, simulate the plan, deploy for real, and inspect any live deployment — it emits the exact spec files you'd write by hand, so the visual and the textual never fork.", + ctaPrimary: { label: "launch studio ^^", href: STUDIO_URL }, + ctaSecondary: { label: "view on github", href: REPO_URL }, + screenshotAlt: "reDeploy Studio canvas — drag-and-drop contract graph authoring", +}; + +export interface Package { + name: string; description: string; - screenshotCaption: string; } -export const FEATURES: Feature[] = [ - { - id: "canvas", - title: "Graph authoring canvas", - description: - "Drag contracts onto a canvas, wire constructor arguments and inter-contract links, and let " + - "reDeploy resolve dependency ordering. The graph is the spec — Studio emits spec files directly " + - "from what you draw.", - screenshotCaption: "Studio canvas — drag-and-drop contract graph authoring", - }, - { - id: "inspector", - title: "Deployment inspector", - description: - "Load an existing deployment and inspect its live contract graph and configuration state, powered " + - "by the same typed reader API available to external systems.", - screenshotCaption: "Deployment inspector — live contract graph and state", - }, - { - id: "templates", - title: "Template gallery", - description: - "Start from a built-in or saved template instead of an empty canvas — reusable starting points for " + - "common multi-contract topologies.", - screenshotCaption: "Template gallery — start from a reusable deployment template", - }, - { - id: "deploy-flow", - title: "Simulate, then deploy", - description: - "Review a deployment plan and simulate it before executing, so you see what would happen before it " + - "happens on chain.", - screenshotCaption: "Deploy flow — simulate a plan before executing it", - }, +export const PACKAGES: Package[] = [ + { name: "@redeploy/core", description: "spec → Ignition module · ordering · idempotent deploys" }, + { name: "@redeploy/config", description: "resumable post-deploy configuration steps" }, + { name: "@redeploy/verify", description: "source verification + on-chain drift detection" }, + { name: "@redeploy/reader", description: "typed read-only state API + snapshots" }, + { name: "@redeploy/studio", description: "visual authoring & inspection" }, ]; -export const FOOTER_LINKS = [ - { label: "GitHub", href: REPO_URL }, - { label: "Docs", href: DOCS_URL }, -]; +export interface FamilyLink { + label: string; + href?: string; + current?: boolean; +} + +export const FOOTER = { + promptUser: "roberto@thesolidchain:~$", + promptRest: "a product of The Solid Chain", + family: [ + { label: "reCode ", href: "#" }, + { label: "reDeploy ^^", current: true }, + { label: "reDeFi <=>", href: "#" }, + ] as FamilyLink[], +}; diff --git a/apps/website/src/styles.css b/apps/website/src/styles.css index f006059..e23ee86 100644 --- a/apps/website/src/styles.css +++ b/apps/website/src/styles.css @@ -1,20 +1,14 @@ :root { - color-scheme: light dark; - --color-bg: #0b0f19; - --color-bg-alt: #121828; - --color-fg: #e7ebf3; - --color-fg-muted: #9aa5b8; - --color-accent: #6ea8fe; - --color-border: #232b40; - --max-width: 72rem; - font-family: - system-ui, - -apple-system, - "Segoe UI", - Roboto, - Helvetica, - Arial, - sans-serif; + --bg: #0d0a05; + --panel: #121008; + --edge: #292112; + --ink: #e4dac2; + --dim: #8a7b5c; + --accent: #f5a524; + --lime: #d6ff7a; + --ok: #3fb950; + --hi: #f7eed9; + --mono: ui-monospace, "Cascadia Code", "JetBrains Mono", Menlo, Consolas, monospace; } * { @@ -23,256 +17,420 @@ body { margin: 0; - background: var(--color-bg); - color: var(--color-fg); - line-height: 1.5; + color: var(--ink); + font-family: var(--mono); + font-size: 13.5px; + line-height: 1.65; + background: + repeating-linear-gradient(0deg, rgba(245, 165, 36, 0.015) 0 1px, transparent 1px 3px), + var(--bg); } .page { - display: flex; - flex-direction: column; min-height: 100vh; } -main { - flex: 1; +.wrap { + max-width: 1080px; + margin: 0 auto; + padding: 0 18px; } -section { - max-width: var(--max-width); - margin: 0 auto; - padding: 4rem 1.5rem; +.hl { + color: var(--lime); } -h1, -h2, -h3 { - line-height: 1.15; +/* topbar */ +.topbar { + display: flex; + align-items: center; + gap: 14px; + padding: 12px 18px; + border-bottom: 1px solid var(--edge); + background: var(--panel); + position: sticky; + top: 0; + z-index: 5; + font-size: 12px; } -/* Hero */ -.hero { - text-align: center; - padding-top: 6rem; - padding-bottom: 5rem; +.topbar .mark { + width: 30px; + height: 30px; } -.hero__eyebrow { - text-transform: uppercase; - letter-spacing: 0.08em; - font-size: 0.85rem; - color: var(--color-accent); +.topbar b { + color: var(--hi); font-weight: 600; - margin: 0 0 1rem; } -.hero__headline { - font-size: clamp(2rem, 5vw, 3.25rem); - margin: 0 0 1.5rem; +.topbar b .re { + color: var(--dim); + font-weight: 500; } -.hero__subhead { - font-size: 1.15rem; - color: var(--color-fg-muted); - max-width: 46rem; - margin: 0 auto 2.5rem; +.topbar nav { + margin-left: auto; + display: flex; + gap: 16px; } -.hero__cta { - display: inline-block; - background: var(--color-accent); - color: #0b0f19; - font-weight: 600; +.topbar a { + color: var(--dim); text-decoration: none; - padding: 0.85rem 2rem; - border-radius: 0.5rem; } -.hero__cta:hover, -.hero__cta:focus-visible { - opacity: 0.9; +.topbar a:hover, +.topbar a:focus-visible { + color: var(--accent); + outline: none; } -/* Section shared */ -.section-lede { - color: var(--color-fg-muted); - max-width: 46rem; - margin: 0 0 2.5rem; +/* hero */ +.heroText { + text-align: center; + padding: 52px 0 8px; } -/* Benefits */ -.benefits { - background: var(--color-bg-alt); - max-width: none; - padding: 0; +h1 { + font-family: "Space Grotesk", var(--mono); + font-size: clamp(30px, 5vw, 44px); + margin: 0; + color: var(--hi); + letter-spacing: -0.015em; } -/* Inner content wrapper mirrors the plain `section` box (same max-width + - padding) so the heading/lede/grid line up with hero and features, while - `.benefits` itself stays full-bleed for the background band. */ -.benefits__inner { - max-width: var(--max-width); - margin: 0 auto; - padding: 4rem 1.5rem; +.heroText p { + color: var(--dim); + max-width: 64ch; + margin: 12px auto 0; } -.benefits__list { - list-style: none; - margin: 0; - padding: 0; +.heroText p b { + color: var(--ink); +} + +.split { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0; + margin: 34px 0 10px; + background: var(--panel); + border: 1px solid var(--edge); + border-radius: 8px; + overflow: hidden; +} + +@media (max-width: 860px) { + .split { + grid-template-columns: 1fr; + } +} + +.pane { + min-width: 0; +} + +.pane + .pane { + border-left: 1px solid var(--edge); +} + +@media (max-width: 860px) { + .pane + .pane { + border-left: 0; + border-top: 1px solid var(--edge); + } +} + +.pane .head { display: flex; - flex-wrap: wrap; - justify-content: center; - gap: 1.5rem; + justify-content: space-between; + gap: 10px; + padding: 9px 16px; + border-bottom: 1px solid var(--edge); + font-size: 12px; + color: var(--dim); } -.benefits__item { - flex: 1 1 16rem; - max-width: 21rem; - border: 1px solid var(--color-border); - border-radius: 0.75rem; - padding: 1.5rem; +.pane .head .cmd { + color: var(--accent); } -.benefits__item h3 { - margin: 0 0 0.5rem; - font-size: 1.1rem; +.pane .head .cmd::before { + content: "$ "; + color: var(--dim); } -.benefits__item p { - color: var(--color-fg-muted); - margin: 0 0 0.75rem; +.code { + padding: 16px 18px; + font-size: 12.5px; + line-height: 1.75; + overflow-x: auto; + white-space: pre; } -.benefits__package { - font-size: 0.8rem; - color: var(--color-accent); - background: rgba(110, 168, 254, 0.1); - padding: 0.2rem 0.5rem; - border-radius: 0.35rem; +.code .k { + color: var(--dim); +} /* keys */ +.code .s { + color: var(--ink); +} /* strings */ +.code .ref { + color: var(--lime); +} /* refs */ +.code .par { + color: var(--accent); +} /* params/exprs */ +.code .pun { + color: #5a4f38; } -/* Features */ -.features__list { - list-style: none; - margin: 0; - padding: 0; +.graphPane { display: flex; - flex-direction: column; - gap: 3rem; + align-items: center; + justify-content: center; + padding: 14px; } -.features__item { +.caption { + text-align: center; + color: var(--dim); + font-size: 12px; + margin: 10px 0 54px; +} + +/* pipeline: the bar through the links */ +.mh { + color: var(--dim); + font-size: 12px; + letter-spacing: 0.22em; + text-transform: uppercase; + margin: 0 0 18px; +} + +.mh::before { + content: "$ "; + color: var(--accent); + letter-spacing: 0; + text-transform: none; +} + +.mh b { + color: var(--hi); + letter-spacing: 0.22em; +} + +.pipe { + position: relative; display: grid; - grid-template-columns: 1fr 1fr; - gap: 2rem; - align-items: center; + grid-template-columns: repeat(4, 1fr); + gap: 14px; + margin-bottom: 8px; } -.features__item:nth-child(even) .features__text { - order: 2; +@media (max-width: 860px) { + .pipe { + grid-template-columns: 1fr 1fr; + } } -.features__text h3 { - margin: 0 0 0.75rem; - font-size: 1.3rem; +@media (max-width: 520px) { + .pipe { + grid-template-columns: 1fr; + } } -.features__text p { - color: var(--color-fg-muted); - margin: 0; +.pipe::before { + content: ""; + position: absolute; + left: 2%; + right: 2%; + top: 34px; + height: 3px; + background: var(--accent); + opacity: 0.55; } -/* Feature screenshot (real Studio capture) */ -.feature-screenshot { - margin: 0; +@media (max-width: 860px) { + .pipe::before { + display: none; + } } -.feature-screenshot__box { - aspect-ratio: 16 / 10; - border: 1px solid var(--color-border); - border-radius: 0.75rem; - overflow: hidden; - background: var(--color-bg-alt); +.stage { + position: relative; + background: var(--panel); + border: 1px solid var(--edge); + border-radius: 8px; + padding: 16px; } -.feature-screenshot__box img { - display: block; - width: 100%; - height: 100%; - object-fit: cover; - object-position: top; +.stage .num { + position: absolute; + top: -11px; + left: 14px; + background: var(--bg); + border: 1px solid var(--accent); + color: var(--accent); + border-radius: 12px; + font-size: 10.5px; + padding: 1px 10px; } -.feature-screenshot__caption { - margin-top: 0.75rem; - font-size: 0.85rem; - color: var(--color-fg-muted); - text-align: center; +.stage h3 { + margin: 12px 0 6px; + font-size: 14px; + color: var(--hi); + font-family: "Space Grotesk", var(--mono); } -/* Footer */ -.footer { - border-top: 1px solid var(--color-border); - padding: 2rem 1.5rem; - max-width: var(--max-width); - margin: 0 auto; - display: flex; - flex-direction: column; +.stage p { + margin: 0; + font-size: 12.3px; + color: var(--ink); +} + +.stage p code { + color: var(--lime); + background: rgba(214, 255, 122, 0.06); + padding: 0 4px; + border-radius: 3px; + font-size: 11.5px; +} + +.pipeNote { + color: var(--dim); + font-size: 12px; + margin: 10px 0 54px; +} + +/* studio band */ +.studio { + display: grid; + grid-template-columns: 1.1fr 1fr; + gap: 24px; align-items: center; - gap: 1rem; - text-align: center; + background: var(--panel); + border: 1px solid var(--edge); + border-radius: 8px; + padding: 22px; + margin-bottom: 54px; } -.footer__links { - display: flex; - gap: 1.5rem; +@media (max-width: 860px) { + .studio { + grid-template-columns: 1fr; + } } -.footer__links a { - color: var(--color-fg); +.studio h2 { + font-family: "Space Grotesk", var(--mono); + margin: 0 0 8px; + font-size: 22px; + color: var(--hi); +} + +.studio p { + margin: 0 0 14px; + font-size: 13px; +} + +.btn { + display: inline-block; + padding: 10px 20px; + border: 1px solid var(--lime); + border-radius: 5px; + background: rgba(214, 255, 122, 0.07); + color: var(--lime); + font-weight: 700; text-decoration: none; + font-size: 13px; + margin-right: 10px; } -.footer__links a:hover, -.footer__links a:focus-visible { - color: var(--color-accent); +.btn:hover, +.btn:focus-visible { + background: var(--lime); + color: #141804; + outline: none; } -.footer__attribution { - color: var(--color-fg-muted); - font-size: 0.85rem; - margin: 0; +.btn.amber { + border-color: var(--accent); + background: rgba(245, 165, 36, 0.07); + color: var(--accent); } -.footer__attribution a { - color: var(--color-fg-muted); +.btn.amber:hover, +.btn.amber:focus-visible { + background: var(--accent); + color: #140f04; } -/* Responsive */ -@media (max-width: 48rem) { - section { - padding: 3rem 1.25rem; - } +.shot { + border: 1px solid var(--edge); + border-radius: 6px; + overflow: hidden; +} - .hero { - padding-top: 4rem; - } +.shot img { + display: block; + width: 100%; + height: auto; +} - .benefits__inner { - padding: 3rem 1.25rem; - } +/* packages */ +.pkgs { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 12px; + margin-bottom: 54px; +} - .benefits__item { - flex-basis: 100%; - max-width: none; - } +.pkg { + background: var(--panel); + border: 1px solid var(--edge); + border-radius: 6px; + padding: 14px; +} - .features__item { - grid-template-columns: 1fr; - } +.pkg .n { + color: var(--lime); + font-weight: 600; + font-size: 12.5px; +} - .features__item:nth-child(even) .features__text { - order: initial; - } +.pkg .d { + color: var(--dim); + font-size: 11.8px; + margin-top: 4px; +} + +/* footer */ +footer { + border-top: 1px solid var(--edge); + padding: 14px 0 40px; + font-size: 11px; + color: var(--dim); + display: flex; + justify-content: space-between; + gap: 14px; + flex-wrap: wrap; +} + +footer .prompt { + color: var(--accent); +} + +footer .fam a { + color: var(--dim); + text-decoration: none; +} + +footer .fam a:hover { + color: var(--ink); +} + +footer .fam .cur { + color: var(--lime); } diff --git a/apps/website/test/App.test.tsx b/apps/website/test/App.test.tsx index d629ed9..f3ada95 100644 --- a/apps/website/test/App.test.tsx +++ b/apps/website/test/App.test.tsx @@ -1,66 +1,83 @@ -import { render, screen } from "@testing-library/react"; +import { render, screen, within } from "@testing-library/react"; import App from "../src/App.js"; -import { HERO, BENEFITS, FEATURES, REPO_URL } from "../src/content.js"; +import { GRAPH_PANE, NAV_LINKS, PACKAGES, PIPELINE_STAGES, REPO_URL, STUDIO_SECTION, STUDIO_URL } from "../src/content.js"; describe("App", () => { - it("renders the hero pitch and primary CTA", () => { + it("renders a sticky topbar with the four nav anchors", () => { render(); - expect(screen.getByRole("heading", { level: 1, name: HERO.headline })).toBeInTheDocument(); + for (const link of NAV_LINKS) { + const anchor = screen.getByRole("link", { name: link.label }); + expect(anchor).toHaveAttribute("href", link.href); + } + }); + + it("renders the hero headline and subhead", () => { + render(); - const cta = screen.getByRole("link", { name: HERO.ctaLabel }); - expect(cta).toHaveAttribute("href", REPO_URL); + expect(screen.getByRole("heading", { level: 1, name: /One spec\. One graph\. One truth\./ })).toBeInTheDocument(); + expect(screen.getByText(/Built on Hardhat Ignition\./)).toBeInTheDocument(); }); - it("lists all five reDeploy packages as benefits", () => { + it("renders the spec pane with the example contracts and the dependency graph", () => { render(); - for (const benefit of BENEFITS) { - expect(screen.getByText(benefit.packageName)).toBeInTheDocument(); + expect(screen.getByText("cat protocol.spec.json")).toBeInTheDocument(); + for (const value of ["Token", "Registry", "Vault", "feeBps", "KEEPER"]) { + expect(screen.getAllByText(new RegExp(value)).length).toBeGreaterThan(0); } - expect(BENEFITS.map((b) => b.packageName)).toEqual([ - "@redeploy/core", - "@redeploy/config", - "@redeploy/verify", - "@redeploy/reader", - "@redeploy/studio", - ]); + expect(screen.getByRole("img", { name: GRAPH_PANE.svgLabel })).toBeInTheDocument(); }); - it("renders a real Studio screenshot for every feature, with a matching caption", () => { + it("renders all four pipeline stages, in order, with inline code terms", () => { render(); - const images = screen.getAllByRole("img"); - expect(images.length).toBe(FEATURES.length); + const pipeline = document.getElementById("pipeline"); + expect(pipeline).not.toBeNull(); + const stages = within(pipeline as HTMLElement).getAllByRole("heading", { level: 3 }); + expect(stages.map((h) => h.textContent?.trim())).toEqual(["Deploy ^^", "Configure", "Verify", "Read"]); + expect(PIPELINE_STAGES).toHaveLength(4); - const srcStemById: Record = { - canvas: "studio-canvas", - inspector: "studio-inspector", - templates: "studio-templates", - "deploy-flow": "studio-deploy-flow", - }; + expect(within(pipeline as HTMLElement).getByText("ref")).toBeInTheDocument(); + expect(within(pipeline as HTMLElement).getByText("grantRole")).toBeInTheDocument(); + }); + + it("renders the Studio band with a real screenshot and the two real CTAs", () => { + render(); - for (const feature of FEATURES) { - const img = screen.getByAltText(feature.screenshotCaption) as HTMLImageElement; - expect(screen.getByText(feature.screenshotCaption)).toBeInTheDocument(); + const img = screen.getByAltText(STUDIO_SECTION.screenshotAlt) as HTMLImageElement; + expect(img.getAttribute("src")).toContain("studio-canvas"); - const src = img.getAttribute("src"); - expect(src).toBeTruthy(); - expect(src).toContain(srcStemById[feature.id]); - } + const launch = screen.getByRole("link", { name: STUDIO_SECTION.ctaPrimary.label }); + expect(launch).toHaveAttribute("href", STUDIO_URL); - const srcs = images.map((img) => img.getAttribute("src")); - expect(new Set(srcs).size).toBe(FEATURES.length); + const github = screen.getByRole("link", { name: STUDIO_SECTION.ctaSecondary.label }); + expect(github).toHaveAttribute("href", REPO_URL); }); - it("links to the GitHub repo in the footer", () => { + it("lists all five reDeploy packages, none dropped or duplicated", () => { render(); - const githubLinks = screen.getAllByRole("link", { name: "GitHub" }); - expect(githubLinks.length).toBeGreaterThan(0); - for (const link of githubLinks) { - expect(link).toHaveAttribute("href", REPO_URL); + for (const pkg of PACKAGES) { + expect(screen.getByText(pkg.name)).toBeInTheDocument(); } + expect(PACKAGES).toHaveLength(5); + }); + + it("renders the footer prompt line and family strip with reDeploy as current", () => { + render(); + + expect(screen.getByText("roberto@thesolidchain:~$")).toBeInTheDocument(); + expect(screen.getByText("reDeploy ^^")).toBeInTheDocument(); + expect(screen.getByRole("link", { name: "reCode " })).toHaveAttribute("href", "#"); + expect(screen.getByRole("link", { name: "reDeFi <=>" })).toHaveAttribute("href", "#"); + }); + + it("links to the real GitHub repo from the topbar", () => { + render(); + + const githubLink = screen.getByRole("link", { name: "github" }); + expect(githubLink).toHaveAttribute("href", REPO_URL); }); }); diff --git a/apps/website/test/Benefits.test.tsx b/apps/website/test/Benefits.test.tsx deleted file mode 100644 index d7e2bc8..0000000 --- a/apps/website/test/Benefits.test.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { render, screen } from "@testing-library/react"; -import Benefits from "../src/components/Benefits.js"; -import { BENEFITS } from "../src/content.js"; - -describe("Benefits", () => { - it("labels the section with the heading via aria-labelledby", () => { - render(); - - const heading = screen.getByRole("heading", { level: 2, name: "Why teams use reDeploy" }); - expect(heading).toHaveAttribute("id", "benefits-heading"); - - const section = heading.closest("section"); - expect(section).toHaveAttribute("aria-labelledby", "benefits-heading"); - }); - - it("wraps the heading, lede, and card list in a single alignment container", () => { - render(); - - const heading = screen.getByRole("heading", { level: 2, name: "Why teams use reDeploy" }); - const list = screen.getByRole("list"); - - // Regression guard for #132: heading and list must share one common - // parent (the `.benefits__inner` wrapper) so they get identical - // horizontal alignment instead of each being centered independently. - const headingWrapper = heading.parentElement; - expect(headingWrapper).toHaveClass("benefits__inner"); - expect(headingWrapper).toContainElement(list); - }); - - it("renders all five benefit cards, none dropped or duplicated", () => { - render(); - - const list = screen.getByRole("list"); - const items = list.querySelectorAll(":scope > .benefits__item"); - expect(items).toHaveLength(5); - expect(items).toHaveLength(BENEFITS.length); - - for (const benefit of BENEFITS) { - expect(screen.getByRole("heading", { level: 3, name: benefit.title })).toBeInTheDocument(); - expect(screen.getByText(benefit.packageName)).toBeInTheDocument(); - } - }); - - it("uses a wrapping flex layout for the card list so an uneven last row centers instead of stretching", () => { - // jsdom doesn't compute real layout, but we can assert the structural - // choice that guards against the 4-on-top + 1-orphan grid regression: - // every card is a direct
  • of the same - //
      , with no per-item inline width hacks that - // would fight the CSS wrap/centering behavior. - render(); - - const list = screen.getByRole("list"); - expect(list).toHaveClass("benefits__list"); - - const items = list.querySelectorAll(":scope > li"); - expect(items).toHaveLength(5); - for (const item of items) { - expect(item).toHaveClass("benefits__item"); - expect(item.getAttribute("style")).toBeNull(); - } - }); -}); diff --git a/apps/website/test/Packages.test.tsx b/apps/website/test/Packages.test.tsx new file mode 100644 index 0000000..268bf31 --- /dev/null +++ b/apps/website/test/Packages.test.tsx @@ -0,0 +1,32 @@ +import { render, screen } from "@testing-library/react"; +import Packages from "../src/components/Packages.js"; +import { PACKAGES, PACKAGES_HEADING } from "../src/content.js"; + +describe("Packages", () => { + it("labels the section with the micro-heading", () => { + render(); + + expect(screen.getByText(PACKAGES_HEADING.label)).toBeInTheDocument(); + const section = screen.getByText(PACKAGES_HEADING.label).closest("section"); + expect(section).toHaveAttribute("id", "packages"); + }); + + it("renders all five package cards, none dropped or duplicated", () => { + render(); + + expect(PACKAGES).toHaveLength(5); + for (const pkg of PACKAGES) { + expect(screen.getByText(pkg.name)).toBeInTheDocument(); + expect(screen.getByText(pkg.description)).toBeInTheDocument(); + } + }); + + it("wraps every card in a .pkg element inside a single .pkgs grid", () => { + render(); + + const grid = document.querySelector(".pkgs"); + expect(grid).not.toBeNull(); + const cards = grid?.querySelectorAll(":scope > .pkg"); + expect(cards).toHaveLength(PACKAGES.length); + }); +}); diff --git a/apps/website/test/SpecCode.test.tsx b/apps/website/test/SpecCode.test.tsx new file mode 100644 index 0000000..f5e2684 --- /dev/null +++ b/apps/website/test/SpecCode.test.tsx @@ -0,0 +1,48 @@ +import { render } from "@testing-library/react"; +import SpecCode, { tokenizeSpec } from "../src/components/SpecCode.js"; +import { SPEC_JSON } from "../src/content.js"; + +describe("tokenizeSpec", () => { + it("reassembles to the exact source text", () => { + const tokens = tokenizeSpec(SPEC_JSON); + expect(tokens.map((t) => t.text).join("")).toBe(SPEC_JSON); + }); + + it("classes {\"ref\": ...} objects as ref and {\"param\"/\"read\": ...} objects as par", () => { + const tokens = tokenizeSpec(SPEC_JSON); + const refTokens = tokens.filter((t) => t.cls === "ref"); + const parTokens = tokens.filter((t) => t.cls === "par"); + + expect(refTokens.map((t) => t.text)).toEqual(['{"ref": "Token"}', '{"ref": "Vault"}']); + expect(parTokens.map((t) => t.text)).toEqual(['{"param": "feeBps"}', '{"read": "Registry.opsAddress"}']); + }); + + it("classes object keys as k and plain string values as s", () => { + const tokens = tokenizeSpec(SPEC_JSON); + const keys = tokens.filter((t) => t.cls === "k").map((t) => t.text); + const strings = tokens.filter((t) => t.cls === "s").map((t) => t.text); + + expect(keys).toEqual( + expect.arrayContaining(['"contracts"', '"id"', '"args"', '"after"', '"config"', '"wire"', '"with"', '"grantRole"', '"on"', '"to"']), + ); + expect(strings).toEqual(expect.arrayContaining(['"Token"', '"Registry"', '"Vault"', '"KEEPER"'])); + }); + + it("classes JSON punctuation as pun and leaves whitespace unclassed", () => { + const tokens = tokenizeSpec('{"a": 1}\n '); + expect(tokens.filter((t) => t.cls === "pun").map((t) => t.text)).toEqual(["{", ":", "}"]); + expect(tokens.some((t) => !t.cls && /\s/.test(t.text))).toBe(true); + }); +}); + +describe("SpecCode", () => { + it("renders the tokenized source inside a .code block", () => { + const { container } = render(); + + const code = container.querySelector(".code"); + expect(code).not.toBeNull(); + expect(code?.textContent).toBe(SPEC_JSON); + expect(code?.querySelectorAll(".ref")).toHaveLength(2); + expect(code?.querySelectorAll(".par")).toHaveLength(2); + }); +}); diff --git a/apps/website/test/content.test.ts b/apps/website/test/content.test.ts index 0c28a1b..9c32e74 100644 --- a/apps/website/test/content.test.ts +++ b/apps/website/test/content.test.ts @@ -1,32 +1,94 @@ -import { BENEFITS, FEATURES, FOOTER_LINKS, REPO_URL } from "../src/content.js"; +import { + FOOTER, + GRAPH_PANE, + HERO, + NAV_LINKS, + PACKAGES, + PIPELINE_HEADING, + PIPELINE_NOTE, + PIPELINE_STAGES, + REPO_URL, + SPEC_JSON, + SPEC_PANE, + SPLIT_CAPTION, + STUDIO_SECTION, + STUDIO_URL, + type RichSegment, +} from "../src/content.js"; + +function flatten(segments: RichSegment[]): string { + return segments.map((s) => s.text).join(""); +} describe("content", () => { - it("has non-empty copy for every benefit", () => { - expect(BENEFITS.length).toBe(5); - for (const benefit of BENEFITS) { - expect(benefit.title.length).toBeGreaterThan(0); - expect(benefit.description.length).toBeGreaterThan(0); - expect(benefit.packageName.startsWith("@redeploy/")).toBe(true); - } + it("points every real link at the actual repo / studio source", () => { + expect(REPO_URL).toBe("https://github.com/robercano/reDeploy"); + expect(STUDIO_URL).toBe("https://github.com/robercano/reDeploy/tree/main/apps/studio"); + + const github = NAV_LINKS.find((l) => l.label === "github"); + expect(github?.href).toBe(REPO_URL); + + expect(STUDIO_SECTION.ctaPrimary.href).toBe(STUDIO_URL); + expect(STUDIO_SECTION.ctaSecondary.href).toBe(REPO_URL); }); - it("has non-empty copy and a screenshot caption for every feature", () => { - expect(FEATURES.length).toBeGreaterThan(0); - for (const feature of FEATURES) { - expect(feature.title.length).toBeGreaterThan(0); - expect(feature.description.length).toBeGreaterThan(0); - expect(feature.screenshotCaption.length).toBeGreaterThan(0); - } + it("has the four topbar nav anchors, in order", () => { + expect(NAV_LINKS.map((l) => l.label)).toEqual(["pipeline", "studio", "packages", "github"]); + expect(NAV_LINKS.find((l) => l.label === "pipeline")?.href).toBe("#pipeline"); + expect(NAV_LINKS.find((l) => l.label === "studio")?.href).toBe("#studio"); + expect(NAV_LINKS.find((l) => l.label === "packages")?.href).toBe("#packages"); + }); - expect(FEATURES.map((f) => f.id)).toEqual(["canvas", "inspector", "templates", "deploy-flow"]); + it("has a non-empty hero headline and subhead", () => { + expect(flatten(HERO.headline)).toContain("One truth."); + expect(flatten(HERO.subhead).length).toBeGreaterThan(0); + expect(HERO.subhead.some((s) => s.as === "b")).toBe(true); + }); - const captions = FEATURES.map((f) => f.screenshotCaption); - expect(new Set(captions).size).toBe(captions.length); + it("has a well-formed example spec that parses as JSON", () => { + expect(() => JSON.parse(SPEC_JSON)).not.toThrow(); + const parsed = JSON.parse(SPEC_JSON); + expect(parsed.contracts.map((c: { id: string }) => c.id)).toEqual(["Token", "Registry", "Vault"]); + expect(parsed.config).toHaveLength(2); }); - it("points footer links at the real GitHub repo", () => { - const github = FOOTER_LINKS.find((link) => link.label === "GitHub"); - expect(github?.href).toBe(REPO_URL); - expect(REPO_URL).toBe("https://github.com/robercano/reDeploy"); + it("labels the spec/graph panes", () => { + expect(SPEC_PANE.command).toBe("cat protocol.spec.json"); + expect(GRAPH_PANE.command).toBe("redeploy simulate"); + expect(GRAPH_PANE.svgLabel.length).toBeGreaterThan(0); + }); + + it("has a non-empty split caption", () => { + expect(flatten(SPLIT_CAPTION).length).toBeGreaterThan(0); + }); + + it("has all four pipeline stages, in order, each with a title and description", () => { + expect(PIPELINE_STAGES.map((s) => s.id)).toEqual(["deploy", "configure", "verify", "read"]); + for (const stage of PIPELINE_STAGES) { + expect(flatten(stage.title).length).toBeGreaterThan(0); + expect(flatten(stage.description).length).toBeGreaterThan(0); + } + expect(PIPELINE_HEADING.label).toBe("THE PIPELINE"); + expect(flatten(PIPELINE_NOTE).length).toBeGreaterThan(0); + }); + + it("lists all five reDeploy packages, none dropped or duplicated", () => { + expect(PACKAGES.map((p) => p.name)).toEqual([ + "@redeploy/core", + "@redeploy/config", + "@redeploy/verify", + "@redeploy/reader", + "@redeploy/studio", + ]); + for (const pkg of PACKAGES) { + expect(pkg.description.length).toBeGreaterThan(0); + } + }); + + it("has a footer prompt line and a three-entry family strip with exactly one current item", () => { + expect(FOOTER.promptUser).toBe("roberto@thesolidchain:~$"); + expect(FOOTER.family).toHaveLength(3); + expect(FOOTER.family.filter((f) => f.current)).toHaveLength(1); + expect(FOOTER.family.find((f) => f.current)?.label).toBe("reDeploy ^^"); }); }); diff --git a/apps/website/vite.config.ts b/apps/website/vite.config.ts index 0ff82b5..417129d 100644 --- a/apps/website/vite.config.ts +++ b/apps/website/vite.config.ts @@ -8,6 +8,10 @@ import react from "@vitejs/plugin-react"; export default defineConfig({ plugins: [react()], + // Relative asset paths so the static build works unmodified when served + // from a custom domain root on GitHub Pages (see public/CNAME). + base: "./", + server: { port: 5180, strictPort: true }, preview: { port: 5180, strictPort: true },