diff --git a/web/eslint-local/index.js b/web/eslint-local/index.js new file mode 100644 index 0000000..550299b --- /dev/null +++ b/web/eslint-local/index.js @@ -0,0 +1,18 @@ +// @ts-check +/** + * Local ESLint plugin for the trading web app. + * + * Rules implement the preventive guards from docs/ui/UI_AUDIT.md §5. + * Wired into eslint.config.js as plugin "bytelyst-trading". + */ +import truncateNeedsTitle from './rules/truncate-needs-title.js'; +import gridNeedsMinmax from './rules/grid-needs-minmax.js'; +import noButtonWithStackedChildren from './rules/no-button-with-stacked-children.js'; + +export default { + rules: { + 'truncate-needs-title': truncateNeedsTitle, + 'grid-needs-minmax': gridNeedsMinmax, + 'no-button-with-stacked-children': noButtonWithStackedChildren, + }, +}; diff --git a/web/eslint-local/rules/grid-needs-minmax.js b/web/eslint-local/rules/grid-needs-minmax.js new file mode 100644 index 0000000..e981e9c --- /dev/null +++ b/web/eslint-local/rules/grid-needs-minmax.js @@ -0,0 +1,50 @@ +// @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 }; + }, +}; diff --git a/web/eslint-local/rules/no-button-with-stacked-children.js b/web/eslint-local/rules/no-button-with-stacked-children.js new file mode 100644 index 0000000..a9d6cda --- /dev/null +++ b/web/eslint-local/rules/no-button-with-stacked-children.js @@ -0,0 +1,57 @@ +// @ts-check +/** + * Warn when a `