onyx-dot-app/onyx
Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.
30 files reviewed·July 9, 2026
More findings
Verdict
Every screen here has a sound structural skeleton, loading, error, and success states all exist, but the moments users actually notice get abandoned: dead-end errors, unlabeled spinners, and a destructive reset button with zero confirmation. The riskiest gap is that danger-styled button submitting instantly, paired with error text that screen readers never hear at all.
Files Rams reviewed
web/src/app/admin/connectors/[connector]/pages/gdrive/GoogleDrivePage.tsx
web/src/app/admin/connectors/[connector]/pages/gmail/GmailPage.tsx
web/src/app/admin/connectors/[connector]/pages/Advanced.tsx
web/src/app/admin/connectors/[connector]/pages/ConnectorInput/FileInput.tsx
web/src/app/admin/connectors/[connector]/pages/ConnectorInput/NumberInput.tsx
web/src/app/admin/connectors/[connector]/pages/ConnectorInput/SelectInput.tsx
web/src/app/admin/connectors/[connector]/pages/DynamicConnectorCreationForm.tsx
web/src/app/admin/connectors/[connector]/pages/FieldRendering.tsx
web/src/app/admin/connectors/[connector]/pages/gdrive/Credential.tsx
web/src/app/admin/connectors/[connector]/pages/gmail/Credential.tsx
web/src/app/app/components/AppPopup.tsx
web/src/app/app/components/WelcomeMessage.tsx
web/src/app/app/components/files/images/FullImageModal.tsx
web/src/app/app/components/files/images/InMessageImage.tsx
web/src/app/app/components/tools/GeneratingImageDisplay.tsx
web/src/app/components/nrf/SettingsPanel.tsx
web/src/app/craft/components/AgentSwitcher.tsx
web/src/app/craft/components/BigButton.tsx
web/src/app/craft/components/BuildMessageList.tsx
web/src/app/craft/components/BuildWelcome.tsx
web/src/app/craft/components/CometEdge.tsx
web/src/app/craft/components/CompactionMarker.tsx
web/src/app/craft/components/ConnectDataBanner.tsx
web/src/app/craft/components/ContextRing.tsx
web/src/app/craft/components/CraftInputBar.tsx
web/src/app/craft/components/CraftingLoader.tsx
web/src/app/craft/components/IntroBackground.tsx
web/src/app/craft/components/IntroContent.tsx
web/src/app/craft/components/ModelPickerButton.tsx
web/src/app/craft/components/SandboxAsleepNotice.tsx
Accessibility
Label's htmlFor points to an id the select never receives
The `<label htmlFor={name}>` targets an id, but Formik's `<Field as="select" name={name} ...>` only wires up `name`, `value`, `onChange`, and `onBlur`: it does not set `id` unless passed explicitly. The rendered `<select>` has no `id={name}`, so the label-to-control association never forms.
Why it matters
Screen readers won't announce the label when the select receives focus, and clicking the label text won't move focus into the control. Every instance of this component ships an unlabeled form field.
Fix
Pass an explicit id to Field that matches the label's htmlFor.
<Field
as="select"
name={name}
className="w-full p-2 border border-border-03 rounded-08 bg-transparent text-text-04 focus:ring-2 focus:ring-lighter-agent focus:border-lighter-agent focus:outline-hidden"
><Field
as="select"
id={name}
name={name}
className="w-full p-2 border border-border-03 rounded-08 bg-transparent text-text-04 focus:ring-2 focus:ring-lighter-agent focus:border-lighter-agent focus:outline-hidden"
>Retry button in the fix above needs an explicit type
The suggested `<button onClick={handleRefresh}>Try again</button>` inside `ErrorCallout` has no `type` attribute, so it defaults to `type="submit"`.
Why it matters
If this button ever renders inside a `<form>`, clicking 'Try again' submits the form unintentionally, causing unrelated side effects the user didn't ask for.
Fix
Set type='button' on any button that isn't intentionally submitting a form.
<button onClick={handleRefresh}>Try again</button><button type="button" onClick={handleRefresh}>Try again</button>Validation error appears with no live region so screen readers never announce it
When `meta.touched && meta.error` becomes true, `{meta.error}` is inserted into a plain `<div>` with no `role="alert"` or `aria-live`.
Why it matters
Sighted users see the red error text appear under the file field, but screen reader users get no announcement that validation failed, so they submit again with no idea what went wrong.
Fix
Add role="alert" (or aria-live="polite") to the conditionally rendered error so assistive tech announces it when it mounts.
<div className="text-red-500 text-sm mt-1">{meta.error}</div><div role="alert" className="text-error-500 text-sm mt-1">{meta.error}</div>Error text below the input has no programmatic link to it for screen readers
The `<input>` has no `id` or `aria-describedby`, and the `<ErrorMessage>` div rendered below it has no matching `id`. The two elements are only related visually by proximity.
Why it matters
Screen reader users focusing the number input never hear the associated error text, so they get no explanation of why the value is invalid or what to fix, unlike sighted users who see the pink border and error line together.
Fix
Give the input an id and point `aria-describedby` at a matching id on the error element so assistive tech announces the error with the field.
<input
{...field}
type="number"
min="-1"
onChange={handleChange}
value={
field.value === undefined || field.value === null ? "" : field.value
}<input
{...field}
id={name}
type="number"
min="-1"
onChange={handleChange}
aria-describedby={meta.error ? `${name}-error` : undefined}
value={
field.value === undefined || field.value === null ? "" : field.value
}UX
Danger-styled Reset button submits instantly with no confirmation step
The `Button variant="danger"` labeled "Reset" has `type="submit"` and no confirmation dialog or intercepting handler. Clicking it fires the form submit directly.
Why it matters
A destructive, danger-styled action wired straight to form submission means one accidental click triggers whatever the reset does (likely clearing pruning/refresh config) with no way to back out.
Fix
Wrap destructive submits in a confirmation step before the mutating action runs.
<Button variant="danger" icon={SvgTrash} type="submit">
Reset
</Button><Button
variant="danger"
icon={SvgTrash}
type="button"
onClick={() => {
if (window.confirm("Reset all advanced configuration for this connector?")) {
formikSubmitReset();
}
}}
>
Reset
</Button>Failed credential loads dead-end users with no retry option
Both error branches render `<ErrorCallout errorTitle="Failed to load credentials." />` and `<ErrorCallout errorTitle="Failed to load Google Drive credentials." />` with no action attached. The component already has `refreshCredentials` in scope from `usePublicCredentials`, but neither error state offers it.
Why it matters
A user who hits either error state has no way to recover except a full page reload, so setup friction compounds right at the connector configuration entry point.
Fix
Pass a retry action into the error state so users can recover in place.
if (credentialsError || !credentialsData) {
return <ErrorCallout errorTitle="Failed to load credentials." />;
}if (credentialsError || !credentialsData) {
return (
<ErrorCallout
errorTitle="Failed to load credentials."
errorMsg={<button onClick={refreshCredentials} className="underline">Try again</button>}
/>
);
}Typography
Color
Spacing
Components
Motion
Craft
Working well
- The `isZip` and `multiple` props are consistently threaded into both `setSelectedFiles` (deciding array vs single value) and the `multiple`/`accept` props passed to `FileUpload`: the two places that need to agree on 'is this a single-zip upload' actually do agree, which avoids a common bug where the accept filter and the value-shape logic drift apart.
- Deriving `selectedFiles` with `Array.isArray(field.value) ? field.value : field.value ? [field.value] : []` handles the single-file, multi-file, and empty-value cases in one clear expression instead of scattering null checks through the render: good defensive normalization at the boundary between Formik's value shape and the FileUpload prop contract.
- Handling the two credential sources (`usePublicCredentials` and `useGoogleCredentials`) as separate loading and error checks before ever reaching the main render is the right structure: it guarantees `DriveAuthSection` only ever mounts once both dependencies are confirmed present, avoiding null-reference bugs downstream.
- The disabled and invalid states use visually distinct treatments (`disabled:bg-background-50` with muted text vs a bordered/colored invalid state) rather than a single dimmed look for both, so users can tell 'can't edit this' apart from 'fix this' at a glance.
Score your own repo with Rams.
Free on public repos. Rams reviews every pull request for accessibility, color, type, spacing, components, motion, UX, and craft, and posts inline fixes.