All findings from static code analysis of 54 SCSS/CSS files and 40+ React/view components.
--sk-*, --op-*, --color-*) still coexist, and the legacy flash/alert styles use hardcoded Bootstrap-style hex values. The most impactful single fix is adding a loading indicator when a project loads — the LoadProjectView component currently returns null for 2–5 seconds, leaving users on a blank screen during the app's primary flow. Three WCAG violations exist around outline: 0 on focusable elements without a :focus-visible replacement, and the confirm dialog lacks role="dialog" and focus trap. Completing the Optics migration (replacing --color-* and --sk-* variables with Optics tokens) would eliminate approximately 46 hardcoded hex values and consolidate the token system — estimated 1–2 days of focused cleanup.
Visual coherence across screens; brand clarity; heading hierarchy; color semantics.
--sk-* (legacy Sayfa, ~15 tokens in variables.scss), --op-* (Optics, 204 declarations), and --color-* (old legacy, ~20 variables for alert colors). Components pick from all three, making the token contract unclear. The --sk-* system partially overrides --op-* values in theme files (e.g. --op-color-primary-plus-six: var(--sk-navbar-button-background-color)), which is the correct migration pattern but leaves old --sk-* consumers unfixed.
#fdf3d1, #856404, #f3d8da, #721c24) instead of Optics alert tokens. These are Bootstrap 4-era color values. Optics provides --op-color-alerts-warning-* and --op-color-alerts-danger-* scales that would replace all 6 hardcoded values in this file with a single token each.
moddex-theme-core.scss imports Inter and Rubik from Google Fonts. The --op-font-family is set to 'Rubik', 'Roboto', and variables.scss sets --font-primary: 'Helvetica' (commented note). Inter is not referenced anywhere in the codebase. This is a ~200KB unused network request per page load.
base.scss reads: "Re-enable this? Some parts of the UI have too little contrast on a near-white background (they're fine on true white)." The commented-out rule sets the body background to --color-contrast-lower (#f9f9f9), a subtle off-white that would improve perceived depth. This suggests a known visual issue that was deferred rather than resolved.
Structure, active states, landmarks, skip navigation, and orientation cues.
Menu.jsx's toggle button has title="Menu" but no aria-expanded attribute. Screen readers announce the button but cannot communicate whether the menu is currently open or closed. The fix is one line: aria-expanded={this.state.visible} on the button element.
menuLinks prop but applies no active/current class based on the current route. Users navigating via keyboard or screen reader have no programmatic way to determine where they are. The action bar page title (@action_bar_title) provides visual context, but there's no aria-current="page" on any nav item.
application.html.slim layout uses = yield directly after the header with no wrapping <main> element. The login view correctly uses main.app-body, but every other page omits it. Without a <main> landmark, screen reader users cannot jump directly to content. WCAG 2.4.1 (Level A) requires a mechanism to bypass repeated navigation.
Decision density, chunking, form complexity, and progressive disclosure.
.card.form__section but the sections aren't clearly delineated with headings — just <p class="form__label"> used as section labels. This is a <p> masquerading as an <h3>, both semantically incorrect and visually understated.
Core tasks evaluated: (1) Load a project and edit it, (2) Create a new project, (3) Export documentation.
withLoadedProject.jsx's LoadProjectView returns null while status === 'pending'. For a complex 3D drawing, this load can take 2–5 seconds. Users see a white void after clicking a project, with no indication that anything is happening. The error state ('rejected') renders only <div>Error loading project</div> with no guidance, retry action, or back button.
DotMenuItems.jsx calls this._drawingEditor().clearProjectDrawing() immediately on click with no confirmation. The adjacent "Delete Project" correctly uses TurboConfirm. A misclick on "Reset Canvas" destroys the user's current drawing state without any safety net. This is especially risky because the dot menu is small and the items are close together.
autocomplete="first_name", autocomplete="last_name", and autocomplete="organization_name". The WHATWG HTML spec and all major password managers recognize "given-name", "family-name", and "organization" instead. With the non-standard values, browsers cannot auto-fill these fields, increasing form friction for returning users.
Loading states, success/error feedback, autosave indicators, and flash messaging.
_flash.html.slim) renders the flash message text but includes no close button in the markup — only a .flash__close class exists in the CSS with no associated element. The 5-second auto-dismiss via CSS animation is the only way messages disappear. Users with slower reading speed or cognitive load may miss critical feedback. The animation uses -webkit-animation (vendor-prefixed) which is not needed for a Chrome-only app.
role="status" (for notices) or role="alert" (for errors). Screen readers will not automatically announce these messages when they appear. This means authentication feedback, save confirmations, and error messages are invisible to assistive technology users.
LoadProjectView fails (status === 'rejected'), it renders only <div>Error loading project</div>. There is no error message, no retry button, no back navigation, and no log of what went wrong. The console.error(error) call captures the error in dev tools but users see only a generic message.
Component reuse, behavioral consistency, and platform convention alignment.
Menu.jsx (page nav) and MoreMenu.jsx (project row actions) solve the same problem but diverge in implementation: Menu conditionally renders _renderMenu() (null when closed), while MoreMenu always renders the dropdown div and toggles visibility via CSS class (hamburger-menu__list--active). They share the same CSS classes and visual appearance but have different DOM behavior, different accessibility implications, and different close-on-outside-click handling. A single DropdownMenu component would eliminate the duplication.
tc.confirm()). "Reset Canvas" is a destructive action that clears the current drawing but skips this pattern entirely — no confirmation, no undo, no feedback. This is inconsistent with both the project's own patterns and user expectations for irreversible operations. The fix is a 4-line addition of TurboConfirm matching the Delete Project implementation.
@mixin action-bar__dropdown-menu applies box-shadow: 0px 5px 15px #232323 — a hardcoded dark gray shadow rather than using the --shadow token (0px 8px 16px rgba(0, 0, 0, 0.08)) defined in variables.scss. This inconsistency means the dropdown shadow will not respond to theme changes and uses an opaque color instead of a transparent one.
<p class="form__label">. This is semantically incorrect — these are section headers for groups of related form fields. Using <h3> (or <legend> inside <fieldset>) would correctly express the document structure and allow screen reader users to navigate by heading.
WCAG 2.1 AA compliance, ARIA usage, focus management, and semantic HTML.
.form__input:focus sets outline: 0 and substitutes a border-color change to --color-contrast-high. WCAG 2.4.11 (Focus Appearance, Level AA) requires focus indicators to have a minimum contrast ratio of 3:1 between focused and unfocused states, and a minimum area. A border-color change alone — especially a subtle gray-to-darker-gray shift — almost certainly fails this. A visible focus ring (e.g. box-shadow: 0 0 0 2px var(--op-color-primary-base)) is needed as the replacement.
_confirm.html.slim and its styles in confirm.scss have no role="dialog", no aria-modal="true", no aria-labelledby pointing to the title, and no focus trap. When the dialog opens, keyboard users can tab behind it to the page content. Screen readers don't announce it as a modal. The backdrop also has outline: 0. The dialog is visually modal but functionally not — this is a Level A accessibility failure for a dialog that appears before destructive actions.
%modal-wrapper-global sets outline: 0 on the modal wrapper element. While the modal dialog itself should trap focus rather than receive focus, removing the outline on the wrapper can interfere with certain focus management approaches and assistive technology behavior. Combined with no explicit role="dialog" or aria-modal on the modals rendered via this pattern, modals may not be properly announced.
MoreMenu.jsx's icon button has only a title="More Menu" attribute. Like the hamburger menu, it needs aria-expanded={this.state.visible} and ideally aria-haspopup="menu" to correctly communicate its state to screen readers.
role="status" for notices and role="alert" for errors to trigger screen reader announcements when inserted into the DOM.
autocomplete="first_name" and autocomplete="last_name" are not WHATWG-recognized token values. The correct values are "given-name" and "family-name". autocomplete="organization_name" should be "organization". These are minor fixes (3 string changes) with meaningful UX impact for returning users.
<p class="form__label"> elements used as section headers within the organization settings form are not announced as headings by screen readers, breaking the document's heading hierarchy.
Breakpoint coverage, touch targets, layout adaptation. Note: this is a CAD tool — desktop-primary is expected.
.flash is positioned with top: 72px; left: calc(50% - 250px); width: 500px. On a 375px-wide screen, this results in left: -62.5px — the notification bleeds off the left edge. The login and account pages are the most likely mobile views, and both trigger flash messages (login errors, password changes).
@media queries. While the drawing editor is intentionally desktop-only, auth pages (login, invitation acceptance, password reset) and the account settings page have no responsive adaptation. The login card and forms would benefit from responsive padding/width adjustments for mobile users.
Observable performance signals from static analysis only — the app could not be started due to missing node_modules.
100..900) loads a CSS file and potentially multiple font files on every page, despite Inter never being used. For a Chrome-only app with a defined font stack (Rubik → Roboto → sans-serif), this is pure overhead.
moddex-theme-core.scss imports Roboto as a fallback in the Google Fonts URL. --op-font-family: 'Rubik', 'Roboto', sans-serif lists both. Roboto is a system-available font on Android and Chrome OS, so loading it from Google Fonts is unnecessary when it's already the OS fallback. The Google Fonts import for Roboto can be removed; the CSS fallback in --op-font-family can stay.
--op-* namespace, and the theme file correctly overrides Optics variables with brand values. The remaining cleanup is mechanical: replace --color-notice, --color-error, --color-warning, and --color-info in variables.scss with Optics alert tokens; replace --sk-* references in non-theme files with their --op-* equivalents. This is achievable in 1–2 focused days and would eliminate ~46 hardcoded hex values and all three parallel token systems.
withLoadedProject.jsx when status === 'pending' is the single change with the highest user-visible impact. The fix is ~10 lines of JSX. A simple spinner inside the full-page canvas area would prevent the blank-screen confusion during what is the app's primary flow (opening a project).
DropdownMenu component that accepts trigger, items, and handles open/close state, aria-expanded, and focus management would make both instances correct simultaneously. This is the right abstraction because the existing duplication has already diverged in accessibility behavior.
Mapping observed hardcoded values to @rolemodel/optics 2.2.0 tokens. 613 usages already reference --op-* tokens — the project is in good shape. The gaps below are the remaining legacy values.
46 hardcoded hex values across 11 files. Top concentration: variables.scss (20), flash.scss (6), PartsListView.scss (5).
| Hardcoded Value | Used In | Optics Token | Fit |
|---|---|---|---|
#155724 |
variables.scss (--color-notice) | --op-color-alerts-notice-base |
Close |
#c3e6cb |
variables.scss (--color-notice-light) | --op-color-alerts-notice-plus-five |
Close |
#d4edda |
variables.scss (--color-notice-lighter) | --op-color-alerts-notice-plus-seven |
Close |
#721c24 |
variables.scss (--color-error), flash.scss | --op-color-alerts-danger-minus-seven |
Close |
#f5c6cb |
variables.scss (--color-error-light), flash.scss | --op-color-alerts-danger-plus-five |
Close |
#f8d7da |
variables.scss (--color-error-lighter), flash.scss | --op-color-alerts-danger-plus-seven |
Close |
#856404 |
variables.scss (--color-warning), flash.scss | --op-color-alerts-warning-minus-five |
Close |
#fff3cd |
variables.scss (--color-warning-lighter) | --op-color-alerts-warning-plus-seven |
Close |
#ffeeba |
variables.scss (--color-warning-light), flash.scss | --op-color-alerts-warning-plus-five |
Close |
#f9f9f9 |
variables.scss (--color-contrast-lower) | --op-color-neutral-plus-eight |
Close |
#ebebeb |
variables.scss (--color-contrast-low) | --op-color-neutral-plus-seven |
Close |
#e0e0e0 |
variables.scss (--color-contrast-medium) | --op-color-neutral-plus-five |
Close |
#b3b3b3 |
variables.scss (--color-contrast-medium-high) | --op-color-neutral-plus-two |
Miss |
#5e6168 |
variables.scss (--color-contrast-high) | --op-color-neutral-minus-five |
Close |
#ccc |
variables.scss (--input-border-color) | --op-color-neutral-plus-four |
Close |
#232323 |
action-bar.scss (box-shadow) | Use rgba(0,0,0,0.3) or var(--shadow) |
Miss |
#003dff |
form.scss (radio/checkbox active) | --op-color-primary-base |
Close |
| Value | Location | Token | Fit |
|---|---|---|---|
2px | DocumentsDownloadModal, PropertiesPanel, DrawingEditorActionBar, StairAngleControl | --op-radius-small | Close |
3px | flash.scss, StairAngleModal, form.scss | --op-radius-medium (4px) | Close |
50% | DocumentsDownloadModal (circular elements) | --op-radius-circle | Exact |
8px | form.scss:251 | --op-radius-large | Close |
0.2rem | form.scss (plain input) | --op-radius-small | Close |
| ID | Title | Section | Severity | WCAG |
|---|---|---|---|---|
| C1 | Form inputs remove outline with only border-color as substitute | Accessibility | Critical | 2.4.7, 2.4.11 |
| C2 | Confirm dialog missing role, aria-modal, focus trap | Accessibility | Critical | 1.3.1, 4.1.2 |
| C3 | Modal wrapper sets outline: 0 without replacement | Accessibility | Critical | 2.4.7 |
| H1 | Hamburger menu button missing aria-expanded | Navigation | High | 4.1.2 |
| H2 | No active/current state on navigation items | Navigation | High | 2.4.8 |
| H3 | No <main> landmark in application layout | Navigation | High | 2.4.1 |
| H4 | No skip navigation link | Navigation | High | 2.4.1 |
| H5 | Project list table has 9 columns — exceeds Miller's Law | Cognitive Load | High | — |
| H6 | Project load shows blank screen | Key Flows | High | — |
| H7 | Reset Canvas has no confirmation dialog | Key Flows | High | — |
| H8 | Flash messages auto-dismiss with no dismiss button | Feedback | High | — |
| H9 | Flash messages not announced to screen readers | Feedback | High | 4.1.3 |
| H10 | MoreMenu button missing aria-expanded | Accessibility | High | 4.1.2 |
| H11 | Flash messages not announced to screen readers (accessibility section) | Accessibility | High | 4.1.3 |
| H12 | Three-token-system migration (strategic) | Strategic | High | — |
| H13 | Loading state gap — top user-facing fix (strategic) | Strategic | High | — |
| H14 | Consolidate Menu and MoreMenu (strategic) | Strategic | High | — |
| M1 | Three overlapping custom property systems | Visual Coherence | Medium | — |
| M2 | Flash styles use hardcoded Bootstrap hex colors | Visual Coherence | Medium | — |
| M3 | Inter font imported but never used | Visual Coherence | Medium | — |
| M4 | Unresolved TODO in base styles | Visual Coherence | Medium | — |
| M5 | Org settings form lacks visual hierarchy | Cognitive Load | Medium | — |
| M6 | Non-standard autocomplete values on profile fields | Key Flows | Medium | — |
| M7 | Error state for project load provides no guidance | Feedback | Medium | — |
| M8 | Action bar dropdown uses hardcoded box-shadow color | Consistency | Medium | — |
| M9 | Section labels in forms use <p> instead of <h3> | Consistency | Medium | 1.3.1 |
| M10 | Autocomplete values are non-standard | Accessibility | Medium | — |
| M11 | Form section labels use <p> not headings (accessibility) | Accessibility | Medium | 1.3.1 |
| M12 | Flash message overflows small screens | Mobile | Medium | — |
| M13 | Only 2 @media queries — auth pages have no responsive adaptation | Mobile | Medium | — |
| M14 | Unused Inter font adds network overhead | Performance | Medium | — |
| M15 | Roboto imported from Google Fonts unnecessarily | Performance | Medium | — |
| P1 | Two diverging hamburger menu implementations | Consistency | Pattern | — |
| P2 | Reset Canvas bypasses confirmation pattern | Consistency | Pattern | — |