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>
48 lines
1.6 KiB
JavaScript
48 lines
1.6 KiB
JavaScript
// @ts-check
|
|
/**
|
|
* Warn when a JSX element uses Tailwind's `truncate` (or `line-clamp-*` /
|
|
* `text-ellipsis`) without a paired `title=` or `aria-label=` so users have
|
|
* no way to reveal full content on hover/focus.
|
|
*
|
|
* Pattern E in docs/ui/UI_AUDIT.md.
|
|
*/
|
|
const TRUNCATE_RE = /\b(truncate|line-clamp-\d+|text-ellipsis)\b/;
|
|
|
|
function classNameValue(attr) {
|
|
if (!attr || !attr.value) return null;
|
|
if (attr.value.type === 'Literal') return String(attr.value.value || '');
|
|
if (attr.value.type === 'JSXExpressionContainer') {
|
|
const e = attr.value.expression;
|
|
if (e.type === 'TemplateLiteral') return e.quasis.map(q => q.value.cooked).join(' ');
|
|
if (e.type === 'Literal') return String(e.value || '');
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export default {
|
|
meta: {
|
|
type: 'suggestion',
|
|
docs: { description: 'truncate without a title/aria-label is a UX regression' },
|
|
schema: [],
|
|
messages: {
|
|
missing: 'Element uses `truncate`/`line-clamp-*` but has no `title=` or `aria-label=`. Add one so users can reveal the full text on hover/focus.',
|
|
},
|
|
},
|
|
create(context) {
|
|
return {
|
|
JSXOpeningElement(node) {
|
|
const cls = node.attributes.find(a => a.type === 'JSXAttribute' && a.name && a.name.name === 'className');
|
|
if (!cls) return;
|
|
const val = classNameValue(cls);
|
|
if (!val || !TRUNCATE_RE.test(val)) return;
|
|
const hasAffordance = node.attributes.some(a =>
|
|
a.type === 'JSXAttribute' && a.name && (a.name.name === 'title' || a.name.name === 'aria-label')
|
|
);
|
|
if (!hasAffordance) {
|
|
context.report({ node: cls, messageId: 'missing' });
|
|
}
|
|
},
|
|
};
|
|
},
|
|
};
|