From 24a3dc00bfa357bb33747d4480a7842b69ddfc96 Mon Sep 17 00:00:00 2001 From: Anurag Goel Date: Wed, 23 Sep 2026 23:06:39 -0700 Subject: [PATCH] Restyle the factory UI with the Render design system The UI used a blue gradient, rounded panels with shadows, and the Inter face, which is not the look of Render. Now it uses the foundations of the Render design system, in the dark palette only, as the UI did before: - Semantic color tokens. Each component refers only to them. - Roobert for headings, PP Neue Montreal for text, and PP Neue Montreal Mono for labels, stages, and dates. The page loads the open fallbacks, Manrope and Roboto Mono, from Google Fonts: the brand faces have a license for internal use only, and this repository is public. - 1px borders, square corners, and no shadows. - Purple only for the primary button on hover, the link to the live website, and one phrase of the headline. - Green for stages that are done and runs that deployed, and red for failures. The history shows each status in its own element, so that the stylesheet can color it. - A top bar with the Render logo, as inline SVG in the color of the text. The text of the page does not change, so the talk track of the demo still agrees with it. The gateway test looked for the headline as one string, and the purple phrase now divides it in the HTML. The test now looks for the title of the page. Co-Authored-By: Claude Opus 5.5 --- public/app.js | 8 +- public/index.html | 49 ++- public/style.css | 708 ++++++++++++++++++++++++++++++------------ tests/gateway.test.ts | 2 +- 4 files changed, 552 insertions(+), 215 deletions(-) diff --git a/public/app.js b/public/app.js index 05e8b89..5deefc7 100644 --- a/public/app.js +++ b/public/app.js @@ -173,8 +173,14 @@ function renderHistory() { const name = document.createElement("strong"); name.textContent = run.appName || truncate(run.prompt, 34); + // The status is its own element, so the stylesheet can color it by state. + const state = document.createElement("span"); + state.className = "run-state"; + state.dataset.status = run.status; + state.textContent = label(run.status); const meta = document.createElement("span"); - meta.textContent = `${label(run.status)} · ${formatDate(run.createdAt)}`; + meta.className = "run-meta"; + meta.append(state, ` · ${formatDate(run.createdAt)}`); button.append(name, meta); button.addEventListener("click", () => selectRun(run.runId)); return button; diff --git a/public/index.html b/public/index.html index afa24b2..e95881a 100644 --- a/public/index.html +++ b/public/index.html @@ -5,20 +5,35 @@ Vibe Code Demo + + + + +
+ + Vibe Code Demo +
+
-
+

Render Workflows demo

-

Describe it. Ship it.

+

Describe it. Ship it.

Submit an app idea and follow it from design through a live Render deployment.

-
+
- +
@@ -74,11 +91,13 @@

Your website

aria-labelledby="delete-title" aria-describedby="delete-description" > -
-

Delete

-

Delete this app?

+ +
+

Delete

+

Delete this app?

+

-
+
diff --git a/public/style.css b/public/style.css index 547f421..a9244fe 100644 --- a/public/style.css +++ b/public/style.css @@ -1,10 +1,41 @@ +/* + * Render design foundations, in Render's dark palette only, as the UI was + * before. Each component refers only to the semantic tokens. Purple is for links, the + * one primary action, and one phrase of the headline. Status colors are for + * outcomes that are known: green for done, red for failed. + */ :root { color-scheme: dark; - font-family: - Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", - sans-serif; - background: #090b10; - color: #f4f5f7; + --bg: #0d0d0d; + --surface-muted: #141414; + --chip: #141414; + --border: #272727; + --border-strong: #4d4d4d; + --text: #ffffff; + --text-secondary: #c7c7c7; + --text-faint: #b3b3b3; + --link: #d1b8ff; + --link-hover: #e7dbff; + --accent: #8a05ff; + --accent-display: #c29eff; + --accent-strong: #c29eff; + --on-accent: #ffffff; + --primary-bg: #ffffff; + --primary-text: #0d0d0d; + --success: #00db7c; + --danger: #f0989e; + --danger-fill: #af1d27; + --on-danger: #ffffff; + --code-bg: #141414; + --code-text: #f0f0f0; + --hover-tint: rgb(255 255 255 / 3%); + --selected: rgb(255 255 255 / 10%); + --backdrop: rgb(0 0 0 / 70%); + --font-brand: "Roobert", "Manrope", ui-sans-serif, system-ui, sans-serif; + --font-default: "PP Neue Montreal", "Manrope", ui-sans-serif, system-ui, sans-serif; + --font-mono: + "PP Neue Montreal Mono", "Roboto Mono", ui-monospace, SFMono-Regular, Menlo, monospace; + --ease: cubic-bezier(0.9, 0.1, 0.1, 0.9); } * { @@ -14,365 +45,646 @@ body { margin: 0; min-height: 100vh; - background: - radial-gradient(circle at 15% 5%, rgb(78 97 255 / 20%), transparent 35rem), - #090b10; + background: var(--bg); + color: var(--text); + font-family: var(--font-default); + font-size: 16px; + line-height: 24px; + letter-spacing: 0.01em; } -main { - width: min(1120px, calc(100% - 32px)); - margin: 0 auto; - padding: 72px 0; -} - -header { - margin-bottom: 32px; +h1, +h2, +h3, +p { + margin: 0; } h1, h2, -p { - margin-top: 0; +h3 { + font-family: var(--font-brand); + font-weight: 400; + text-wrap: balance; +} + +code { + padding: 1px 6px; + background: var(--chip); + font-family: var(--font-mono); + font-size: 14px; + letter-spacing: 0; +} + +/* Top bar: the wordmark and the name of the demo. */ +.topbar { + display: flex; + align-items: center; + gap: 16px; + min-height: 56px; + padding: 0 32px; + border-bottom: 1px solid var(--border); } -h1 { - margin-bottom: 12px; - font-size: clamp(2.5rem, 8vw, 5rem); - line-height: 0.96; - letter-spacing: -0.06em; +.wordmark { + display: inline-flex; + color: var(--text); } -h2 { - margin-bottom: 0; - font-size: 1.35rem; +.wordmark svg { + width: auto; + height: 20px; +} + +.topbar-label { + padding-left: 16px; + border-left: 1px solid var(--border); + color: var(--text-faint); + font-family: var(--font-mono); + font-size: 12px; + font-weight: 500; + line-height: 16px; + letter-spacing: 0.02em; + text-transform: uppercase; +} + +main { + display: flex; + flex-direction: column; + gap: 24px; + max-width: 1120px; + margin: 0 auto; + padding: 56px 32px 96px; } .eyebrow { - margin-bottom: 10px; - color: #9ca8ff; - font-size: 0.75rem; - font-weight: 700; - letter-spacing: 0.12em; + color: var(--text-faint); + font-family: var(--font-mono); + font-size: 12px; + font-weight: 500; + line-height: 16px; + letter-spacing: 0.02em; text-transform: uppercase; } -.lede, -.progress { - color: #aeb3c0; - line-height: 1.6; +.intro { + display: flex; + flex-direction: column; + gap: 16px; + padding-bottom: 16px; } +.intro h1 { + font-size: 64px; + font-weight: 300; + line-height: 68px; + letter-spacing: -0.02em; +} + +.intro h1 .accent { + color: var(--accent-display); +} + +.lede { + max-width: 58ch; + color: var(--text-secondary); + font-size: 18px; + line-height: 26px; +} + +/* Panels are bordered, with square corners and no shadow. */ .panel { - margin-top: 16px; padding: 24px; - border: 1px solid #282d39; - border-radius: 18px; - background: rgb(18 21 28 / 86%); - box-shadow: 0 20px 60px rgb(0 0 0 / 24%); + border: 1px solid var(--border); + background: var(--bg); + color: var(--text); +} + +.panel-heading { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; +} + +.heading-text { + display: flex; + flex-direction: column; + gap: 6px; + min-width: 0; +} + +.panel h2 { + font-size: 24px; + line-height: 28px; + letter-spacing: -0.01em; +} + +/* Forms */ +.prompt-form, +.delete-form, +.confirm-field { + display: flex; + flex-direction: column; + gap: 12px; +} + +.delete-form { + gap: 20px; } label { - display: block; - margin-bottom: 8px; - font-size: 0.85rem; - font-weight: 650; + color: var(--text); + font-size: 16px; + line-height: 24px; } textarea, input { width: 100%; - border: 1px solid #333948; - border-radius: 10px; - background: #0d1016; - color: inherit; + border: 1px solid var(--border-strong); + border-radius: 0; + background: var(--bg); + color: var(--text); font: inherit; outline: none; } -textarea:focus, -input:focus { - border-color: #7886ff; - box-shadow: 0 0 0 3px rgb(120 134 255 / 16%); +textarea::placeholder, +input::placeholder { + color: var(--text-faint); } -input { - min-height: 44px; - padding: 0 14px; +textarea:focus, +input:focus { + border-color: var(--accent); + box-shadow: inset 0 0 0 1px var(--accent); } textarea { - min-height: 180px; - padding: 14px; + min-height: 160px; + padding: 12px 14px; resize: vertical; - line-height: 1.5; +} + +input { + min-height: 44px; + padding: 0 12px; + font-family: var(--font-mono); + font-size: 14px; } .form-actions { display: flex; flex-wrap: wrap; - gap: 10px; justify-content: flex-end; - margin-top: 18px; + gap: 12px; } -button, -.primary-link { +/* + * Buttons change color at once on hover, with no transition, as a tool does. + * The primary button is neutral until hover. Only one is in view. + */ +.primary-button, +.secondary-button, +.danger-button { display: inline-flex; - justify-content: center; align-items: center; - min-height: 44px; - padding: 0 18px; - border: 0; - border-radius: 10px; - background: #7886ff; - color: #090b10; - font: inherit; - font-weight: 750; - text-decoration: none; + justify-content: center; + border-radius: 0; + font-family: var(--font-default); + font-weight: 400; + letter-spacing: 0.01em; cursor: pointer; } -button:disabled { - cursor: wait; - opacity: 0.65; +.primary-button { + min-height: 44px; + padding: 0 20px; + border: 1px solid var(--primary-bg); + background: var(--primary-bg); + color: var(--primary-text); + font-size: 16px; + line-height: 24px; } -.workspace { - display: grid; - grid-template-columns: minmax(240px, 0.7fr) minmax(0, 1.5fr); - gap: 16px; - align-items: start; +.primary-button:hover:not(:disabled) { + border-color: var(--accent); + background: var(--accent); + color: var(--on-accent); } -.history-heading { - display: flex; - gap: 12px; - justify-content: space-between; - align-items: center; +.primary-button:disabled { + opacity: 0.5; + cursor: wait; } .secondary-button, .danger-button { - min-height: 34px; - padding: 0 11px; - border: 1px solid #333948; + min-height: 36px; + padding: 0 14px; background: transparent; - color: #c5cad4; - font-size: 0.75rem; + font-size: 14px; + line-height: 20px; } -.danger-button { - border-color: #693942; - color: #ffabb8; +.secondary-button { + border: 1px solid var(--text); + color: var(--text); } -.danger-button:hover:not(:disabled) { - background: #3a2027; +.secondary-button:hover { + border-color: var(--accent-strong); + color: var(--accent-strong); } -.danger-button:disabled { - cursor: not-allowed; +.danger-button { + border: 1px solid var(--danger); + color: var(--danger); } -.delete-dialog { - width: min(480px, calc(100% - 32px)); - margin: auto; - /* Opaque, so that the page behind the dialog does not show through. */ - background: #12151c; - color: inherit; +.danger-button:hover:not(:disabled) { + border-color: var(--danger-fill); + background: var(--danger-fill); + color: var(--on-danger); } -.delete-dialog::backdrop { - background: rgb(9 11 16 / 72%); +.danger-button:disabled { + opacity: 0.4; + cursor: not-allowed; } -.delete-dialog h2 { - margin-bottom: 12px; +/* Workspace: the run history beside the selected run. */ +.workspace { + display: grid; + grid-template-columns: minmax(260px, 340px) minmax(0, 1fr); + gap: 24px; + align-items: start; } -.delete-dialog code { - color: #ffabb8; - font: 0.85rem ui-monospace, SFMono-Regular, Menlo, monospace; +.history-panel { + display: flex; + flex-direction: column; + gap: 16px; } .empty-history { - margin: 24px 0 0; - color: #747b8c; - font-size: 0.85rem; + color: var(--text-faint); + font-size: 14px; + line-height: 20px; } .run-list { - display: grid; - gap: 8px; - margin-top: 20px; + display: flex; + flex-direction: column; max-height: 560px; overflow-y: auto; + border-top: 1px solid var(--border); } .run-item { - display: grid; - gap: 5px; - justify-content: stretch; - min-height: 0; + display: flex; + flex-direction: column; + gap: 4px; + width: 100%; padding: 12px; - border: 1px solid transparent; - background: #171b23; - color: #f4f5f7; + border: 0; + border-bottom: 1px solid var(--border); + border-radius: 0; + background: transparent; + color: var(--text); + font: inherit; text-align: left; + cursor: pointer; } .run-item:hover { - border-color: #3e4658; + background: var(--hover-tint); } .run-item.selected { - border-color: #7886ff; - background: #252b57; + background: var(--selected); } .run-item strong { overflow: hidden; - font-size: 0.85rem; + font-size: 15px; + font-weight: 400; + line-height: 22px; text-overflow: ellipsis; white-space: nowrap; } -.run-item span { - color: #9299aa; - font-size: 0.72rem; +/* Medium weight too, so that the selected run differs from a hover. */ +.run-item.selected strong { font-weight: 500; } -.status-heading { +.run-meta { + color: var(--text-faint); + font-family: var(--font-mono); + font-size: 12px; + line-height: 16px; + letter-spacing: 0; +} + +.run-state[data-status="running"], +.run-state[data-status="deleting"] { + color: var(--text); +} + +.run-state[data-status="deployed"] { + color: var(--success); +} + +.run-state[data-status$="failed"] { + color: var(--danger); +} + +/* The selected run */ +.run-panel { + display: flex; + flex-direction: column; + gap: 20px; +} + +.run-panel.failed { + border-color: var(--danger); +} + +.status-line { display: flex; - flex-wrap: wrap; - gap: 12px; - justify-content: space-between; align-items: center; + gap: 12px; +} + +.status-line h2 { + font-size: 32px; + line-height: 36px; + letter-spacing: -0.012em; } +/* A status dot is round, as the brand allows for status. */ .activity { - width: 14px; - height: 14px; - border: 2px solid #4a5162; - border-top-color: #9ca8ff; + display: inline-block; + flex: none; + width: 10px; + height: 10px; border-radius: 50%; - animation: spin 0.8s linear infinite; + background: var(--text); + animation: pulse 1.2s var(--ease) infinite; } +/* + * The stages read down the first column and then the second. Done is green, + * the stage that runs now is the text color, and a stage that failed is red. + */ .stages { display: grid; - grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); - gap: 8px; - margin: 22px 0; + grid-auto-flow: column; + grid-template-rows: repeat(5, auto); + grid-template-columns: repeat(2, minmax(0, 1fr)); + column-gap: 24px; + margin: 0; padding: 0; + border-top: 1px solid var(--border); list-style: none; } .stages li { - padding: 8px 10px; - border-radius: 8px; - background: #171b23; - color: #747b8c; - font-size: 0.78rem; + display: flex; + align-items: center; + gap: 10px; + padding: 8px 0; + border-bottom: 1px solid var(--border); + color: var(--text-faint); + font-family: var(--font-mono); + font-size: 13px; + line-height: 20px; + letter-spacing: 0; } -.stages li.active { - background: #252b57; - color: #cbd0ff; +.stages li::before { + flex: none; + width: 10px; + height: 10px; + border: 1px solid var(--border-strong); + content: ""; } .stages li.complete { - color: #93dfbd; + color: var(--success); +} + +.stages li.complete::before { + border-color: var(--success); + background: var(--success); +} + +.stages li.active { + color: var(--text); + font-weight: 500; +} + +.stages li.active::before { + border-color: var(--text); + background: var(--text); + animation: pulse 1.2s var(--ease) infinite; } .stages li.failed-stage { - background: #3a2027; - color: #ffabb8; + color: var(--danger); } -.result { - display: grid; - gap: 12px; - margin-top: 20px; - padding: 24px; - border: 1px solid rgb(147 223 189 / 28%); - border-radius: 14px; - background: - linear-gradient(135deg, rgb(147 223 189 / 10%), transparent 60%), - #10151a; +.stages li.failed-stage::before { + border-color: var(--danger); + background: var(--danger); +} + +.progress { + color: var(--text-secondary); + font-size: 14px; + line-height: 20px; } -.result[hidden], -.stages[hidden], -.danger-button[hidden] { +.progress:empty { display: none; } +/* The result of a deployed run: the one block on the page that is lifted. */ +.result { + display: flex; + flex-direction: column; + gap: 12px; + padding: 24px; + border: 1px solid var(--border); + background: var(--surface-muted); +} + .result .eyebrow { - margin-bottom: 0; - color: #93dfbd; + color: var(--success); } .result h3 { - margin: 0; - font-size: clamp(1.5rem, 4vw, 2.25rem); - letter-spacing: -0.035em; + font-size: 32px; + line-height: 36px; + letter-spacing: -0.012em; } .result-summary { - max-width: 65ch; - margin-bottom: 4px; - color: #c5cad4; - line-height: 1.6; + max-width: 62ch; + color: var(--text-secondary); } -.result .primary-link { - justify-self: start; +/* A link, so it is purple, with the underline that wipes in from the left. */ +.primary-link { + display: inline-flex; + align-items: center; + align-self: flex-start; + gap: 8px; + background-image: linear-gradient(currentColor, currentColor); + background-position: 0 100%; + background-repeat: no-repeat; + background-size: 0% 1px; + color: var(--link); + font-size: 18px; + line-height: 26px; + text-decoration: none; + transition: background-size 200ms var(--ease); } -a { - color: #aab4ff; +.primary-link::after { + content: "\2192"; + transition: transform 200ms var(--ease); } -pre { - margin: 20px 0 0; - padding: 14px; +.primary-link:hover { + background-size: 100% 1px; + color: var(--link-hover); +} + +.primary-link:hover::after { + transform: translateX(2px); +} + +/* Code stays dark in both themes. */ +.run-details { + max-height: 420px; + margin: 0; + padding: 16px; overflow: auto; - border-radius: 10px; - background: #0b0e13; - color: #c5cad4; + border: 1px solid var(--border); + background: var(--code-bg); + color: var(--code-text); + font-family: var(--font-mono); + font-size: 13px; + line-height: 20px; white-space: pre-wrap; - font: 0.82rem/1.55 ui-monospace, SFMono-Regular, Menlo, monospace; + overflow-wrap: anywhere; +} + +/* Delete dialog */ +.delete-dialog { + width: min(480px, calc(100% - 32px)); + max-width: none; + margin: auto; +} + +.delete-dialog::backdrop { + background: var(--backdrop); } -.failed { - border-color: #693942; +/* Keyboard focus: a purple ring, as on every Render control. */ +.primary-button:focus-visible, +.secondary-button:focus-visible, +.danger-button:focus-visible, +.run-item:focus-visible, +.primary-link:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; } -@keyframes spin { - to { - transform: rotate(360deg); +@keyframes pulse { + 50% { + opacity: 0.25; } } -@media (max-width: 800px) { +@media (prefers-reduced-motion: reduce) { + .activity, + .stages li.active::before { + animation: none; + } + + .primary-link, + .primary-link::after { + transition: none; + } +} + +/* Tablet: one column. */ +@media (max-width: 1023px) { + .topbar { + padding: 0 24px; + } + + main { + padding: 40px 24px 72px; + } + + .intro h1 { + font-size: 48px; + line-height: 52px; + } + .workspace { - grid-template-columns: 1fr; + grid-template-columns: minmax(0, 1fr); } .run-list { - grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); - max-height: 280px; + max-height: 320px; } } -@media (max-width: 560px) { +/* Mobile: one column of stages, and full-width form buttons. */ +@media (max-width: 767px) { + .topbar { + padding: 0 16px; + } + main { - padding-top: 40px; + gap: 16px; + padding: 32px 16px 64px; + } + + .intro h1 { + font-size: 40px; + line-height: 44px; + } + + .panel { + padding: 16px; + } + + .stages { + grid-auto-flow: row; + grid-template-rows: none; + grid-template-columns: minmax(0, 1fr); + } + + .form-actions { + flex-direction: column-reverse; } - button { + .form-actions .primary-button, + .form-actions .secondary-button, + .form-actions .danger-button { width: 100%; + min-height: 44px; } } + +/* Last, so that it wins over each display value above. */ +[hidden] { + display: none; +} diff --git a/tests/gateway.test.ts b/tests/gateway.test.ts index de28489..795d456 100644 --- a/tests/gateway.test.ts +++ b/tests/gateway.test.ts @@ -221,7 +221,7 @@ describe("browser UI", () => { headers: { authorization }, }); expect(response.status).toBe(200); - expect(await response.text()).toContain("Describe it. Ship it."); + expect(await response.text()).toContain("Vibe Code Demo"); }); it("allows the explicit auth bypass only outside production", async () => {