learning_ai_invt_trdg/web/eslint-local/rules/grid-needs-minmax.js
Devin a0fcb65f5d chore(web): add local eslint plugin with UI audit guards (UI audit #6)
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>
2026-05-10 09:23:05 +00:00

51 lines
1.7 KiB
JavaScript

// @ts-check
/**
* Warn when a CSS `gridTemplateColumns` value uses a bare `Nfr` track
* without `minmax(0, ...)`. Default `min-width: auto` makes grid items
* refuse to shrink below their content, causing overflow.
*
* Pattern F in docs/ui/UI_AUDIT.md.
*/
const HAS_FR_RE = /\b\d*\.?\d*fr\b/;
const HAS_MINMAX_RE = /minmax\s*\(/;
function checkValue(context, node, raw) {
if (typeof raw !== 'string' || !HAS_FR_RE.test(raw)) return;
if (!HAS_MINMAX_RE.test(raw)) {
context.report({ node, messageId: 'bare', data: { value: raw } });
return;
}
// Has minmax somewhere — check whether *every* fr is wrapped.
const stripped = raw.replace(/minmax\s*\([^)]*\)/g, 'MM');
if (HAS_FR_RE.test(stripped)) {
context.report({ node, messageId: 'bare', data: { value: raw } });
}
}
export default {
meta: {
type: 'suggestion',
docs: { description: 'grid 1fr tracks should be wrapped in minmax(0, ...) to allow shrinking' },
schema: [],
messages: {
bare: 'gridTemplateColumns "{{ value }}" has a bare Nfr track. Wrap each in minmax(0, Nfr) so cell content can shrink below min-content.',
},
},
create(context) {
function visitProperty(node) {
if (!node.key) return;
const keyName = node.key.name || (node.key.type === 'Literal' && node.key.value);
if (keyName !== 'gridTemplateColumns') return;
const v = node.value;
if (!v) return;
if (v.type === 'Literal' && typeof v.value === 'string') {
checkValue(context, v, v.value);
} else if (v.type === 'TemplateLiteral') {
const raw = v.quasis.map(q => q.value.cooked).join('${...}');
checkValue(context, v, raw);
}
}
return { Property: visitProperty };
},
};