UX/UI Audit — Internal

RapidAir System Designer

Product being audited: RapidAir System Designer

Auditor(s): Claude (ux-audit skill, /ux-audit --internal)

Date: February 20, 2026

Tech stack: Rails 8 · React 16 · SCSS · Slim templates · @rolemodel/lightning-cad-skin

Design system target: Optics (@rolemodel/optics v2.3.0) — not yet installed

Live URL: http://localhost:3000

Figma file: gnJ9S1Bf1o8cWIxKpCy1Ec

How to use this document:

This is a completed internal audit of the RapidAir System Designer. Checklist items are marked as evaluated. Observations below each section contain the actual findings from code scan + heuristic walkthrough. Severity key:

Holding up well
Opportunity
Significant gap

Code scan data is in the appendix: token mapping tables, hardcoded values by file, component mapping grid, and full findings summary table.

Executive Summary
225
Hardcoded CSS values
4
Critical WCAG violations
22
Unique colors (0 in Optics)
23
Total findings (4C·8H·7M·4P)
The RapidAir System Designer is a functional, domain-specific CAD tool with solid core mechanics — autosave, real-time validation, a purpose-built pipe-sizing engine — built on a partially-tokenized CSS architecture that has accumulated 225 hardcoded values across 36 stylesheet files. The most urgent work is a cluster of 4 Critical accessibility violations (focus styles suppressed globally, pinch-zoom disabled on both viewport meta tags, incorrect nav ARIA role, missing modal semantics) that are low-effort to fix and directly block WCAG AA compliance. Below that layer, a dual token system (CSS custom properties + SCSS variables running in parallel) makes the codebase harder to maintain than necessary. The natural migration path: (1) fix Critical accessibility issues in one sprint, (2) adopt Optics for the non-CAD UI layer (Rails views, header, forms, modals), (3) leave @rolemodel/lightning-cad-skin in place for the CAD editor. Token coverage today is ~40% exact or close matches to Optics — migration scaffolding is largely already there.

Section 1: First Impressions & Visual Coherence

What a user feels in the first 30 seconds — and whether the product looks and feels like one thing.

Aesthetic-Usability Effect: Users perceive aesthetically pleasing design as more usable.

Law of Similarity / Law of Uniform Connectedness: Inconsistent visual treatment signals inconsistent relationships.

Von Restorff Effect: Key actions should stand out. If everything competes for attention, nothing wins.

Observations:
The editor interface is clean and task-focused — this is the product's visual high point. The drawing canvas with tool panels communicates purpose clearly. The registration/login page is where first impressions suffer. Two-column form layout with no brand imagery, no product screenshots, and no value proposition beyond 3 paragraphs of account migration text. TYPOGRAPHY GAP (M1): Only h2 is defined in typography.scss (22px, 600 weight, letter-spacing: 0.06em, hardcoded color #1E2D3E). No h1, h3, body text, or caption styles are specified — the type scale is implicit and ad hoc. COLOR COHERENCE: Two parallel color systems undermine consistency. CSS custom properties (--color-primary, --color-white, --color-black, --color-error) are used in most places, but SCSS variables ($button-green: #008001, $rapid-grey: #2C2C2C, $alert-yellow: #DEC33A) live alongside them. SCSS vars are compiled away — not accessible at runtime. BUTTON VARIANTS: 11 button classes (.btn--primary, .btn--outline, .btn--dark, .btn--secondary, .btn--danger, .btn--get-parts-list, .btn--white, .btn--back, .btn--modal, .btn--large, .btn--settings-grid) — each with its own hardcoded values, no shared token backbone. Some use --color-* vars; others use $scss-vars; some hardcode directly. BRAND: RapidAir teal/blue (var(--color-primary)) comes through consistently in primary buttons and headers. Body background is hardcoded #F4F5F7 (base.scss:10), close to Optics neutral-plus-eight but not using it.

Section 2: Navigation & Wayfinding

Can users find what they need, understand where they are, and get back if they get lost?

Jakob's Law: Users expect your product to work like ones they already know. Deviation has a cost.

Nielsen's principle: Users need a clearly marked "emergency exit" from unwanted states.

Observations:
CRITICAL — C3: nav role="menu" in shared/_header.html.slim:3. The role="menu" ARIA role indicates an application menu (keyboard-driven, arrow-key navigated, like a desktop right-click menu) — NOT site navigation. Screen readers announce this as a "menu" and expect role="menuitem" children with arrow-key navigation. The correct semantic is a plain <nav> element (already has implicit navigation landmark role). WCAG 4.1.2. WAYFINDING GAPS: No current-page indicators (no aria-current="page", no active nav state styling). No breadcrumbs on most screens — only admin customer detail view (customers/show.html.slim) has breadcrumbs. Page identity comes from an h2 in the header via `title:` helper — this is h2 semantically, not h1. HamburgerMenu (HamburgerMenu.jsx:43): Toggle button has no aria-label, no aria-expanded, no aria-haspopup. The dropdown open/close state is invisible to screen readers. DEPTH: Flow is shallow — Customer list → Customer detail → Project editor. 2-3 clicks to any core function. Good. MOBILE NAV: HamburgerMenu is the mobile entry point for editor actions but has no ARIA attributes. On very small screens (< 638px) a device-warning banner appears directing users away from the mobile editor entirely — this is acknowledged behavior for a CAD tool but the non-editor pages should still be navigable.

Section 3: Cognitive Load & Complexity

How hard is the product making users think? Are we asking for more mental effort than necessary?

Hick's Law: Decision time increases with number and complexity of choices. Miller's Law: Average person holds 7±2 items in working memory.

Observations:
NEW PROJECT FORM DENSITY: quoted_projects/_form.html.slim presents 3 columns simultaneously — Basic Setup (7 fields: name, mount type, ceiling type, ceiling height, drop height, building width × length), Pipe System (LineSizeCalculator + compressed parts checkbox), Starting Layout (radio buttons + legend SVG). This is cognitively dense for first-time users with no progressive disclosure or stepped guidance. LEGEND WITHOUT ALT TEXT (H6): The Starting Layout section uses img src='/images/legend.svg' with no alt attribute. The radio button options require the legend to interpret — screen reader users and users with images disabled cannot understand what layout they're selecting. SMART DEFAULTS: Form fields use defaults (selected: 'on-wall' for mount type, selected: 'finished' for ceiling type, placeholder: 'Default: 4 ft' for drop height). Good practice preserved. WINDOW.CONFIRM FOR RESET (M6): RapidAirDrawingEditorActionBar.jsx:34 uses window.confirm('Are you sure you want to reset the drawing?'). The native browser dialog is abrupt, inconsistently styled, and cannot be made accessible or branded. Replace with an in-app modal confirmation. ACCOUNT MIGRATION COPY: The sign-up/login page (devise/registrations/new.html.slim) includes 3 paragraphs explaining account migration — creates cognitive load and anxiety before the user has done anything. New users who never had a RapidAir account don't know what "migrated accounts" means for them. NO PROGRESS INDICATORS: No wizard flow or step indicator for new project creation. Users go directly into a full-form → canvas without knowing where they are in a larger setup process.

Section 4: Key Flows & Task Completion

Walk through the most critical user journeys. Can users accomplish what they came to do?

Core tasks being evaluated:

1. Create account / sign in with RapidAir Web Store
2. Create a new project with floor plan layout
3. Generate a part list from a completed drawing

Fitts's Law: Time to acquire a target is a function of its distance and size.

Nielsen's principle: Error messages should be in plain language, identify the problem, and suggest a solution.

Postel's Law: Products should gracefully handle unexpected input rather than failing.

Peak-End Rule: Users judge an experience by its most intense moment and its end.

Observations:
SIGN-IN FLOW: Primary path ("Sign in with RapidAir Web Store") is OAuth — correct implementation. However, it sits below a large "New User? Sign Up" two-column form, which draws the eye first. Returning users may search for a login field before finding the OAuth button. NEW PROJECT FLOW: Customer → New Project → form → editor is logical. The new project form is dense (3 columns, 7+ fields) but defaults are well-chosen. The legend SVG for layout type selection has no alt text (H6) — a first-time user relying on a screen reader cannot interpret the layout options. EMPTY STATE MISSING (H5): customers/show.html.slim:43-53 renders the "Existing Quoted Projects" table regardless of project count. When @quoted_projects is empty, the table renders with empty tbody and no thead — no empty state message, no guidance, just empty space above the "New Project" button. PART LIST FLOW: PartListButton triggers PartListDisclaimerModal — functional. The modal lacks role="dialog" and aria-modal="true" (C4). The flow correctly stops autosave (PartListButton.jsx:53) before generating. The "Get Parts List" button uses $button-green (#008001) — a green separate from the token system. PEAK MOMENT: The drawing editor's real-time validation system (ErrorsPanel subscribes to addValidatedObserver + addErrorObserver) delivers immediate, component-specific feedback when pipe connections are invalid or sizes are out of spec. This is the product's strongest UX moment. Protect it. ERROR MESSAGES: Component-level errors via ErrorsPanel (real-time). Form validation via Rails SimpleForm inline errors. No global async error notification / toast pattern.

Section 5: Feedback & System Communication

Does the product keep users informed and in control?

Visibility of System Status: Keep users informed through appropriate feedback within a reasonable time.

Observations:
LOADING STATES — MIXED: ModalSpinner covers editor initialization (App.jsx:304: if (!this.state.loaded) return <ModalSpinner>). DownloadsButton shows a button-level spinner during downloads (DownloadsButton.jsx:33,117). Both are good implementations. AUTOSAVE STATUS — ABSENT (H4): Autosave exists (PartListButton.jsx:53 calls repository.stopAutosave()). CSS class .action-bar__button-saved (buttons.scss:163) suggests a visual saved state, but there's no persistent status indicator showing "Last saved X minutes ago" or "Saving...". Users have no confidence about whether their work is persisted. FLASH MESSAGES: Rails flash messages render via shared/_header.html.slim:18-20 as .flash.flash-#{key} divs. No dismiss button or auto-timeout in the template — flash stays until next navigation. Not announced to screen readers (no role="alert"). UNDO/REDO: Editor inherits undo/redo from lightning-cad-skin DrawingEditorActionBar — correct and present. REAL-TIME VALIDATION: ErrorsPanel subscribes to addValidatedObserver and addErrorObserver — users get immediate component-level feedback. Strongest feedback pattern in the product. MINOR: ErrorsPanel.jsx:69 renders <div></div> (not null) when there are no errors — returns empty div that may affect layout flow.

Section 6: Consistency & Standards

Does the product behave predictably, and does it follow platform conventions?

Nielsen's principle: Users should not have to wonder whether different words, situations, or actions mean the same thing.

Law of Uniform Connectedness: Consistent visual treatment of interactive elements prevents confusion.

Observations:
TWO FORM SYSTEMS (M3): Customer form (customers/_form.html.slim) uses SimpleForm with default styling. Project form (quoted_projects/_form.html.slim) uses a custom .form__group/.form__label system. These look visually distinct — users moving between the two contexts will notice the inconsistency. MODAL ARCHITECTURE SPLIT: Two modal patterns coexist. Rails partial (shared/_modal.html.slim) handles DOM-injection modals. React modals (AccessoriesModal, EditorDisclaimerModal, PartListDisclaimerModal, InstructionalVideoModal, LegendModal) manage their own state. Both patterns lack role="dialog" and aria-modal. Neither has focus trapping. (See C4.) MISLEADINGLY NAMED CLASS: .btn--white (buttons.scss:92) only sets text color to white (color: var(--color-white)) — does NOT set a white background. The name implies a white-background button variant. NO PREFERS-REDUCED-MOTION (M4): Modal CSS (modals.scss) uses transform: scale(0.9)→scale(1) and opacity transitions. No @media (prefers-reduced-motion: reduce) override anywhere in the codebase. PARALLEL TOKEN SYSTEMS (M2/P1): CSS custom properties (--color-*, --space-*, --radius-*) and SCSS variables ($body-font, $button-green, $rapid-grey) run in parallel. SCSS vars are compiled away — cannot be overridden at runtime, invisible to design tooling. 225 hardcoded values across 36 files means every stylesheet is its own source of truth. .btn--get-parts-list uses $button-green: #008001 — a green completely separate from the semantic token system. Not connected to alert/success color semantics.

Section 7: Accessibility

Who is being left out — and what's the effort to fix it? Findings from static code scan.

Fitts's Law: Target size directly affects accuracy and ease of interaction.

Observations:
CRITICAL — C1: FOCUS STYLES SUPPRESSED GLOBALLY File: app/javascript/stylesheets/base/buttons.scss:8 Rule: button { outline: none; } Applied to ALL button elements. No :focus-visible replacement exists anywhere in the codebase. Impact: Keyboard-only users cannot see focus position on any button in the application. WCAG: 2.4.7 Focus Visible (Level AA) Fix: button:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; } Effort: 15 minutes CRITICAL — C2: PINCH-TO-ZOOM DISABLED File: app/views/layouts/application.html.slim:5 AND :15 (DUPLICATE VIEWPORT META TAG) Line 5: content="width=device-width, initial-scale=1, maximum-scale=1,user-scalable=0" Line 15: content="width=device-width, initial-scale=1" (second, redundant viewport meta) Impact: Prevents mobile users with low vision from zooming the page. WCAG: 1.4.4 Resize Text (Level AA) Fix: Remove maximum-scale=1,user-scalable=0 from line 5. Remove duplicate line 15 entirely. Effort: 5 minutes CRITICAL — C3: INCORRECT NAV ARIA ROLE File: app/views/shared/_header.html.slim:3 Rule: nav role="menu" The role="menu" ARIA role is for application menus (keyboard-navigated with arrow keys, like a desktop context menu). It is not for site navigation. Screen readers will announce this as a "menu" and expect role="menuitem" children with arrow-key navigation, causing confusion. WCAG: 4.1.2 Name, Role, Value (Level AA) Fix: Remove role="menu" — the <nav> element already has an implicit navigation landmark role. Effort: 2 minutes CRITICAL — C4: MODALS WITHOUT DIALOG SEMANTICS Files: shared/_modal.html.slim (Rails modal), AccessoriesModal.jsx, PartListDisclaimerModal.jsx, EditorDisclaimerModal.jsx, InstructionalVideoModal.jsx, LegendModal.jsx (6 modals total) Issues: No role="dialog", no aria-modal="true", no aria-labelledby pointing to modal title, no focus trapping Impact: Screen reader users are not told they're in a modal. Focus travels to background content. WCAG: 4.1.2 Name, Role, Value (Level AA) Fix: Add role="dialog" aria-modal="true" aria-labelledby="[title-id]" to each modal. Implement focus trap (tab cycling within modal while open). Effort: Medium — requires focus trap logic in each React modal component HIGH — H1: 3 IMAGES WITHOUT ALT TEXT shared/_header.html.slim:6 — Logo img (should be alt="RapidAir logo") quoted_projects/_form.html.slim:62 — Legend SVG img src='/images/legend.svg' (critical: required to understand layout radio options) quoted_projects/new.html.slim:9 — Process image img src=asset_path("process.png") WCAG: 1.1.1 Non-text Content (Level A) Effort: Low HIGH — H2: ICON-ONLY BUTTONS WITHOUT LABELS HamburgerMenu.jsx:43 — no aria-label, no aria-expanded, no aria-haspopup DotMenu.jsx:37 — no aria-label, no aria-expanded shared/_modal.html.slim:6 — modal close icon with no label WCAG: 4.1.2 Name, Role, Value (Level AA) Effort: Low MEDIUM — M1: NO SKIP NAVIGATION LINK — keyboard users must tab through entire header on every page load MEDIUM — M2: NO <main> LANDMARK — no <main> element in any Slim template; main content uses .wrapper divs MEDIUM — M3: NO PREFERS-REDUCED-MOTION — modal transition (transform + opacity) has no reduced-motion override MEDIUM — M4: TABLE WITHOUT scope — customers/index.html.slim <th> elements have no scope="col"

Section 8: Mobile & Responsive Behavior

Does the layout adapt gracefully? Are touch targets appropriately sized? Can mobile users complete core tasks?

Observations:
MOBILE STRATEGY: Primary approach in mobile.scss is display:none — hiding large portions of the UI rather than adapting them. Elements hidden at $mobile-breakpoint: .action-bar__section:nth-child(2) (entire middle action bar with zoom/perspective tools), .drawing-scale, .floating-controls, .project-components__popup, .project-components__instructions, .project-components__title, .footer--components-panel, .header__title, .btn--secondary (sign-out button), .btn--white. These represent the majority of the editor's functionality. USER-SCALABLE DISABLED (C2): user-scalable=0 and maximum-scale=1 prevent pinch-to-zoom. WCAG 1.4.4 violation. See Section 7. BREAKPOINTS: Only 2 breakpoints exist — $mobile-breakpoint (defined in variables.scss, not verified value) and 1064px (hides .btn--restart). No intermediate tablet breakpoint. This is a binary desktop/mobile split with no gradient. VIDEO MODAL SIZES: InstructionalVideoModal renders an iframe. mobile.scss uses hardcoded px values for the video container: 640×360px at the mobile breakpoint, 300×200px at very small. Not fluid. DEVICE WARNING: A .device-warning banner appears at viewports < 638px (mobile.scss:215: @media only screen and (min-width: 638px) { display: none; }). This directs mobile users away from the editor — an acknowledged limitation for a CAD tool, but pre-editor pages (login, new project form, customer list) should still be mobile-optimized. FORM INPUT TYPES: Some numeric fields (building width/length, ceiling height) use default text inputs without type="number" or type="tel" — on mobile, this doesn't trigger the numeric keyboard.

Section 9: Performance Perception

We're not doing a technical performance audit — but we note what we can observe from the code.

Doherty Threshold: Productivity drops when users have to wait. Perceived performance matters even when actual speed isn't our scope.

Observations:
BUNDLE SIZE: Two large compiled JS artifacts in app/assets/builds: 2cd54c463b054697a66dbdba24954623.js (contains PDF.js and CAD engine) and application-a7795cbac241cadbdd1a.digested.js. Both Turbolinks-tracked. No code splitting apparent — editor and non-editor code are likely bundled together. LOADING STATE: ModalSpinner (full-viewport overlay) covers editor initialization (App.jsx:304). Functional, but no skeleton content for the surrounding UI — users see blank screen then full app. MODAL TRANSITIONS: fade + scale animation (modals.scss: transform scale(0.9)→1, opacity 0→1) feels polished and appropriate for a tool-grade application. Duration via --modal-transition-speed CSS var — not hardcoded to a specific ms value. IMAGES: process.png and legend.svg loaded without lazy loading hints or explicit width/height attributes in Slim templates. Potential layout shift on slow connections. No WebP format or responsive srcset. AUTOSAVE: Runs periodically in the background (repository layer). No perceived impact from the UI code.

Section 10: Strategic & Forward-Looking Notes

Step back from the individual findings. What's the bigger picture?

Observations:
STRONGEST MOMENT: The drawing editor's real-time validation system. ErrorsPanel subscribes to validated and error observers and delivers immediate, component-specific feedback when pipe connections are invalid or sizes are out of spec. This is purpose-built domain logic that distinguishes RapidAir from generic drawing tools. Protect this. Build the rest of the UI around it. BIGGEST CONSTRAINT FROM PAST DECISIONS: The parallel token systems. CSS custom properties (--color-*, --space-*, --radius-*) and SCSS variables ($body-font, $button-green, $rapid-grey) run side-by-side. SCSS vars are compile-time only — they cannot participate in runtime theming and are invisible to design system tooling. 225 hardcoded values across 36 files mean every stylesheet is its own source of truth, making CSS changes high-risk. HIGHEST IMPACT / MODERATE EFFORT: 1. Fix 4 Critical accessibility issues — one sprint, high compliance return, no architectural change required. Focus styles (15 min), viewport meta (5 min), nav role (2 min), modal semantics (1-2 days for all 6 modals with focus trapping). 2. Adopt Optics for the non-CAD UI layer — header, forms, Rails views, modals, buttons. @rolemodel/lightning-cad-skin already handles the CAD editor; Optics handles the application shell. The two coexist cleanly. MODERNIZATION OPPORTUNITY: The architecture already has the right seam. @rolemodel/lightning-cad-skin is correctly isolated to the drawing editor. The Rails UI layer (Slim templates, base SCSS) is entirely Optics-eligible. Token coverage gap is lower than it appears: 4 of 5 spacing tokens are exact or close Optics matches; 2 of 3 radius tokens match exactly. Migration is scaffolded. WHAT "NEXT" LOOKS LIKE: A version where the non-editor UI (login, new project wizard, customer pages, part list export) is built on Optics with semantic color tokens, accessible focus management, and a defined type scale. The editor stays on lightning-cad-skin. The visual language becomes coherent across both surfaces. Accessibility violations are gone. Users on mobile can complete pre-editor flows (account creation, project setup) without friction, then switch to desktop for the CAD work — a completely natural two-device workflow for field contractors.

Token Mapping: Current → Optics

Unique values from the codebase mapped to Optics equivalents. Phase 2 code scan. Note: @rolemodel/optics is not installed — these represent the migration target.

Spacing Tokens

Current VariableValueOptics TokenFit
--space-xs4px--op-space-2x-smallExact
--space-sm8px--op-space-x-smallExact
--space-md16px--op-space-mediumExact
--space-lg24px--op-space-x-largeExact
--space-xl32pxMiss (Optics: 28px or 40px)

Border Radius Tokens

Current VariableValueOptics TokenFit
--radius-sm2px--op-radius-smallExact
--radius-md6pxMiss (Optics: 4px or 8px)
--radius-lg8px--op-radius-largeExact

Color Tokens

Current Variable / ValueSampleOptics TokenFit
var(--color-primary) / ~#197FA4#197FA4--op-color-primary-base (brand H:197 S:58%)Close
$rapid-grey / #2C2C2C#2C2C2C--op-color-neutral-minus-nineClose
#F4F5F7 (body bg, base.scss:10)#F4F5F7--op-color-neutral-plus-eightClose
$sidebar-background / #F6F6F6#F6F6F6--op-color-neutral-plus-eightClose
$header-background / #6F6F6F#6F6F6F--op-color-neutral-baseClose
$button-green / #008001#008001--op-color-alerts-notice-minus-threeClose
$alert-yellow / #DEC33A#DEC33A--op-color-alerts-warning-baseClose
$validation-yellow / #FFDD36#FFDD36--op-color-alerts-warning-plus-twoClose
$notice-blue / #B6DAE3#B6DAE3--op-color-alerts-info-plus-fiveClose
#1E2D3E (h2 color, typography.scss:9)#1E2D3E--op-color-neutral-minus-nineClose
#F0F0F0 (.btn--active, buttons.scss:67)#F0F0F0--op-color-neutral-plus-sevenClose
var(--color-error) / ~red--op-color-alerts-danger-baseClose

Typography — Font Sizes (10 unique values, 32 occurrences)

Hardcoded ValueOptics TokenFit
10px--op-font-size-x-smallExact
12px--op-font-size-smallExact
13pxMiss
14px--op-font-size-medium-smallExact
15pxMiss
16px--op-font-size-mediumExact
17pxMiss
18px--op-font-size-medium-largeExact
20px--op-font-size-largeExact
22pxMiss (Optics: 20px or 24px)

Hardcoded Values by File

225 total hardcoded values across 36 files (31 hex colors · 140 px spacing · 32 font-size · 18 box-shadow · 4 border-radius). Sorted by estimated count descending. Red >20, orange 10–20, green <10.

components/ProjectComponentsPanel.scss
36
base/forms.scss
16
layouts/mobile.scss
14
base/buttons.scss
13
variables.scss
11
base/modals.scss
8
components/DrawingEditorActionBar.scss
8
components/PropertiesPanel.scss
7
components/ZoomControl.scss
7
layouts/new-project.scss
6
base/typography.scss
5
layouts/parts-list.scss
5
base/cards.scss
4
layouts/customers.scss
4
remaining 22 files
~81

Component Mapping: Custom → Optics

Current custom components and their closest Optics equivalents. CAD editor components stay on @rolemodel/lightning-cad-skin.

.btn + 11 variants
op-button
primary, outline, ghost, danger
.modal / React modals
op-modal
role="dialog", focus trap, aria-modal
.form__group / SimpleForm
op-input / op-select
Unifies 2 form systems → 1
.table--primary
op-table
Adds scope, responsive layout
header.header--main
op-app-header
Fixes nav role, adds skip-nav, main landmark
.flash messages
op-alert
Dismissible, role="alert", auto-timeout
.card
op-card
Token-based shadow, radius, padding
HamburgerMenu / DotMenu
op-dropdown
aria-expanded, aria-haspopup, focus mgmt

Findings Summary

23 total findings. All reference verified file paths and line numbers from the actual codebase. Prioritized by severity and effort.

#FindingSectionSeverityImpactEffort
C1Focus styles suppressed globally — button { outline: none }, no :focus-visible replacement (buttons.scss:8)7 · AccessibilityCriticalHighLow
C2Pinch-to-zoom disabled — user-scalable=0, maximum-scale=1 on both viewport meta tags; duplicate viewport tag (application.html.slim:5,15)7 · AccessibilityCriticalHighLow
C3Incorrect ARIA role — nav role="menu" misidentifies site nav as application menu (_header.html.slim:3)2 · NavigationCriticalHighLow
C4All 6 modals lack role="dialog", aria-modal="true", and focus trapping (shared/_modal.html.slim + 5 React modals)7 · AccessibilityCriticalHighMedium
H13 images without alt text — logo (_header.html.slim:6), legend SVG (_form.html.slim:62), process image (new.html.slim:9)7 · AccessibilityHighHighLow
H2Icon-only buttons without labels — HamburgerMenu (jsx:43), DotMenu (jsx:37), modal close (_modal.html.slim:6)7 · AccessibilityHighHighLow
H3HamburgerMenu missing aria-expanded, aria-haspopup — open/close state invisible to screen readers (HamburgerMenu.jsx:43)2 · NavigationHighMediumLow
H4No autosave status indicator — users cannot see save state; autosave stops silently during part list generation (PartListButton.jsx:53)5 · FeedbackHighHighMedium
H5No empty state for projects table — empty tbody with no message when customer has no projects (customers/show.html.slim:43)4 · Key FlowsHighMediumLow
H6Legend SVG has no alt text — Starting Layout radio options require the legend image to interpret (_form.html.slim:62)3 · Cognitive LoadHighMediumLow
H7Account migration copy creates first-impression anxiety — 3 dense paragraphs confuse new users (registrations/new.html.slim)1 · First ImpressionsHighHighLow
H8Mobile strategy is display:none — 11+ sections hidden rather than adapted; mobile.scss hides core editor functionality8 · MobileHighHighHigh
M1No type scale defined — only h2 styled in typography.scss (22px, #1E2D3E); no h1, h3, p, caption styles1 · First ImpressionsMediumMediumMedium
M2Two parallel token systems — CSS vars + SCSS vars running side-by-side; 225 hardcoded values across 36 files6 · ConsistencyMediumHighHigh
M3Two divergent form styling systems — SimpleForm defaults (customer form) vs custom .form__group (project form)6 · ConsistencyMediumMediumMedium
M4No prefers-reduced-motion support — modal fade+scale transitions have no reduced-motion override7 · AccessibilityMediumMediumLow
M5No skip navigation link and no <main> landmark — keyboard users tab through entire header on every page7 · AccessibilityMediumMediumLow
M6window.confirm() for "Start Over" — native browser dialog inconsistent with app design (RapidAirDrawingEditorActionBar.jsx:34)3 · Cognitive LoadMediumLowMedium
M7Customer table missing scope attributes — <th> has no scope="col" (customers/index.html.slim)7 · AccessibilityMediumLowLow
P111 button variants with no shared token backbone — each hardcodes its own color; cohesive theming impossible6 · ConsistencyPatternHighHigh
P2$button-green (#008001) outside the token system — not connected to semantic alert/success tokens; appears in 2+ files6 · ConsistencyPatternMediumLow
P3Icon-only interactive elements without labels — pattern across HamburgerMenu, DotMenu, modal close, likely CAD editor icon buttons7 · AccessibilityPatternHighLow
P4ProjectComponentsPanel.scss has 36 hardcoded values — highest-density file; best first token migration candidate6 · ConsistencyPatternMediumMedium

Claude Prompting Notes

Prompts that worked well on this audit — building a shared library for future audits.

Useful patterns from this audit: 1. PARALLEL TOOL CALLS FOR ACCESSIBILITY: Running aria-label grep, img-without-alt grep, and outline-none grep simultaneously saved significant time over sequential scans. Use multi-tool calls aggressively in Phase 3. 2. READ COMPONENT FILES AFTER GREP: Pattern: "grep for onClick/onKeyDown → get file list → read top files" gives precise line-number findings. HamburgerMenu.jsx and DotMenu.jsx were found this way. 3. EVERY FINDING MUST HAVE A FILE:LINE: Never report a finding without a verified code location. This audit's 23 findings all reference specific file paths (e.g., buttons.scss:8, application.html.slim:5, _header.html.slim:3). 4. TOKEN MAPPING: mcp__optics__search_tokens by category (spacing, border) returns full Optics token lists. Compare manually against SCSS variable values. More reliable than mcp__optics__suggest_token_migration for custom-system colors. 5. CHECK FOR DUPLICATE TAGS: The duplicate viewport meta (application.html.slim:5 AND :15) was found by reading the full layout file, not via grep. Always read layout files completely. 6. MODAL ARCHITECTURE PATTERN: Check for both Rails partials (grep shared/_modal) AND React components (glob **/*Modal.jsx) — projects often have both patterns with different accessibility characteristics.