# Hone UI ESLint plugin

Use to catch wrong Hone UI usage while you lint: invalid prop values, missing accessible names, system props that do nothing, fixed `sx` colors, a broken `renderInput` and `sx` colors that ignore dark mode. The rules read the props manifest of the `@hone-ui/react` the app has installed, so they match its version. The package is not published yet.

## Setup

```js
// eslint.config.mjs
import tseslint from 'typescript-eslint';
import hone from '@hone-ui/eslint-plugin';

export default [
  {
    files: ['**/*.{ts,tsx}'],
    languageOptions: { parser: tseslint.parser },
  },
  hone.configs.recommended,
];
```

Install it with ESLint and the parser package that the config imports: `npm i -D eslint typescript-eslint @hone-ui/eslint-plugin`, or `pnpm add -D eslint typescript-eslint @hone-ui/eslint-plugin`. Without `typescript-eslint`, the `import tseslint` line fails. The plugin needs ESLint 9 or later and a parser that reads JSX. For plain JavaScript, set `languageOptions.parserOptions.ecmaFeatures.jsx` to `true`. `hone.configs.recommended` turns on six rules as `error` and `sx-hardcoded-color` as `warn`.

## Rules

```txt
@hone-ui/valid-prop-value
  A literal prop value outside what the component accepts. Closed string unions, and number
  props with min or max. Suggestion: replace the string with the closest valid value.
@hone-ui/icon-button-label
  IconButton without aria-label or aria-labelledby. A Tooltip parent with a text title counts.
  No fix: write the label.
@hone-ui/field-label
  TextField, Select, Slider, Checkbox, Radio, Switch, Tabs, Dialog or the input an Autocomplete
  renders, with no accessible name. A <label htmlFor> with a matching literal id counts, and so
  does a FormControlLabel around a toggle or a DialogTitle in a Dialog. An aria-label on a
  Checkbox, Radio or Switch lands on the root, not the input, and is reported: put it in
  slotProps.input or inputProps. No fix: write the label.
@hone-ui/no-system-props
  A system prop (fontWeight, p, display, ...) on Box, Stack or Typography. They do nothing.
  No fix: move the value into sx.
@hone-ui/sx-hardcoded-color
  A literal hex, rgb() or hsl() color in sx. No fix: use a palette key or an --hn-* variable.
@hone-ui/render-input-params
  Autocomplete renderInput that never uses its params. Suggestion for a TextField:
  insert {...params}.
@hone-ui/sx-scheme-function
  theme.palette inside an sx function. Suggestion: theme.vars.palette, which follows dark mode.
```

## Examples

```tsx
<Button size="huge">Save</Button>  // valid-prop-value, suggests size="large"
<Button size="large">Save</Button>

<IconButton><DeleteIcon /></IconButton>  // icon-button-label
<IconButton aria-label="Delete"><DeleteIcon /></IconButton>

<TextField />  // field-label
<TextField label="Email" />

<Switch />  // field-label
<FormControlLabel control={<Switch />} label="Wi-Fi" />

<Typography fontWeight={600}>Hi</Typography>  // no-system-props
<Typography sx={{ fontWeight: 600 }}>Hi</Typography>

<Box sx={{ color: '#ff0000' }} />  // sx-hardcoded-color
<Box sx={{ color: 'error.main' }} />

<Autocomplete options={options} renderInput={(params) => <TextField label="City" />} />  // render-input-params
<Autocomplete options={options} renderInput={(params) => <TextField {...params} label="City" />} />

<Box sx={(theme) => ({ color: theme.palette.primary.main })} />  // sx-scheme-function
<Box sx={(theme) => ({ color: theme.vars.palette.primary.main })} />
```

`Select` takes its name from `labelId`, `aria-label` or `aria-labelledby`. Its `label` prop only cuts the notch in the outline, so it does not count. `Slider` takes `aria-label`, `aria-labelledby` or `getAriaLabel`. `Tabs` takes `aria-label` or `aria-labelledby`.

## How rules find the library

A rule reads `node_modules/@hone-ui/react/dist/editor-manifest.json`. It starts in the linted file's folder and walks up to the nearest `node_modules/@hone-ui/react`. In a monorepo, each app is checked against its own installed version. A rule tracks only components imported from `@hone-ui/react`, as a named import, a namespace import or a default import from a subpath such as `@hone-ui/react/Button`. Renamed imports work. Type-only imports, local components and names that an inner scope shadows are ignored. A file with no absolute path, as with `eslint --stdin`, starts from ESLint's working folder. The manifest is cached by modification time, so an upgrade needs no restart.

If no `@hone-ui/react` is installed, or its manifest is missing, `valid-prop-value` reports nothing. The label rules do not need the manifest.

## What the rules skip

The rules report only what they can read. They stay quiet when they cannot know.

- Any element with a spread attribute, such as `<IconButton {...props} />`. The spread may carry the label or the value.
- Values that are not literals: `size={size}`, a template string with an expression, a conditional.
- Open unions, such as `color`, which accepts any palette key, and props typed as a plain string.
- A Checkbox, Radio or Switch passed as a prop to another component, such as `<Row control={<Switch />} />`, and a Dialog with a child that is not a Hone UI component or an expression such as `{children}`. Those may supply the name.
- Named colors such as `white`, and `var(--hn-*)` values.
- An object prop that is a variable or holds a spread, such as `inputProps={{ ...p }}`. It may carry the name.
- A `renderInput` that is not an inline function, has a destructured first parameter, or returns something other than JSX. Any read of the parameter in the function counts as using it.
- A `renderInput` element that carries its own spread besides the params.
- A `theme` parameter that is destructured, and `theme.palette.mode`, `getContrastText`, `augmentColor`, `tonalOffset` and `contrastThreshold`, which `theme.vars.palette` does not have.

## Don't

- Don't silence `icon-button-label` or `field-label` with a placeholder label such as `aria-label=""`. An empty value does not count as a name.
- Don't add `// eslint-disable` to pass `valid-prop-value`. Run `get_props` from `@hone-ui/mcp`, or read the component guide, for the allowed values.
