Linting Rules

Lint-enforced governance — form controls, focus rings, and the three-tier icon hierarchy.

Three pieces of governance are enforced by lint checks in pnpm lint (Biome v2 + custom scripts). New code that violates them fails CI; existing exceptions are documented below.

Form-control rule

Raw <input> / <textarea> / <select> are banned in apps/**. Use the primitives from @nebutra/ui/primitives:

import {
  Input,
  Textarea,
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
  Field,
  Checkbox,
  RadioGroup,
} from "@nebutra/ui/primitives";

<Field label="Email *" htmlFor="email">
  <Input id="email" type="email" name="email" required />
</Field>

<Field label="Plan *" htmlFor="plan">
  <Select name="plan" defaultValue="pro">
    <SelectTrigger id="plan">
      <SelectValue />
    </SelectTrigger>
    <SelectContent>
      <SelectItem value="pro">Pro</SelectItem>
      <SelectItem value="enterprise">Enterprise</SelectItem>
    </SelectContent>
  </Select>
</Field>

Native opt-out

Add data-allow-native for legitimate cases — hidden inputs, file inputs with a custom button trigger, filter selects that need empty-string "all" semantics:

<input data-allow-native type="hidden" name="orgId" value={orgId} />

<input data-allow-native type="file" ref={inputRef} className="sr-only" />

<select
  data-allow-native
  value={filters.outcome ?? ""}
  onChange={(e) => setOutcome(e.target.value || null)}
>
  <option value="">All</option>
  <option value="success">Success</option>
  <option value="failure">Failure</option>
</select>

Enforcement

  • Script: scripts/lint-no-raw-inputs.mjs (runs in pnpm lint)
  • Whitelist: Storybook stories, design-docs / sailor-docs previews, test files, all packages/**/primitives/**.

Focus-ring rule

Do NOT add component-level focus rings. The global :focus-visible rule in packages/design/design-tokens/static/base.css provides a translucent 2px outline (hsl(var(--ring) / 0.5)) with 2px offset to every focusable element. Keyboard users get the ring; mouse users don't.

// āœ… Correct — no focus classes needed
<button type="button" aria-label="Close dialog" className="rounded-md p-1">
  <X className="h-4 w-4" />
</button>

// āœ… Correct — input border-color change is OK for mouse focus feedback
<input className="rounded-md border border-neutral-7 focus:border-[hsl(var(--ring))] focus:outline-none" />

// āŒ Wrong — reintroduces hardcoded brand-blue ring at 100% saturation,
// double-rings with the global rule
<button className="focus:ring-[var(--blue-9)] focus:ring-offset-1">…</button>

Override only when a specific component genuinely needs a different ring (rare). Coordinate with the design system maintainers first.

Icon governance (three-tier)

Three icon libraries are allowed; each has exactly one slot:

TierPackageWhere to use
1 — default@nebutra/icons (Geist 541)All product / app / dashboard surfaces. Same visual as Vercel & v0.
2 — marketing@phosphor-icons/react/lightOnly inside packages/design/ui/src/marketing/** for thin / duotone AI-brand emphasis.
3 — deprecatedlucide-reactZero new imports allowed. Existing references were swept on 2026-05-14; further additions fail lint.
// āœ… Default (product surfaces)
import {
  MagnifyingGlass,
  SettingsGear,
  ChevronRight,
  Sparkles,
} from "@nebutra/icons";

// āœ… Marketing (thin / duotone for AI brand emphasis) — restricted folder
import { Brain } from "@phosphor-icons/react/dist/ssr";
// allowed ONLY inside packages/design/ui/src/marketing/**

// āŒ Banned in new code
import { Search } from "lucide-react";

Also banned: inline <svg> icon paths in product code — pick from tier 1 or 2 above. SVG math is still fine for data-viz primitives (e.g. Gauge).

Console rule

console.log is disallowed in production code. Use @nebutra/logger:

import { logger } from "@nebutra/logger";

logger.info({ tenantId }, "subscription.activated");

Biome allows console.warn, console.error, and console.assert for explicit warning / error paths.

How is this guide?

Edit on GitHub

Last updated on

On this page