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 @@
-- 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. -
-{benefit.description}
-{benefit.packageName}
- - The visual studio (@redeploy/studio) authors and inspects your deployment graph. The screenshots - below are real captures of the running app. -
-{feature.description}
-{HERO.eyebrow}
-
+
+
{seg.text};
+ case "em":
+ return {seg.text};
+ case "b":
+ return {seg.text};
+ case "hl":
+ return (
+
+ {seg.text}
+
+ );
+ default:
+ return
+
{STUDIO_SECTION.body}
+ + {STUDIO_SECTION.ctaPrimary.label} + + + {STUDIO_SECTION.ctaSecondary.label} + +, "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 },