RapidAir System Designer — UX Audit + Optics Design Token Analysis

February 2026 | 29 Findings | Codebase: Rails 8 + React 16, MobX 5, lightning-cad | Design System: @rolemodel/optics

Executive Summary

135+
Hardcoded Values
0
Optics Tokens in Use
16 / 24
Optics Components Mappable
Recommendation: Pursue a progressive component replacement over 2-3 weeks. Install Optics and set the theme override immediately (2-3 hours), then replace components in priority order: Buttons + Forms (Week 1), Navbar + Modals + Sidebar (Week 2), remaining components (Week 3). This approach delivers visible improvement each sprint while limiting regression risk. Avoid the "full rewrite" path — the mixed state is manageable and the progressive approach lets each phase be independently reviewed and tested.

Migration Effort: Three Options

A. Minimal Theme Swap

2-3 days
  • Install @rolemodel/optics via npm
  • Set 3 HSL variables for RapidAir brand color
  • Override --op-font-family to 'Lato'
  • Map existing CSS vars to Optics equivalents
  • Leave all component SCSS untouched
+ Instant full color scale + dark mode ready + zero visual regression
- Two systems coexist indefinitely - 135 hardcoded values remain - No component benefits

C. Full Replacement

4-6 weeks
  • Delete all custom SCSS except canvas styles
  • Replace all 135+ hardcoded values
  • Refactor React components to Optics markup
  • Address all 22 original audit findings
  • Rework lightning-cad-skin integration
+ Clean slate + All audit findings resolved + Fully maintainable
- Large PR, hard to review - Higher regression risk - Blocks other feature work

Token Mapping: RapidAir Current → Optics

Colors

RapidAir CurrentValueOptics TokenFit
--color-primary hsl(197, 58%, 55%) --op-color-primary-h: 197 + -s: 58% + -l: 55% Exact
--color-primary-light hsl(197, 58%, 60%) --op-color-primary-plus-two (64% L) Close
--color-primary-dark hsl(197, 58%, 50%) --op-color-primary-plus-one (45% L) Close
--color-error hsl(3, 62%, 60%) --op-color-alerts-danger-h: 3 + -s: 62% + -l: 60% Exact
$rapid-grey #2C2C2C --op-color-neutral-minus-seven Close
$header-background #6F6F6F --op-color-neutral-base Close
$sidebar-background #F6F6F6 --op-color-neutral-plus-eight Exact
$button-green #008001 --op-color-alerts-notice-base Close
$alert-yellow #DEC33A --op-color-alerts-warning-base Close
$validation-yellow #FFDD36 --op-color-alerts-warning-plus-three WCAG Fail on white
--color-contrast-* 4-step gray scale --op-color-neutral-* (17-step scale) Superset

Spacing

RapidAirValueOptics TokenFit
--space-xs4px--op-space-2x-small (4px)Exact
--space-sm8px--op-space-x-small (8px)Exact
--space-md16px--op-space-medium (16px)Exact
--space-lg24px--op-space-x-large (24px)Exact
--space-xl32px--op-space-3x-large (40px) — nearestGap at 32px

Border Radius

RapidAirValueOptics TokenFit
--radius-sm2px--op-radius-small (2px)Exact
--radius-md6px--op-radius-medium (4px)6px vs 4px
--radius-lg8px--op-radius-large (8px)Exact

Typography

RapidAirValueOptics TokenFit
$body-font'Lato', sans-serif--op-font-family (Noto Sans) — override to LatoOverride needed
(no type scale)12 inconsistent sizes--op-font-2x-small--op-font-6x-large (12-step scale)No scale exists
(no weight system)Regular + Bold only--op-font-weight-* (9 weights)Underutilized

Shadows

RapidAirValueOptics TokenFit
$card-shadow0px 5px 15px -5px rgba(0,0,0,0.1)--op-shadow-mediumClose
$login-shadow0px 2px 4px rgba(0,0,0,0.12)--op-shadow-smallNear exact

Component Mapping: Custom SCSS → Optics Components

.btn + 8 variants
Button
47 tokens, primary/secondary/danger/warning variants
.form__* inputs
Form
46 tokens, validation states, label styles
.modal + backdrop
Modal
20 tokens, z-index system, transitions
.header--main nav
Navbar
12 tokens, responsive, active states
Left toolbar sidebar
Sidebar
15 tokens, collapsible, transitions
Right properties panel
SidePanel
8 tokens, slide-in panel
.card + accessories
Card
14 tokens, shadow scale, header/body/footer
.table--primary
Table
19 tokens, striped/hover rows
.spinner custom
Spinner
6 tokens, size variants
window.confirm()
ConfirmDialog
19 tokens, in-app confirmation
Error panels (custom)
Alert
49 tokens, warning/danger/info/notice
Info (i) icon tooltips
Tooltip
13 tokens, hover/focus triggers
Pipe color toggle
Switch
18 tokens, accessible toggle
Tool abbreviation labels
Badge / Tag
23-39 tokens, color variants
Step-by-step videos
Accordion
8 tokens, expand/collapse
Custom pagination
Pagination
1 token, standard nav

Hardcoded Values by File (135+ total across 9 key files)

components/PropertiesPanel
~26
base/forms.scss
~22
base/buttons.scss
~18
layouts/header.scss
~16
components/ActionBar.scss
~16
layouts/devise.scss
~14
base/modals.scss
~13
base/cards.scss
~6
base/tables.scss
~4

Critical — Accessibility & Correctness (7)
C1
outline:none on ALL buttons kills keyboard focus
Global button reset removes the browser focus ring. Keyboard-only users cannot see which element is focused anywhere in the app. Replace with a custom :focus-visible style.
buttons.scss:11
WCAG 2.4.7
Optics Button includes :focus-visible
C2
nav role="menu" is incorrect ARIA
Header navigation uses role="menu" which is meant for application menus (like a right-click context menu), not site navigation. Should be role="navigation" or removed entirely since <nav> is already a landmark.
_header.html.slim:3
WCAG 4.1.2
Optics Navbar uses correct ARIA
C3
Hamburger & Dot menus missing ARIA attributes
Neither menu button has aria-expanded, aria-haspopup, or aria-label. Screen reader users hear an unlabeled button with no state information. Also missing click-outside-to-close.
HamburgerMenu.jsx, DotMenu.jsx
WCAG 4.1.2
C4
Video iframe has no title attribute
YouTube iframe in the instructional video modal has no title. Screen readers announce a frame with no context. Also uses autoplay=1, violating audio control requirements.
InstructionalVideoModal.jsx
WCAG 4.1.2, 1.4.2
C5
Duplicate viewport meta tags conflict
Line 5 sets maximum-scale=1, user-scalable=0 (locks zoom). Line 15 sets a permissive viewport. Browsers use the last one, silently overriding the zoom lock. Remove one.
application.html.slim:5,15
C6
.margin-right-none sets margin-LEFT
Copy-paste bug: the utility class .margin-right-none sets margin-left: 0 instead of margin-right: 0. Any component relying on this class for right-margin removal is silently broken.
utilities.scss
C7
No skip-nav link, no <main> landmark
Layout has no "Skip to main content" link and no <main> element. Keyboard and screen reader users must tab through the entire header on every page.
application.html.slim
WCAG 2.4.1
High — UX Quality (7)
H1
No canvas empty state for new projects
When a user creates a new project, they see a completely blank canvas with no illustration, instructional text, or call-to-action explaining how to start. First-time users have zero guidance.
App.jsx — canvas render
H2
"Start Over" uses native window.confirm()
The destructive "Start Over" action uses a browser-native confirm dialog that is unstyled, inconsistent across browsers, and blocked in some contexts. Replace with an in-app confirmation modal.
RapidAirDrawingEditorActionBar.jsx
Optics ConfirmDialog (19 tokens)
H3
"Apply to All Drops" — no confirmation
Clicking this button instantly overwrites every drop's configuration with no warning, no confirmation, and no undo. A potentially destructive bulk action with zero safeguard.
DropPropertyPanel.jsx
Optics ConfirmDialog (19 tokens)
H4
Public share forces disclaimer on every view
editorDisclaimerViewed: false is hardcoded in the public project view. Every person who opens a shared link sees the disclaimer modal before the project. This creates friction for the primary sharing use case.
public/projects/show.html.slim
H5
Clickable error items look non-interactive
Error items in the part list are clickable (they zoom to the offending component), but have no cursor: pointer, no hover state, and no button styling. Users cannot discover this interaction.
PartListErrorsList.jsx
Optics Alert click states
H6
Unsupported browser page is bare HTML
Two unstyled <h2> tags with no wrapper, no brand styling, no actionable guidance (e.g., "Download Chrome"), and no links. Renders as raw text on a white page.
_unsupported_browser_warning.html.slim
H7
Part list disclaimer — both buttons styled primary
"Return to Drawing" and "I Agree, Continue" both use btn--primary. There is no visual hierarchy distinguishing the forward action from the cancel. Cancel should be btn--secondary.
PartListDisclaimerModal.jsx
Optics Button primary/secondary variants
Medium — Design System & Maintainability (8)
M1
Two parallel token systems, neither consistent
CSS custom properties (--color-primary) and SCSS variables ($rapid-grey, $button-green) coexist. Hardcoded hex values also appear throughout. No single source of truth for design tokens. Optics provides a complete token system to consolidate all three into one.
variables.scss
Optics replaces both systems entirely
M2
Typography only defines h2
No base styles for h1, h3, h4, p, small, strong, or body text. The app relies entirely on browser defaults, causing visual inconsistency across browsers. Optics provides a 12-step font scale with line-height and weight tokens.
typography.scss
Optics font scale: 2x-small → 6x-large
M3
Mixed icon systems
ShareViewOnlyLinkButton uses Material Icons font (add_link) while every other icon uses custom SVGs via RapidAirIconFactory. Adds an extra font request and creates visual inconsistency.
ShareViewOnlyLinkButton.jsx
Optics Icon uses Material Symbols
M4
Magic number left:300px in floating controls
Floating controls (layers, snapping, legend) hardcode left: 300px to sit beside the 260px left panel. If panel width changes, controls overlap. Should use a shared variable.
floating-controls.scss
M5
Building Length input has no label
The width x length dimension inputs show "ft x ft" but only building_width has a <label>. Screen readers cannot announce what the second field is for.
_form.html.slim
Optics Form includes accessible labels
M6
<a> wrapping <button> in help video links
Help video links nest a button inside an anchor — invalid HTML that creates ambiguous keyboard/screen reader behavior. Use one or the other.
ProjectComponentsPanel.jsx
M7
Select placeholder options not disabled
The "Select..." placeholder in dropdowns is not marked disabled. Users can re-select it, leaving fields in an empty state that may cause downstream errors.
CeilingOutletPropertyPanel.jsx, AutomaticCompressorPropertyPanel.jsx
Optics Form select handles placeholders
M8
No autosave status indicator
The editor autosaves but shows no "Saving..." / "Saved" indicator. Users cannot tell whether their work is persisted. Causes anxiety and unnecessary manual save attempts.
Editor — global
Cross-Cutting Patterns (7)
P1
Yellow error text fails contrast
#FFDD36 on white background does not meet WCAG AA 4.5:1 contrast ratio for normal text. Optics' warning tokens include automatic contrast-safe text pairings.
ErrorsPanel.scss
Optics warning-on-* tokens are WCAG safe
P2
No inline form validation
New project form only validates on submit. No real-time feedback as users fill fields. The "Start Designing" button is disabled with no explanation of what's missing.
_form.html.slim
Optics Form has validation state tokens
P3
PDF download — 2 min with only a spinner
PDF generation polls every 1.5s for up to 2 minutes. Only shows a spinner — no progress bar, no elapsed time, no retry option on failure.
DownloadsButton.jsx
Optics Spinner provides size variants
P4
Mobile: features hidden, not adapted
Dozens of features disappear via display:none on mobile with no alternative access. Device warning only shows at 638px, but degradation begins at 884px.
mobile.scss
P5
Two identical disclaimer modal titles
Both the editor disclaimer and part list disclaimer display "Disclaimer" as their title. Users who encounter both cannot distinguish which terms they're agreeing to.
EditorDisclaimerModal.jsx, PartListDisclaimerModal.jsx
P6
No alt text on process & layout images
The onboarding process.png and layout radio button images have no alt attributes. Screen readers receive no information about these visuals.
new.html.slim, _form.html.slim
P7
Header uses $rapid-grey but ignores $header-background
variables.scss defines $header-background: #6F6F6F but header.scss uses $rapid-grey: #2C2C2C instead. The named variable exists but is unused — tokens drift silently. With Optics, both map to the neutral scale and this inconsistency disappears.
header.scss, variables.scss
Optics neutral scale eliminates drift