makeplane/plane
Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.
30 files reviewed·July 9, 2026
More findings
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
Accessibility
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>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"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>UX
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"
/>
);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 freeSpacing
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
Color
Components
Motion
Craft
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.