@blumintinc/eslint-plugin-blumint 1.19.12 → 1.19.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -222,7 +222,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
222
222
  module.exports = {
223
223
  meta: {
224
224
  name: '@blumintinc/eslint-plugin-blumint',
225
- version: '1.19.12',
225
+ version: '1.19.14',
226
226
  },
227
227
  parseOptions: {
228
228
  ecmaVersion: 2020,
@@ -23,6 +23,52 @@ function isAbsoluteOrFixedPosition(propertyName, propertyValue) {
23
23
  function isPointerEventsProperty(propertyName) {
24
24
  return propertyName === 'pointerEvents' || propertyName === 'pointer-events';
25
25
  }
26
+ /**
27
+ * The inset offsets that position a pseudo-element relative to its origin box.
28
+ */
29
+ const INSET_PROPERTIES = new Set(['top', 'right', 'bottom', 'left']);
30
+ /**
31
+ * Classifies the sign of a leading numeric length in a string offset value
32
+ * (e.g. '-6px' -> negative, '0'/'0px' -> zero, '6px' -> positive). Anything
33
+ * that does not start with an optional-minus number is unknown.
34
+ */
35
+ function classifyOffsetString(raw) {
36
+ const match = raw.trim().match(/^(-?)(\d+(?:\.\d+)?|\.\d+)/);
37
+ if (!match)
38
+ return 'unknown';
39
+ const numericPart = parseFloat(match[2]);
40
+ if (numericPart === 0)
41
+ return 'zero';
42
+ return match[1] === '-' ? 'negative' : 'positive';
43
+ }
44
+ /**
45
+ * Classifies an inset offset property value (top/right/bottom/left) as a
46
+ * positive, negative, zero, or unknown length. Only the shapes that can be
47
+ * resolved statically are classified; variables, member expressions, template
48
+ * literals, and calls are treated as unknown and never counted toward the
49
+ * hit-slop exemption.
50
+ */
51
+ function classifyOffsetValue(value) {
52
+ if (value.type === utils_1.AST_NODE_TYPES.Literal) {
53
+ if (typeof value.value === 'number') {
54
+ if (value.value === 0)
55
+ return 'zero';
56
+ return value.value < 0 ? 'negative' : 'positive';
57
+ }
58
+ if (typeof value.value === 'string') {
59
+ return classifyOffsetString(value.value);
60
+ }
61
+ return 'unknown';
62
+ }
63
+ // Negative numeric literals parse as `-` UnaryExpression over a number.
64
+ if (value.type === utils_1.AST_NODE_TYPES.UnaryExpression &&
65
+ value.operator === '-' &&
66
+ value.argument.type === utils_1.AST_NODE_TYPES.Literal &&
67
+ typeof value.argument.value === 'number') {
68
+ return value.argument.value === 0 ? 'zero' : 'negative';
69
+ }
70
+ return 'unknown';
71
+ }
26
72
  function formatSelector(selector) {
27
73
  if (!selector)
28
74
  return 'pseudo-element';
@@ -63,12 +109,21 @@ exports.ensurePointerEventsNone = (0, createRule_1.createRule)({
63
109
  const absolutePositionedStyles = new Map();
64
110
  // Track style objects that already have pointer-events defined
65
111
  const stylesWithPointerEvents = new Map();
112
+ // Track style objects that are hit-slop touch-target extensions: an
113
+ // absolute/fixed overlay whose inset offsets only extend beyond the origin
114
+ // box (>=1 negative, none positive). A browser attributes pointer events on
115
+ // a pseudo-element to its origin element, so such an overlay cannot occlude
116
+ // the control and must not be flagged (its autofix would shrink the tap
117
+ // target, the very accessibility regression this rule exists to prevent).
118
+ const hitSlopStyles = new Map();
66
119
  /**
67
120
  * Process a CSS-in-JS style object to check for position: absolute/fixed and pointer-events
68
121
  */
69
122
  function processStyleObject(node) {
70
123
  let hasAbsolutePosition = false;
71
124
  let pointerEventsValue;
125
+ let hasNegativeOffset = false;
126
+ let hasPositiveOffset = false;
72
127
  // Check each property in the style object
73
128
  for (const property of node.properties) {
74
129
  if (property.type !== utils_1.AST_NODE_TYPES.Property)
@@ -103,12 +158,26 @@ exports.ensurePointerEventsNone = (0, createRule_1.createRule)({
103
158
  pointerEventsValue = property.value.name;
104
159
  }
105
160
  }
161
+ // Track inset offsets to detect hit-slop touch-target extensions
162
+ if (INSET_PROPERTIES.has(propertyName)) {
163
+ const sign = classifyOffsetValue(property.value);
164
+ if (sign === 'negative') {
165
+ hasNegativeOffset = true;
166
+ }
167
+ else if (sign === 'positive') {
168
+ hasPositiveOffset = true;
169
+ }
170
+ }
106
171
  }
107
172
  // Store the results for this style object
108
173
  absolutePositionedStyles.set(node, hasAbsolutePosition);
109
174
  if (pointerEventsValue !== undefined) {
110
175
  stylesWithPointerEvents.set(node, pointerEventsValue);
111
176
  }
177
+ // A hit-slop extension only enlarges the tappable area: it is
178
+ // absolute/fixed and its inset offsets extend outward (>=1 negative, none
179
+ // positive). Such overlays cannot occlude the control they belong to.
180
+ hitSlopStyles.set(node, hasAbsolutePosition && hasNegativeOffset && !hasPositiveOffset);
112
181
  }
113
182
  /**
114
183
  * Check if a style object needs pointer-events: none
@@ -117,6 +186,13 @@ exports.ensurePointerEventsNone = (0, createRule_1.createRule)({
117
186
  const isPseudoElement = selector && hasPseudoElementSelector(selector);
118
187
  const isAbsolutePositioned = absolutePositionedStyles.get(node) || false;
119
188
  const pointerEventsValue = stylesWithPointerEvents.get(node);
189
+ // A hit-slop touch-target extension extends the origin element's tappable
190
+ // area outward; because pointer events on it are attributed to the origin
191
+ // control, it cannot block anything. Skip reporting (and its destructive
192
+ // shrink-the-tap-target autofix).
193
+ if (hitSlopStyles.get(node)) {
194
+ return;
195
+ }
120
196
  // If this is a pseudo-element with absolute positioning but no pointer-events
121
197
  if (isPseudoElement &&
122
198
  isAbsolutePositioned &&
@@ -118,6 +118,13 @@ function getTypeReferenceName(node) {
118
118
  function typeNodeComposesWithProps(typeNode, propsTypeName) {
119
119
  switch (typeNode.type) {
120
120
  case utils_1.AST_NODE_TYPES.TSTypeReference: {
121
+ // A direct reference to the child's whole props type (bare `ChildProps`
122
+ // or generic-instantiated `ChildProps<T>`) is the maximal form of
123
+ // composition: the entire surface is inherited verbatim, strictly
124
+ // stronger than Pick/Omit, so no duplication/drift is possible.
125
+ if (getTypeReferenceName(typeNode) === propsTypeName) {
126
+ return true;
127
+ }
121
128
  if (typeReferenceContainsPickOrOmit(typeNode, propsTypeName)) {
122
129
  return true;
123
130
  }
@@ -313,6 +320,16 @@ function findComponentFunction(program, name, seen = new Set()) {
313
320
  }
314
321
  return null;
315
322
  }
323
+ /**
324
+ * A rendered child that resolves in-file to a component function taking no
325
+ * parameters has no props surface to compose with, so it is not a composition
326
+ * dependency (same category as a decorative icon). Only in-file resolution is
327
+ * used; imported children are left to the normal composition check.
328
+ */
329
+ function isZeroPropComponent(program, name) {
330
+ const fn = findComponentFunction(program, name);
331
+ return fn !== null && fn.params.length === 0;
332
+ }
316
333
  /**
317
334
  * Resolve the type node that defines a rendered dependency's props: its
318
335
  * `{Dep}Props` alias if one exists, otherwise the dependency component's
@@ -446,7 +463,8 @@ exports.requirePropsComposition = (0, createRule_1.createRule)({
446
463
  // Filter to non-excluded custom components
447
464
  const depComponents = Array.from(allJsxNames).filter((name) => !excludeComponents.has(name) &&
448
465
  !isDecorativeIcon(name) &&
449
- name !== componentName);
466
+ name !== componentName &&
467
+ !isZeroPropComponent(prog, name));
450
468
  if (depComponents.length < minDependencyCount) {
451
469
  return;
452
470
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.19.12",
3
+ "version": "1.19.14",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,32 @@
1
1
  [
2
+ {
3
+ "version": "1.19.14",
4
+ "date": "2026-07-17T21:35:22.880Z",
5
+ "rules": [
6
+ {
7
+ "name": "require-props-composition",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1316
11
+ ],
12
+ "summary": "recognize direct whole-props refs and skip zero-prop children (closes #1316)"
13
+ }
14
+ ]
15
+ },
16
+ {
17
+ "version": "1.19.13",
18
+ "date": "2026-07-17T21:25:11.513Z",
19
+ "rules": [
20
+ {
21
+ "name": "ensure-pointer-events-none",
22
+ "changeType": "fix",
23
+ "issues": [
24
+ 1315
25
+ ],
26
+ "summary": "exempt hit-slop touch-target pseudo-elements (closes #1315)"
27
+ }
28
+ ]
29
+ },
2
30
  {
3
31
  "version": "1.19.12",
4
32
  "date": "2026-07-17T19:23:55.157Z",