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 inpnpm 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:
| Tier | Package | Where to use |
|---|---|---|
| 1 ā default | @nebutra/icons (Geist 541) | All product / app / dashboard surfaces. Same visual as Vercel & v0. |
| 2 ā marketing | @phosphor-icons/react/light | Only inside packages/design/ui/src/marketing/** for thin / duotone AI-brand emphasis. |
| 3 ā deprecated | lucide-react | Zero 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.
Related
How is this guide?
Last updated on