Rams MCP · The full engine, now in your coding agent
antiwork on GitHub

antiwork/gumroad

Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.

30 files reviewed·July 23, 2026

View on GitHub

Low

Design risk in this codebase.

8issues
Serious & Moderate · top 6 shown below

Top fix

Add inline error messaging when entered email doesn't match

See the fix

Verdict

Polished components sit on top of silent failure paths. Buttons are labelled and states look right, but wrong emails, dead mailto links, and duplicate CTAs leave users stranded with no signal.

Files Rams reviewed

app/javascript/pages/HelpCenter/Layout.tsx

app/javascript/pages/UrlRedirects/ConfirmPage.tsx

app/javascript/pages/UrlRedirects/DownloadPage.tsx

app/javascript/pages/UrlRedirects/UnavailablePageLayout.tsx

app/javascript/components/AffiliatedPage.tsx

app/javascript/components/Analytics/AnalyticsLayout.tsx

app/javascript/components/ApiDocumentation/Endpoints/PublicProductPage.tsx

app/javascript/components/Authentication/Layout.tsx

app/javascript/components/BundleEdit/Layout.tsx

app/javascript/components/CheckoutDashboard/Layout.tsx

app/javascript/components/Collaborators/Layout.tsx

app/javascript/components/CollabsPage.tsx

app/javascript/components/Developer/Layout.tsx

app/javascript/components/Discover/Layout.tsx

app/javascript/components/Download/PageListLayout.tsx

app/javascript/components/EmailAction/Layout.tsx

app/javascript/components/EmailsPage/Layout.tsx

app/javascript/components/Library/Layout.tsx

app/javascript/components/Product/Layout.tsx

app/javascript/components/ProductsLayout.tsx

app/javascript/components/ProductsPage.tsx

app/javascript/components/Profile/EditPage.tsx

app/javascript/components/Profile/Layout.tsx

app/javascript/components/Public/LookupLayout.tsx

app/javascript/components/Settings/Layout.tsx

app/javascript/components/WorkflowsPage/Layout.tsx

app/javascript/components/server-components/ProductEditPage.tsx

app/javascript/pages/Admin/ActionCallDashboard/Index.tsx

app/javascript/pages/Admin/ApiTokens/Index.tsx

app/javascript/pages/Admin/Base/Index.tsx

90/100

UX

5 serious
UXSerious

app/javascript/pages/UrlRedirects/ConfirmPage.tsx:58

No error path exists when the entered email doesn't match a purchase

`useForm` destructures `data, setData, post, processing` but never `errors`, and `EmailConfirmation` renders no error message anywhere. If the server rejects the submitted email, the UI shows no feedback.

Why it matters

A user who mistypes or forgets which email they used gets no indication the submission failed. They're left resubmitting the same form or assuming the page is broken, with no way to correct course.

Fix

Surface server-side validation errors inline near the field that caused them.

const { data, setData, post, processing } = useForm({
  id: confirmation_info.id,
  destination: confirmation_info.destination ?? "",
  display: confirmation_info.display ?? "",
  email: confirmation_info.email ?? "",
  authenticity_token,
});
const { data, setData, post, processing, errors } = useForm({
  id: confirmation_info.id,
  destination: confirmation_info.destination ?? "",
  display: confirmation_info.display ?? "",
  email: confirmation_info.email ?? "",
  authenticity_token,
});
UXSerious

app/javascript/pages/HelpCenter/Layout.tsx:27

"Email support" mailto link fails silently with no visible address to fall back on

Line 27 renders `<a href={mailto:${SUPPORT_EMAIL}}>Email support</a>`. The link text is generic and never shows the actual address.

Why it matters

On a device or browser with no mail client configured, clicking does nothing and gives no feedback: since the address itself is never shown, the user has no way to email support manually.

Fix

Show the real email address as the link text so it remains usable even when the mailto handler fails to open.

<a href={`mailto:${SUPPORT_EMAIL}`}>Email support</a>
<a href={`mailto:${SUPPORT_EMAIL}`}>{SUPPORT_EMAIL}</a>
UXSerious

app/javascript/pages/UrlRedirects/UnavailablePageLayout.tsx:15

Malformed props crash the unavailable-page layout itself

`useUnavailablePageProps` calls `typia.assert<UnavailablePageProps>(usePage().props)` directly, with no try/catch or fallback rendering around it.

Why it matters

If Inertia ever sends props that don't match `UnavailablePageProps`, `typia.assert` throws and the page hard-crashes with an unhandled exception, on the exact component whose job is to show a graceful 'unavailable' message to the user.

Fix

Validate props defensively and render a fallback state instead of letting an assertion throw uncaught on a user-facing error page.

export const useUnavailablePageProps = () => typia.assert<UnavailablePageProps>(usePage().props);
export const useUnavailablePageProps = () => {
  const result = typia.validate<UnavailablePageProps>(usePage().props);
  if (!result.success) throw new Error("Invalid UnavailablePageProps");
  return result.data;
};
UXSerious

app/javascript/pages/UrlRedirects/ConfirmPage.tsx:96

Email input renders no inline error even though errors are now tracked

The `Input` for `confirm-email` has no error slot below it. Rendering `errors.email` next to the field (once destructured from `useForm`) is the missing piece that turns the tracked error into visible feedback.

Why it matters

Validation state that exists in code but never reaches the screen is functionally the same as having no validation at all for the person using the form.

Fix

Render field-level errors directly beneath the input they belong to, with `role="alert"` so assistive tech announces it.

<Input
  id="confirm-email"
  type="email"
  name="email"
  autoComplete="email"
  placeholder="Email address"
  onChange={(e) => setData("email", e.target.value)}
  defaultValue={confirmation_info.email ?? ""}
/>
<Button type="submit" color="accent" disabled={processing}>
<Input
  id="confirm-email"
  type="email"
  name="email"
  autoComplete="email"
  placeholder="Email address"
  onChange={(e) => setData("email", e.target.value)}
  defaultValue={confirmation_info.email ?? ""}
/>
{errors.email ? (
  <p role="alert" className="text-red">
    {errors.email}
  </p>
) : null}
<Button type="submit" color="accent" disabled={processing}>
UXSerious

app/javascript/pages/HelpCenter/Layout.tsx:20

Two solid support buttons give users no reason to pick one over the other

The header actions render two buttons: - "Email support" (`<Button asChild>` wrapping a mailto anchor, no explicit color) - "Contact support" (`<Button color="accent">`, opens a modal) Both sit in the same row with no copy explaining what separates a direct email from the modal flow.

Why it matters

When two CTAs address the same need with no stated difference, users spend extra time deciding instead of acting, and some will pick the wrong one and have to backtrack.

Fix

Give each support path a line of copy or a visually secondary treatment so one action reads as the default and the other as an alternative.

98/100

Accessibility

1 serious
AccessibilitySerious

app/javascript/pages/UrlRedirects/UnavailablePageLayout.tsx:26

Placeholder image ships with no alt text at the call site

`<PlaceholderImage src={placeholderImage} />` is rendered with no `alt` prop passed in. Nothing at this call site tells assistive tech whether the comic-stars image is decorative or informative.

Why it matters

If the underlying component defaults to an empty or missing alt, screen reader users on this unavailable-page state get either silence or a raw filename read aloud, right when they most need the page's message to be clear.

Fix

Pass an explicit alt value at the call site: empty string if purely decorative, descriptive text if it conveys meaning.

<PlaceholderImage src={placeholderImage} />
<PlaceholderImage src={placeholderImage} alt="" />

Typography

No issues found

Color

No issues found

Spacing

No issues found

Components

No issues found

Motion

No issues found

Craft

No issues found

Working well

  • The polling lifecycle is cleanly scoped: audioDurationsPoll only starts while hasUnprocessedAudio is true and stops otherwise, and the same pattern applies to mediaLocationsPoll. This keeps background network activity tied to actual need rather than running indefinitely, which is the right default for a download page a user may leave open.
  • The submit button switches its own label to "Confirming..." and sets `disabled={processing}` in the same breath. This is the correct pattern for async actions: it gives visible feedback and blocks a second click from firing a duplicate request, solving two problems with one state variable.
  • The form ships a real `action` and `method="post"` plus a hidden `authenticity_token`, so it still works as a standard HTML form submission if JavaScript fails, while `onSubmit` hijacks it for Inertia in the normal case. That's a resilient way to layer a JS framework over a plain form.
  • The email input has a `label` with `htmlFor="confirm-email"` matching the input's `id`, even though it's visually hidden with `sr-only`. Screen reader users still get a proper accessible name, which is the part that actually matters when a design calls for no visible label.

Scored July 23, 2026 with Rams Engine v0.0.2 · Engine changelog
First scored July 23, 2026: 59/100. This rescore on v0.0.2: 86/100.

This page is an automated design review of antiwork/gumroad’s UI code: 30 files read against 291 versioned rules covering accessibility, color, typography, spacing, components, UX, motion, and craft. The score is out of 100; confirmed criticals cap it — one at 59, two at 49, three or more at 39.

More design scores

Score your own repo.

Free on public repos, no account. The same engine that scored this page reads your UI code and mints a score page like this one.

Public repos only. The full engine reviews the UI code and mints a public score page — we email you the link too. Already-scored repos open instantly.

Or get a design review on every pull requestInstall Rams