Adds a local ESLint plugin (web/eslint-local/) with three custom rules
implementing the preventive guardrails from docs/ui/UI_AUDIT.md §5:
- bytelyst-trading/truncate-needs-title
flags JSX elements using Tailwind 'truncate' / 'line-clamp-*' /
'text-ellipsis' without a paired title= or aria-label= (Pattern E)
- bytelyst-trading/grid-needs-minmax
flags gridTemplateColumns string values with bare Nfr tracks not
wrapped in minmax(0, ...). Catches both literal and template-string
forms; verifies *every* fr is wrapped, not just one (Pattern F)
- bytelyst-trading/no-button-with-stacked-children
flags <Button> from @bytelyst/ui wrapping 2+ block children. The
Button primitive applies whitespace-nowrap + fixed h-{size} which
collapses stacked content; recommends native <button class="card-button">
(Pattern A)
All wired into eslint.config.js as 'warn' (not error) so existing code
isn't broken; new violations show up immediately.
Also fixes the two bare-Nfr grids the new rule caught:
- components/strategy/CodeStrategyEditor.tsx :270 — repeat(5, 1fr)
- views/ScreenerView.tsx :142 — '100px 1fr 90px ...'
eslint src/ now reports zero bytelyst-trading/* warnings.
Generated with [Devin](https://cli.devin.ai/docs)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
58 lines
2.3 KiB
JavaScript
58 lines
2.3 KiB
JavaScript
// @ts-check
|
|
/**
|
|
* Warn when a `<Button>` (from @bytelyst/ui) wraps content with multiple
|
|
* block-level children (stacked spans/divs/headings).
|
|
*
|
|
* The Button primitive uses `whitespace-nowrap` + fixed `h-{size}` which
|
|
* collapses multi-line content. For card-shaped picker UI, use a native
|
|
* `<button>` with the `.card-button` utility class instead (see
|
|
* docs/ui/UI_AUDIT.md Pattern A).
|
|
*/
|
|
const BLOCK_TAGS = new Set(['div', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'section', 'article', 'header', 'footer']);
|
|
|
|
function isLikelyBlock(child) {
|
|
if (child.type !== 'JSXElement') return false;
|
|
const opening = child.openingElement;
|
|
const name = opening.name && opening.name.type === 'JSXIdentifier' ? opening.name.name : null;
|
|
if (!name) return false;
|
|
if (BLOCK_TAGS.has(name)) return true;
|
|
// <span> with style={{display:'block'}} or className containing 'block'
|
|
if (name === 'span') {
|
|
for (const attr of opening.attributes) {
|
|
if (attr.type !== 'JSXAttribute') continue;
|
|
if (attr.name.name === 'className') {
|
|
const v = attr.value;
|
|
const text = v?.type === 'Literal' ? String(v.value || '')
|
|
: v?.type === 'JSXExpressionContainer' && v.expression.type === 'TemplateLiteral'
|
|
? v.expression.quasis.map(q => q.value.cooked).join(' ')
|
|
: '';
|
|
if (/\bblock\b/.test(text)) return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
export default {
|
|
meta: {
|
|
type: 'suggestion',
|
|
docs: { description: '<Button> with stacked block children collapses content; use native <button class="card-button"> instead' },
|
|
schema: [],
|
|
messages: {
|
|
stacked: '<Button> contains {{ count }} block-level children. The Button primitive applies whitespace-nowrap + fixed height which collapses stacked content. Use a native <button className="card-button"> instead — see docs/ui/UI_AUDIT.md Pattern A.',
|
|
},
|
|
},
|
|
create(context) {
|
|
return {
|
|
JSXElement(node) {
|
|
const opening = node.openingElement;
|
|
if (!opening.name || opening.name.type !== 'JSXIdentifier' || opening.name.name !== 'Button') return;
|
|
const blockCount = node.children.filter(isLikelyBlock).length;
|
|
if (blockCount >= 2) {
|
|
context.report({ node: opening, messageId: 'stacked', data: { count: blockCount } });
|
|
}
|
|
},
|
|
};
|
|
},
|
|
};
|