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

hcengineering/platform

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

30 files reviewed·July 22, 2026

View on GitHub

Elevated

Design risk in this codebase.

12issues
Critical, Serious & Moderate · top 6 shown below

Top fix

Wire keyboard support into card drag-and-drop movement

See the fix

Verdict

Solid token-driven visuals sit on top of mouse-only interaction logic, with keyboard navigation built but never connected to the one action that matters: moving a card. The bigger worry is a drag-cancel bug that can silently drop a card off the board, a data-integrity issue disguised as a polish gap.

Files Rams reviewed

packages/hls/src/components/HlsVideo.svelte

packages/kanban/src/components/Kanban.svelte

packages/kanban/src/components/KanbanRow.svelte

packages/panel/src/components/Panel.svelte

packages/presentation/src/components/ActionContext.svelte

packages/presentation/src/components/AttributeBarEditor.svelte

packages/presentation/src/components/AttributeEditor.svelte

packages/presentation/src/components/AttributesBar.svelte

packages/presentation/src/components/Card.svelte

packages/presentation/src/components/DocPopup.svelte

packages/presentation/src/components/DownloadFileButton.svelte

packages/presentation/src/components/DrawingBoard.svelte

packages/presentation/src/components/DrawingBoardColorSelectorIcon.svelte

packages/presentation/src/components/DrawingBoardToolbar.svelte

packages/presentation/src/components/DrawingBoardToolbarColorIcon.svelte

packages/presentation/src/components/FilePreview.svelte

packages/presentation/src/components/FilePreviewPopup.svelte

packages/presentation/src/components/FileTypeIcon.svelte

packages/presentation/src/components/HTMLViewer.svelte

packages/presentation/src/components/IconWithEmoji.svelte

packages/presentation/src/components/Image.svelte

packages/presentation/src/components/InlineAttributeBar.svelte

packages/presentation/src/components/InlineAttributeBarEditor.svelte

packages/presentation/src/components/LiteMessageViewer.svelte

packages/presentation/src/components/MessageBox.svelte

packages/presentation/src/components/MessageViewer.svelte

packages/presentation/src/components/NavLink.svelte

packages/presentation/src/components/ObjectPopup.svelte

packages/presentation/src/components/ObjectSearchPopup.svelte

packages/presentation/src/components/PDFViewer.svelte

91/100

Accessibility

3 critical
AccessibilityCritical

packages/kanban/src/components/Kanban.svelte:285

Moving a card between columns only works with a mouse drag

The column drop zone `.panel-container` is a plain `<div>` wired only to `on:dragover`/`on:drop`, with an explicit `<!-- svelte-ignore a11y-no-static-element-interactions -->` suppressing the linter's warning. The `select()` function already gives keyboard users focus movement between cards, but there is no keyboard path that actually calls `move(state)` to change a card's category.

Why it matters

Keyboard-only and motor-impaired users can select and view cards but have no way to perform the board's core action: moving a card to another state. The interaction is entirely inaccessible to them, not just inconvenient.

Fix

Give the drop zone a keyboard-triggerable path to the same `move(state)` action, e.g. a focusable target with `role="group"`, `aria-label` naming the column, and a keydown handler that invokes `move` for the currently selected card.

<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
  class="panel-container"
  bind:this={stateRefs[si]}
  on:dragover={(event) => {
    panelDragOver(event, state)
  }}
  on:drop={() => {
    void move(state).then(() => {
      isDragging = false
    })
  }}
>
<div
  class="panel-container"
  role="group"
  aria-label={typeof state === 'object' ? state.name : String(state)}
  bind:this={stateRefs[si]}
  on:dragover={(event) => {
    panelDragOver(event, state)
  }}
  on:drop={() => {
    void move(state).then(() => {
      isDragging = false
    })
  }}
  on:keydown={(event) => {
    if (event.key === 'Enter' && dragCard !== undefined) {
      void move(state).then(() => { isDragging = false })
    }
  }}
>
AccessibilityCritical

packages/kanban/src/components/KanbanRow.svelte:117

Card container is a div with drag, context-menu, and selection logic but no keyboard path

The `.card-container` div carries `on:contextmenu`, `on:dragstart`, `class:selection`, and `class:checked` state, with `<!-- svelte-ignore a11y-no-static-element-interactions -->` suppressing the lint warning. It has no `role`, no `tabindex`, and `on:focus={() => {}}` is a no-op, so keyboard and screen-reader users can never focus a card, select it, or open its context menu.

Why it matters

Keyboard-only and screen-reader users cannot select a kanban card, open its actions menu, or move it, which locks a whole class of users out of the board's core interaction.

Fix

Give the interactive card a role, a tab stop, a real focus handler, and a keydown path to the same menu the mouse triggers.

<div
  class="card-container"
  class:selection={selection !== undefined ? objects[selection]?._id === object._id : false}
  class:checked={checkedSet.has(object._id)}
  on:mouseover={mouseAttractor(() => dispatch('obj-focus', object))}
  on:mouseenter={mouseAttractor(() => dispatch('obj-focus', object))}
  on:focus={() => {}}
  on:contextmenu={(evt) => {
    showMenu(evt, object)
  }}
  draggable={true}
  class:draggable={true}
<div
  class="card-container"
  class:selection={selection !== undefined ? objects[selection]?._id === object._id : false}
  class:checked={checkedSet.has(object._id)}
  role="button"
  tabindex="0"
  aria-selected={selection !== undefined ? objects[selection]?._id === object._id : false}
  on:mouseover={mouseAttractor(() => dispatch('obj-focus', object))}
  on:mouseenter={mouseAttractor(() => dispatch('obj-focus', object))}
  on:focus={mouseAttractor(() => dispatch('obj-focus', object))}
  on:contextmenu={(evt) => {
    showMenu(evt, object)
  }}
  on:keydown={(evt) => {
    if (evt.key === 'Enter' || evt.key === 'ContextMenu') {
      showMenu(evt, object)
    }
  }}
  draggable={true}
  class:draggable={true}
AccessibilityCritical

packages/hls/src/components/HlsVideo.svelte:130

Captions are force-disabled and the compiler's own warning is silenced

The template opens with `<!-- svelte-ignore a11y-media-has-caption -->` above the `<video>` element, there is no `<track>` child, and `options.captions = { active: false }` explicitly turns off Plyr's caption toggle. The `controls` array passed to Plyr also omits `'captions'` entirely.

Why it matters

Deaf and hard-of-hearing users get no way to follow the video's audio content at all: this is a WCAG 1.2.2 failure, not a nice-to-have, and suppressing the linter's warning means the gap will never resurface in code review.

Fix

Ship at least one caption track and let Plyr's caption toggle stay available instead of forcing it off.

options.captions = {
  active: false
}
options.captions = {
  active: true
}
95/100

UX

1 critical1 serious
UXCritical

packages/kanban/src/components/Kanban.svelte:164

Cancelling a drag can silently drop the card from the board

In `panelDragLeave`, restoring a card's original position calls `setGroupByValues(groupByDocs, dragCardInitialPosition, newArr)`. The third parameter should be the category (`dragCardInitialState`), but `dragCardInitialPosition` (a number) is passed instead: the restored array is written under the wrong key.

Why it matters

When a user drags a card out of its column and releases it somewhere that cancels the move (or drags back over the origin), the card can be written into a bogus bucket instead of its original column, making it disappear from the board with no error or recovery path.

Fix

Pass the actual category value, not the numeric position, as the key argument to `setGroupByValues`.

if (dragCardInitialPosition !== undefined) {
  const newArr = getGroupByValues(groupByDocs, dragCardInitialState)
  newArr.splice(dragCardInitialPosition, 0, dragCard)
  setGroupByValues(groupByDocs, dragCardInitialPosition, newArr)
}
if (dragCardInitialPosition !== undefined) {
  const newArr = getGroupByValues(groupByDocs, dragCardInitialState)
  newArr.splice(dragCardInitialPosition, 0, dragCard)
  setGroupByValues(groupByDocs, dragCardInitialState, newArr)
}
UXSerious

packages/kanban/src/components/KanbanRow.svelte:124

Card's action menu only opens on right-click, with no keyboard equivalent

`on:contextmenu={(evt) => { showMenu(evt, object) }}` is the only entry point into a card's menu (line 124). There is no visible menu button and no keydown handler tied to the same `showMenu` function, so the action list is reachable only with a mouse right-click.

Why it matters

Any per-card action reachable exclusively through `showMenu` is unavailable to keyboard and switch-control users, and it's undiscoverable since nothing in the rendered card signals a menu exists.

Fix

Route the same `showMenu` handler through a keydown listener (or an icon-button trigger) so the menu opens without a pointer.

on:contextmenu={(evt) => {
  showMenu(evt, object)
}}
on:contextmenu={(evt) => {
  showMenu(evt, object)
}}
on:keydown={(evt) => {
  if (evt.key === 'ContextMenu' || (evt.shiftKey && evt.key === 'F10')) {
    showMenu(evt, object)
  }
}}

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

Components

1 serious
ComponentsSerious

packages/panel/src/components/Panel.svelte:198

Inline style:align-self bypasses the class-based layout system used everywhere else

The header tools row uses `class="buttons-group xsmall-gap ml-4" style:align-self={'flex-start'}`: every other row in this file (aside actions, aside-tabs, custom-attributes) expresses layout purely through utility classes, but this one row reaches for an inline style directive instead.

Why it matters

A one-off inline style can't be themed, overridden by a parent layout change, or picked up by future utility-class refactors: it's a silent exception future maintainers won't know to look for.

Fix

Express alignment through a utility class consistent with the rest of the file instead of an inline style directive.

<div class="buttons-group xsmall-gap ml-4" style:align-self={'flex-start'}>
<div class="buttons-group xsmall-gap ml-4 self-start">

Typography

No issues found

Color

No issues found

Spacing

No issues found

Motion

No issues found

Craft

No issues found

Working well

  • The `select()` function already builds real keyboard traversal across cards and categories (vertical and horizontal), dispatching `obj-focus` and calling `scrollIntoView` on the right panel. That's the harder half of accessible kanban navigation done correctly; wiring the actual column-move action to a key handler is a much smaller lift given this foundation.
  • `.checked` and `.selection` use two different visual mechanisms, a solid background fill versus a box-shadow ring, instead of just swapping one hue for another. That gives the two states a distinct shape as well as a distinct color, which helps them read as different states rather than just different intensities of the same one.
  • Every color in the stylesheet (`var(--theme-kanban-card-bg-color)`, `var(--highlight-select)`, `var(--primary-button-default)`, etc.) is a theme token rather than a hardcoded hex value. That's what makes this component survive a theme change or dark-mode variant without a single line of CSS being touched.
  • The component leans entirely on `Scroller`, `HeaderAdaptive`, and the imported `Panel` primitive for scroll/header mechanics rather than hand-rolling scroll containers or a custom header bar: this avoids re-implementing focus and scroll-restoration logic that the shared primitives already own.

Scored July 22, 2026 with Rams Engine v0.0.2 · Engine changelog

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 this review on every PR.