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
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
recommendedand 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 themeableis a reviewable statement. A file-level disable is not.
Not this
- Do not use
no-status-without-sourceas a reason to compute status in the view. Move the computation to where the thresholds live, not to auseMemo. - Do not add rules that require type information without saying so.
valid-compositionneeds the generated composition data; a project that has not rungeneratewill see it silently pass. - Do not lint generated output. Exclude
.source/,lib/generated/,registry/__index__.tsandpublic/r/.
Gotchas
- A colour in a variable is invisible.
const c = "#e11d48"used in aclassNamecannot be caught without type-aware analysis; the runtime warning is the backstop. - Arbitrary values need parsing, not matching.
bg-[var(--opsin-surface)]is correct andbg-[#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-axescannot see a spread.{...props}carrying both defeats it, which is one more reason to name props explicitly.
Related
- 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.
Tooling
Two lint plugins that turn the system's most important rules from advice into build failures. A rule enforced only by review is a rule enforced only sometimes.
Stylelint plugin
The proposed CSS rule set catches hardcoded colours, tier violations, and a status token applied to a category surface.