COmpoeentes standardization 2

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-02 00:27:27 +00:00
parent a4691ad2da
commit f28f641fd5
50 changed files with 622 additions and 490 deletions

View File

@@ -15,6 +15,7 @@
* 2. no-raw-input - Use <InputField> from components/form/input/InputField
* 3. no-raw-select - Use <Select> from components/form/Select
* 4. no-raw-textarea - Use <TextArea> from components/form/input/TextArea
* 5. no-icon-children - Use startIcon/endIcon props instead of icon children in Button
*
* USAGE: Import in eslint.config.js and enable rules
*/
@@ -226,5 +227,144 @@ module.exports = {
};
},
},
/**
* Disallow icon components as direct children of Button, ButtonGroupItem
* Icons must be passed via startIcon or endIcon props to ensure proper horizontal layout.
* When icons are children, they get wrapped in spans causing vertical stacking.
*
* WRONG: <Button><PlusIcon />Add Item</Button> -- icon appears ABOVE text
* RIGHT: <Button startIcon={<PlusIcon />}>Add Item</Button> -- icon appears LEFT of text
*/
'no-icon-children': {
meta: {
type: 'problem',
docs: {
description: 'Disallow icon components as children of Button. Use startIcon/endIcon props instead.',
recommended: true,
},
messages: {
useIconProps: 'Icon "{{iconName}}" should be passed via startIcon or endIcon prop, not as a child. Icons as children cause vertical stacking. Use: <Button startIcon={<{{iconName}} />}>Text</Button>',
useIconPropsGeneric: 'Icons should be passed via startIcon or endIcon prop, not as children. Icons as children cause vertical stacking.',
},
schema: [],
},
create(context) {
// Components that support startIcon/endIcon props
const buttonComponents = ['Button', 'ButtonGroupItem'];
// Pattern to detect icon component names (ends with Icon or is a known icon)
const iconPatterns = [
/Icon$/, // Any component ending in "Icon" (PlusIcon, RefreshCwIcon, etc.)
/^Icon[A-Z]/, // Icon prefix pattern
];
function isIconComponent(name) {
return iconPatterns.some(pattern => pattern.test(name));
}
return {
JSXElement(node) {
const openingElement = node.openingElement;
// Check if this is a Button or ButtonGroupItem
if (openingElement.name.type !== 'JSXIdentifier') {
return;
}
const componentName = openingElement.name.name;
if (!buttonComponents.includes(componentName)) {
return;
}
// Check if startIcon or endIcon props are already provided
const hasIconProp = openingElement.attributes.some(attr => {
return attr.type === 'JSXAttribute' &&
attr.name &&
(attr.name.name === 'startIcon' || attr.name.name === 'endIcon' || attr.name.name === 'icon');
});
// If icon props are present, likely the component is correctly configured
// Still check children for additional icons
// Check children for icon components
const children = node.children || [];
for (const child of children) {
// Direct JSX element child
if (child.type === 'JSXElement') {
const childName = child.openingElement.name;
if (childName.type === 'JSXIdentifier' && isIconComponent(childName.name)) {
context.report({
node: child,
messageId: 'useIconProps',
data: { iconName: childName.name },
});
}
}
// JSX expression containing icon (e.g., {loading ? <Spinner /> : <PlusIcon />})
if (child.type === 'JSXExpressionContainer') {
const expression = child.expression;
// Direct icon in expression: {<PlusIcon />}
if (expression.type === 'JSXElement') {
const exprName = expression.openingElement.name;
if (exprName.type === 'JSXIdentifier' && isIconComponent(exprName.name)) {
context.report({
node: expression,
messageId: 'useIconProps',
data: { iconName: exprName.name },
});
}
}
// Conditional expression: {condition ? <IconA /> : <IconB />}
if (expression.type === 'ConditionalExpression') {
const consequent = expression.consequent;
const alternate = expression.alternate;
if (consequent.type === 'JSXElement') {
const consName = consequent.openingElement.name;
if (consName.type === 'JSXIdentifier' && isIconComponent(consName.name)) {
context.report({
node: consequent,
messageId: 'useIconProps',
data: { iconName: consName.name },
});
}
}
if (alternate.type === 'JSXElement') {
const altName = alternate.openingElement.name;
if (altName.type === 'JSXIdentifier' && isIconComponent(altName.name)) {
context.report({
node: alternate,
messageId: 'useIconProps',
data: { iconName: altName.name },
});
}
}
}
// Logical expression: {showIcon && <PlusIcon />}
if (expression.type === 'LogicalExpression') {
const right = expression.right;
if (right.type === 'JSXElement') {
const rightName = right.openingElement.name;
if (rightName.type === 'JSXIdentifier' && isIconComponent(rightName.name)) {
context.report({
node: right,
messageId: 'useIconProps',
data: { iconName: rightName.name },
});
}
}
}
}
}
},
};
},
},
},
};