Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | 4x 2x 2x 2x 2x 2x 5x 5x 1x 5x 2x 2x 1x 2x 1x 2x 4x 11x 11x 4x 7x 7x 7x 8x 7x 7x 7x 7x 7x 7x 7x 7x 5x 2x 2x 2x 2x | /**
* Compliance row: no per-modifier or out-of-carveout per-state hook names.
*
* The narrow state-hook carveout — `-hover`/`-focus`/`-active`/`-disabled`
* on the three color categories — is recognized as canonical and does NOT
* trip this check. Any other state descriptor (`visited`, `selected`,
* `focus-visible`, etc.) or any modifier-flavored segment (`-brand`,
* `-large`, etc.) is an invention that must be reassigned under a
* pseudo-class / modifier selector in the theme file instead.
*/
import type { ComplianceCheck, ComplianceRow } from '../types.js';
import { classifyHook } from './helpers/classifyHook.js';
import {
hookDefsFromThemeData,
sampleHooks,
NOT_MIGRATED_DETAIL,
type HookDefRow,
} from './helpers/themeDataAccess.js';
const LABEL = 'No per-modifier or out-of-carveout per-state hook names';
function fixForInvented(c: {
hook: string;
classification: string;
category: string;
element: string | null;
}): string {
const elementSegment = c.element ? `${c.element}-` : '';
const canonical = `--slds-c-${elementSegment}color-${c.category}`;
const reassignTarget =
c.classification === 'invented-state'
? 'the matching pseudo-class selector'
: 'the `.<modifier>` selector';
return `Delete \`${c.hook}\` and reassign the canonical hook (e.g. \`${canonical}\`) under ${reassignTarget} in the theme file.`;
}
function locationOf(def: HookDefRow | undefined): string | null {
Eif (!def?.source) return null;
return def.line ? `${def.source}:${def.line}` : def.source;
}
function buildPassDetail(canonicalStateCount: number): string {
const base =
'No invented state or modifier hook names found — styles for other states/modifiers are expected to live under their pseudo-class or modifier selectors in the theme files.';
if (canonicalStateCount === 0) return base;
const verb = canonicalStateCount === 1 ? 'stays' : 'stay';
return `${base} ${canonicalStateCount} state hook${canonicalStateCount === 1 ? ' ' : 's '}${verb} within the RFC carveout (\`-hover\`, \`-focus\`, \`-active\`, \`-disabled\`).`;
}
function buildFailParts(inventedStateCount: number, inventedModifierCount: number): string[] {
const parts: string[] = [];
if (inventedStateCount > 0) {
parts.push(
`${inventedStateCount} state hook${inventedStateCount === 1 ? '' : 's'} outside the \`-hover\`/\`-focus\`/\`-active\`/\`-disabled\` carveout (e.g. \`visited\`, \`selected\`, \`focus-visible\`) — move the override under the matching pseudo-class selector in the theme file instead of inventing a new hook name.`,
);
}
if (inventedModifierCount > 0) {
parts.push(
`${inventedModifierCount} modifier-flavored hook${inventedModifierCount === 1 ? '' : 's'} (e.g. \`-brand\`, \`-large\`) — move the override under the \`.modifier\` selector in the theme file instead of inventing a new hook name.`,
);
}
return parts;
}
export const noInventedStateModifierHooks: ComplianceCheck = (input): ComplianceRow => {
const { componentName: name, themeData } = input;
if (!themeData) {
return {
id: 'no-invented-state-modifier-hooks',
label: LABEL,
status: 'info',
count: 0,
detail: NOT_MIGRATED_DETAIL,
};
}
const defs = hookDefsFromThemeData(themeData);
const firstDefByHook = new Map<string, HookDefRow>();
for (const def of defs) {
if (!firstDefByHook.has(def.prop)) firstDefByHook.set(def.prop, def);
}
const uniqueHooksDefined = [...firstDefByHook.keys()];
const classified = uniqueHooksDefined.map((h) => ({ hook: h, ...classifyHook(h, name) }));
const colorHooks = classified.filter((c): c is typeof c & { isColor: true } => c.isColor);
const canonicalState = colorHooks.filter((c) => c.classification === 'canonical-state');
const inventedState = colorHooks.filter((c) => c.classification === 'invented-state');
const inventedModifier = colorHooks.filter((c) => c.classification === 'invented-modifier');
const invented = [...inventedState, ...inventedModifier];
if (invented.length === 0) {
return {
id: 'no-invented-state-modifier-hooks',
label: LABEL,
status: 'pass',
count: 0,
detail: buildPassDetail(canonicalState.length),
};
}
const parts = buildFailParts(inventedState.length, inventedModifier.length);
return {
id: 'no-invented-state-modifier-hooks',
label: LABEL,
status: 'fail',
count: invented.length,
detail: `${invented.length} invented hook name${invented.length === 1 ? '' : 's'} break the state/modifier convention: ${sampleHooks(invented.map((c) => c.hook))}. ${parts.join(' ')}`,
offenders: invented.map((c) => ({
hook: c.hook,
location: locationOf(firstDefByHook.get(c.hook)) ?? undefined,
fix: fixForInvented(c),
})),
};
};
|