npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@airframeui/eslint-plugin

v0.5.0

Published

Airframe UI ESLint plugin — catch Tailwind/Bootstrap drift, unknown af-* classes, and redundant defaults in JSX and Angular templates

Readme

@airframeui/eslint-plugin

ESLint rules that keep JSX and Angular templates on Airframe: leftover Tailwind/Bootstrap utilities, unknown af-* names, redundant defaults, structure classes, and field-error ARIA.

String literals on class / className in .js / .jsx / .ts / .tsx, plus Angular class="…", [class]="'…'", [class.token], and [ngClass] string/map keys in .html (and inline template: via angular-eslint processInlineTemplates). It does not parse Vue bindings or class:list. For those hosts, use MCP validate_markup (or pretty-print @airframeui/core/catalog when MCP is off).

Same version as @airframeui/core. Docs: airframeui.com/docs/eslint-plugin · Changelog

Installation

npm install -D @airframeui/eslint-plugin

Requires ESLint 9+ (flat config) or ESLint 8.57+ with eslintrc. Core and tokens are optional peers — if they are installed, their catalogs win so lint matches the CSS you ship.

Angular templates need @angular-eslint/template-parser (Angular 20+, typically via angular-eslint) on **/*.html. The plugin does not parse HTML as JavaScript.

Setup

Flat config (ESLint 9+)

// eslint.config.js
import airframe from '@airframeui/eslint-plugin';

export default [airframe.configs['flat/recommended']];

Drop airframe.configs['flat/recommended'] into an existing export default [ ... ] array. It enables JSX parsing and matches .js, .jsx, .ts, and .tsx (ESLint does not lint .jsx / .ts unless files says so).

Angular templates (v20+)

Use alongside angular-eslint. flat/angular turns the Airframe rules on for **/*.html; it does not set a parser — templateRecommended (or languageOptions.parser: angular.templateParser) must already apply.

// eslint.config.js
import airframe from '@airframeui/eslint-plugin';
import angular from 'angular-eslint';

export default [
  ...airframe.configs['flat/recommended'],
  {
    files: ['**/*.ts'],
    processor: angular.processInlineTemplates,
  },
  ...angular.configs.templateRecommended,
  ...airframe.configs['flat/angular'],
];

processInlineTemplates extracts @Component({ template: \…` })so the same rules run on inline templates. External*.htmlfiles are covered byflat/angular`.

Override a rule after the preset:

export default [
  airframe.configs['flat/recommended'],
  {
    rules: {
      '@airframeui/no-tailwind-classes': 'off',
    },
  },
];

Legacy eslintrc

// .eslintrc.cjs
module.exports = {
  plugins: ['@airframeui'],
  extends: ['plugin:@airframeui/recommended'],
};

Rules

All seven rules ship in recommended (JSX) and flat/angular (templates). They inspect string literals on class / className in JSX, and Angular class, [class]="'…'", [class.token], and [ngClass] string/map keys. Existing af-* tokens are ignored by the Tailwind and Bootstrap detectors.

| Rule | Recommended | Autofix | What it does | | -------------------------------------------- | ----------- | ------- | -------------------------------------------------------------------------------------------------------------------- | | @airframeui/no-bootstrap-classes | warn | no | Leftover Bootstrap tokens (btn, row, col-*, d-flex, …) | | @airframeui/no-redundant-defaults | warn | yes | Restated defaults: af-card af-stack, af-btn af-is-primary, af-container af-container-xl, matching af-text-h* | | @airframeui/no-redundant-width | warn | yes | af-w-full on form controls; af-block on af-btn | | @airframeui/no-tailwind-classes | warn | no | Leftover Tailwind utilities (flex, gap-4, md:gap-4, bg-blue-600, …) | | @airframeui/no-unknown-airframe-class | error | yes* | Invented af-* names. Autofix for known aliases and catalog heuristics | | @airframeui/prefer-airframe-layout-recipes | warn | yes | Rewrites flex flex-col, grid grid-cols-*, row, container, … | | @airframeui/require-field-error-aria | error | no | af-field__error needs id; error inputs need aria-describedby |

Autofix

Rules with a fix rewrite the class / className string (quotes stay; an empty class attribute is removed). The same rewrite applies to static class="…" in Angular templates and to [class]="'…'" string literals. [class.token] and [ngClass] object maps are reported without a rewrite.

npx eslint . --fix

Same command as the editor Fix all auto-fixable problems action.

Autofix only runs when the replacement is deterministic: known aliases, catalog heuristics, structure classes, redundant defaults, and redundant width. Unknown names with only a fuzzy guess, leftover Tailwind/Bootstrap that is not a structure rewrite, and field-error ARIA are reported without a fix.

Rewrites

Structure classes

@airframeui/prefer-airframe-layout-recipes — skipped if the attribute already has af-stack, af-inline, af-grid, af-cluster, af-spread, af-center, or af-container.

Gap numbers map to the Airframe named scale (0 · xs · sm · md · lg · xl · 2xl · 3xl · 4xl). Default md (gap-3 / g-3) is omitted because af-stack / af-grid already use it.

| Before | After | | ----------------------------------------- | ----------------------------------------- | | flex flex-col / flex flex-col gap-4 | af-stack / af-stack af-gap-lg | | flex flex-wrap / flex flex-wrap gap-2 | af-inline / af-inline af-gap-sm | | grid / grid grid-cols-3 gap-6 | af-grid / af-grid af-grid-3 af-gap-xl | | d-flex flex-column | af-stack | | d-flex flex-wrap | af-inline | | d-grid / row | af-grid | | container / container-fluid | af-container |

Unknown af-* names

@airframeui/no-unknown-airframe-class — prefers the project's @airframeui/core/classes.json, then the snapshot bundled at plugin build time.

| Invented | Autofix | | ----------------------------------------------------------------------------- | -------------------------------------------- | | af-btn-primary | af-btn | | af-btn-secondary / af-btn-danger | af-btn af-is-secondary / af-is-danger | | af-btn-destructive | af-btn af-is-danger | | af-btn-block / af-is-block / af-is-full | af-w-full | | af-{pattern}-{variant} when both af-{pattern} and af-is-{variant} exist | af-{pattern} af-is-{variant} | | Anything else unknown | Message may suggest a close name; no autofix |

Do not invent af-* names. Use catalog classes (af-btn af-is-danger, not af-btn-danger).

Redundant markup

| Before | After | Rule | | ------------------------------------------ | ---------------------- | ----------------------- | | af-btn af-is-primary | af-btn | no-redundant-defaults | | af-card af-stack af-gap-md | af-card | no-redundant-defaults | | af-grid af-gap-md / af-stack af-gap-md | af-grid / af-stack | no-redundant-defaults | | af-container af-container-xl | af-container-xl | no-redundant-defaults | | <h1 class="af-text-h1"> | <h1> | no-redundant-defaults | | af-input af-w-full | af-input | no-redundant-width | | af-btn af-block | af-btn af-w-full | no-redundant-width |

Kept: af-card af-gap-lg, af-card af-inline, af-container af-container-lg@xl, <h2 class="af-text-h1">, af-btn af-w-full, af-input af-is-autosize af-w-full.

Form controls that already span full width: af-input, af-textarea, af-select, af-range, af-progress, af-meter, af-input-group, af-field__control.

Field-error ARIA

No autofix — you must choose the id.

| Markup | Required | | -------------------------------------------------------- | --------------------------------------------- | | class contains af-field__error | id on that element (for aria-describedby) | | class contains af-is-error, or aria-invalid="true" | aria-describedby on that element |

<label className="af-field">
  <span className="af-field__label">Email</span>
  <input className="af-input af-is-error" aria-describedby="email-error" />
  <p className="af-field__error" id="email-error">
    Enter a valid email.
  </p>
</label>

For coding agents

Enable recommended, then treat lint as the source of truth for af-* markup:

write class / className (and Angular class / [class] / [ngClass]) → npx eslint . --fix → fix remaining reports by hand
  • Apply --fix first. Do not invent a different af-* name when the message already suggests one.
  • Map leftover Tailwind/Bootstrap layout combos with the tables above. Do not leave flex flex-col next to af-stack.
  • For field-error ARIA, add a real id and matching aria-describedby. There is no autofix.
  • Do not wrap classes in template literals, clsx, or class:list to dodge the rules — those forms are not checked yet, and the markup is still wrong.

Current limitations

  • JSX and Angular templates. String literals on class / className in .js / .jsx / .ts / .tsx. Angular *.html (v20+ control flow included) with @angular-eslint/template-parser: class="…", [class]="'…'", [class.token], [ngClass] strings and map keys. Not Vue/Svelte bindings, class:list, template literals, Astro expression attributes, or [class]="variable". For other HTML hosts, use MCP validate_markup.
  • @airframeui/no-unknown-airframe-class prefers the project's @airframeui/core/classes.json, then the snapshot generated from core at plugin build time (same classes.json + token catalog as IntelliSense for Airframe).
  • Tailwind/Bootstrap detectors ignore existing af-* tokens so af-grid / af-btn are not flagged.

Related

License

MIT