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

makeplane/plane

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

30 files reviewed·July 9, 2026

Elevated

Design risk in this codebase.

6issues
Critical & Serious

Top fix

Uncomment the dead render path so search results actually display

See the fix

Verdict

Clean component architecture keeps getting undermined by unfinished states: a fully commented-out render path ships a blank search panel, and a tone selector highlights the wrong button entirely. The AI and search surfaces look polished but are quietly broken underneath, which is the real risk here.

Files Rams reviewed

apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx

apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/layout.tsx

apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/page.tsx

apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/pages/page.tsx

apps/web/core/components/power-k/ui/pages/work-item-selection-page.tsx

apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/layout.tsx

apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx

apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/header.tsx

apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/pages/header.tsx

apps/admin/app/components/404.tsx

apps/web/ce/components/command-palette/power-k/pages/context-based/work-item/state-menu-item.tsx

apps/web/ce/components/pages/editor/ai/ask-pi-menu.tsx

apps/web/ce/components/pages/editor/ai/menu.tsx

apps/web/ce/components/pages/editor/embed/issue-embed-upgrade-card.tsx

apps/web/ce/components/pages/header/lock-control.tsx

apps/web/ce/components/pages/navigation-pane/tab-panels/empty-states/assets.tsx

apps/web/ce/components/pages/navigation-pane/tab-panels/empty-states/outline.tsx

apps/web/core/components/pages/dropdowns/actions.tsx

apps/web/core/components/pages/editor/content-limit-banner.tsx

apps/web/core/components/pages/editor/editor-body.tsx

apps/web/core/components/pages/editor/header/logo-picker.tsx

apps/web/core/components/pages/editor/header/root.tsx

apps/web/core/components/pages/editor/page-root.tsx

apps/web/core/components/pages/editor/summary/content-browser.tsx

apps/web/core/components/pages/editor/summary/heading-components.tsx

apps/web/core/components/pages/editor/title.tsx

apps/web/core/components/pages/editor/toolbar/color-dropdown.tsx

apps/web/core/components/pages/editor/toolbar/options-dropdown.tsx

apps/web/core/components/pages/editor/toolbar/root.tsx

apps/web/core/components/pages/editor/toolbar/toolbar.tsx

93/100

Accessibility

1 critical2 serious
Accessibility·apps/web/ce/components/pages/editor/ai/ask-pi-menu.tsx:118Critical

Send icon in the AI query box has no click handler or button semantics

The `CircleArrowUp` icon that visually reads as "send" is wrapped in a plain `<span className="grid size-4...">` with no `onClick`, no `<button>`, and no keyboard path. Typing into "Tell AI what to do..." has no visible way to submit: no `onKeyDown` on the input either.

Why it matters

Users see a send affordance that does nothing when clicked or tabbed to, so the AI prompt input has no working submission path for mouse or keyboard users. Keyboard-only users cannot use this control at all.

Fix

Wrap the send icon in a real `<button>` with an accessible name and wire it (or Enter-to-submit) to the actual send handler.

<span className="grid size-4 flex-shrink-0 place-items-center">
  <CircleArrowUp className="size-4 text-secondary" />
</span>
<button
  type="button"
  aria-label="Send prompt"
  className="grid size-4 flex-shrink-0 place-items-center outline-none focus-visible:ring-2 focus-visible:ring-primary"
  onClick={handleSubmitQuery}
>
  <CircleArrowUp className="size-4 text-secondary" />
</button>
Accessibility·apps/web/ce/components/pages/header/lock-control.tsx:84Serious

Lock button hit area is 24px, below the 44px touch target guidance

Both the neutral state button (`size-6` = 24px) and the locked state button (`h-6` = 24px tall) render well under the 44px AAA target and even the 24px AA floor is only just met with no margin. This is a primary header action a user taps repeatedly.

Why it matters

On mobile or with any pointer imprecision, users mis-tap this control or miss it entirely, adding friction to a frequent action.

Fix

Extend the clickable area with a pseudo-element or padding wrapper while keeping the visual icon at its current size.

className="grid size-6 flex-shrink-0 place-items-center rounded-sm text-secondary transition-colors hover:bg-layer-1 hover:text-primary"
className="relative grid size-6 flex-shrink-0 place-items-center rounded-sm text-secondary transition-colors hover:bg-layer-1 hover:text-primary after:absolute after:size-10 after:top-1/2 after:left-1/2 after:-translate-x-1/2 after:-translate-y-1/2"
Accessibility·apps/web/ce/components/pages/editor/ai/menu.tsx:215Serious

Regenerate and "Add to next line" icon buttons have no accessible name

Two `<button>` elements render only an icon (`CornerDownRight`, `RefreshCcw`) with no text and no `aria-label`, relying entirely on a `Tooltip` wrapper (`tooltipContent="Add to next line"` / `"Re-generate response"`) that shows on hover only.

Why it matters

Screen reader users hear an unlabeled button with no indication of what it does, since visual tooltips don't reliably expose accessible names. Keyboard-only users get the same blank announcement on Tab.

Fix

Add aria-label to icon-only buttons matching their tooltip text so screen readers announce a real name.

<button
  type="button"
  className="grid size-6 flex-shrink-0 place-items-center rounded-sm outline-none hover:bg-layer-1"
  onClick={() => handleInsertText(true)}
>
  <CornerDownRight className="size-4 text-tertiary" />
</button>
<button
  type="button"
  aria-label="Add to next line"
  className="grid size-6 flex-shrink-0 place-items-center rounded-sm outline-none hover:bg-layer-1"
  onClick={() => handleInsertText(true)}
>
  <CornerDownRight className="size-4 text-tertiary" />
</button>
95/100

UX

1 critical1 serious
UX·apps/web/core/components/power-k/ui/pages/work-item-selection-page.tsx:188Critical

Component fetches issue data but renders a blank fragment to the user

`WorkItemSelectionPage` calls `workspaceService.fetchWorkspaceRecents` and `workspaceService.searchEntity` in two `useEffect` hooks, then returns: ``` return ( <> {/* ...150 lines of commented JSX... */} </> ); ``` No loading, empty, or results state is ever rendered.

Why it matters

A user typing into Power K's work item search sees an empty panel no matter what they type, whether the search is loading, empty, or full of matches, since none of those states have a visible path to the screen.

Fix

Render every reachable state (loading, empty, results) instead of leaving the return path commented out.

return (
  <>
    {/* {searchTerm === "" ? (
      ...
    )} */}
  </>
);
return (
  <CommandPaletteEntityList
    heading="Issues"
    items={debouncedSearchTerm ? _issueResults : _recentIssues}
    getKey={(issue) => issue.id}
    getLabel={(issue) => issue.name}
    renderItem={(issue) => <span className="truncate">{issue.name}</span>}
    emptyText="Search for issue id or issue title"
  />
);
UX·apps/web/ce/components/pages/editor/ai/menu.tsx:260Serious

Tone button active class checks a constant key, not selection state

Tone button className: `{ "bg-accent-primary/20 text-accent-primary": tone.key === "default" }`. This condition is static and can never change at runtime, so no tone button ever visually reflects the current selection after the first click.

Why it matters

The only feedback for a tone change is buried in the regenerated text; without a persistent visual state, users lose track of which tone is applied when they return to the panel.

Fix

Derive the active class from the tracked selected-tone state, and call setSelectedTone inside handleToneChange.

className={cn(
  "rounded-sm bg-layer-1 p-1 text-11 font-medium text-secondary transition-colors outline-none",
  {
    "bg-accent-primary/20 text-accent-primary": tone.key === "default",
  }
)}
onClick={(e) => {
  e.preventDefault();
  e.stopPropagation();
  handleToneChange(tone.key);
}}
className={cn(
  "rounded-sm bg-layer-1 p-1 text-11 font-medium text-secondary transition-colors outline-none",
  {
    "bg-accent-primary/20 text-accent-primary": tone.key === selectedTone,
  }
)}
onClick={(e) => {
  e.preventDefault();
  e.stopPropagation();
  setSelectedTone(tone.key);
  handleToneChange(tone.key);
}}

Want this on your repo?

Rams reviews your next PR automatically and posts inline fix suggestions.

Review my public repo for free
98/100

Spacing

1 serious
Spacing·apps/web/ce/components/pages/editor/embed/issue-embed-upgrade-card.tsx:16Serious

Arbitrary border-[0.5px] bypasses the spacing/border scale

The card wrapper uses `border-[0.5px]` as its base border, then swaps to `border-2` when `props.selected` is true.

Why it matters

A bracket value outside the standard border scale drifts from every other bordered surface in the app, and switching from 0.5px to 2px on selection shifts the box's rendered size, nudging neighboring content when the state toggles.

Fix

Use a standard border-width token (e.g. border) and vary color or a ring instead of width to indicate selection.

"flex w-full items-center justify-between gap-5 rounded-md border-[0.5px] border-subtle bg-layer-1 px-5 py-2 shadow-raised-100 max-md:flex-wrap",
{
  "border-2": props.selected,
}
"flex w-full items-center justify-between gap-5 rounded-md border border-subtle bg-layer-1 px-5 py-2 shadow-raised-100 max-md:flex-wrap",
{
  "ring-2 ring-primary": props.selected,
}

Typography

No issues found

Color

No issues found

Components

No issues found

Motion

No issues found

Craft

No issues found

Working well

  • The state icon color (state.color) is passed through as a prop to StateGroupIcon rather than baked into an inline style at this call site, and the selection state is carried by isSelected plus PowerKModalCommandItem's own rendering rather than a bare color swap here, keeping the state-color-as-only-differentiator problem out of this file's scope.
  • The two `useEffect` hooks separate 'load recent issues on mount' from 'search on debounced term change' cleanly, each with its own `.catch()` resetting state to an empty array rather than leaving stale or undefined data on failure. That's the right instinct for resilience once the render path is restored.
  • The three-state model (neutral, locked, unlocked) that reverts to neutral after 600ms is a well-scoped bit of state machine design: it gives clear transient feedback on unlock without leaving a permanent 'unlocked' badge cluttering the header, and the timer cleanup on unmount avoids a classic memory leak.
  • Sizing the icon buttons consistently at `size-6` with `grid place-items-center` keeps the two secondary actions visually aligned and predictable next to the primary "Replace selection" text action, avoiding the common mistake of mismatched icon-button footprints in a single toolbar.

Score your own repo with Rams.

Free on public repos. Rams reviews every pull request for accessibility, color, type, spacing, components, motion, UX, and craft, and posts inline fixes.