opsinjs
HandbookCorrectness and costTooling

ESLint plugin

The proposed rule set for JSX and TypeScript covers raw colours, mixed axes, banned words, and the component contracts a reviewer should not have to remember.

The short version

NOT IMPLEMENTED. This component does not exist in any released version of opsinjs. There is no package to install, no module to import and no props interface to generate code against. Everything on this page is a specification of intended behaviour and may change without notice. Do not write code against it.

This component is not built yet

There is nothing to render because there is nothing to install. What you can read on this page is the specification the implementation will have to satisfy.

PlannedRoadmapWhat “planned” means

eslint-plugin-opsinjs is specified but not built. It would ship a recommended config containing the rules below, all of which are decidable from the source.

// eslint.config.mjs is proposed and not yet publishable
import opsinjs from "eslint-plugin-opsinjs"

export default [opsinjs.configs.recommended]

How it works

Proposed rules

opsinjs/no-raw-color runs at error level. Flags a colour literal in a className, a style prop or a styled call: hex, rgb(), hsl(), a named CSS colour, and a Tailwind arbitrary value containing any of them. Suggests the nearest token where one is unambiguous. Why: a raw colour opts out of the theme, both modes, the contrast floor and the two axes in one keystroke.

opsinjs/no-mixed-axes runs at error level. Flags a component receiving both a status and a category where the component's documented contract says only one may change the surface, and flags a status token used in a category role or the reverse. Why: the two colour axes. This is the rule the plugin exists for.

opsinjs/no-status-without-source runs at warn level. Flags a literal status value written directly into JSX. A status should be derived from a value and a range, not typed by a developer choosing a colour. Why: a hardcoded status="urgent" is a clinical claim with nothing behind it.

opsinjs/no-banned-word runs at error level. Flags "normal", "abnormal", "good"/"bad" applied to a reading, and the other terms listed in Reference ranges and Writing status and alerts, when they appear in JSX text or in a prop that renders as user-visible copy. Why: these words make claims about a person that the product cannot support.

opsinjs/require-unit runs at warn level. Flags a numeric health value rendered without a unit, such as a Value with no unit prop or a bare number interpolated next to a metric label. Why: a number without a unit is an incomplete fact; see Numbers, units and precision.

opsinjs/no-motion-for-urgency runs at warn level. Flags an animation or transition class applied to an element that also carries a status, where the animation is one of the attention-seeking families (pulse, bounce, shake, ping). Why: Motion in health UI. Urgency carried by motion is invisible to a large group of readers and harmful to another.

opsinjs/valid-composition runs at error level. Flags wrong nesting of a compound component's parts, checked against the generated composition trees. Why: wrong nesting is the most common generation failure, by humans and by agents alike.

opsinjs/no-inline-threshold runs at warn level. Flags a numeric comparison against a health value in JSX, such as a ternary on value > 140 deciding a colour. Why: thresholds have a named owner and belong in one place, not scattered through render functions.

Do this

  • Enable recommended and treat the errors as errors. The four rules at error level are the ones with no legitimate exception.
  • Pair the plugin with check-theme. Lint catches the literal in the source; the theme check catches the token that was defined wrongly.
  • Disable narrowly. // eslint-disable-next-line opsinjs/no-raw-color -- brand logo, not themeable is a reviewable statement. A file-level disable is not.

Not this

  • Do not use no-status-without-source as a reason to compute status in the view. Move the computation to where the thresholds live, not to a useMemo.
  • Do not add rules that require type information without saying so. valid-composition needs the generated composition data; a project that has not run generate will see it silently pass.
  • Do not lint generated output. Exclude .source/, lib/generated/, registry/__index__.ts and public/r/.

Gotchas

  • A colour in a variable is invisible. const c = "#e11d48" used in a className cannot be caught without type-aware analysis; the runtime warning is the backstop.
  • Arbitrary values need parsing, not matching. bg-[var(--opsin-surface)] is correct and bg-[#fff] is not, and they differ only inside the brackets.
  • Banned-word detection produces false positives on documentation. A page explaining why "normal" is banned contains the word; the rule must exclude MDX and test fixtures.
  • no-mixed-axes cannot see a spread. {...props} carrying both defeats it, which is one more reason to name props explicitly.
  • Tooling explains why lint rules and not documentation.
  • Stylelint plugin has the same invariants in CSS.
  • Error codes covers the runtime layer, with aligned names.
  • Rules for agents has the same constraints expressed for a generator rather than a linter.

On this page