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

novuhq/novu

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

30 files reviewed·August 1, 2026

View on GitHub

Elevated

Design risk in this codebase.

5issues
Critical & Serious

Top fix

Make the code/preview toggle a real, keyboard-operable control

See the fix

Verdict

Custom floating badges look interactive but lead nowhere for keyboard users, while the underlying Tiptap plumbing is handled with real care. The biggest risk is that unusable controls like the repeat indicator and code toggle sit right next to well-built native pieces, so the gap reads as inconsistency, not incapacity.

Files Rams reviewed

packages/novu/src/commands/init/templates/app-react-email/ts/app/components/NotificationToast/Notifications.module.css

playground/nextjs/src/app/agents-mcp/components/flow-diagram.tsx

apps/dashboard/src/components/maily/views/for-view.tsx

apps/dashboard/src/components/maily/views/html-view.tsx

apps/dashboard/src/components/maily/views/maily-variables-list-view.tsx

apps/dashboard/src/components/maily/views/variable-view.tsx

apps/dashboard/src/pages/access-denied-page.tsx

apps/dashboard/src/pages/agent-slack-setup-page.tsx

apps/dashboard/src/pages/agent-telegram-mobile-setup-page.tsx

apps/dashboard/src/pages/agent-whatsapp-signup-page.tsx

apps/dashboard/src/pages/edit-layout.tsx

apps/dashboard/src/pages/inbox-embed-page.tsx

apps/dashboard/src/pages/inbox-embed-success-page.tsx

apps/dashboard/src/pages/inbox-usecase-page.tsx

apps/dashboard/src/pages/integration-store-telegram-mobile-setup-page.tsx

apps/dashboard/src/pages/integrations-list-page.tsx

apps/dashboard/src/pages/server-error-page.tsx

apps/dashboard/src/pages/vercel-integration-page.tsx

apps/dashboard/src/pages/webhooks-page.tsx

apps/dashboard/src/pages/welcome-page.tsx

packages/novu/src/commands/init/templates/app-agent-ai-sdk/ts/app/page.tsx

packages/novu/src/commands/init/templates/app-agent-langchain/ts/app/page.tsx

packages/novu/src/commands/init/templates/app-agent/ts/app/page.tsx

playground/nextjs/src/app/agent-toolkit/page.tsx

playground/nextjs/src/app/novu-agent/page.tsx

apps/dashboard/src/components/agents/agent-integration-guides/agent-integration-guide-layout.tsx

apps/dashboard/src/components/agents/public-token-page.tsx

apps/dashboard/src/components/dashboard-layout.tsx

apps/dashboard/src/components/translations/translation-onboarding-page.tsx

apps/dashboard/src/components/workflow-editor/steps/conditions/edit-step-conditions-layout.tsx

94/100

Accessibility

2 critical
AccessibilityCritical

apps/dashboard/src/components/maily/views/for-view.tsx:18

Repeat block indicator is unreachable and unusable by keyboard

In `ForView`, the `div` with `role="button"` and `data-repeat-indicator` (lines 18-25) fires `editor.commands.setNodeSelection` on `onClick`, but it has no `tabIndex`, no `onKeyDown`, and no native `<button>` semantics. A screen reader or keyboard user can tab through the editor but will never land on this control.

Why it matters

Keyboard-only and screen reader users cannot select the repeat block at all, which locks them out of a core editing action available to mouse users.

Fix

Replace the div with a native button element, or add tabIndex=0 and an onKeyDown handler that fires on Enter and Space.

<div
  role="button"
  data-repeat-indicator=""
  contentEditable={false}
  onClick={() => {
    editor.commands.setNodeSelection(getPos());
  }}
  className="border-soft-100 absolute right-[-2px] top-[-3px] flex cursor-grab items-center justify-center gap-[2px] rounded border bg-white px-1 py-[2px]"
>
<button
  type="button"
  data-repeat-indicator=""
  contentEditable={false}
  onClick={() => {
    editor.commands.setNodeSelection(getPos());
  }}
  className="border-soft-100 absolute right-[-2px] top-[-3px] flex cursor-grab items-center justify-center gap-[2px] rounded border bg-white px-1 py-[2px]"
>
AccessibilityCritical

apps/dashboard/src/components/maily/views/html-view.tsx:94

Code/preview toggle for HTML block is a div with no keyboard path

The outer wrapper `<div className="group relative cursor-pointer" onClick={onClick}>` (line 94) is the sole way to toggle between code and preview mode, but it carries no `role`, `tabIndex`, or key handler.

Why it matters

Keyboard-only users cannot switch the HTML block into preview mode at all, cutting off a primary editing interaction from anyone not using a mouse.

Fix

Give the toggle native button semantics, or add role="button", tabIndex={0}, and an onKeyDown handler for Enter and Space.

<div className="group relative cursor-pointer" onClick={onClick}>
<div
  role="button"
  tabIndex={0}
  className="group relative cursor-pointer"
  onClick={onClick}
  onKeyDown={(e) => {
    if (e.key === 'Enter' || e.key === ' ') {
      e.preventDefault();
      onClick?.();
    }
  }}
>
98/100

Color

1 serious
Design SystemSerious

apps/dashboard/src/components/maily/views/html-view.tsx:98

Preview container border hardcodes a hex value instead of a token

The inner preview div's hover border uses `group-hover:border-[#E4E4E7]` (line 98) as a raw hex value, while the same file elsewhere uses the `border-soft-100` token for identical gray borders (lines 25, 110).

Why it matters

Two different sources for the same gray means a future palette or dark-mode change updates one border and misses the other, so the preview hover state silently drifts from the rest of the editor's borders.

Fix

Replace the hardcoded hex with the existing border-soft-100 token already used for the same visual role elsewhere in this file.

'-mx-2 min-h-[42px] rounded-md border px-2',
'border-transparent group-hover:border-[#E4E4E7]',
'flex flex-col justify-center'
'-mx-2 min-h-[42px] rounded-md border px-2',
'border-transparent group-hover:border-soft-100',
'flex flex-col justify-center'

Get this score on every PR.

Rams reviews each pull request on your repo and posts inline one-click fixes — about a minute per review.

Install Rams free
98/100

UX

1 serious
UXSerious

apps/dashboard/src/components/maily/views/html-view.tsx:110

'html' badge shows a grab cursor for a drag that never happens

The floating badge with the `RiCodeBlock` icon and "html" label (line 110) is styled with `cursor-grab`, signaling a draggable handle. Nothing in this component wires up drag behavior on that element; it only appears on `group-hover` as a static indicator.

Why it matters

A grab cursor tells users they can drag the block from this handle. When dragging does nothing, the mismatch between affordance and behavior costs the user a failed attempt and erodes trust in every other drag handle in the editor.

Fix

Match the cursor style to the actual interaction: use cursor-default or cursor-pointer if the badge is only a status indicator, and reserve cursor-grab for elements with real drag handlers.

<div className="border-soft-100 absolute right-[-10px] top-[-3px] hidden cursor-grab items-center justify-center gap-[2px] rounded border bg-white px-1 py-[2px] group-hover:flex">
<div className="border-soft-100 absolute right-[-10px] top-[-3px] hidden cursor-default items-center justify-center gap-[2px] rounded border bg-white px-1 py-[2px] group-hover:flex">
98/100

Craft

1 serious
Design SystemSerious

apps/dashboard/src/components/maily/views/for-view.tsx:25

Repeat badge positioning uses four one-off pixel values

The repeat indicator's wrapper div (line 24-25) uses `right-[-2px]`, `top-[-3px]`, `gap-[2px]`, and `py-[2px]`, all arbitrary Tailwind values instead of scale tokens like `-right-0.5`, `-top-1`, `gap-0.5`, `py-0.5`.

Why it matters

Every arbitrary value is a number nobody chose deliberately and nobody will remember to update; small positioning drifts accumulate across the editor's badges and nothing stays visually aligned as the design system evolves.

Fix

Use Tailwind's spacing scale tokens instead of arbitrary pixel values so the badge stays consistent with other floating indicators.

className="border-soft-100 absolute right-[-2px] top-[-3px] flex cursor-grab items-center justify-center gap-[2px] rounded border bg-white px-1 py-[2px]"
className="border-soft-100 absolute -right-0.5 -top-1 flex cursor-grab items-center justify-center gap-0.5 rounded border bg-white px-1 py-0.5"

Typography

No issues found

Spacing

No issues found

Components

No issues found

Motion

No issues found

Working well

  • The HTML view keeps NodeViewContent mounted during preview mode with a comment explaining it's required for ProseMirror's contentDOM to stay attached. That's a deliberate, documented tradeoff rather than an accidental leftover, and it's the right call given the constraint.
  • The repeat block's NodeViewWrapper uses draggable="true" with data-drag-handle, leveraging Tiptap's native drag system instead of building a custom drag implementation from scratch, which keeps drag behavior consistent with the rest of the editor's node types.

Scored August 1, 2026 with Rams Engine v0.0.3 · Engine changelog
First scored July 29, 2026: 59/100. This rescore on v0.0.3: 59/100.

This page is an automated design review of novuhq/novu’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