export type RawHtmlPrecisionTier = | "exact-html" | "html-advisory" | "input-type" | "role-reimpl" | "interactive-nonsemantic" | "icon-only"; export interface RawHtmlCanonicalMatch { canonical: string; tier: RawHtmlPrecisionTier; } const EXACT_HTML_CANONICALS = new Map([["button", "Button"]]); const ADVISORY_HTML_CANONICALS = new Map([ ["textarea", "Textarea"], ["select", "Select"], ["dialog", "Dialog"], ["progress", "Progress"], ["details", "Collapsible"], ["table", "Table"], ]); const INPUT_TYPE_CANONICALS = new Map([ ["checkbox", "Checkbox"], ["radio", "Radio"], ["number", "NumberInput"], ["password", "PasswordInput"], ["range", "Slider"], ]); const ROLE_CANONICALS = new Map([ ["button", "Button"], ["checkbox", "Checkbox"], ["radio", "Radio"], ["switch", "Switch"], ["tab", "Tabs"], ["tablist", "Tabs"], ["dialog", "Dialog"], ["menu", "Menu"], ["status", "Alert"], ]); export const RAW_HTML_CANONICAL_TAGS = Object.freeze( Array.from(EXACT_HTML_CANONICALS, ([tagName, canonical]) => ({ tagName, canonical, tier: "exact-html" as const, })) ); export const RAW_HTML_ADVISORY_TAGS = Object.freeze( Array.from(ADVISORY_HTML_CANONICALS, ([tagName, canonical]) => ({ tagName, canonical, tier: "html-advisory" as const, })) ); export const RAW_HTML_INPUT_TYPE_CANONICALS = Object.freeze( Array.from(INPUT_TYPE_CANONICALS, ([type, canonical]) => ({ type, canonical, tier: "input-type" as const, })) ); export const RAW_HTML_ROLE_CANONICALS = Object.freeze( Array.from(ROLE_CANONICALS, ([role, canonical]) => ({ role, canonical, tier: "role-reimpl" as const, })) ); export function resolveCanonicalForRawHtml( tagName: string, inputType?: string ): RawHtmlCanonicalMatch | null { const tag = tagName.toLowerCase(); if (tag === "input") { const normalizedType = inputType?.trim().toLowerCase(); if (!normalizedType) return null; const canonical = INPUT_TYPE_CANONICALS.get(normalizedType); return canonical ? { canonical, tier: "input-type" } : null; } const exactCanonical = EXACT_HTML_CANONICALS.get(tag); if (exactCanonical) return { canonical: exactCanonical, tier: "exact-html" }; const advisoryCanonical = ADVISORY_HTML_CANONICALS.get(tag); return advisoryCanonical ? { canonical: advisoryCanonical, tier: "html-advisory" } : null; } export function resolveCanonicalForAriaRole(role: string): RawHtmlCanonicalMatch | null { const canonical = ROLE_CANONICALS.get(role.trim().toLowerCase()); return canonical ? { canonical, tier: "role-reimpl" } : null; } export function isRawHtmlAdvisoryTier(tier: RawHtmlPrecisionTier): boolean { return ( tier === "html-advisory" || tier === "role-reimpl" || tier === "interactive-nonsemantic" || tier === "icon-only" ); } /** * Low-frequency, typically-1:1 semantic tags where a per-component inferred * `htmlEquivalent` mapping is safe even with a custom-named component * (`` → Modal). Mirrors `ADVISORY_HTML_CANONICALS` + the `input` family. * * Deliberately EXCLUDES generic structural containers (`div`, `span`) and the * ambiguous common tags (`button`, `a`): those are handled by the stricter * agreement check in `isEnforceableHtmlEquivalent`. */ const SEMANTIC_HTML_EQUIVALENT_TAGS = new Set([ "input", "textarea", "select", "dialog", "progress", "details", "table", ]); /** * Specific interactive tags that an EXPLICITLY-DECLARED project mapping may * enforce against ANY canonical component NAME — making the rule component- * library agnostic (so `@acme/ui`'s `AcmeButton` fires on a raw `