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

a4ril/episode

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

7 files reviewed·August 7, 2026

View on GitHub

Elevated

Design risk in this codebase.

9issues
Critical & Serious · top 6 shown below

Top fix

Give the Place row's action button a real accessible action

See the fix

Verdict

A quiet, gesture-first places UI that goes silent for anyone not looking at the screen. Three critical VoiceOver blockers (delete, place rows, map pins) mean the core flow is effectively unusable non-visually, and that's the biggest risk here.

Files Rams reviewed

Episode/Views/EditorView.swift

Episode/Views/MapView.swift

Episode/Views/PlaceSheetView.swift

Episode/Views/ReaderView.swift

Episode/Views/HomeView.swift

Episode/Theme.swift

Episode/EpisodeApp.swift

92/100

Accessibility

2 critical1 serious
AccessibilityCritical

Episode/Views/ReaderView.swift:53

Delete toolbar button announces nothing to VoiceOver

In ReaderView.swift the destructive toolbar Button wraps only Image(systemName: "trash") with no accessibilityLabel, so VoiceOver users hear no description of a destructive delete action before triggering it.

Why it matters

A user relying on VoiceOver can trigger episode deletion without any spoken confirmation of what the control does, which is exactly the kind of ambiguity that leads to accidental data loss.

Fix

Every icon-only button needs an accessibilityLabel describing its action in plain words, especially destructive ones.

Button(role: .destructive) {
    confirmErase = true
} label: {
    Image(systemName: "trash")
        .font(.system(size: 14))
        .foregroundStyle(T.dim)
}
Button(role: .destructive) {
    confirmErase = true
} label: {
    Image(systemName: "trash")
        .font(.system(size: 14))
        .foregroundStyle(T.dim)
}
.accessibilityLabel("Delete episode")
AccessibilityCritical

Episode/Views/HomeView.swift:476

Place row's empty Button action blocks VoiceOver activation entirely

In HomeView.swift's groupView, the row Button has an empty action closure (Button { } label: { ... }), with the real open-editor logic firing from PressHighlightStyle's isPressed callback triggered by a physical press gesture.

Why it matters

VoiceOver and Switch Control activate controls by calling the action closure directly, not by simulating a press-and-release gesture. Since the action is empty, assistive technology users can select the row but nothing happens when they activate it, locking them out of opening any episode.

Fix

Put the real navigation logic inside the Button's action closure so every activation path, including assistive technology, triggers it.

Button {
} label: {
    HStack(alignment: .firstTextBaseline, spacing: 10) {
Button {
    commitBurst()
} label: {
    HStack(alignment: .firstTextBaseline, spacing: 10) {
AccessibilitySerious

Episode/Views/PlaceSheetView.swift:20

"PLACES" section label set at 9pt reads as barely-there text

In PlaceSheetView.swift, Text("PLACES") uses .font(T.sans(9, .medium)) with 2.2pt kerning. 9pt is below the ~12px floor generally considered legible body text.

Why it matters

Section labels at this size strain readability for users with normal vision and become nearly unreadable for anyone with mild vision impairment, even before Dynamic Type is considered.

Fix

Set section labels no smaller than 11-12pt, and prefer Dynamic Type-relative styles for legibility across text sizes.

Text("PLACES")
    .font(T.sans(9, .medium))
    .kerning(2.2)
Text("PLACES")
    .font(T.sans(11, .medium))
    .kerning(1.6)

Typography

No issues found

Color

No issues found

Spacing

No issues found

Components

No issues found

Motion

No issues found

UX

No issues found

Craft

No issues found

Working well

  • The destructive delete button correctly uses role: .destructive on both the toolbar trigger and the confirmationDialog action, inheriting the system's destructive styling automatically.
  • burstCover is always rendered (opacity-driven, not conditionally inserted) specifically to avoid the insertion/animation race, which is a subtle and correct SwiftUI timing fix.
  • The heroDotPlaces/dotsLayer physics-based bearing simulation gives the ring a real spatial meaning (dot direction = actual place bearing) rather than decorative motion.
  • Each place row uses .contentShape(Rectangle()) with .buttonStyle(.plain), giving the full row a consistent, reliably tappable surface instead of just the text glyphs.

Scored August 7, 2026 with Rams Engine v0.0.4 · Engine changelog

This page is an automated design review of a4ril/episode’s UI code: 7 files read against 308 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