oxlint-plugin-react-doctor 0.9.1-dev.49c5c1e → 0.9.1-dev.4ebd0a0
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/dist/index.d.ts +288 -0
- package/dist/index.js +3240 -596
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -15542,6 +15542,8 @@ const COMMON_UI_FONT_FAMILIES = new Set([
|
|
|
15542
15542
|
"roboto",
|
|
15543
15543
|
"space grotesk"
|
|
15544
15544
|
]);
|
|
15545
|
+
const DECORATIVE_RADIAL_SPOTLIGHT_TRANSPARENT_ALPHA_MAX = .05;
|
|
15546
|
+
const RADIAL_HALO_TRANSPARENT_ALPHA_MAX = .05;
|
|
15545
15547
|
const EXCESSIVE_MOTION_STAGGER_SECONDS = .08;
|
|
15546
15548
|
const TAILWIND_TEXT_SIZE_PX = new Map([
|
|
15547
15549
|
["text-xs", 12],
|
|
@@ -15559,12 +15561,31 @@ const TAILWIND_TEXT_SIZE_PX = new Map([
|
|
|
15559
15561
|
["text-9xl", 128]
|
|
15560
15562
|
]);
|
|
15561
15563
|
const GENERIC_MARKETING_PHRASES = new Set([
|
|
15564
|
+
"best of breed",
|
|
15565
|
+
"best-in-class",
|
|
15566
|
+
"built for the modern",
|
|
15562
15567
|
"cutting-edge",
|
|
15568
|
+
"drive engagement",
|
|
15569
|
+
"drive growth",
|
|
15570
|
+
"drive results",
|
|
15571
|
+
"empower your",
|
|
15572
|
+
"enterprise-grade",
|
|
15563
15573
|
"future-proof",
|
|
15574
|
+
"game changing",
|
|
15575
|
+
"game-changer",
|
|
15576
|
+
"harness the power",
|
|
15577
|
+
"industry-leading",
|
|
15578
|
+
"leverage the power",
|
|
15564
15579
|
"next-generation",
|
|
15565
15580
|
"seamless experience",
|
|
15566
|
-
"
|
|
15581
|
+
"seamlessly integrate",
|
|
15582
|
+
"streamline your",
|
|
15583
|
+
"supercharge your",
|
|
15567
15584
|
"transform your business",
|
|
15585
|
+
"trusted by leading",
|
|
15586
|
+
"trusted by the world",
|
|
15587
|
+
"unleash the power",
|
|
15588
|
+
"unleash your",
|
|
15568
15589
|
"unlock your potential",
|
|
15569
15590
|
"world-class"
|
|
15570
15591
|
]);
|
|
@@ -15645,6 +15666,367 @@ const TAILWIND_DISPLAY_TOKENS = new Set([
|
|
|
15645
15666
|
const SPACE_AXIS_PATTERN = /(?:^|\s)(?:-)?space-(x|y)-(\d+(?:\.\d+)?|\[[^\]]+\])(?=$|[\s:])/;
|
|
15646
15667
|
const TRAILING_THREE_PERIOD_ELLIPSIS_PATTERN = /[\p{L}\p{N}]\.\.\./u;
|
|
15647
15668
|
//#endregion
|
|
15669
|
+
//#region src/plugin/rules/design/utils/get-style-property-key.ts
|
|
15670
|
+
const getStylePropertyKey = (property) => getStaticPropertyKeyName(property, { allowComputedString: true });
|
|
15671
|
+
//#endregion
|
|
15672
|
+
//#region src/plugin/rules/design/utils/get-effective-style-property.ts
|
|
15673
|
+
const getEffectiveStyleProperty = (properties, propertyName) => {
|
|
15674
|
+
for (const property of [...properties ?? []].reverse()) {
|
|
15675
|
+
const currentPropertyName = getStylePropertyKey(property);
|
|
15676
|
+
if (!currentPropertyName) return null;
|
|
15677
|
+
if (currentPropertyName === propertyName && isNodeOfType(property, "Property")) return property;
|
|
15678
|
+
}
|
|
15679
|
+
return null;
|
|
15680
|
+
};
|
|
15681
|
+
//#endregion
|
|
15682
|
+
//#region src/plugin/rules/design/utils/get-inline-style-expression.ts
|
|
15683
|
+
const isConstObjectBindingMutated = (symbol) => symbol.references.some((reference) => {
|
|
15684
|
+
if (reference.flag !== "read") return true;
|
|
15685
|
+
let referenceExpression = reference.identifier;
|
|
15686
|
+
while (referenceExpression.parent && isNodeOfType(referenceExpression.parent, "MemberExpression") && referenceExpression.parent.object === referenceExpression) referenceExpression = referenceExpression.parent;
|
|
15687
|
+
const parent = referenceExpression.parent;
|
|
15688
|
+
return Boolean(isNodeOfType(parent, "AssignmentExpression") && parent.left === referenceExpression || isNodeOfType(parent, "UpdateExpression") && parent.argument === referenceExpression || isNodeOfType(parent, "UnaryExpression") && parent.operator === "delete" && parent.argument === referenceExpression || isNodeOfType(parent, "CallExpression") && parent.arguments?.some((argument) => argument === referenceExpression));
|
|
15689
|
+
});
|
|
15690
|
+
const resolveInlineStyleObjectExpression = (expression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
15691
|
+
const candidate = stripParenExpression(expression);
|
|
15692
|
+
if (isNodeOfType(candidate, "ObjectExpression")) return candidate;
|
|
15693
|
+
if (!scopes || !isNodeOfType(candidate, "Identifier")) return null;
|
|
15694
|
+
const symbol = scopes.symbolFor(candidate);
|
|
15695
|
+
if (!symbol || symbol.kind !== "const" || !symbol.initializer || visitedSymbolIds.has(symbol.id) || isConstObjectBindingMutated(symbol)) return null;
|
|
15696
|
+
const nextVisitedSymbolIds = new Set(visitedSymbolIds);
|
|
15697
|
+
nextVisitedSymbolIds.add(symbol.id);
|
|
15698
|
+
return resolveInlineStyleObjectExpression(symbol.initializer, scopes, nextVisitedSymbolIds);
|
|
15699
|
+
};
|
|
15700
|
+
const getInlineStyleExpression = (node, scopes) => {
|
|
15701
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "style") return null;
|
|
15702
|
+
if (!isNodeOfType(node.value, "JSXExpressionContainer")) return null;
|
|
15703
|
+
return resolveInlineStyleObjectExpression(node.value.expression, scopes);
|
|
15704
|
+
};
|
|
15705
|
+
//#endregion
|
|
15706
|
+
//#region src/plugin/rules/design/utils/get-string-from-class-name-attr.ts
|
|
15707
|
+
const getStringFromClassNameAttr = (node) => {
|
|
15708
|
+
if (!isNodeOfType(node, "JSXOpeningElement")) return null;
|
|
15709
|
+
const classAttr = getAuthoritativeJsxAttribute(node.attributes ?? [], "className");
|
|
15710
|
+
if (!classAttr?.value) return null;
|
|
15711
|
+
if (isNodeOfType(classAttr.value, "Literal") && typeof classAttr.value.value === "string") return classAttr.value.value;
|
|
15712
|
+
if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "Literal") && typeof classAttr.value.expression.value === "string") return classAttr.value.expression.value;
|
|
15713
|
+
if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "TemplateLiteral") && classAttr.value.expression.quasis?.length === 1) return classAttr.value.expression.quasis[0].value?.raw ?? null;
|
|
15714
|
+
return null;
|
|
15715
|
+
};
|
|
15716
|
+
//#endregion
|
|
15717
|
+
//#region src/plugin/rules/design/utils/get-style-property-string-value.ts
|
|
15718
|
+
const getStylePropertyStringValue = (property) => {
|
|
15719
|
+
if (!isNodeOfType(property, "Property")) return null;
|
|
15720
|
+
if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "string") return property.value.value;
|
|
15721
|
+
return null;
|
|
15722
|
+
};
|
|
15723
|
+
//#endregion
|
|
15724
|
+
//#region src/plugin/constants/tailwind.ts
|
|
15725
|
+
const TAILWIND_NAMED_BREAKPOINTS = [
|
|
15726
|
+
"sm",
|
|
15727
|
+
"md",
|
|
15728
|
+
"lg",
|
|
15729
|
+
"xl",
|
|
15730
|
+
"2xl"
|
|
15731
|
+
];
|
|
15732
|
+
const TAILWIND_BREAKPOINT_NAMES = ["", ...TAILWIND_NAMED_BREAKPOINTS];
|
|
15733
|
+
const TAILWIND_BREAKPOINT_RANKS = new Map(TAILWIND_NAMED_BREAKPOINTS.map((breakpointName, breakpointIndex) => [breakpointName, breakpointIndex]));
|
|
15734
|
+
//#endregion
|
|
15735
|
+
//#region src/plugin/utils/normalize-tailwind-arbitrary-utility-value.ts
|
|
15736
|
+
const normalizeTailwindArbitraryUtilityValue = (value) => value.replace(/(?<!\\)_/g, " ");
|
|
15737
|
+
//#endregion
|
|
15738
|
+
//#region src/plugin/utils/get-tailwind-visibility-effect.ts
|
|
15739
|
+
const DISPLAY_VISIBILITY_EFFECTS = new Map([
|
|
15740
|
+
["hidden", {
|
|
15741
|
+
isVisible: false,
|
|
15742
|
+
propertyName: "display"
|
|
15743
|
+
}],
|
|
15744
|
+
["block", {
|
|
15745
|
+
isVisible: true,
|
|
15746
|
+
propertyName: "display"
|
|
15747
|
+
}],
|
|
15748
|
+
["contents", {
|
|
15749
|
+
isVisible: true,
|
|
15750
|
+
propertyName: "display"
|
|
15751
|
+
}],
|
|
15752
|
+
["flex", {
|
|
15753
|
+
isVisible: true,
|
|
15754
|
+
propertyName: "display"
|
|
15755
|
+
}],
|
|
15756
|
+
["flow-root", {
|
|
15757
|
+
isVisible: true,
|
|
15758
|
+
propertyName: "display"
|
|
15759
|
+
}],
|
|
15760
|
+
["grid", {
|
|
15761
|
+
isVisible: true,
|
|
15762
|
+
propertyName: "display"
|
|
15763
|
+
}],
|
|
15764
|
+
["inline", {
|
|
15765
|
+
isVisible: true,
|
|
15766
|
+
propertyName: "display"
|
|
15767
|
+
}],
|
|
15768
|
+
["inline-block", {
|
|
15769
|
+
isVisible: true,
|
|
15770
|
+
propertyName: "display"
|
|
15771
|
+
}],
|
|
15772
|
+
["inline-flex", {
|
|
15773
|
+
isVisible: true,
|
|
15774
|
+
propertyName: "display"
|
|
15775
|
+
}],
|
|
15776
|
+
["inline-grid", {
|
|
15777
|
+
isVisible: true,
|
|
15778
|
+
propertyName: "display"
|
|
15779
|
+
}],
|
|
15780
|
+
["inline-table", {
|
|
15781
|
+
isVisible: true,
|
|
15782
|
+
propertyName: "display"
|
|
15783
|
+
}],
|
|
15784
|
+
["list-item", {
|
|
15785
|
+
isVisible: true,
|
|
15786
|
+
propertyName: "display"
|
|
15787
|
+
}],
|
|
15788
|
+
["table", {
|
|
15789
|
+
isVisible: true,
|
|
15790
|
+
propertyName: "display"
|
|
15791
|
+
}],
|
|
15792
|
+
["table-caption", {
|
|
15793
|
+
isVisible: true,
|
|
15794
|
+
propertyName: "display"
|
|
15795
|
+
}],
|
|
15796
|
+
["table-cell", {
|
|
15797
|
+
isVisible: true,
|
|
15798
|
+
propertyName: "display"
|
|
15799
|
+
}],
|
|
15800
|
+
["table-column", {
|
|
15801
|
+
isVisible: true,
|
|
15802
|
+
propertyName: "display"
|
|
15803
|
+
}],
|
|
15804
|
+
["table-column-group", {
|
|
15805
|
+
isVisible: true,
|
|
15806
|
+
propertyName: "display"
|
|
15807
|
+
}],
|
|
15808
|
+
["table-footer-group", {
|
|
15809
|
+
isVisible: true,
|
|
15810
|
+
propertyName: "display"
|
|
15811
|
+
}],
|
|
15812
|
+
["table-header-group", {
|
|
15813
|
+
isVisible: true,
|
|
15814
|
+
propertyName: "display"
|
|
15815
|
+
}],
|
|
15816
|
+
["table-row", {
|
|
15817
|
+
isVisible: true,
|
|
15818
|
+
propertyName: "display"
|
|
15819
|
+
}],
|
|
15820
|
+
["table-row-group", {
|
|
15821
|
+
isVisible: true,
|
|
15822
|
+
propertyName: "display"
|
|
15823
|
+
}]
|
|
15824
|
+
]);
|
|
15825
|
+
const VISIBILITY_VISIBILITY_EFFECTS = new Map([
|
|
15826
|
+
["collapse", {
|
|
15827
|
+
isVisible: false,
|
|
15828
|
+
propertyName: "visibility"
|
|
15829
|
+
}],
|
|
15830
|
+
["invisible", {
|
|
15831
|
+
isVisible: false,
|
|
15832
|
+
propertyName: "visibility"
|
|
15833
|
+
}],
|
|
15834
|
+
["visible", {
|
|
15835
|
+
isVisible: true,
|
|
15836
|
+
propertyName: "visibility"
|
|
15837
|
+
}]
|
|
15838
|
+
]);
|
|
15839
|
+
const VISIBLE_ARBITRARY_DISPLAY_VALUES = new Set([
|
|
15840
|
+
"block",
|
|
15841
|
+
"contents",
|
|
15842
|
+
"flex",
|
|
15843
|
+
"flow-root",
|
|
15844
|
+
"grid",
|
|
15845
|
+
"inline",
|
|
15846
|
+
"inline block",
|
|
15847
|
+
"inline flex",
|
|
15848
|
+
"inline flow-root",
|
|
15849
|
+
"inline grid",
|
|
15850
|
+
"inline table",
|
|
15851
|
+
"list-item",
|
|
15852
|
+
"table",
|
|
15853
|
+
"table-caption",
|
|
15854
|
+
"table-cell",
|
|
15855
|
+
"table-column",
|
|
15856
|
+
"table-column-group",
|
|
15857
|
+
"table-footer-group",
|
|
15858
|
+
"table-header-group",
|
|
15859
|
+
"table-row",
|
|
15860
|
+
"table-row-group"
|
|
15861
|
+
]);
|
|
15862
|
+
const getArbitraryCssPropertyValue = (utility, propertyName) => {
|
|
15863
|
+
const propertyPrefix = `[${propertyName}:`;
|
|
15864
|
+
return utility.toLowerCase().startsWith(propertyPrefix) && utility.endsWith("]") ? utility.slice(propertyPrefix.length, -1) : null;
|
|
15865
|
+
};
|
|
15866
|
+
const getTailwindVisibilityEffect = (utility) => {
|
|
15867
|
+
const knownEffect = DISPLAY_VISIBILITY_EFFECTS.get(utility) ?? VISIBILITY_VISIBILITY_EFFECTS.get(utility);
|
|
15868
|
+
if (knownEffect) return {
|
|
15869
|
+
...knownEffect,
|
|
15870
|
+
status: "known"
|
|
15871
|
+
};
|
|
15872
|
+
const arbitraryDisplayValue = getArbitraryCssPropertyValue(utility, "display");
|
|
15873
|
+
if (arbitraryDisplayValue !== null) {
|
|
15874
|
+
const displayValue = normalizeTailwindArbitraryUtilityValue(arbitraryDisplayValue).trim().toLowerCase();
|
|
15875
|
+
if (displayValue === "none") return {
|
|
15876
|
+
isVisible: false,
|
|
15877
|
+
propertyName: "display",
|
|
15878
|
+
status: "known"
|
|
15879
|
+
};
|
|
15880
|
+
return VISIBLE_ARBITRARY_DISPLAY_VALUES.has(displayValue) ? {
|
|
15881
|
+
isVisible: true,
|
|
15882
|
+
propertyName: "display",
|
|
15883
|
+
status: "known"
|
|
15884
|
+
} : {
|
|
15885
|
+
isVisible: null,
|
|
15886
|
+
propertyName: "display",
|
|
15887
|
+
status: "unknown"
|
|
15888
|
+
};
|
|
15889
|
+
}
|
|
15890
|
+
const arbitraryVisibilityValue = getArbitraryCssPropertyValue(utility, "visibility");
|
|
15891
|
+
if (arbitraryVisibilityValue === null) return {
|
|
15892
|
+
isVisible: null,
|
|
15893
|
+
propertyName: null,
|
|
15894
|
+
status: "not-relevant"
|
|
15895
|
+
};
|
|
15896
|
+
const visibilityValue = normalizeTailwindArbitraryUtilityValue(arbitraryVisibilityValue).trim().toLowerCase();
|
|
15897
|
+
if (visibilityValue === "visible") return {
|
|
15898
|
+
isVisible: true,
|
|
15899
|
+
propertyName: "visibility",
|
|
15900
|
+
status: "known"
|
|
15901
|
+
};
|
|
15902
|
+
return visibilityValue === "hidden" || visibilityValue === "collapse" ? {
|
|
15903
|
+
isVisible: false,
|
|
15904
|
+
propertyName: "visibility",
|
|
15905
|
+
status: "known"
|
|
15906
|
+
} : {
|
|
15907
|
+
isVisible: null,
|
|
15908
|
+
propertyName: "visibility",
|
|
15909
|
+
status: "unknown"
|
|
15910
|
+
};
|
|
15911
|
+
};
|
|
15912
|
+
//#endregion
|
|
15913
|
+
//#region src/plugin/utils/parse-tailwind-class-name-token.ts
|
|
15914
|
+
const isCharacterEscaped = (value, characterIndex) => {
|
|
15915
|
+
let backslashCount = 0;
|
|
15916
|
+
for (let precedingIndex = characterIndex - 1; precedingIndex >= 0 && value[precedingIndex] === "\\"; precedingIndex -= 1) backslashCount += 1;
|
|
15917
|
+
return backslashCount % 2 === 1;
|
|
15918
|
+
};
|
|
15919
|
+
const parseTailwindClassNameToken = (rawToken) => {
|
|
15920
|
+
const variants = [];
|
|
15921
|
+
let segmentStartIndex = 0;
|
|
15922
|
+
for (const characterIndex of getTailwindTopLevelCharacterIndices(rawToken, (character) => character === ":")) {
|
|
15923
|
+
variants.push(rawToken.slice(segmentStartIndex, characterIndex));
|
|
15924
|
+
segmentStartIndex = characterIndex + 1;
|
|
15925
|
+
}
|
|
15926
|
+
let utility = rawToken.slice(segmentStartIndex);
|
|
15927
|
+
const hasTrailingImportantModifier = utility.endsWith("!") && !isCharacterEscaped(utility, utility.length - 1);
|
|
15928
|
+
const isImportant = utility.startsWith("!") || hasTrailingImportantModifier;
|
|
15929
|
+
if (utility.startsWith("!")) utility = utility.slice(1);
|
|
15930
|
+
if (hasTrailingImportantModifier) utility = utility.slice(0, -1);
|
|
15931
|
+
return {
|
|
15932
|
+
isImportant,
|
|
15933
|
+
utility,
|
|
15934
|
+
variants
|
|
15935
|
+
};
|
|
15936
|
+
};
|
|
15937
|
+
//#endregion
|
|
15938
|
+
//#region src/plugin/utils/get-tailwind-visibility-at-breakpoints.ts
|
|
15939
|
+
const getResponsiveVariantScope = (variants) => {
|
|
15940
|
+
let minimumBreakpointIndex = 0;
|
|
15941
|
+
let maximumBreakpointIndex = TAILWIND_BREAKPOINT_NAMES.length;
|
|
15942
|
+
for (const variant of variants) {
|
|
15943
|
+
const minimumVariantIndex = TAILWIND_BREAKPOINT_NAMES.indexOf(variant);
|
|
15944
|
+
if (minimumVariantIndex > 0) {
|
|
15945
|
+
minimumBreakpointIndex = Math.max(minimumBreakpointIndex, minimumVariantIndex);
|
|
15946
|
+
continue;
|
|
15947
|
+
}
|
|
15948
|
+
if (variant.startsWith("max-")) {
|
|
15949
|
+
const maximumVariantIndex = TAILWIND_BREAKPOINT_NAMES.indexOf(variant.slice(4));
|
|
15950
|
+
if (maximumVariantIndex > 0) {
|
|
15951
|
+
maximumBreakpointIndex = Math.min(maximumBreakpointIndex, maximumVariantIndex);
|
|
15952
|
+
continue;
|
|
15953
|
+
}
|
|
15954
|
+
}
|
|
15955
|
+
return null;
|
|
15956
|
+
}
|
|
15957
|
+
return {
|
|
15958
|
+
maximumBreakpointIndex,
|
|
15959
|
+
minimumBreakpointIndex,
|
|
15960
|
+
specificity: variants.length
|
|
15961
|
+
};
|
|
15962
|
+
};
|
|
15963
|
+
const resolveVisibilityProperty = (scopedEffects, breakpointIndex, propertyName) => {
|
|
15964
|
+
const applicableEffects = scopedEffects.filter(({ effect, scope }) => effect.propertyName === propertyName && breakpointIndex >= scope.minimumBreakpointIndex && breakpointIndex < scope.maximumBreakpointIndex);
|
|
15965
|
+
if (applicableEffects.length === 0) return true;
|
|
15966
|
+
const highestImportanceEffects = applicableEffects.some(({ token }) => token.isImportant) ? applicableEffects.filter(({ token }) => token.isImportant) : applicableEffects;
|
|
15967
|
+
const maximumSpecificity = Math.max(...highestImportanceEffects.map(({ scope }) => scope.specificity));
|
|
15968
|
+
const highestSpecificityEffects = highestImportanceEffects.filter(({ scope }) => scope.specificity === maximumSpecificity);
|
|
15969
|
+
const maximumMinimumBreakpoint = Math.max(...highestSpecificityEffects.map(({ scope }) => scope.minimumBreakpointIndex));
|
|
15970
|
+
const latestMinimumEffects = highestSpecificityEffects.filter(({ scope }) => scope.minimumBreakpointIndex === maximumMinimumBreakpoint);
|
|
15971
|
+
const minimumMaximumBreakpoint = Math.min(...latestMinimumEffects.map(({ scope }) => scope.maximumBreakpointIndex));
|
|
15972
|
+
const highestPriorityStates = new Set(latestMinimumEffects.filter(({ scope }) => scope.maximumBreakpointIndex === minimumMaximumBreakpoint).map(({ effect }) => effect.isVisible));
|
|
15973
|
+
return highestPriorityStates.size === 1 ? highestPriorityStates.values().next().value ?? null : null;
|
|
15974
|
+
};
|
|
15975
|
+
const getTailwindVisibilityAtBreakpoints = (className) => {
|
|
15976
|
+
const scopedEffects = [];
|
|
15977
|
+
for (const token of splitTailwindClassName(className).map(parseTailwindClassNameToken)) {
|
|
15978
|
+
const resolution = getTailwindVisibilityEffect(token.utility);
|
|
15979
|
+
if (resolution.status === "not-relevant") continue;
|
|
15980
|
+
const scope = getResponsiveVariantScope(token.variants);
|
|
15981
|
+
if (scope === null) return null;
|
|
15982
|
+
if (!scope || scope.minimumBreakpointIndex >= scope.maximumBreakpointIndex) continue;
|
|
15983
|
+
if (resolution.status === "unknown" || resolution.propertyName === null || resolution.isVisible === null) return null;
|
|
15984
|
+
const effect = {
|
|
15985
|
+
isVisible: resolution.isVisible,
|
|
15986
|
+
propertyName: resolution.propertyName
|
|
15987
|
+
};
|
|
15988
|
+
scopedEffects.push({
|
|
15989
|
+
effect,
|
|
15990
|
+
scope,
|
|
15991
|
+
token
|
|
15992
|
+
});
|
|
15993
|
+
}
|
|
15994
|
+
const visibilityAtBreakpoints = [];
|
|
15995
|
+
for (let breakpointIndex = 0; breakpointIndex < TAILWIND_BREAKPOINT_NAMES.length; breakpointIndex += 1) {
|
|
15996
|
+
const displayVisibility = resolveVisibilityProperty(scopedEffects, breakpointIndex, "display");
|
|
15997
|
+
const visibilityVisibility = resolveVisibilityProperty(scopedEffects, breakpointIndex, "visibility");
|
|
15998
|
+
if (displayVisibility === null || visibilityVisibility === null) return null;
|
|
15999
|
+
visibilityAtBreakpoints.push(displayVisibility && visibilityVisibility);
|
|
16000
|
+
}
|
|
16001
|
+
return visibilityAtBreakpoints;
|
|
16002
|
+
};
|
|
16003
|
+
//#endregion
|
|
16004
|
+
//#region src/plugin/utils/is-inside-statically-hidden-jsx-subtree.ts
|
|
16005
|
+
const isStaticallyHiddenOpeningElement = (openingElement, context) => {
|
|
16006
|
+
if (isHiddenFromScreenReader(openingElement, context.settings)) return true;
|
|
16007
|
+
const styleAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "style");
|
|
16008
|
+
const styleExpression = styleAttribute ? getInlineStyleExpression(styleAttribute, context.scopes) : null;
|
|
16009
|
+
if (styleExpression) {
|
|
16010
|
+
const displayProperty = getEffectiveStyleProperty(styleExpression.properties, "display");
|
|
16011
|
+
if (displayProperty && getStylePropertyStringValue(displayProperty)?.toLowerCase() === "none") return true;
|
|
16012
|
+
const visibilityProperty = getEffectiveStyleProperty(styleExpression.properties, "visibility");
|
|
16013
|
+
const visibilityValue = visibilityProperty ? getStylePropertyStringValue(visibilityProperty)?.toLowerCase() : null;
|
|
16014
|
+
if (visibilityValue === "hidden" || visibilityValue === "collapse") return true;
|
|
16015
|
+
}
|
|
16016
|
+
const className = getStringFromClassNameAttr(openingElement);
|
|
16017
|
+
if (!className) return false;
|
|
16018
|
+
const visibilityAtBreakpoints = getTailwindVisibilityAtBreakpoints(className);
|
|
16019
|
+
return Boolean(visibilityAtBreakpoints && visibilityAtBreakpoints.every((isVisible) => !isVisible));
|
|
16020
|
+
};
|
|
16021
|
+
const isInsideStaticallyHiddenJsxSubtree$1 = (node, context) => {
|
|
16022
|
+
let currentNode = node;
|
|
16023
|
+
while (currentNode) {
|
|
16024
|
+
if (isNodeOfType(currentNode, "JSXElement") && isStaticallyHiddenOpeningElement(currentNode.openingElement, context)) return true;
|
|
16025
|
+
currentNode = currentNode.parent;
|
|
16026
|
+
}
|
|
16027
|
+
return false;
|
|
16028
|
+
};
|
|
16029
|
+
//#endregion
|
|
15648
16030
|
//#region src/plugin/rules/react-ui/utils/get-opening-element-tag-name.ts
|
|
15649
16031
|
const getOpeningElementTagName = (openingElement) => {
|
|
15650
16032
|
if (!openingElement) return null;
|
|
@@ -15675,8 +16057,20 @@ const isInsideExcludedTypographyAncestor = (jsxTextNode) => {
|
|
|
15675
16057
|
//#endregion
|
|
15676
16058
|
//#region src/plugin/rules/react-ui/no-em-dash-in-jsx-text.ts
|
|
15677
16059
|
const EM_DASH = "—";
|
|
15678
|
-
const
|
|
16060
|
+
const EM_DASH_ENTITY_PATTERN = /&(?:mdash|#0*8212|#x0*2014);/gi;
|
|
16061
|
+
const LONG_FORM_CONTENT_PATH_PATTERN$1 = /(?:^|[/\\])(?:articles?|blog|changelog|content|docs?|posts?)(?:[/\\]|$)/i;
|
|
15679
16062
|
const PROSE_EM_DASH_PATTERN = /\p{L}[^—\n]*—[^—\n]*\p{L}/u;
|
|
16063
|
+
const LETTER_WORD_PATTERN = /\p{L}+/gu;
|
|
16064
|
+
const LINE_BREAK_PATTERN = /[\r\n]/u;
|
|
16065
|
+
const hasProseEmDash = (text) => text.includes(EM_DASH) && text.split(LINE_BREAK_PATTERN).some((line) => PROSE_EM_DASH_PATTERN.test(line) && (line.match(LETTER_WORD_PATTERN)?.length ?? 0) >= 5);
|
|
16066
|
+
const hasProseEmDashInStaticExpression = (rawExpression) => {
|
|
16067
|
+
const expression = stripParenExpression(rawExpression);
|
|
16068
|
+
if (isNodeOfType(expression, "Literal")) return typeof expression.value === "string" && hasProseEmDash(expression.value);
|
|
16069
|
+
if (isNodeOfType(expression, "TemplateLiteral")) return expression.quasis.some((quasi) => hasProseEmDash(quasi.value.cooked ?? quasi.value.raw ?? ""));
|
|
16070
|
+
if (isNodeOfType(expression, "ConditionalExpression")) return hasProseEmDashInStaticExpression(expression.consequent) || hasProseEmDashInStaticExpression(expression.alternate);
|
|
16071
|
+
if (isNodeOfType(expression, "LogicalExpression")) return hasProseEmDashInStaticExpression(expression.right);
|
|
16072
|
+
return false;
|
|
16073
|
+
};
|
|
15680
16074
|
const noEmDashInJsxText = defineRule({
|
|
15681
16075
|
id: "design-no-em-dash-in-jsx-text",
|
|
15682
16076
|
title: "Em dash in JSX text",
|
|
@@ -15685,17 +16079,30 @@ const noEmDashInJsxText = defineRule({
|
|
|
15685
16079
|
defaultEnabled: false,
|
|
15686
16080
|
category: "Architecture",
|
|
15687
16081
|
recommendation: "Replace em dashes in UI text with commas, colons, semicolons, or parentheses so the copy reads less like AI output.",
|
|
15688
|
-
create: (context) =>
|
|
15689
|
-
if (context.filename && LONG_FORM_CONTENT_PATH_PATTERN.test(context.filename)) return;
|
|
15690
|
-
|
|
15691
|
-
|
|
15692
|
-
|
|
15693
|
-
|
|
15694
|
-
|
|
15695
|
-
|
|
15696
|
-
|
|
15697
|
-
|
|
15698
|
-
|
|
16082
|
+
create: (context) => {
|
|
16083
|
+
if (context.filename && LONG_FORM_CONTENT_PATH_PATTERN$1.test(context.filename)) return {};
|
|
16084
|
+
return {
|
|
16085
|
+
JSXExpressionContainer(node) {
|
|
16086
|
+
if (node.parent && isNodeOfType(node.parent, "JSXAttribute")) return;
|
|
16087
|
+
if (isInsideExcludedTypographyAncestor(node)) return;
|
|
16088
|
+
if (isInsideStaticallyHiddenJsxSubtree$1(node, context)) return;
|
|
16089
|
+
if (!hasProseEmDashInStaticExpression(node.expression)) return;
|
|
16090
|
+
context.report({
|
|
16091
|
+
node,
|
|
16092
|
+
message: "Em dash (—) in UI text reads like AI output to your users."
|
|
16093
|
+
});
|
|
16094
|
+
},
|
|
16095
|
+
JSXText(node) {
|
|
16096
|
+
if (!hasProseEmDash((typeof node.value === "string" ? node.value : "").replace(EM_DASH_ENTITY_PATTERN, EM_DASH))) return;
|
|
16097
|
+
if (isInsideExcludedTypographyAncestor(node)) return;
|
|
16098
|
+
if (isInsideStaticallyHiddenJsxSubtree$1(node, context)) return;
|
|
16099
|
+
context.report({
|
|
16100
|
+
node,
|
|
16101
|
+
message: "Em dash (—) in UI text reads like AI output to your users."
|
|
16102
|
+
});
|
|
16103
|
+
}
|
|
16104
|
+
};
|
|
16105
|
+
}
|
|
15699
16106
|
});
|
|
15700
16107
|
//#endregion
|
|
15701
16108
|
//#region src/plugin/rules/react-ui/utils/collect-axis-shorthand-pairs.ts
|
|
@@ -37454,19 +37861,6 @@ const getStaticMotionPropObject = (openingElement, propertyName, scopes) => {
|
|
|
37454
37861
|
return attribute.value.expression;
|
|
37455
37862
|
};
|
|
37456
37863
|
//#endregion
|
|
37457
|
-
//#region src/plugin/rules/design/utils/get-style-property-key.ts
|
|
37458
|
-
const getStylePropertyKey = (property) => getStaticPropertyKeyName(property, { allowComputedString: true });
|
|
37459
|
-
//#endregion
|
|
37460
|
-
//#region src/plugin/rules/design/utils/get-effective-style-property.ts
|
|
37461
|
-
const getEffectiveStyleProperty = (properties, propertyName) => {
|
|
37462
|
-
for (const property of [...properties ?? []].reverse()) {
|
|
37463
|
-
const currentPropertyName = getStylePropertyKey(property);
|
|
37464
|
-
if (!currentPropertyName) return null;
|
|
37465
|
-
if (currentPropertyName === propertyName && isNodeOfType(property, "Property")) return property;
|
|
37466
|
-
}
|
|
37467
|
-
return null;
|
|
37468
|
-
};
|
|
37469
|
-
//#endregion
|
|
37470
37864
|
//#region src/plugin/rules/correctness/motion-keyframe-times-mismatch.ts
|
|
37471
37865
|
const getKeyframeLengths = (animationObject) => {
|
|
37472
37866
|
const keyframeLengths = [];
|
|
@@ -37510,76 +37904,9 @@ const motionKeyframeTimesMismatch = defineRule({
|
|
|
37510
37904
|
} })
|
|
37511
37905
|
});
|
|
37512
37906
|
//#endregion
|
|
37513
|
-
//#region src/plugin/utils/parse-tailwind-class-name-token.ts
|
|
37514
|
-
const isCharacterEscaped = (value, characterIndex) => {
|
|
37515
|
-
let backslashCount = 0;
|
|
37516
|
-
for (let precedingIndex = characterIndex - 1; precedingIndex >= 0 && value[precedingIndex] === "\\"; precedingIndex -= 1) backslashCount += 1;
|
|
37517
|
-
return backslashCount % 2 === 1;
|
|
37518
|
-
};
|
|
37519
|
-
const parseTailwindClassNameToken = (rawToken) => {
|
|
37520
|
-
const variants = [];
|
|
37521
|
-
let segmentStartIndex = 0;
|
|
37522
|
-
for (const characterIndex of getTailwindTopLevelCharacterIndices(rawToken, (character) => character === ":")) {
|
|
37523
|
-
variants.push(rawToken.slice(segmentStartIndex, characterIndex));
|
|
37524
|
-
segmentStartIndex = characterIndex + 1;
|
|
37525
|
-
}
|
|
37526
|
-
let utility = rawToken.slice(segmentStartIndex);
|
|
37527
|
-
const hasTrailingImportantModifier = utility.endsWith("!") && !isCharacterEscaped(utility, utility.length - 1);
|
|
37528
|
-
const isImportant = utility.startsWith("!") || hasTrailingImportantModifier;
|
|
37529
|
-
if (utility.startsWith("!")) utility = utility.slice(1);
|
|
37530
|
-
if (hasTrailingImportantModifier) utility = utility.slice(0, -1);
|
|
37531
|
-
return {
|
|
37532
|
-
isImportant,
|
|
37533
|
-
utility,
|
|
37534
|
-
variants
|
|
37535
|
-
};
|
|
37536
|
-
};
|
|
37537
|
-
//#endregion
|
|
37538
37907
|
//#region src/plugin/utils/get-unvariant-class-name-tokens.ts
|
|
37539
37908
|
const getUnvariantClassNameTokens = (classNameValue) => splitTailwindClassName(classNameValue).map(parseTailwindClassNameToken).filter((token) => token.variants.length === 0).map((token) => token.utility);
|
|
37540
37909
|
//#endregion
|
|
37541
|
-
//#region src/plugin/rules/design/utils/get-inline-style-expression.ts
|
|
37542
|
-
const isConstObjectBindingMutated = (symbol) => symbol.references.some((reference) => {
|
|
37543
|
-
if (reference.flag !== "read") return true;
|
|
37544
|
-
let referenceExpression = reference.identifier;
|
|
37545
|
-
while (referenceExpression.parent && isNodeOfType(referenceExpression.parent, "MemberExpression") && referenceExpression.parent.object === referenceExpression) referenceExpression = referenceExpression.parent;
|
|
37546
|
-
const parent = referenceExpression.parent;
|
|
37547
|
-
return Boolean(isNodeOfType(parent, "AssignmentExpression") && parent.left === referenceExpression || isNodeOfType(parent, "UpdateExpression") && parent.argument === referenceExpression || isNodeOfType(parent, "UnaryExpression") && parent.operator === "delete" && parent.argument === referenceExpression || isNodeOfType(parent, "CallExpression") && parent.arguments?.some((argument) => argument === referenceExpression));
|
|
37548
|
-
});
|
|
37549
|
-
const resolveInlineStyleObjectExpression = (expression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
37550
|
-
const candidate = stripParenExpression(expression);
|
|
37551
|
-
if (isNodeOfType(candidate, "ObjectExpression")) return candidate;
|
|
37552
|
-
if (!scopes || !isNodeOfType(candidate, "Identifier")) return null;
|
|
37553
|
-
const symbol = scopes.symbolFor(candidate);
|
|
37554
|
-
if (!symbol || symbol.kind !== "const" || !symbol.initializer || visitedSymbolIds.has(symbol.id) || isConstObjectBindingMutated(symbol)) return null;
|
|
37555
|
-
const nextVisitedSymbolIds = new Set(visitedSymbolIds);
|
|
37556
|
-
nextVisitedSymbolIds.add(symbol.id);
|
|
37557
|
-
return resolveInlineStyleObjectExpression(symbol.initializer, scopes, nextVisitedSymbolIds);
|
|
37558
|
-
};
|
|
37559
|
-
const getInlineStyleExpression = (node, scopes) => {
|
|
37560
|
-
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "style") return null;
|
|
37561
|
-
if (!isNodeOfType(node.value, "JSXExpressionContainer")) return null;
|
|
37562
|
-
return resolveInlineStyleObjectExpression(node.value.expression, scopes);
|
|
37563
|
-
};
|
|
37564
|
-
//#endregion
|
|
37565
|
-
//#region src/plugin/rules/design/utils/get-string-from-class-name-attr.ts
|
|
37566
|
-
const getStringFromClassNameAttr = (node) => {
|
|
37567
|
-
if (!isNodeOfType(node, "JSXOpeningElement")) return null;
|
|
37568
|
-
const classAttr = getAuthoritativeJsxAttribute(node.attributes ?? [], "className");
|
|
37569
|
-
if (!classAttr?.value) return null;
|
|
37570
|
-
if (isNodeOfType(classAttr.value, "Literal") && typeof classAttr.value.value === "string") return classAttr.value.value;
|
|
37571
|
-
if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "Literal") && typeof classAttr.value.expression.value === "string") return classAttr.value.expression.value;
|
|
37572
|
-
if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "TemplateLiteral") && classAttr.value.expression.quasis?.length === 1) return classAttr.value.expression.quasis[0].value?.raw ?? null;
|
|
37573
|
-
return null;
|
|
37574
|
-
};
|
|
37575
|
-
//#endregion
|
|
37576
|
-
//#region src/plugin/rules/design/utils/get-style-property-string-value.ts
|
|
37577
|
-
const getStylePropertyStringValue = (property) => {
|
|
37578
|
-
if (!isNodeOfType(property, "Property")) return null;
|
|
37579
|
-
if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "string") return property.value.value;
|
|
37580
|
-
return null;
|
|
37581
|
-
};
|
|
37582
|
-
//#endregion
|
|
37583
37910
|
//#region src/plugin/rules/correctness/motion-layout-on-inline-element.ts
|
|
37584
37911
|
const ENABLED_LAYOUT_VALUES = new Set([
|
|
37585
37912
|
"position",
|
|
@@ -47575,6 +47902,155 @@ const noAsyncEventHandlerWithoutReentryGuard = defineRule({
|
|
|
47575
47902
|
}
|
|
47576
47903
|
});
|
|
47577
47904
|
//#endregion
|
|
47905
|
+
//#region src/plugin/rules/design/no-auto-scrolling-content.ts
|
|
47906
|
+
const MOVEMENT_CONTROL_ACTION_PATTERN = /\b(?:next|pause|play|prev|previous|resume|stop)\b/i;
|
|
47907
|
+
const MOVEMENT_CONTROL_CONTEXT_PATTERN = /\b(?:carousel|marquee|slide|slider|ticker)\b/i;
|
|
47908
|
+
const LIVE_REGION_ROLES$1 = new Set([
|
|
47909
|
+
"alert",
|
|
47910
|
+
"log",
|
|
47911
|
+
"progressbar",
|
|
47912
|
+
"status",
|
|
47913
|
+
"timer"
|
|
47914
|
+
]);
|
|
47915
|
+
const HORIZONTAL_MOTION_PROPERTY_NAMES = ["x", "translateX"];
|
|
47916
|
+
const getPercentageValue = (node) => {
|
|
47917
|
+
if (!isNodeOfType(node, "Literal") || typeof node.value !== "string") return null;
|
|
47918
|
+
const match = /^(-?(?:\d+(?:\.\d+)?|\.\d+))%$/.exec(node.value.trim());
|
|
47919
|
+
return match ? Number.parseFloat(match[1]) : null;
|
|
47920
|
+
};
|
|
47921
|
+
const getPercentageValues = (node) => {
|
|
47922
|
+
if (!isNodeOfType(node, "ArrayExpression")) {
|
|
47923
|
+
const percentageValue = getPercentageValue(node);
|
|
47924
|
+
return percentageValue === null ? null : [percentageValue];
|
|
47925
|
+
}
|
|
47926
|
+
const percentageValues = [];
|
|
47927
|
+
for (const element of node.elements) {
|
|
47928
|
+
if (!element || isNodeOfType(element, "SpreadElement")) return null;
|
|
47929
|
+
const percentageValue = getPercentageValue(element);
|
|
47930
|
+
if (percentageValue === null) return null;
|
|
47931
|
+
percentageValues.push(percentageValue);
|
|
47932
|
+
}
|
|
47933
|
+
return percentageValues.length > 0 ? percentageValues : null;
|
|
47934
|
+
};
|
|
47935
|
+
const isInfiniteRepeatProperty = (property, context) => isNodeOfType(property, "Property") && (isNodeOfType(property.value, "Identifier") && property.value.name === "Infinity" && context.scopes.isGlobalReference(property.value) || isNodeOfType(property.value, "Literal") && property.value.value === Infinity);
|
|
47936
|
+
const hasInfiniteMotionRepeat = (openingElement, animateObject, context) => {
|
|
47937
|
+
const transitionObjects = [];
|
|
47938
|
+
const transitionObject = getStaticMotionPropObject(openingElement, "transition", context.scopes);
|
|
47939
|
+
if (transitionObject) transitionObjects.push(transitionObject);
|
|
47940
|
+
const nestedTransitionProperty = getEffectiveStyleProperty(animateObject.properties, "transition");
|
|
47941
|
+
if (nestedTransitionProperty && isNodeOfType(nestedTransitionProperty.value, "ObjectExpression")) transitionObjects.push(nestedTransitionProperty.value);
|
|
47942
|
+
return transitionObjects.some((candidate) => {
|
|
47943
|
+
const repeatProperty = getEffectiveStyleProperty(candidate.properties, "repeat");
|
|
47944
|
+
return Boolean(repeatProperty && isInfiniteRepeatProperty(repeatProperty, context));
|
|
47945
|
+
});
|
|
47946
|
+
};
|
|
47947
|
+
const getHorizontalTravel = (openingElement, animateObject, context) => {
|
|
47948
|
+
const initialObject = getStaticMotionPropObject(openingElement, "initial", context.scopes);
|
|
47949
|
+
for (const propertyName of HORIZONTAL_MOTION_PROPERTY_NAMES) {
|
|
47950
|
+
const animateProperty = getEffectiveStyleProperty(animateObject.properties, propertyName);
|
|
47951
|
+
if (!animateProperty) continue;
|
|
47952
|
+
const animateValues = getPercentageValues(animateProperty.value);
|
|
47953
|
+
if (!animateValues) continue;
|
|
47954
|
+
const percentageValues = [...animateValues];
|
|
47955
|
+
if (percentageValues.length === 1 && initialObject) {
|
|
47956
|
+
const initialProperty = getEffectiveStyleProperty(initialObject.properties, propertyName);
|
|
47957
|
+
if (!initialProperty) continue;
|
|
47958
|
+
const initialValues = getPercentageValues(initialProperty.value);
|
|
47959
|
+
if (!initialValues || initialValues.length !== 1) continue;
|
|
47960
|
+
percentageValues.push(initialValues[0]);
|
|
47961
|
+
}
|
|
47962
|
+
if (percentageValues.length < 2) continue;
|
|
47963
|
+
return Math.max(...percentageValues) - Math.min(...percentageValues);
|
|
47964
|
+
}
|
|
47965
|
+
return null;
|
|
47966
|
+
};
|
|
47967
|
+
const hasDynamicJsxAttributeValue = (attribute) => {
|
|
47968
|
+
if (!attribute.value || isNodeOfType(attribute.value, "Literal")) return false;
|
|
47969
|
+
if (!isNodeOfType(attribute.value, "JSXExpressionContainer")) return true;
|
|
47970
|
+
const expression = attribute.value.expression;
|
|
47971
|
+
return !(isNodeOfType(expression, "Literal") || isNodeOfType(expression, "TemplateLiteral") && expression.expressions.length === 0);
|
|
47972
|
+
};
|
|
47973
|
+
const hasUnresolvedTextTrackContent = (node) => {
|
|
47974
|
+
if (isNodeOfType(node, "JSXExpressionContainer")) {
|
|
47975
|
+
const expression = node.expression;
|
|
47976
|
+
if (isNodeOfType(expression, "Literal") || isNodeOfType(expression, "TemplateLiteral") && expression.expressions.length === 0 || isNodeOfType(expression, "JSXEmptyExpression")) return false;
|
|
47977
|
+
if (isNodeOfType(expression, "JSXElement") || isNodeOfType(expression, "JSXFragment")) return hasUnresolvedTextTrackContent(expression);
|
|
47978
|
+
return true;
|
|
47979
|
+
}
|
|
47980
|
+
if (isNodeOfType(node, "JSXElement")) {
|
|
47981
|
+
if (node.openingElement.selfClosing || !isNodeOfType(node.openingElement.name, "JSXIdentifier") || !/^[a-z]/.test(node.openingElement.name.name) || hasJsxSpreadAttribute(node.openingElement.attributes) || node.openingElement.attributes.some((attribute) => isNodeOfType(attribute, "JSXAttribute") && hasDynamicJsxAttributeValue(attribute))) return true;
|
|
47982
|
+
return node.children.some(hasUnresolvedTextTrackContent);
|
|
47983
|
+
}
|
|
47984
|
+
if (isNodeOfType(node, "JSXFragment")) return node.children.some(hasUnresolvedTextTrackContent);
|
|
47985
|
+
return isNodeOfType(node, "JSXSpreadChild");
|
|
47986
|
+
};
|
|
47987
|
+
const hasUnresolvedOrLiveSemantics = (openingElement, context) => {
|
|
47988
|
+
if (hasJsxSpreadAttribute(openingElement.attributes)) return true;
|
|
47989
|
+
const roleAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "role");
|
|
47990
|
+
const roleValues = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : [];
|
|
47991
|
+
if (!roleValues) return true;
|
|
47992
|
+
if (roleValues.some((role) => role.toLowerCase().split(/\s+/).some((roleToken) => LIVE_REGION_ROLES$1.has(roleToken)))) return true;
|
|
47993
|
+
const ariaLiveAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "aria-live");
|
|
47994
|
+
if (!ariaLiveAttribute) return false;
|
|
47995
|
+
const ariaLiveValues = getJsxPropStaticStringValues(ariaLiveAttribute, context.scopes);
|
|
47996
|
+
return !ariaLiveValues || ariaLiveValues.some((value) => value.toLowerCase() !== "off");
|
|
47997
|
+
};
|
|
47998
|
+
const isInsideUnresolvedOrLiveRegion = (element, context) => {
|
|
47999
|
+
let currentNode = element;
|
|
48000
|
+
while (currentNode) {
|
|
48001
|
+
if (isNodeOfType(currentNode, "JSXElement") && hasUnresolvedOrLiveSemantics(currentNode.openingElement, context)) return true;
|
|
48002
|
+
currentNode = currentNode.parent;
|
|
48003
|
+
}
|
|
48004
|
+
return false;
|
|
48005
|
+
};
|
|
48006
|
+
const findNearestJsxContainer = (element) => {
|
|
48007
|
+
let ancestor = element.parent;
|
|
48008
|
+
while (ancestor) {
|
|
48009
|
+
if (isNodeOfType(ancestor, "JSXElement")) return ancestor;
|
|
48010
|
+
ancestor = ancestor.parent;
|
|
48011
|
+
}
|
|
48012
|
+
return null;
|
|
48013
|
+
};
|
|
48014
|
+
const hasPauseOrCarouselControl = (containers, movingElement, context) => {
|
|
48015
|
+
const movingIdAttribute = getAuthoritativeJsxAttribute(movingElement.openingElement.attributes, "id");
|
|
48016
|
+
const movingIds = movingIdAttribute ? getJsxPropStaticStringValues(movingIdAttribute, context.scopes) : [];
|
|
48017
|
+
return containers.flatMap((container) => getStaticJsxDescendantOpeningElements(container)).some((openingElement) => {
|
|
48018
|
+
if (getElementType(openingElement, context.settings) !== "button") return false;
|
|
48019
|
+
const ariaLabelAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "aria-label");
|
|
48020
|
+
const ariaLabelValues = ariaLabelAttribute ? getJsxPropStaticStringValues(ariaLabelAttribute, context.scopes) : [];
|
|
48021
|
+
const buttonElement = openingElement.parent;
|
|
48022
|
+
const visibleLabel = isNodeOfType(buttonElement, "JSXElement") ? getStaticJsxText(buttonElement).trim() : "";
|
|
48023
|
+
const controlLabels = [...ariaLabelValues ?? [], visibleLabel].filter(Boolean);
|
|
48024
|
+
if (!controlLabels.some((label) => MOVEMENT_CONTROL_ACTION_PATTERN.test(label))) return false;
|
|
48025
|
+
if (controlLabels.some((label) => MOVEMENT_CONTROL_CONTEXT_PATTERN.test(label))) return true;
|
|
48026
|
+
const ariaControlsAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "aria-controls");
|
|
48027
|
+
const controlledIds = ariaControlsAttribute ? getJsxPropStaticStringValues(ariaControlsAttribute, context.scopes) : [];
|
|
48028
|
+
return Boolean(movingIds && controlledIds && controlledIds.some((controlledId) => movingIds.includes(controlledId)));
|
|
48029
|
+
});
|
|
48030
|
+
};
|
|
48031
|
+
const noAutoScrollingContent = defineRule({
|
|
48032
|
+
id: "no-auto-scrolling-content",
|
|
48033
|
+
title: "Content auto-scrolls forever",
|
|
48034
|
+
severity: "warn",
|
|
48035
|
+
defaultEnabled: false,
|
|
48036
|
+
tags: ["design", "test-noise"],
|
|
48037
|
+
recommendation: "Keep content still so readers control the pace, or provide an accessible pause control for essential live movement.",
|
|
48038
|
+
create: (context) => ({ JSXElement(node) {
|
|
48039
|
+
if (isInsideUnresolvedOrLiveRegion(node, context)) return;
|
|
48040
|
+
if (node.children.some(hasUnresolvedTextTrackContent) || !getStaticJsxText(node).trim()) return;
|
|
48041
|
+
const animateObject = getStaticMotionPropObject(node.openingElement, "animate", context.scopes);
|
|
48042
|
+
if (!animateObject || !hasInfiniteMotionRepeat(node.openingElement, animateObject, context)) return;
|
|
48043
|
+
const horizontalTravel = getHorizontalTravel(node.openingElement, animateObject, context);
|
|
48044
|
+
if (horizontalTravel === null || horizontalTravel < 20) return;
|
|
48045
|
+
const container = findNearestJsxContainer(node);
|
|
48046
|
+
if (hasPauseOrCarouselControl([container, container ? findNearestJsxContainer(container) : null].filter((candidate) => candidate !== null), node, context)) return;
|
|
48047
|
+
context.report({
|
|
48048
|
+
node: node.openingElement,
|
|
48049
|
+
message: "This content moves horizontally on an endless loop, so readers cannot control its pace. Keep it still or provide an accessible pause control."
|
|
48050
|
+
});
|
|
48051
|
+
} })
|
|
48052
|
+
});
|
|
48053
|
+
//#endregion
|
|
47578
48054
|
//#region src/plugin/rules/a11y/no-autofocus.ts
|
|
47579
48055
|
const MESSAGE$56 = "`autoFocus` moves focus on load, which can disrupt screen reader and keyboard users. Remove it and let users choose where to focus.";
|
|
47580
48056
|
const resolveSettings$21 = (settings) => {
|
|
@@ -50907,9 +51383,9 @@ const PADDING_SIDES_BY_PREFIX$1 = new Map([
|
|
|
50907
51383
|
]);
|
|
50908
51384
|
const TAILWIND_PADDING_PATTERN = /^(p[trblesxy]?)-(px|[\d.]+)$/;
|
|
50909
51385
|
const ARBITRARY_PADDING_PATTERN = /^(p[trblesxy]?)-\[([\d.]+)(px|rem)\]$/;
|
|
50910
|
-
const TAILWIND_BACKGROUND_COLOR_PATTERN = /^bg-(?!opacity-|auto$|center$|clip-|contain$|cover$|fixed$|left$|local$|none$|origin-|repeat|right$|scroll$|top$|\[(?:length|position|size):).+/;
|
|
51386
|
+
const TAILWIND_BACKGROUND_COLOR_PATTERN$2 = /^bg-(?!opacity-|auto$|center$|clip-|contain$|cover$|fixed$|left$|local$|none$|origin-|repeat|right$|scroll$|top$|\[(?:length|position|size):).+/;
|
|
50911
51387
|
const TAILWIND_BORDER_GEOMETRY_PATTERN = /^border(?:-[trblxy])?(?:(?:-(?:px|\d+(?:\.\d+)?|\[\d+(?:\.\d+)?px\]))|-(?:hidden|none|solid|dashed|dotted|double))?$/;
|
|
50912
|
-
const TAILWIND_SHADOW_GEOMETRY_PATTERN = /^(?:ring(?:-(?:px|\d+(?:\.\d+)?|\[\d+(?:\.\d+)?px\]))?|shadow(?:-none|-(?:2xl|inner|lg|md|sm|xl|xs))?)$/;
|
|
51388
|
+
const TAILWIND_SHADOW_GEOMETRY_PATTERN$1 = /^(?:ring(?:-(?:px|\d+(?:\.\d+)?|\[\d+(?:\.\d+)?px\]))?|shadow(?:-none|-(?:2xl|inner|lg|md|sm|xl|xs))?)$/;
|
|
50913
51389
|
const BOUNDED_CONTAINER_TAG_NAMES = new Set([
|
|
50914
51390
|
"article",
|
|
50915
51391
|
"aside",
|
|
@@ -51067,9 +51543,9 @@ const noCrampedContainerPadding = defineRule({
|
|
|
51067
51543
|
if (classNameValue && hasCapabilityOrUnspecified(context.settings, "tailwind")) {
|
|
51068
51544
|
const tokens = getUnvariantClassNameTokensWithImportantModifiers(classNameValue);
|
|
51069
51545
|
const paddingResolution = getTailwindPaddingResolution(tokens);
|
|
51070
|
-
const backgroundResolution = getTailwindUtilityResolution(tokens, (utility) => TAILWIND_BACKGROUND_COLOR_PATTERN.test(utility));
|
|
51546
|
+
const backgroundResolution = getTailwindUtilityResolution(tokens, (utility) => TAILWIND_BACKGROUND_COLOR_PATTERN$2.test(utility));
|
|
51071
51547
|
const borderResolution = getTailwindUtilityResolution(tokens, (utility) => TAILWIND_BORDER_GEOMETRY_PATTERN.test(utility));
|
|
51072
|
-
const shadowResolution = getTailwindUtilityResolution(tokens, (utility) => TAILWIND_SHADOW_GEOMETRY_PATTERN.test(utility));
|
|
51548
|
+
const shadowResolution = getTailwindUtilityResolution(tokens, (utility) => TAILWIND_SHADOW_GEOMETRY_PATTERN$1.test(utility));
|
|
51073
51549
|
const isBackgroundProtected = backgroundResolution.isImportant || backgroundResolution.isAmbiguous;
|
|
51074
51550
|
const isBorderProtected = borderResolution.isImportant || borderResolution.isAmbiguous;
|
|
51075
51551
|
const isShadowProtected = shadowResolution.isImportant || shadowResolution.isAmbiguous;
|
|
@@ -53286,7 +53762,7 @@ const splitShadowLayers = (shadowValue) => {
|
|
|
53286
53762
|
};
|
|
53287
53763
|
const RGB_COLOR_PATTERN = /rgba?\([^)]*\)/i;
|
|
53288
53764
|
const HEX_COLOR_PATTERN = /#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{4}|[0-9a-f]{3})\b/i;
|
|
53289
|
-
const ZERO_ALPHA_PATTERN = /^[+-]?(?:0+(?:\.0*)?|\.0+)%?$/;
|
|
53765
|
+
const ZERO_ALPHA_PATTERN$1 = /^[+-]?(?:0+(?:\.0*)?|\.0+)%?$/;
|
|
53290
53766
|
const isAtTopLevel = (value, index) => {
|
|
53291
53767
|
let parenthesisDepth = 0;
|
|
53292
53768
|
for (let characterIndex = 0; characterIndex < index; characterIndex += 1) {
|
|
@@ -53307,9 +53783,9 @@ const isShadowLayerFullyTransparent = (layer) => {
|
|
|
53307
53783
|
if (rgbMatch?.index === void 0 || !isAtTopLevel(layer, rgbMatch.index)) return false;
|
|
53308
53784
|
const colorArguments = rgbMatch[0].slice(rgbMatch[0].indexOf("(") + 1, -1);
|
|
53309
53785
|
const slashIndex = colorArguments.lastIndexOf("/");
|
|
53310
|
-
if (slashIndex !== -1) return ZERO_ALPHA_PATTERN.test(colorArguments.slice(slashIndex + 1).trim());
|
|
53786
|
+
if (slashIndex !== -1) return ZERO_ALPHA_PATTERN$1.test(colorArguments.slice(slashIndex + 1).trim());
|
|
53311
53787
|
const legacyArguments = colorArguments.split(",");
|
|
53312
|
-
return legacyArguments.length === 4 && ZERO_ALPHA_PATTERN.test(legacyArguments[3].trim());
|
|
53788
|
+
return legacyArguments.length === 4 && ZERO_ALPHA_PATTERN$1.test(legacyArguments[3].trim());
|
|
53313
53789
|
};
|
|
53314
53790
|
const extractColorFromShadowLayer = (layer) => {
|
|
53315
53791
|
const colorMatch = layer.match(RGB_COLOR_PATTERN) ?? layer.match(HEX_COLOR_PATTERN);
|
|
@@ -53385,7 +53861,7 @@ const hasStrongBlur = (tokens) => tokens.some((token) => {
|
|
|
53385
53861
|
const match = token.match(ARBITRARY_BLUR_PATTERN);
|
|
53386
53862
|
return Boolean(match && parseFloat(match[1]) >= 24);
|
|
53387
53863
|
});
|
|
53388
|
-
const hasOnlyWhitespaceChildren = (node) => node.children.every((child) => isNodeOfType(child, "JSXText") && (child.value ?? "").trim().length === 0);
|
|
53864
|
+
const hasOnlyWhitespaceChildren$1 = (node) => node.children.every((child) => isNodeOfType(child, "JSXText") && (child.value ?? "").trim().length === 0);
|
|
53389
53865
|
const noDecorativeBlurOrb = defineRule({
|
|
53390
53866
|
id: "no-decorative-blur-orb",
|
|
53391
53867
|
title: "Empty blurred color orb decorates the layout",
|
|
@@ -53394,7 +53870,7 @@ const noDecorativeBlurOrb = defineRule({
|
|
|
53394
53870
|
tags: ["design", "test-noise"],
|
|
53395
53871
|
recommendation: "Use structure, imagery, or a restrained surface treatment instead of an oversized blurred color blob.",
|
|
53396
53872
|
create: (context) => ({ JSXElement(node) {
|
|
53397
|
-
if (!isNodeOfType(node.openingElement.name, "JSXIdentifier") || !DECORATIVE_ORB_ELEMENT_NAMES.has(node.openingElement.name.name) || !hasOnlyWhitespaceChildren(node)) return;
|
|
53873
|
+
if (!isNodeOfType(node.openingElement.name, "JSXIdentifier") || !DECORATIVE_ORB_ELEMENT_NAMES.has(node.openingElement.name.name) || !hasOnlyWhitespaceChildren$1(node)) return;
|
|
53398
53874
|
const classNameValue = getStringFromClassNameAttr(node.openingElement);
|
|
53399
53875
|
if (!classNameValue) return;
|
|
53400
53876
|
const tokens = getUnvariantClassNameTokens(classNameValue);
|
|
@@ -53424,9 +53900,115 @@ const isDataVisualizationContext = (node, filename) => {
|
|
|
53424
53900
|
return false;
|
|
53425
53901
|
};
|
|
53426
53902
|
//#endregion
|
|
53903
|
+
//#region src/plugin/rules/design/utils/split-css-top-level.ts
|
|
53904
|
+
const splitCssTopLevel = (value, separator) => {
|
|
53905
|
+
const parts = [];
|
|
53906
|
+
let depth = 0;
|
|
53907
|
+
let partStartIndex = 0;
|
|
53908
|
+
for (let characterIndex = 0; characterIndex < value.length; characterIndex += 1) {
|
|
53909
|
+
const character = value[characterIndex];
|
|
53910
|
+
if (character === "(") depth += 1;
|
|
53911
|
+
if (character === ")") {
|
|
53912
|
+
depth -= 1;
|
|
53913
|
+
if (depth < 0) return null;
|
|
53914
|
+
}
|
|
53915
|
+
if (character === separator && depth === 0) {
|
|
53916
|
+
parts.push(value.slice(partStartIndex, characterIndex).trim());
|
|
53917
|
+
partStartIndex = characterIndex + 1;
|
|
53918
|
+
}
|
|
53919
|
+
}
|
|
53920
|
+
if (depth !== 0) return null;
|
|
53921
|
+
parts.push(value.slice(partStartIndex).trim());
|
|
53922
|
+
return parts;
|
|
53923
|
+
};
|
|
53924
|
+
//#endregion
|
|
53427
53925
|
//#region src/plugin/rules/design/no-decorative-grid-background.ts
|
|
53428
|
-
const
|
|
53429
|
-
|
|
53926
|
+
const TAILWIND_BACKGROUND_SHORTHAND_PROPERTY_PATTERN = /^\[background:[\s\S]+\]$/i;
|
|
53927
|
+
const TAILWIND_BACKGROUND_PROPERTY_PATTERN = /^\[(?:background|background-image):[\s\S]+\]$/i;
|
|
53928
|
+
const TAILWIND_BACKGROUND_SIZE_PROPERTY_PATTERN = /^\[background-size:[\s\S]+\]$/i;
|
|
53929
|
+
const TAILWIND_BACKGROUND_IMAGE_PATTERN = /^(?:bg-none|bg-(?:conic|linear|radial)(?:-|$)|bg-\[(?!length:)[\s\S]+\])$/i;
|
|
53930
|
+
const TAILWIND_BACKGROUND_SIZE_PATTERN = /^(?:bg-(?:auto|contain|cover)|bg-\[length:[\s\S]+\])$/i;
|
|
53931
|
+
const getLinearGradientBodies = (value) => {
|
|
53932
|
+
const gradientBodies = [];
|
|
53933
|
+
for (const match of value.matchAll(/(?:-(?:moz|ms|o|webkit)-)?linear-gradient\(/gi)) {
|
|
53934
|
+
if (match.index === void 0) continue;
|
|
53935
|
+
const prefix = value.slice(0, match.index).toLowerCase();
|
|
53936
|
+
if (prefix.endsWith("repeating-") || /[\w-]$/.test(prefix)) continue;
|
|
53937
|
+
const bodyStartIndex = match.index + match[0].length;
|
|
53938
|
+
let parenthesisDepth = 1;
|
|
53939
|
+
for (let characterIndex = bodyStartIndex; characterIndex < value.length; characterIndex += 1) {
|
|
53940
|
+
const character = value[characterIndex];
|
|
53941
|
+
if (character === "(") parenthesisDepth += 1;
|
|
53942
|
+
if (character !== ")") continue;
|
|
53943
|
+
parenthesisDepth -= 1;
|
|
53944
|
+
if (parenthesisDepth !== 0) continue;
|
|
53945
|
+
gradientBodies.push(value.slice(bodyStartIndex, characterIndex));
|
|
53946
|
+
break;
|
|
53947
|
+
}
|
|
53948
|
+
}
|
|
53949
|
+
return gradientBodies;
|
|
53950
|
+
};
|
|
53951
|
+
const getHairlineGradientAxis = (gradientBody) => {
|
|
53952
|
+
const normalizedBody = gradientBody.trim().toLowerCase();
|
|
53953
|
+
const hasLeadingHairline = /\b1(?:\.0+)?px\s*,\s*transparent\s+1(?:\.0+)?px\b/i.test(normalizedBody);
|
|
53954
|
+
const hasInvertedHairline = /transparent\s+calc\(\s*100%\s*-\s*1(?:\.0+)?px\s*\)\s*,[\s\S]*\b1(?:\.0+)?px\b/i.test(normalizedBody);
|
|
53955
|
+
if (!hasLeadingHairline && !hasInvertedHairline) return null;
|
|
53956
|
+
if (/^(?:to\s+(?:left|right)|(?:90|270)(?:\.0+)?deg)\s*,/i.test(normalizedBody)) return "vertical";
|
|
53957
|
+
if (/^(?:to\s+(?:top|bottom)|(?:0|180|360)(?:\.0+)?deg)\s*,/i.test(normalizedBody)) return "horizontal";
|
|
53958
|
+
if (/^(?:to\b|[-+.\d]+(?:deg|grad|rad|turn)\b|in\s)/i.test(normalizedBody)) return null;
|
|
53959
|
+
return "horizontal";
|
|
53960
|
+
};
|
|
53961
|
+
const getFixedPixelTileDimensionCount = (value, isShorthand) => {
|
|
53962
|
+
const layers = splitCssTopLevel(value, ",");
|
|
53963
|
+
if (!layers) return 0;
|
|
53964
|
+
let maximumDimensionCount = 0;
|
|
53965
|
+
for (const layer of layers) {
|
|
53966
|
+
let sizeValue = layer.trim();
|
|
53967
|
+
if (isShorthand) {
|
|
53968
|
+
const shorthandParts = splitCssTopLevel(layer, "/");
|
|
53969
|
+
if (!shorthandParts || shorthandParts.length !== 2 || !shorthandParts[1]) continue;
|
|
53970
|
+
sizeValue = shorthandParts[1];
|
|
53971
|
+
}
|
|
53972
|
+
const sizeMatch = sizeValue.match(/^(\d+(?:\.\d+)?)px(?:\s+(\d+(?:\.\d+)?)px)?(?:\s+(?:no-repeat|repeat|round|space)){0,2}$/i);
|
|
53973
|
+
if (!sizeMatch || Number.parseFloat(sizeMatch[1]) <= 0) continue;
|
|
53974
|
+
if (sizeMatch[2] && Number.parseFloat(sizeMatch[2]) <= 0) continue;
|
|
53975
|
+
maximumDimensionCount = Math.max(maximumDimensionCount, sizeMatch[2] ? 2 : 1);
|
|
53976
|
+
}
|
|
53977
|
+
return maximumDimensionCount;
|
|
53978
|
+
};
|
|
53979
|
+
const isDecorativeGridValue = (backgroundValue, backgroundSizeValue, shouldUseBackgroundShorthandSize, isBackgroundSizeShorthand) => {
|
|
53980
|
+
const hairlineAxes = getLinearGradientBodies(backgroundValue).map(getHairlineGradientAxis).filter((axis) => axis !== null);
|
|
53981
|
+
if (hairlineAxes.length === 0) return false;
|
|
53982
|
+
const tileDimensionCount = Math.max(shouldUseBackgroundShorthandSize ? getFixedPixelTileDimensionCount(backgroundValue, true) : 0, backgroundSizeValue ? getFixedPixelTileDimensionCount(backgroundSizeValue, isBackgroundSizeShorthand) : 0);
|
|
53983
|
+
if (hairlineAxes.length >= 2) return new Set(hairlineAxes).size >= 2 && tileDimensionCount >= 1;
|
|
53984
|
+
return tileDimensionCount >= 2;
|
|
53985
|
+
};
|
|
53986
|
+
const getTailwindBackgroundValue = (utility) => {
|
|
53987
|
+
const propertyMatch = utility.match(/^\[([^:\]]+):([\s\S]+)\]$/);
|
|
53988
|
+
if (propertyMatch?.[1] && propertyMatch[2] && ["background", "background-image"].includes(propertyMatch[1].toLowerCase())) return normalizeTailwindArbitraryUtilityValue(propertyMatch[2]);
|
|
53989
|
+
const backgroundMatch = utility.match(/^bg-\[(?:image:)?([\s\S]+)\]$/);
|
|
53990
|
+
if (backgroundMatch?.[1]) return normalizeTailwindArbitraryUtilityValue(backgroundMatch[1]);
|
|
53991
|
+
return null;
|
|
53992
|
+
};
|
|
53993
|
+
const getTailwindBackgroundSizeValue = (utility) => {
|
|
53994
|
+
const propertyMatch = utility.match(/^\[background-size:([\s\S]+)\]$/i);
|
|
53995
|
+
if (propertyMatch?.[1]) return normalizeTailwindArbitraryUtilityValue(propertyMatch[1]);
|
|
53996
|
+
const sizeMatch = utility.match(/^bg-\[length:([\s\S]+)\]$/i);
|
|
53997
|
+
if (sizeMatch?.[1]) return normalizeTailwindArbitraryUtilityValue(sizeMatch[1]);
|
|
53998
|
+
return /^(?:bg-auto|bg-contain|bg-cover)$/i.test(utility) ? utility.slice(3) : null;
|
|
53999
|
+
};
|
|
54000
|
+
const isDecorativeTailwindGrid = (classNameValue, backgroundSizeOverride) => {
|
|
54001
|
+
const tokens = splitTailwindClassName(classNameValue);
|
|
54002
|
+
const backgroundResolution = resolveEffectiveTailwindClassNameToken(tokens, (utility) => Boolean(TAILWIND_BACKGROUND_PROPERTY_PATTERN.test(utility) || TAILWIND_BACKGROUND_IMAGE_PATTERN.test(utility)));
|
|
54003
|
+
const backgroundSizeResolution = resolveEffectiveTailwindClassNameToken(tokens, (utility) => Boolean(TAILWIND_BACKGROUND_SHORTHAND_PROPERTY_PATTERN.test(utility) || TAILWIND_BACKGROUND_SIZE_PROPERTY_PATTERN.test(utility) || TAILWIND_BACKGROUND_SIZE_PATTERN.test(utility)));
|
|
54004
|
+
if (backgroundResolution.isAmbiguous || backgroundSizeResolution.isAmbiguous) return false;
|
|
54005
|
+
const backgroundUtility = backgroundResolution.utility;
|
|
54006
|
+
if (!backgroundUtility) return false;
|
|
54007
|
+
const backgroundValue = getTailwindBackgroundValue(backgroundUtility);
|
|
54008
|
+
if (!backgroundValue) return false;
|
|
54009
|
+
const backgroundSizeUtility = backgroundSizeResolution.utility;
|
|
54010
|
+
const hasExplicitBackgroundSize = backgroundSizeOverride !== void 0 || Boolean(backgroundSizeUtility && !TAILWIND_BACKGROUND_SHORTHAND_PROPERTY_PATTERN.test(backgroundSizeUtility));
|
|
54011
|
+
return isDecorativeGridValue(backgroundValue, backgroundSizeOverride ?? (backgroundSizeUtility ? getTailwindBackgroundSizeValue(backgroundSizeUtility) : null), !hasExplicitBackgroundSize, false);
|
|
53430
54012
|
};
|
|
53431
54013
|
const noDecorativeGridBackground = defineRule({
|
|
53432
54014
|
id: "no-decorative-grid-background",
|
|
@@ -53437,34 +54019,70 @@ const noDecorativeGridBackground = defineRule({
|
|
|
53437
54019
|
recommendation: "Reserve coordinate grids for data or spatial interfaces; use a quieter surface for decoration.",
|
|
53438
54020
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
53439
54021
|
if (isDataVisualizationContext(node, context.filename)) return;
|
|
53440
|
-
|
|
53441
|
-
|
|
53442
|
-
|
|
53443
|
-
|
|
53444
|
-
|
|
53445
|
-
|
|
53446
|
-
return;
|
|
53447
|
-
|
|
53448
|
-
|
|
53449
|
-
|
|
53450
|
-
|
|
53451
|
-
|
|
53452
|
-
|
|
53453
|
-
|
|
53454
|
-
|
|
53455
|
-
|
|
53456
|
-
if (
|
|
53457
|
-
|
|
53458
|
-
|
|
53459
|
-
|
|
53460
|
-
|
|
54022
|
+
let inlineBackgroundSizeValue;
|
|
54023
|
+
const styleAttribute = getAuthoritativeJsxAttribute(node.attributes ?? [], "style");
|
|
54024
|
+
if (!styleAttribute && hasJsxSpreadThatMayProvideAttribute(node.attributes ?? [], "style")) return;
|
|
54025
|
+
if (styleAttribute) {
|
|
54026
|
+
const styleExpression = getInlineStyleExpression(styleAttribute, context.scopes);
|
|
54027
|
+
if (!styleExpression) return;
|
|
54028
|
+
if (styleExpression.properties.some((property) => getStylePropertyKey(property) === null)) return;
|
|
54029
|
+
let backgroundProperty = null;
|
|
54030
|
+
let backgroundSizeProperty = null;
|
|
54031
|
+
for (const property of styleExpression.properties) {
|
|
54032
|
+
if (!isNodeOfType(property, "Property")) return;
|
|
54033
|
+
const propertyName = getStylePropertyKey(property);
|
|
54034
|
+
if (propertyName === "background") {
|
|
54035
|
+
backgroundProperty = property;
|
|
54036
|
+
backgroundSizeProperty = property;
|
|
54037
|
+
}
|
|
54038
|
+
if (propertyName === "backgroundImage") backgroundProperty = property;
|
|
54039
|
+
if (propertyName === "backgroundSize") backgroundSizeProperty = property;
|
|
54040
|
+
}
|
|
54041
|
+
if (backgroundProperty || backgroundSizeProperty) {
|
|
54042
|
+
const backgroundValue = backgroundProperty ? getStylePropertyStringValue(backgroundProperty) : null;
|
|
54043
|
+
const backgroundSizeValue = backgroundSizeProperty ? getStylePropertyStringValue(backgroundSizeProperty) : null;
|
|
54044
|
+
if (backgroundSizeProperty && backgroundSizeProperty !== backgroundProperty && backgroundSizeValue === null) return;
|
|
54045
|
+
if (backgroundSizeProperty !== backgroundProperty && backgroundSizeValue) inlineBackgroundSizeValue = backgroundSizeValue;
|
|
54046
|
+
if (backgroundValue && isDecorativeGridValue(backgroundValue, backgroundSizeValue, backgroundSizeProperty === backgroundProperty, Boolean(backgroundSizeProperty && getStylePropertyKey(backgroundSizeProperty) === "background"))) {
|
|
54047
|
+
context.report({
|
|
54048
|
+
node: backgroundProperty ?? styleAttribute,
|
|
54049
|
+
message: "This fixed-pixel background draws a decorative coordinate grid. Use it only when the grid conveys spatial information."
|
|
54050
|
+
});
|
|
54051
|
+
return;
|
|
54052
|
+
}
|
|
54053
|
+
if (backgroundProperty) return;
|
|
53461
54054
|
}
|
|
53462
54055
|
}
|
|
54056
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
54057
|
+
if (classNameValue && isDecorativeTailwindGrid(classNameValue, inlineBackgroundSizeValue)) context.report({
|
|
54058
|
+
node,
|
|
54059
|
+
message: "This fixed-pixel grid is decorative rather than functional. Simplify the surface or tie the grid to spatial content."
|
|
54060
|
+
});
|
|
53463
54061
|
} })
|
|
53464
54062
|
});
|
|
53465
54063
|
//#endregion
|
|
53466
54064
|
//#region src/plugin/rules/design/no-decorative-pulse.ts
|
|
53467
54065
|
const BUSY_TEXT_PATTERN = /\b(?:loading|processing|saving|syncing|uploading)\b/i;
|
|
54066
|
+
const CURSOR_GLYPH_PATTERN = /^[_|▀-▟■▮❙❚|]$/u;
|
|
54067
|
+
const CURSOR_ANIMATION_NAME_PATTERN = /(?:^|[-_\s])(?:blink|caret|cursor|pulse)(?:$|[-_\s])/i;
|
|
54068
|
+
const INFINITE_ANIMATION_TOKEN_PATTERN = /(?:^|\s)infinite(?:$|\s)/i;
|
|
54069
|
+
const HERO_CONTEXT_NAME_PATTERN = /(?:Hero|Landing|Marketing|Masthead)/;
|
|
54070
|
+
const HERO_CONTEXT_CLASS_PATTERN = /(?:^|[-_:])(?:hero|landing|marketing|masthead)(?:$|[-_:])/i;
|
|
54071
|
+
const PREFORMATTED_CONTEXT_NAME_PATTERN = /(?:Code|Console|Diff|Editor|Syntax|Terminal)/;
|
|
54072
|
+
const PREFORMATTED_CONTEXT_CLASS_PATTERN = /(?:^|[-_:])(?:code|console|diff|editor|syntax|terminal)(?:$|[-_:])/i;
|
|
54073
|
+
const EXCLUDED_CONTENT_PATH_PATTERN$1 = /(?:^|[/\\])(?:docs?|documentation)(?:[/\\]|$)/i;
|
|
54074
|
+
const CURSOR_EXEMPT_ROLES = new Set([
|
|
54075
|
+
"progressbar",
|
|
54076
|
+
"status",
|
|
54077
|
+
"textbox"
|
|
54078
|
+
]);
|
|
54079
|
+
const CURSOR_EXEMPT_ELEMENT_NAMES = new Set([
|
|
54080
|
+
"code",
|
|
54081
|
+
"input",
|
|
54082
|
+
"pre",
|
|
54083
|
+
"textarea"
|
|
54084
|
+
]);
|
|
54085
|
+
const TAILWIND_ANIMATION_UTILITY_PATTERN = /^(?:animate-|\[animation:)/;
|
|
53468
54086
|
const getStaticAttributeValue = (attribute) => {
|
|
53469
54087
|
const value = attribute.value;
|
|
53470
54088
|
if (!value) return true;
|
|
@@ -53479,17 +54097,141 @@ const isBusyStatus = (openingElement) => {
|
|
|
53479
54097
|
const roleValue = roleAttribute && getStaticAttributeValue(roleAttribute);
|
|
53480
54098
|
return roleValue === "status" || roleValue === "progressbar";
|
|
53481
54099
|
};
|
|
54100
|
+
const getStaticCursorText = (node) => {
|
|
54101
|
+
if (isNodeOfType(node, "JSXText")) return node.value;
|
|
54102
|
+
if (isNodeOfType(node, "Literal")) return typeof node.value === "string" ? node.value : null;
|
|
54103
|
+
if (isNodeOfType(node, "TemplateLiteral")) {
|
|
54104
|
+
if (node.expressions.length > 0) return null;
|
|
54105
|
+
return node.quasis.map((quasi) => quasi.value.cooked ?? quasi.value.raw).join("");
|
|
54106
|
+
}
|
|
54107
|
+
if (isNodeOfType(node, "JSXExpressionContainer")) return getStaticCursorText(node.expression);
|
|
54108
|
+
if (isNodeOfType(node, "JSXElement") || isNodeOfType(node, "JSXFragment")) {
|
|
54109
|
+
let text = "";
|
|
54110
|
+
for (const child of node.children) {
|
|
54111
|
+
const childText = getStaticCursorText(child);
|
|
54112
|
+
if (childText === null) return null;
|
|
54113
|
+
text += childText;
|
|
54114
|
+
}
|
|
54115
|
+
return text;
|
|
54116
|
+
}
|
|
54117
|
+
return null;
|
|
54118
|
+
};
|
|
54119
|
+
const getStaticCursorGlyph = (element) => {
|
|
54120
|
+
const glyph = getStaticCursorText(element)?.trim() ?? "";
|
|
54121
|
+
return CURSOR_GLYPH_PATTERN.test(glyph) ? glyph : null;
|
|
54122
|
+
};
|
|
54123
|
+
const hasCursorAnimationName = (value) => CURSOR_ANIMATION_NAME_PATTERN.test(value.replaceAll("\\_", "_"));
|
|
54124
|
+
const isInfiniteCursorAnimation = (value) => {
|
|
54125
|
+
const animationSegments = splitCssTopLevel(value, ",");
|
|
54126
|
+
return Boolean(animationSegments?.length === 1 && INFINITE_ANIMATION_TOKEN_PATTERN.test(value) && hasCursorAnimationName(value));
|
|
54127
|
+
};
|
|
54128
|
+
const getInlineCursorAnimationState = (openingElement, context) => {
|
|
54129
|
+
const styleAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "style");
|
|
54130
|
+
if (!styleAttribute) return null;
|
|
54131
|
+
const styleExpression = getInlineStyleExpression(styleAttribute, context.scopes);
|
|
54132
|
+
if (!styleExpression || styleExpression.properties.some((property) => getStylePropertyKey(property) === null)) return null;
|
|
54133
|
+
const animationProperty = getEffectiveStyleProperty(styleExpression.properties, "animation");
|
|
54134
|
+
if (animationProperty) {
|
|
54135
|
+
const animationValue = getStylePropertyStringValue(animationProperty);
|
|
54136
|
+
return animationValue === null ? null : isInfiniteCursorAnimation(animationValue);
|
|
54137
|
+
}
|
|
54138
|
+
const animationNameProperty = getEffectiveStyleProperty(styleExpression.properties, "animationName");
|
|
54139
|
+
const animationIterationCountProperty = getEffectiveStyleProperty(styleExpression.properties, "animationIterationCount");
|
|
54140
|
+
if (!animationNameProperty && !animationIterationCountProperty) return null;
|
|
54141
|
+
if (!animationNameProperty || !animationIterationCountProperty) return null;
|
|
54142
|
+
const animationName = getStylePropertyStringValue(animationNameProperty);
|
|
54143
|
+
const animationIterationCount = getStylePropertyStringValue(animationIterationCountProperty);
|
|
54144
|
+
if (animationName === null || animationIterationCount === null) return null;
|
|
54145
|
+
return animationIterationCount.toLowerCase() === "infinite" && hasCursorAnimationName(animationName);
|
|
54146
|
+
};
|
|
54147
|
+
const hasStaticTailwindCursorAnimation = (openingElement) => {
|
|
54148
|
+
const classNameValue = getStringFromClassNameAttr(openingElement);
|
|
54149
|
+
if (!classNameValue) return false;
|
|
54150
|
+
const animationResolution = resolveEffectiveTailwindClassNameToken(splitTailwindClassName(classNameValue), (utility) => TAILWIND_ANIMATION_UTILITY_PATTERN.test(utility));
|
|
54151
|
+
if (animationResolution.isAmbiguous || !animationResolution.utility) return false;
|
|
54152
|
+
if (animationResolution.utility === "animate-pulse") return true;
|
|
54153
|
+
const arbitraryAnimation = animationResolution.utility.match(/^animate-\[(.+)\]$/)?.[1] ?? animationResolution.utility.match(/^\[animation:(.+)\]$/)?.[1];
|
|
54154
|
+
return Boolean(arbitraryAnimation && isInfiniteCursorAnimation(arbitraryAnimation.replaceAll("_", " ")));
|
|
54155
|
+
};
|
|
54156
|
+
const hasProvenCursorAnimation = (openingElement, context) => {
|
|
54157
|
+
return getInlineCursorAnimationState(openingElement, context) ?? hasStaticTailwindCursorAnimation(openingElement);
|
|
54158
|
+
};
|
|
54159
|
+
const getAncestorOpeningElements$2 = (element) => {
|
|
54160
|
+
const openingElements = [];
|
|
54161
|
+
let ancestor = element;
|
|
54162
|
+
while (ancestor) {
|
|
54163
|
+
if (isNodeOfType(ancestor, "JSXElement")) openingElements.push(ancestor.openingElement);
|
|
54164
|
+
ancestor = ancestor.parent;
|
|
54165
|
+
}
|
|
54166
|
+
return openingElements;
|
|
54167
|
+
};
|
|
54168
|
+
const hasUnresolvedOrEnabledAttribute = (openingElement, attributeName) => {
|
|
54169
|
+
const attribute = getAuthoritativeJsxAttribute(openingElement.attributes, attributeName, false);
|
|
54170
|
+
if (!attribute) return false;
|
|
54171
|
+
const value = getStaticAttributeValue(attribute);
|
|
54172
|
+
return value !== false && value !== "false";
|
|
54173
|
+
};
|
|
54174
|
+
const hasCursorSemanticExemption = (openingElements) => openingElements.some((openingElement) => {
|
|
54175
|
+
if (hasJsxSpreadAttribute(openingElement.attributes)) return true;
|
|
54176
|
+
const elementName = resolveJsxElementType(openingElement);
|
|
54177
|
+
if (CURSOR_EXEMPT_ELEMENT_NAMES.has(elementName.toLowerCase()) || PREFORMATTED_CONTEXT_NAME_PATTERN.test(elementName) || hasUnresolvedOrEnabledAttribute(openingElement, "contentEditable") || hasUnresolvedOrEnabledAttribute(openingElement, "aria-busy")) return true;
|
|
54178
|
+
const roleAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "role", false);
|
|
54179
|
+
if (roleAttribute) {
|
|
54180
|
+
const role = getStringLiteralAttributeValue(roleAttribute);
|
|
54181
|
+
if (!role || CURSOR_EXEMPT_ROLES.has(role.trim().toLowerCase().split(/\s+/)[0])) return true;
|
|
54182
|
+
}
|
|
54183
|
+
const ariaLiveAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "aria-live", false);
|
|
54184
|
+
if (ariaLiveAttribute) {
|
|
54185
|
+
const ariaLive = getStringLiteralAttributeValue(ariaLiveAttribute);
|
|
54186
|
+
if (!ariaLive || ariaLive.toLowerCase() !== "off") return true;
|
|
54187
|
+
}
|
|
54188
|
+
const classNameValue = getStringFromClassNameAttr(openingElement);
|
|
54189
|
+
return Boolean(classNameValue && splitTailwindClassName(classNameValue).map(parseTailwindClassNameToken).some((token) => PREFORMATTED_CONTEXT_CLASS_PATTERN.test(token.utility)));
|
|
54190
|
+
});
|
|
54191
|
+
const isHeroDisplayContext = (element) => {
|
|
54192
|
+
let ancestor = element.parent;
|
|
54193
|
+
while (ancestor) {
|
|
54194
|
+
if (isNodeOfType(ancestor, "JSXElement")) {
|
|
54195
|
+
const openingElement = ancestor.openingElement;
|
|
54196
|
+
const elementName = resolveJsxElementType(openingElement);
|
|
54197
|
+
if (elementName === "h1" || HERO_CONTEXT_NAME_PATTERN.test(elementName)) return true;
|
|
54198
|
+
const classNameValue = getStringFromClassNameAttr(openingElement);
|
|
54199
|
+
if (classNameValue && splitTailwindClassName(classNameValue).map(parseTailwindClassNameToken).some((token) => HERO_CONTEXT_CLASS_PATTERN.test(token.utility))) return true;
|
|
54200
|
+
if ((elementName === "header" || elementName === "section") && getStaticJsxDescendantOpeningElements(ancestor).some((descendant) => resolveJsxElementType(descendant) === "h1")) return true;
|
|
54201
|
+
}
|
|
54202
|
+
ancestor = ancestor.parent;
|
|
54203
|
+
}
|
|
54204
|
+
return false;
|
|
54205
|
+
};
|
|
54206
|
+
const isExcludedContentPath$1 = (context) => {
|
|
54207
|
+
const rootDirectory = getReactDoctorStringSetting(context.settings, "rootDirectory") ?? "";
|
|
54208
|
+
return EXCLUDED_CONTENT_PATH_PATTERN$1.test(`${rootDirectory}/${context.filename ?? ""}`);
|
|
54209
|
+
};
|
|
53482
54210
|
const noDecorativePulse = defineRule({
|
|
53483
54211
|
id: "no-decorative-pulse",
|
|
53484
54212
|
title: "Stable copy pulses for attention",
|
|
53485
54213
|
severity: "warn",
|
|
53486
54214
|
defaultEnabled: false,
|
|
53487
54215
|
tags: ["design", "test-noise"],
|
|
53488
|
-
recommendation: "Reserve pulsing motion for real in-progress feedback.
|
|
54216
|
+
recommendation: "Reserve pulsing motion for real in-progress feedback. Remove fake blinking cursors and use static hierarchy for decorative emphasis.",
|
|
53489
54217
|
create: (context) => ({ JSXElement(node) {
|
|
53490
54218
|
const openingElement = node.openingElement;
|
|
54219
|
+
const cursorGlyph = getStaticCursorGlyph(node);
|
|
54220
|
+
const possibleCursorGlyph = getStaticJsxText(node).trim();
|
|
54221
|
+
if (cursorGlyph || CURSOR_GLYPH_PATTERN.test(possibleCursorGlyph)) {
|
|
54222
|
+
if (!cursorGlyph) return;
|
|
54223
|
+
const openingElements = getAncestorOpeningElements$2(node);
|
|
54224
|
+
if (isExcludedContentPath$1(context) || hasCursorSemanticExemption(openingElements) || !isHeroDisplayContext(node) || !hasProvenCursorAnimation(openingElement, context)) return;
|
|
54225
|
+
context.report({
|
|
54226
|
+
node: openingElement,
|
|
54227
|
+
message: "This fake cursor blinks continuously in display copy without an editable surface. Remove the simulated typing effect and let the composition hold attention."
|
|
54228
|
+
});
|
|
54229
|
+
return;
|
|
54230
|
+
}
|
|
53491
54231
|
const classNameValue = getStringFromClassNameAttr(openingElement);
|
|
53492
|
-
if (!classNameValue
|
|
54232
|
+
if (!classNameValue) return;
|
|
54233
|
+
const animationResolution = resolveEffectiveTailwindClassNameToken(splitTailwindClassName(classNameValue), (utility) => TAILWIND_ANIMATION_UTILITY_PATTERN.test(utility));
|
|
54234
|
+
if (animationResolution.isAmbiguous || animationResolution.utility !== "animate-pulse") return;
|
|
53493
54235
|
const text = getStaticJsxText(node).replace(/\s+/g, " ").trim();
|
|
53494
54236
|
if (!text || BUSY_TEXT_PATTERN.test(text)) return;
|
|
53495
54237
|
if (isBusyStatus(openingElement)) return;
|
|
@@ -53500,6 +54242,287 @@ const noDecorativePulse = defineRule({
|
|
|
53500
54242
|
} })
|
|
53501
54243
|
});
|
|
53502
54244
|
//#endregion
|
|
54245
|
+
//#region src/plugin/rules/design/utils/get-static-tailwind-background-image.ts
|
|
54246
|
+
const TAILWIND_BACKGROUND_IMAGE_UTILITY_PATTERN = /^(?:bg-none|bg-\[(?:image:[\s\S]+|(?:radial-gradient|repeating-radial-gradient|linear-gradient|repeating-linear-gradient|conic-gradient|repeating-conic-gradient|url)\([\s\S]+\))\]|\[(?:background|background-image):[\s\S]+\])$/i;
|
|
54247
|
+
const getStaticTailwindBackgroundImage = (tokens) => {
|
|
54248
|
+
const resolution = resolveEffectiveTailwindClassNameToken(tokens, (utility) => TAILWIND_BACKGROUND_IMAGE_UTILITY_PATTERN.test(utility), []);
|
|
54249
|
+
const utility = resolution.utility;
|
|
54250
|
+
if (!utility || utility === "bg-none") return {
|
|
54251
|
+
isAmbiguous: resolution.isAmbiguous,
|
|
54252
|
+
isImportant: resolution.isImportant,
|
|
54253
|
+
value: null
|
|
54254
|
+
};
|
|
54255
|
+
let arbitraryValue = null;
|
|
54256
|
+
if (utility.startsWith("bg-[") && utility.endsWith("]")) arbitraryValue = utility.slice(4, -1).replace(/^image:/i, "");
|
|
54257
|
+
else arbitraryValue = utility.match(/^\[(?:background|background-image):([\s\S]+)\]$/i)?.[1] ?? null;
|
|
54258
|
+
return {
|
|
54259
|
+
isAmbiguous: resolution.isAmbiguous,
|
|
54260
|
+
isImportant: resolution.isImportant,
|
|
54261
|
+
value: arbitraryValue ? normalizeTailwindArbitraryUtilityValue(arbitraryValue) : null
|
|
54262
|
+
};
|
|
54263
|
+
};
|
|
54264
|
+
//#endregion
|
|
54265
|
+
//#region src/plugin/rules/design/utils/get-css-function-contents.ts
|
|
54266
|
+
const getCssFunctionContents = (value) => {
|
|
54267
|
+
const openingParenthesisIndex = value.indexOf("(");
|
|
54268
|
+
if (openingParenthesisIndex < 0 || !value.endsWith(")")) return null;
|
|
54269
|
+
let depth = 0;
|
|
54270
|
+
for (let characterIndex = openingParenthesisIndex; characterIndex < value.length; characterIndex += 1) {
|
|
54271
|
+
const character = value[characterIndex];
|
|
54272
|
+
if (character === "(") depth += 1;
|
|
54273
|
+
if (character === ")") depth -= 1;
|
|
54274
|
+
if (depth < 0 || depth === 0 && characterIndex !== value.length - 1) return null;
|
|
54275
|
+
}
|
|
54276
|
+
return depth === 0 ? value.slice(openingParenthesisIndex + 1, -1) : null;
|
|
54277
|
+
};
|
|
54278
|
+
//#endregion
|
|
54279
|
+
//#region src/plugin/rules/design/utils/parse-static-css-color-with-alpha.ts
|
|
54280
|
+
const CSS_ALPHA_VALUE_PATTERN = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?%?$/i;
|
|
54281
|
+
const parseAlpha$1 = (value) => {
|
|
54282
|
+
const trimmedValue = value.trim();
|
|
54283
|
+
if (!CSS_ALPHA_VALUE_PATTERN.test(trimmedValue)) return null;
|
|
54284
|
+
const numericValue = Number.parseFloat(trimmedValue);
|
|
54285
|
+
if (!Number.isFinite(numericValue)) return null;
|
|
54286
|
+
const alpha = trimmedValue.endsWith("%") ? numericValue / 100 : numericValue;
|
|
54287
|
+
return alpha >= 0 && alpha <= 1 ? alpha : null;
|
|
54288
|
+
};
|
|
54289
|
+
const parseFunctionalColorAlpha = (colorValue) => {
|
|
54290
|
+
const contents = getCssFunctionContents(colorValue);
|
|
54291
|
+
if (contents === null) return null;
|
|
54292
|
+
const slashParts = splitCssTopLevel(contents, "/");
|
|
54293
|
+
if (!slashParts || slashParts.length > 2) return null;
|
|
54294
|
+
if (slashParts.length === 2) return parseAlpha$1(slashParts[1]);
|
|
54295
|
+
const commaParts = splitCssTopLevel(contents, ",");
|
|
54296
|
+
if (!commaParts) return null;
|
|
54297
|
+
return commaParts.length === 4 ? parseAlpha$1(commaParts[3]) : 1;
|
|
54298
|
+
};
|
|
54299
|
+
const parseStaticCssColorWithAlpha = (colorValue) => {
|
|
54300
|
+
const normalizedColor = colorValue.trim().toLowerCase();
|
|
54301
|
+
if (normalizedColor === "transparent") return {
|
|
54302
|
+
alpha: 0,
|
|
54303
|
+
blue: 0,
|
|
54304
|
+
green: 0,
|
|
54305
|
+
red: 0
|
|
54306
|
+
};
|
|
54307
|
+
const parsedRgb = parseColorToRgb(normalizedColor);
|
|
54308
|
+
if (!parsedRgb) return null;
|
|
54309
|
+
let alpha = 1;
|
|
54310
|
+
if (/^#[\da-f]{4}$/i.test(normalizedColor)) alpha = Number.parseInt(normalizedColor.slice(4), 16) / 15;
|
|
54311
|
+
else if (/^#[\da-f]{8}$/i.test(normalizedColor)) alpha = Number.parseInt(normalizedColor.slice(7), 16) / 255;
|
|
54312
|
+
else if (/^(?:rgb|hsl)a?\(/i.test(normalizedColor)) {
|
|
54313
|
+
const functionalAlpha = parseFunctionalColorAlpha(normalizedColor);
|
|
54314
|
+
if (functionalAlpha === null) return null;
|
|
54315
|
+
alpha = functionalAlpha;
|
|
54316
|
+
}
|
|
54317
|
+
return {
|
|
54318
|
+
...parsedRgb,
|
|
54319
|
+
alpha
|
|
54320
|
+
};
|
|
54321
|
+
};
|
|
54322
|
+
//#endregion
|
|
54323
|
+
//#region src/plugin/rules/design/utils/parse-static-css-gradient-stop.ts
|
|
54324
|
+
const CSS_STOP_POSITION_PATTERN = /^(?:0|[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:%|px|rem))(?:\s+(?:0|[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:%|px|rem)))?$/i;
|
|
54325
|
+
const parseStaticCssGradientStop = (stop) => {
|
|
54326
|
+
const trimmedStop = stop.trim();
|
|
54327
|
+
const functionalColorMatch = trimmedStop.match(/^(?:rgb|hsl)a?\(/i);
|
|
54328
|
+
if (functionalColorMatch) {
|
|
54329
|
+
let depth = 0;
|
|
54330
|
+
for (let characterIndex = functionalColorMatch[0].length - 1; characterIndex < trimmedStop.length; characterIndex += 1) {
|
|
54331
|
+
const character = trimmedStop[characterIndex];
|
|
54332
|
+
if (character === "(") depth += 1;
|
|
54333
|
+
if (character === ")") depth -= 1;
|
|
54334
|
+
if (depth !== 0) continue;
|
|
54335
|
+
const colorValue = trimmedStop.slice(0, characterIndex + 1);
|
|
54336
|
+
const positionValue = trimmedStop.slice(characterIndex + 1).trim();
|
|
54337
|
+
if (positionValue && !CSS_STOP_POSITION_PATTERN.test(positionValue)) return null;
|
|
54338
|
+
const color = parseStaticCssColorWithAlpha(colorValue);
|
|
54339
|
+
return color ? {
|
|
54340
|
+
color,
|
|
54341
|
+
positions: positionValue ? positionValue.split(/\s+/) : []
|
|
54342
|
+
} : null;
|
|
54343
|
+
}
|
|
54344
|
+
return null;
|
|
54345
|
+
}
|
|
54346
|
+
const colorMatch = trimmedStop.match(/^(?:transparent|#[\da-f]{3,8})(?:\s+|$)/i);
|
|
54347
|
+
if (!colorMatch) return null;
|
|
54348
|
+
const colorValue = colorMatch[0].trim();
|
|
54349
|
+
const positionValue = trimmedStop.slice(colorMatch[0].length).trim();
|
|
54350
|
+
if (positionValue && !CSS_STOP_POSITION_PATTERN.test(positionValue)) return null;
|
|
54351
|
+
const color = parseStaticCssColorWithAlpha(colorValue);
|
|
54352
|
+
return color ? {
|
|
54353
|
+
color,
|
|
54354
|
+
positions: positionValue ? positionValue.split(/\s+/) : []
|
|
54355
|
+
} : null;
|
|
54356
|
+
};
|
|
54357
|
+
//#endregion
|
|
54358
|
+
//#region src/plugin/rules/design/utils/parse-static-radial-gradient.ts
|
|
54359
|
+
const RADIAL_GRADIENT_PRELUDE_PATTERN = /\b(?:at|circle|closest-corner|closest-side|ellipse|farthest-corner|farthest-side)\b/i;
|
|
54360
|
+
const parseStaticRadialGradient = (backgroundValue) => {
|
|
54361
|
+
const normalizedValue = backgroundValue.trim();
|
|
54362
|
+
if (!/^radial-gradient\(/i.test(normalizedValue)) return null;
|
|
54363
|
+
const gradientContents = getCssFunctionContents(normalizedValue);
|
|
54364
|
+
if (gradientContents === null) return null;
|
|
54365
|
+
const gradientParts = splitCssTopLevel(gradientContents, ",");
|
|
54366
|
+
if (!gradientParts || gradientParts.length < 2) return null;
|
|
54367
|
+
const firstStop = parseStaticCssGradientStop(gradientParts[0]);
|
|
54368
|
+
if (!firstStop && !RADIAL_GRADIENT_PRELUDE_PATTERN.test(gradientParts[0])) return null;
|
|
54369
|
+
const stopParts = firstStop ? gradientParts : gradientParts.slice(1);
|
|
54370
|
+
if (stopParts.length < 2) return null;
|
|
54371
|
+
const parsedStops = stopParts.map(parseStaticCssGradientStop);
|
|
54372
|
+
return parsedStops.every((stop) => stop !== null) ? parsedStops : null;
|
|
54373
|
+
};
|
|
54374
|
+
//#endregion
|
|
54375
|
+
//#region src/plugin/rules/design/utils/parse-static-tailwind-length-px.ts
|
|
54376
|
+
const parseStaticTailwindLengthPx = (utility, prefix) => {
|
|
54377
|
+
if (utility === `${prefix}-px`) return 1;
|
|
54378
|
+
const arbitraryMatch = utility.match(new RegExp(`^${prefix}-\\[(?:length:)?(\\d+(?:\\.\\d*)?|\\.\\d+)(px|rem)\\]$`, "i"));
|
|
54379
|
+
if (arbitraryMatch) {
|
|
54380
|
+
const numericValue = Number.parseFloat(arbitraryMatch[1]);
|
|
54381
|
+
return arbitraryMatch[2].toLowerCase() === "rem" ? numericValue * 16 : numericValue;
|
|
54382
|
+
}
|
|
54383
|
+
const scaleMatch = utility.match(new RegExp(`^${prefix}-(\\d+(?:\\.\\d*)?|\\.\\d+)$`));
|
|
54384
|
+
return scaleMatch ? Number.parseFloat(scaleMatch[1]) * 4 : null;
|
|
54385
|
+
};
|
|
54386
|
+
//#endregion
|
|
54387
|
+
//#region src/plugin/rules/design/no-decorative-radial-spotlight.ts
|
|
54388
|
+
const SPOTLIGHT_SURFACE_ELEMENT_NAMES = new Set([
|
|
54389
|
+
"article",
|
|
54390
|
+
"aside",
|
|
54391
|
+
"div",
|
|
54392
|
+
"header",
|
|
54393
|
+
"main",
|
|
54394
|
+
"section"
|
|
54395
|
+
]);
|
|
54396
|
+
const CSS_STATIC_LENGTH_PATTERN = /^(\d+(?:\.\d*)?|\.\d+)(px|rem)$/i;
|
|
54397
|
+
const TAILWIND_WIDTH_UTILITY_PATTERN = /^(?:size|w)-/;
|
|
54398
|
+
const TAILWIND_HEIGHT_UTILITY_PATTERN = /^(?:h|size)-/;
|
|
54399
|
+
const BACKGROUND_STYLE_PROPERTY_NAMES$1 = new Set(["background", "backgroundImage"]);
|
|
54400
|
+
const INLINE_BOTTOM_PROPERTY_NAMES = new Set(["bottom", "inset"]);
|
|
54401
|
+
const INLINE_LEFT_PROPERTY_NAMES = new Set(["inset", "left"]);
|
|
54402
|
+
const INLINE_RIGHT_PROPERTY_NAMES = new Set(["inset", "right"]);
|
|
54403
|
+
const INLINE_TOP_PROPERTY_NAMES = new Set(["inset", "top"]);
|
|
54404
|
+
const hasDecorativeRadialSpotlightGradient = (backgroundValue) => {
|
|
54405
|
+
const parsedStops = parseStaticRadialGradient(backgroundValue);
|
|
54406
|
+
if (!parsedStops) return false;
|
|
54407
|
+
const finalStop = parsedStops.at(-1)?.color;
|
|
54408
|
+
if (!finalStop || finalStop.alpha > .05) return false;
|
|
54409
|
+
const visibleStops = parsedStops.slice(0, -1).map((stop) => stop.color).filter((stop) => stop.alpha > DECORATIVE_RADIAL_SPOTLIGHT_TRANSPARENT_ALPHA_MAX);
|
|
54410
|
+
if (visibleStops.length === 0 || visibleStops.length > 2 || visibleStops.some((stop) => stop.alpha >= .45 || !hasColorChroma(stop))) return false;
|
|
54411
|
+
const firstVisibleStop = visibleStops[0];
|
|
54412
|
+
return visibleStops.every((stop) => stop.red === firstVisibleStop.red && stop.green === firstVisibleStop.green && stop.blue === firstVisibleStop.blue);
|
|
54413
|
+
};
|
|
54414
|
+
const parseStaticLengthPx = (value) => {
|
|
54415
|
+
if (typeof value === "number") return Number.isFinite(value) && value >= 0 ? value : null;
|
|
54416
|
+
if (typeof value !== "string") return null;
|
|
54417
|
+
const match = value.trim().match(CSS_STATIC_LENGTH_PATTERN);
|
|
54418
|
+
if (!match) return null;
|
|
54419
|
+
const numericValue = Number.parseFloat(match[1]);
|
|
54420
|
+
return match[2].toLowerCase() === "rem" ? numericValue * 16 : numericValue;
|
|
54421
|
+
};
|
|
54422
|
+
const parseStaticZero = (value) => {
|
|
54423
|
+
if (typeof value === "number") return Number.isFinite(value) ? value === 0 : null;
|
|
54424
|
+
if (typeof value !== "string") return null;
|
|
54425
|
+
return /^[+-]?(?:0+(?:\.0*)?|\.0+)(?:px|rem)?$/i.test(value.trim());
|
|
54426
|
+
};
|
|
54427
|
+
const getTailwindInsetEdgeEvidence = (tokens, edge) => {
|
|
54428
|
+
const axis = edge === "top" || edge === "bottom" ? "y" : "x";
|
|
54429
|
+
const resolution = resolveEffectiveTailwindClassNameToken(tokens, (utility) => {
|
|
54430
|
+
const unsignedUtility = utility.startsWith("-") ? utility.slice(1) : utility;
|
|
54431
|
+
return /^inset-(?![xy]-)/.test(unsignedUtility) || unsignedUtility.startsWith(`inset-${axis}-`) || unsignedUtility.startsWith(`${edge}-`) || (edge === "left" || edge === "right") && (unsignedUtility.startsWith("start-") || unsignedUtility.startsWith("end-"));
|
|
54432
|
+
}, []);
|
|
54433
|
+
const utility = resolution.utility;
|
|
54434
|
+
const utilityPrefix = utility?.slice(0, utility.lastIndexOf("-")) ?? null;
|
|
54435
|
+
const unsignedUtility = utility?.startsWith("-") ? utility.slice(1) : utility;
|
|
54436
|
+
const isLogicalInset = unsignedUtility?.startsWith("start-") || unsignedUtility?.startsWith("end-");
|
|
54437
|
+
return {
|
|
54438
|
+
isAmbiguous: resolution.isAmbiguous,
|
|
54439
|
+
isImportant: resolution.isImportant,
|
|
54440
|
+
isZero: utility && utilityPrefix && !isLogicalInset ? parseStaticTailwindLengthPx(utility, utilityPrefix) === 0 : utility === null ? null : false
|
|
54441
|
+
};
|
|
54442
|
+
};
|
|
54443
|
+
const getTailwindSurfaceEvidence = (tokens) => {
|
|
54444
|
+
const widthResolution = resolveEffectiveTailwindClassNameToken(tokens, (utility) => TAILWIND_WIDTH_UTILITY_PATTERN.test(utility), []);
|
|
54445
|
+
const heightResolution = resolveEffectiveTailwindClassNameToken(tokens, (utility) => TAILWIND_HEIGHT_UTILITY_PATTERN.test(utility), []);
|
|
54446
|
+
const positionResolution = resolveEffectiveTailwindClassNameToken(tokens, (utility) => utility === "absolute" || utility === "fixed" || utility === "relative" || utility === "static" || utility === "sticky", []);
|
|
54447
|
+
const widthPx = widthResolution.utility ? parseStaticTailwindLengthPx(widthResolution.utility, "size") ?? parseStaticTailwindLengthPx(widthResolution.utility, "w") : null;
|
|
54448
|
+
return {
|
|
54449
|
+
heightPx: heightResolution.utility ? parseStaticTailwindLengthPx(heightResolution.utility, "size") ?? parseStaticTailwindLengthPx(heightResolution.utility, "h") : null,
|
|
54450
|
+
heightIsAmbiguous: heightResolution.isAmbiguous,
|
|
54451
|
+
heightIsImportant: heightResolution.isImportant,
|
|
54452
|
+
hasHeight: heightResolution.utility !== null || heightResolution.isAmbiguous,
|
|
54453
|
+
hasWidth: widthResolution.utility !== null || widthResolution.isAmbiguous,
|
|
54454
|
+
insetBottom: getTailwindInsetEdgeEvidence(tokens, "bottom"),
|
|
54455
|
+
insetLeft: getTailwindInsetEdgeEvidence(tokens, "left"),
|
|
54456
|
+
insetRight: getTailwindInsetEdgeEvidence(tokens, "right"),
|
|
54457
|
+
insetTop: getTailwindInsetEdgeEvidence(tokens, "top"),
|
|
54458
|
+
positionIsAmbiguous: positionResolution.isAmbiguous,
|
|
54459
|
+
positionIsFixed: positionResolution.isAmbiguous ? null : positionResolution.utility === null ? null : positionResolution.utility === "fixed",
|
|
54460
|
+
positionIsImportant: positionResolution.isImportant,
|
|
54461
|
+
widthPx,
|
|
54462
|
+
widthIsAmbiguous: widthResolution.isAmbiguous,
|
|
54463
|
+
widthIsImportant: widthResolution.isImportant
|
|
54464
|
+
};
|
|
54465
|
+
};
|
|
54466
|
+
const getEffectiveInsetEdgeIsZero = (styleProperties, propertyNames, tailwindEvidence) => {
|
|
54467
|
+
if (tailwindEvidence.isAmbiguous) return null;
|
|
54468
|
+
const inlineProperty = getEffectiveStylePropertyAmong(styleProperties, propertyNames);
|
|
54469
|
+
if (tailwindEvidence.isImportant) return tailwindEvidence.isZero;
|
|
54470
|
+
if (!inlineProperty) return tailwindEvidence.isZero;
|
|
54471
|
+
return parseStaticZero(getStylePropertyNumberValue(inlineProperty) ?? getStylePropertyStringValue(inlineProperty));
|
|
54472
|
+
};
|
|
54473
|
+
const noDecorativeRadialSpotlight = defineRule({
|
|
54474
|
+
id: "no-decorative-radial-spotlight",
|
|
54475
|
+
title: "Large decorative radial spotlight",
|
|
54476
|
+
severity: "warn",
|
|
54477
|
+
defaultEnabled: false,
|
|
54478
|
+
tags: [
|
|
54479
|
+
"design",
|
|
54480
|
+
"test-noise",
|
|
54481
|
+
"react-jsx-only"
|
|
54482
|
+
],
|
|
54483
|
+
recommendation: "Use product-specific imagery, structure, or a restrained solid surface instead of a large translucent radial glow.",
|
|
54484
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
54485
|
+
if (!isProvenIntrinsicJsxElement(node, context.scopes) || !isNodeOfType(node.name, "JSXIdentifier") || !SPOTLIGHT_SURFACE_ELEMENT_NAMES.has(node.name.name) || isDataVisualizationContext(node, context.filename)) return;
|
|
54486
|
+
const classNameAttribute = getAuthoritativeJsxAttribute(node.attributes, "className");
|
|
54487
|
+
const styleAttribute = getAuthoritativeJsxAttribute(node.attributes, "style");
|
|
54488
|
+
if (!classNameAttribute && hasJsxSpreadThatMayProvideAttribute(node.attributes, "className") || !styleAttribute && hasJsxSpreadThatMayProvideAttribute(node.attributes, "style")) return;
|
|
54489
|
+
const className = classNameAttribute ? getStringFromClassNameAttr(node) : "";
|
|
54490
|
+
if (classNameAttribute && className === null) return;
|
|
54491
|
+
const styleExpression = styleAttribute ? getInlineStyleExpression(styleAttribute, context.scopes) : null;
|
|
54492
|
+
if (styleAttribute && !styleExpression || styleExpression?.properties.some((property) => getStylePropertyKey(property) === null)) return;
|
|
54493
|
+
const tokens = className && hasCapabilityOrUnspecified(context.settings, "tailwind") ? splitTailwindClassName(className) : [];
|
|
54494
|
+
const tailwindBackground = getStaticTailwindBackgroundImage(tokens);
|
|
54495
|
+
if (tailwindBackground.isAmbiguous) return;
|
|
54496
|
+
const inlineBackgroundProperty = getEffectiveStylePropertyAmong(styleExpression?.properties, BACKGROUND_STYLE_PROPERTY_NAMES$1);
|
|
54497
|
+
const inlineBackgroundValue = inlineBackgroundProperty ? getStylePropertyStringValue(inlineBackgroundProperty) : null;
|
|
54498
|
+
const backgroundValue = inlineBackgroundProperty && !tailwindBackground.isImportant ? inlineBackgroundValue : tailwindBackground.value;
|
|
54499
|
+
if (!backgroundValue || !hasDecorativeRadialSpotlightGradient(backgroundValue)) return;
|
|
54500
|
+
const tailwindSurface = getTailwindSurfaceEvidence(tokens);
|
|
54501
|
+
if (tailwindSurface.widthIsAmbiguous || tailwindSurface.heightIsAmbiguous) return;
|
|
54502
|
+
const inlineWidthProperty = getEffectiveStyleProperty(styleExpression?.properties, "width");
|
|
54503
|
+
const inlineHeightProperty = getEffectiveStyleProperty(styleExpression?.properties, "height");
|
|
54504
|
+
const inlineWidthPx = inlineWidthProperty ? parseStaticLengthPx(getStylePropertyNumberValue(inlineWidthProperty) ?? getStylePropertyStringValue(inlineWidthProperty)) : null;
|
|
54505
|
+
const inlineHeightPx = inlineHeightProperty ? parseStaticLengthPx(getStylePropertyNumberValue(inlineHeightProperty) ?? getStylePropertyStringValue(inlineHeightProperty)) : null;
|
|
54506
|
+
const widthPx = tailwindSurface.widthIsImportant ? tailwindSurface.widthPx : inlineWidthProperty ? inlineWidthPx : tailwindSurface.widthPx;
|
|
54507
|
+
const heightPx = tailwindSurface.heightIsImportant ? tailwindSurface.heightPx : inlineHeightProperty ? inlineHeightPx : tailwindSurface.heightPx;
|
|
54508
|
+
const hasWidth = tailwindSurface.widthIsImportant ? tailwindSurface.hasWidth : inlineWidthProperty !== null || tailwindSurface.hasWidth;
|
|
54509
|
+
const hasHeight = tailwindSurface.heightIsImportant ? tailwindSurface.hasHeight : inlineHeightProperty !== null || tailwindSurface.hasHeight;
|
|
54510
|
+
const positionProperty = getEffectiveStyleProperty(styleExpression?.properties, "position");
|
|
54511
|
+
const positionIsFixed = tailwindSurface.positionIsAmbiguous ? null : tailwindSurface.positionIsImportant ? tailwindSurface.positionIsFixed : positionProperty ? getStylePropertyStringValue(positionProperty)?.toLowerCase() === "fixed" : tailwindSurface.positionIsFixed;
|
|
54512
|
+
const insetEdgesAreZero = [
|
|
54513
|
+
getEffectiveInsetEdgeIsZero(styleExpression?.properties, INLINE_TOP_PROPERTY_NAMES, tailwindSurface.insetTop),
|
|
54514
|
+
getEffectiveInsetEdgeIsZero(styleExpression?.properties, INLINE_RIGHT_PROPERTY_NAMES, tailwindSurface.insetRight),
|
|
54515
|
+
getEffectiveInsetEdgeIsZero(styleExpression?.properties, INLINE_BOTTOM_PROPERTY_NAMES, tailwindSurface.insetBottom),
|
|
54516
|
+
getEffectiveInsetEdgeIsZero(styleExpression?.properties, INLINE_LEFT_PROPERTY_NAMES, tailwindSurface.insetLeft)
|
|
54517
|
+
].every((isZero) => isZero === true);
|
|
54518
|
+
if (!(!hasWidth && !hasHeight && positionIsFixed === true && insetEdgesAreZero || widthPx !== null && widthPx >= 240 && heightPx !== null && heightPx >= 160)) return;
|
|
54519
|
+
context.report({
|
|
54520
|
+
node: inlineBackgroundProperty ?? node,
|
|
54521
|
+
message: "This large translucent radial glow is generic decorative scaffolding. Replace it with a visual treatment tied to the product or simplify the surface."
|
|
54522
|
+
});
|
|
54523
|
+
} })
|
|
54524
|
+
});
|
|
54525
|
+
//#endregion
|
|
53503
54526
|
//#region src/plugin/rules/architecture/no-default-props.ts
|
|
53504
54527
|
const hasReachingWriteOnAliasPath = (receiver, context) => {
|
|
53505
54528
|
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
@@ -59978,7 +61001,7 @@ const noExcessiveMotionStagger = defineRule({
|
|
|
59978
61001
|
});
|
|
59979
61002
|
//#endregion
|
|
59980
61003
|
//#region src/plugin/rules/design/no-excessive-pill-treatment.ts
|
|
59981
|
-
const HORIZONTAL_PADDING_PATTERN$
|
|
61004
|
+
const HORIZONTAL_PADDING_PATTERN$2 = /^px-(?:px|[\d.]+|\[[^\]]+\])$/;
|
|
59982
61005
|
const isPillTreatment = (openingElement) => {
|
|
59983
61006
|
const element = openingElement.parent;
|
|
59984
61007
|
if (!isNodeOfType(element, "JSXElement")) return false;
|
|
@@ -59987,7 +61010,7 @@ const isPillTreatment = (openingElement) => {
|
|
|
59987
61010
|
const classNameValue = getStringFromClassNameAttr(openingElement);
|
|
59988
61011
|
if (!classNameValue) return false;
|
|
59989
61012
|
const tokens = getUnvariantClassNameTokens(classNameValue);
|
|
59990
|
-
return tokens.includes("rounded-full") && tokens.some((token) => HORIZONTAL_PADDING_PATTERN$
|
|
61013
|
+
return tokens.includes("rounded-full") && tokens.some((token) => HORIZONTAL_PADDING_PATTERN$2.test(token)) && hasVisibleTailwindFillOrEdge(tokens);
|
|
59991
61014
|
};
|
|
59992
61015
|
const noExcessivePillTreatment = defineRule({
|
|
59993
61016
|
id: "no-excessive-pill-treatment",
|
|
@@ -60998,13 +62021,13 @@ const getFontSizePx = (property) => {
|
|
|
60998
62021
|
const value = Number.parseFloat(match[1]);
|
|
60999
62022
|
return match[2] === "rem" ? value * 16 : value;
|
|
61000
62023
|
};
|
|
61001
|
-
const getStaticEffectiveFontSize = (openingElement, hasTailwind) => {
|
|
62024
|
+
const getStaticEffectiveFontSize = (openingElement, hasTailwind, scopes) => {
|
|
61002
62025
|
const classNameValue = getStringFromClassNameAttr(openingElement);
|
|
61003
62026
|
const tailwindFontSize = hasTailwind ? getStaticTailwindFontSize(classNameValue) : null;
|
|
61004
62027
|
if (classNameValue && hasTailwind && hasImportantTailwindFontSize(classNameValue)) return tailwindFontSize;
|
|
61005
62028
|
const styleAttribute = getAuthoritativeJsxAttribute(openingElement.attributes ?? [], "style");
|
|
61006
62029
|
if (!styleAttribute) return hasJsxSpreadAttribute(openingElement.attributes) ? null : tailwindFontSize;
|
|
61007
|
-
const styleExpression = getInlineStyleExpression(styleAttribute);
|
|
62030
|
+
const styleExpression = getInlineStyleExpression(styleAttribute, scopes);
|
|
61008
62031
|
if (!styleExpression) return null;
|
|
61009
62032
|
const fontSizeProperty = getEffectiveStyleProperty(styleExpression.properties, "fontSize");
|
|
61010
62033
|
if (fontSizeProperty) return getFontSizePx(fontSizeProperty);
|
|
@@ -61368,7 +62391,7 @@ const getProvenRenderLocation = (openingElement) => {
|
|
|
61368
62391
|
}
|
|
61369
62392
|
return null;
|
|
61370
62393
|
};
|
|
61371
|
-
const getStaticBooleanAttributeState = (openingElement, attributeName) => {
|
|
62394
|
+
const getStaticBooleanAttributeState$1 = (openingElement, attributeName) => {
|
|
61372
62395
|
const attribute = hasJsxPropIgnoreCase(openingElement.attributes, attributeName);
|
|
61373
62396
|
if (!attribute) return false;
|
|
61374
62397
|
if (!attribute.value) return true;
|
|
@@ -61401,11 +62424,11 @@ const hasStaticallyHidingStyle = (openingElement) => {
|
|
|
61401
62424
|
const isStaticallyExcludedOpeningElement = (openingElement, isTarget) => {
|
|
61402
62425
|
const tagName = isNodeOfType(openingElement.name, "JSXIdentifier") ? openingElement.name.name : null;
|
|
61403
62426
|
if (!tagName || !/^[a-z]/.test(tagName)) return true;
|
|
61404
|
-
const hiddenState = getStaticBooleanAttributeState(openingElement, "hidden");
|
|
61405
|
-
const inertState = getStaticBooleanAttributeState(openingElement, "inert");
|
|
62427
|
+
const hiddenState = getStaticBooleanAttributeState$1(openingElement, "hidden");
|
|
62428
|
+
const inertState = getStaticBooleanAttributeState$1(openingElement, "inert");
|
|
61406
62429
|
if (hiddenState !== false || inertState !== false || hasStaticallyHidingClassName(openingElement) || hasStaticallyHidingStyle(openingElement) || tagName === "template") return true;
|
|
61407
|
-
if (tagName === "fieldset" && getStaticBooleanAttributeState(openingElement, "disabled") !== false) return true;
|
|
61408
|
-
if ((tagName === "dialog" || tagName === "details") && getStaticBooleanAttributeState(openingElement, "open") !== true) return true;
|
|
62430
|
+
if (tagName === "fieldset" && getStaticBooleanAttributeState$1(openingElement, "disabled") !== false) return true;
|
|
62431
|
+
if ((tagName === "dialog" || tagName === "details") && getStaticBooleanAttributeState$1(openingElement, "open") !== true) return true;
|
|
61409
62432
|
if (isTarget && tagName === "input") {
|
|
61410
62433
|
const typeAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "type");
|
|
61411
62434
|
if (typeAttribute && (!typeAttribute.value || getJsxPropStringValue(typeAttribute) === null)) return true;
|
|
@@ -61912,6 +62935,89 @@ const noGenericHandlerNames = defineRule({
|
|
|
61912
62935
|
});
|
|
61913
62936
|
//#endregion
|
|
61914
62937
|
//#region src/plugin/rules/design/no-generic-marketing-copy.ts
|
|
62938
|
+
const MARKETING_COPY_EXCLUDED_ELEMENT_NAMES = new Set([
|
|
62939
|
+
"code",
|
|
62940
|
+
"codeblock",
|
|
62941
|
+
"codesnippet",
|
|
62942
|
+
"demo",
|
|
62943
|
+
"example",
|
|
62944
|
+
"fixture",
|
|
62945
|
+
"kbd",
|
|
62946
|
+
"markdown",
|
|
62947
|
+
"markdownblock",
|
|
62948
|
+
"markdowncontent",
|
|
62949
|
+
"markdownrenderer",
|
|
62950
|
+
"markdowntext",
|
|
62951
|
+
"markdownview",
|
|
62952
|
+
"mdx",
|
|
62953
|
+
"mdxcontent",
|
|
62954
|
+
"mdxremote",
|
|
62955
|
+
"playground",
|
|
62956
|
+
"pre",
|
|
62957
|
+
"preview",
|
|
62958
|
+
"reactmarkdown",
|
|
62959
|
+
"renderproxy",
|
|
62960
|
+
"samp",
|
|
62961
|
+
"script",
|
|
62962
|
+
"story",
|
|
62963
|
+
"style",
|
|
62964
|
+
"syntaxhighlighter",
|
|
62965
|
+
"template"
|
|
62966
|
+
]);
|
|
62967
|
+
const STATIC_COPY_BOUNDARY$1 = "\0";
|
|
62968
|
+
const LEXICAL_CHARACTER_PATTERN = /[\p{L}\p{N}]/u;
|
|
62969
|
+
const isExcludedCopyElement$1 = (openingElement) => {
|
|
62970
|
+
const elementName = resolveJsxElementType(openingElement).split(".").at(-1);
|
|
62971
|
+
return Boolean(elementName && MARKETING_COPY_EXCLUDED_ELEMENT_NAMES.has(elementName.toLowerCase()));
|
|
62972
|
+
};
|
|
62973
|
+
const getStaticRenderedCopy = (node, context) => {
|
|
62974
|
+
if (!node) return "";
|
|
62975
|
+
if (isNodeOfType(node, "JSXText")) return node.value ?? "";
|
|
62976
|
+
if (isNodeOfType(node, "JSXEmptyExpression")) return "";
|
|
62977
|
+
if (isNodeOfType(node, "Literal")) return typeof node.value === "string" ? node.value : "";
|
|
62978
|
+
if (isNodeOfType(node, "TemplateLiteral")) {
|
|
62979
|
+
const staticSegments = (node.quasis ?? []).map((quasi) => quasi.value?.raw ?? "");
|
|
62980
|
+
if (node.expressions.length === 0) return staticSegments.join("");
|
|
62981
|
+
return `${STATIC_COPY_BOUNDARY$1}${staticSegments.join(STATIC_COPY_BOUNDARY$1)}${STATIC_COPY_BOUNDARY$1}`;
|
|
62982
|
+
}
|
|
62983
|
+
if (isNodeOfType(node, "JSXExpressionContainer")) return getStaticRenderedCopy(node.expression, context);
|
|
62984
|
+
if (isNodeOfType(node, "JSXElement")) {
|
|
62985
|
+
if (isExcludedCopyElement$1(node.openingElement) || isInsideStaticallyHiddenJsxSubtree$1(node, context)) return "";
|
|
62986
|
+
return (node.children ?? []).map((child) => getStaticRenderedCopy(child, context)).join(" ");
|
|
62987
|
+
}
|
|
62988
|
+
if (isNodeOfType(node, "JSXFragment")) return (node.children ?? []).map((child) => getStaticRenderedCopy(child, context)).join(" ");
|
|
62989
|
+
if (isNodeOfType(node, "ConditionalExpression")) return `${STATIC_COPY_BOUNDARY$1}${getStaticRenderedCopy(node.consequent, context)}${STATIC_COPY_BOUNDARY$1}${getStaticRenderedCopy(node.alternate, context)}${STATIC_COPY_BOUNDARY$1}`;
|
|
62990
|
+
if (isNodeOfType(node, "LogicalExpression")) return `${STATIC_COPY_BOUNDARY$1}${getStaticRenderedCopy(node.right, context)}${STATIC_COPY_BOUNDARY$1}`;
|
|
62991
|
+
return STATIC_COPY_BOUNDARY$1;
|
|
62992
|
+
};
|
|
62993
|
+
const isInsideExcludedCopyElement = (node) => {
|
|
62994
|
+
let ancestor = node.parent;
|
|
62995
|
+
while (ancestor) {
|
|
62996
|
+
if (isNodeOfType(ancestor, "JSXElement") && isExcludedCopyElement$1(ancestor.openingElement)) return true;
|
|
62997
|
+
ancestor = ancestor.parent;
|
|
62998
|
+
}
|
|
62999
|
+
return false;
|
|
63000
|
+
};
|
|
63001
|
+
const findFirstMarketingPhrase = (pageText) => {
|
|
63002
|
+
let firstPhrase = null;
|
|
63003
|
+
let firstPhraseIndex = pageText.length;
|
|
63004
|
+
for (const phrase of GENERIC_MARKETING_PHRASES) {
|
|
63005
|
+
let searchStartIndex = 0;
|
|
63006
|
+
while (searchStartIndex < pageText.length) {
|
|
63007
|
+
const phraseIndex = pageText.indexOf(phrase, searchStartIndex);
|
|
63008
|
+
if (phraseIndex < 0 || phraseIndex >= firstPhraseIndex) break;
|
|
63009
|
+
const precedingCharacter = pageText[phraseIndex - 1];
|
|
63010
|
+
const followingCharacter = pageText[phraseIndex + phrase.length];
|
|
63011
|
+
if ((!precedingCharacter || !LEXICAL_CHARACTER_PATTERN.test(precedingCharacter)) && (!followingCharacter || !LEXICAL_CHARACTER_PATTERN.test(followingCharacter))) {
|
|
63012
|
+
firstPhrase = phrase;
|
|
63013
|
+
firstPhraseIndex = phraseIndex;
|
|
63014
|
+
break;
|
|
63015
|
+
}
|
|
63016
|
+
searchStartIndex = phraseIndex + phrase.length;
|
|
63017
|
+
}
|
|
63018
|
+
}
|
|
63019
|
+
return firstPhrase;
|
|
63020
|
+
};
|
|
61915
63021
|
const noGenericMarketingCopy = defineRule({
|
|
61916
63022
|
id: "no-generic-marketing-copy",
|
|
61917
63023
|
title: "Page uses generic marketing language",
|
|
@@ -61920,9 +63026,8 @@ const noGenericMarketingCopy = defineRule({
|
|
|
61920
63026
|
tags: ["design", "test-noise"],
|
|
61921
63027
|
recommendation: "Replace broad promotional phrases with concrete capabilities, outcomes, or evidence.",
|
|
61922
63028
|
create: (context) => ({ JSXElement(node) {
|
|
61923
|
-
if (!isTopLevelPageCopyRoot(node)) return;
|
|
61924
|
-
const
|
|
61925
|
-
const matchedPhrase = [...GENERIC_MARKETING_PHRASES].find((phrase) => pageText.includes(phrase));
|
|
63029
|
+
if (!isTopLevelPageCopyRoot(node) || isInsideExcludedCopyElement(node)) return;
|
|
63030
|
+
const matchedPhrase = findFirstMarketingPhrase(getStaticRenderedCopy(node, context).replace(/\s+/g, " ").toLowerCase());
|
|
61926
63031
|
if (!matchedPhrase) return;
|
|
61927
63032
|
context.report({
|
|
61928
63033
|
node: node.openingElement,
|
|
@@ -62333,12 +63438,69 @@ const HERO_HEADING_SIZE_CLASSES = new Set([
|
|
|
62333
63438
|
"text-8xl",
|
|
62334
63439
|
"text-9xl"
|
|
62335
63440
|
]);
|
|
63441
|
+
const PSEUDO_ELEMENT_NAMES$1 = ["before", "after"];
|
|
63442
|
+
const PSEUDO_DISPLAY_UTILITIES = new Set(["block", "inline-block"]);
|
|
63443
|
+
const PSEUDO_CONTENT_PATTERN = /^(?:content-\[(?:""|'')\]|\[content:(?:""|'')\])$/;
|
|
63444
|
+
const TAILWIND_BACKGROUND_COLOR_PATTERN$1 = /^bg-(?:transparent|black|white|current|inherit|(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d+|\[(?!url\(|(?:image|length|position|size):).+\])(?:\/.+)?$/;
|
|
63445
|
+
const CHROMATIC_TAILWIND_BACKGROUND_PATTERN$1 = /^bg-(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d+$/;
|
|
63446
|
+
const TEXT_TRANSFORM_UTILITIES = new Set([
|
|
63447
|
+
"capitalize",
|
|
63448
|
+
"lowercase",
|
|
63449
|
+
"normal-case",
|
|
63450
|
+
"uppercase"
|
|
63451
|
+
]);
|
|
63452
|
+
const BASE_ROUNDING_PATTERN = /^rounded(?:$|-(?!(?:b|bl|br|e|ee|es|l|r|s|se|ss|t|tl|tr)-))/;
|
|
63453
|
+
const HORIZONTAL_PADDING_PATTERN$1 = /^p(?:x)?-/;
|
|
63454
|
+
const STATIC_ARBITRARY_TRACKING_PATTERN = /^tracking-\[(?:length:)?(-?(?:\d+(?:\.\d*)?|\.\d+))(?:em|px|rem)\]$/i;
|
|
62336
63455
|
const hasPositivePillPadding = (token) => {
|
|
62337
63456
|
const spacingMatch = token.match(/^p(?:x)?-(px|[\d.]+)$/);
|
|
62338
63457
|
if (spacingMatch) return spacingMatch[1] === "px" || parseFloat(spacingMatch[1]) > 0;
|
|
62339
63458
|
const arbitraryMatch = token.match(/^p(?:x)?-\[([\d.]+)(?:px|rem)\]$/);
|
|
62340
63459
|
return Boolean(arbitraryMatch && parseFloat(arbitraryMatch[1]) > 0);
|
|
62341
63460
|
};
|
|
63461
|
+
const isNonNormalTrackingUtility = (utility) => {
|
|
63462
|
+
if (utility === "tracking-normal") return false;
|
|
63463
|
+
if (/^tracking-(?:tight|tighter|wide|wider|widest)$/.test(utility)) return true;
|
|
63464
|
+
const arbitraryTracking = utility.match(STATIC_ARBITRARY_TRACKING_PATTERN);
|
|
63465
|
+
return Boolean(arbitraryTracking && Number.parseFloat(arbitraryTracking[1]) !== 0);
|
|
63466
|
+
};
|
|
63467
|
+
const hasChromaticTailwindBackground$1 = (utility) => {
|
|
63468
|
+
if (CHROMATIC_TAILWIND_BACKGROUND_PATTERN$1.test(utility)) return true;
|
|
63469
|
+
const arbitraryColor = utility.match(/^bg-\[(?:color:)?(.+)\]$/)?.[1];
|
|
63470
|
+
if (!arbitraryColor || /^(?:rgba|hsla)\(/i.test(arbitraryColor)) return false;
|
|
63471
|
+
if (/^#[\da-f]{4}(?:[\da-f]{4})?$/i.test(arbitraryColor)) return false;
|
|
63472
|
+
const parsedColor = parseColorToRgb(arbitraryColor);
|
|
63473
|
+
return parsedColor ? hasColorChroma(parsedColor) : false;
|
|
63474
|
+
};
|
|
63475
|
+
const hasStaticDashPseudoElement = (classNameValue) => {
|
|
63476
|
+
const utilitiesByPseudoElement = new Map(PSEUDO_ELEMENT_NAMES$1.map((pseudoElementName) => [pseudoElementName, []]));
|
|
63477
|
+
for (const rawToken of splitTailwindClassName(classNameValue)) {
|
|
63478
|
+
const parsedToken = parseTailwindClassNameToken(rawToken);
|
|
63479
|
+
if (parsedToken.variants.length !== 1 || !PSEUDO_ELEMENT_NAMES$1.includes(parsedToken.variants[0])) continue;
|
|
63480
|
+
utilitiesByPseudoElement.get(parsedToken.variants[0])?.push(parsedToken.isImportant ? `!${parsedToken.utility}` : parsedToken.utility);
|
|
63481
|
+
}
|
|
63482
|
+
for (const utilities of utilitiesByPseudoElement.values()) {
|
|
63483
|
+
const contentResolution = resolveEffectiveTailwindClassNameToken(utilities, (utility) => utility.startsWith("content-") || utility.startsWith("[content:"), []);
|
|
63484
|
+
const displayResolution = resolveEffectiveTailwindClassNameToken(utilities, (utility) => PSEUDO_DISPLAY_UTILITIES.has(utility) || [
|
|
63485
|
+
"hidden",
|
|
63486
|
+
"contents",
|
|
63487
|
+
"flex",
|
|
63488
|
+
"grid",
|
|
63489
|
+
"inline",
|
|
63490
|
+
"inline-flex",
|
|
63491
|
+
"inline-grid"
|
|
63492
|
+
].includes(utility), []);
|
|
63493
|
+
const widthResolution = resolveEffectiveTailwindClassNameToken(utilities, (utility) => utility.startsWith("w-"), []);
|
|
63494
|
+
const heightResolution = resolveEffectiveTailwindClassNameToken(utilities, (utility) => utility.startsWith("h-"), []);
|
|
63495
|
+
const backgroundResolution = resolveEffectiveTailwindClassNameToken(utilities, (utility) => TAILWIND_BACKGROUND_COLOR_PATTERN$1.test(utility), []);
|
|
63496
|
+
const backgroundOpacityResolution = resolveEffectiveTailwindClassNameToken(utilities, (utility) => utility.startsWith("bg-opacity-"), []);
|
|
63497
|
+
if (contentResolution.isAmbiguous || displayResolution.isAmbiguous || widthResolution.isAmbiguous || heightResolution.isAmbiguous || backgroundResolution.isAmbiguous || backgroundOpacityResolution.isAmbiguous || !contentResolution.utility || !PSEUDO_CONTENT_PATTERN.test(contentResolution.utility) || !displayResolution.utility || !PSEUDO_DISPLAY_UTILITIES.has(displayResolution.utility) || !widthResolution.utility || !heightResolution.utility || !backgroundResolution.utility || !hasChromaticTailwindBackground$1(backgroundResolution.utility) || backgroundOpacityResolution.utility && backgroundOpacityResolution.utility !== "bg-opacity-100") continue;
|
|
63498
|
+
const widthPx = parseStaticTailwindLengthPx(widthResolution.utility, "w");
|
|
63499
|
+
const heightPx = parseStaticTailwindLengthPx(heightResolution.utility, "h");
|
|
63500
|
+
if (widthPx !== null && widthPx >= 8 && widthPx <= 80 && heightPx !== null && heightPx >= 1 && heightPx <= 6) return true;
|
|
63501
|
+
}
|
|
63502
|
+
return false;
|
|
63503
|
+
};
|
|
62342
63504
|
const noHeroEyebrowChip = defineRule({
|
|
62343
63505
|
id: "no-hero-eyebrow-chip",
|
|
62344
63506
|
title: "Hero uses a decorative eyebrow label",
|
|
@@ -62351,14 +63513,21 @@ const noHeroEyebrowChip = defineRule({
|
|
|
62351
63513
|
if (!labelText || labelText.length > 32) return;
|
|
62352
63514
|
const classNameValue = getStringFromClassNameAttr(node.openingElement);
|
|
62353
63515
|
if (!classNameValue) return;
|
|
62354
|
-
const labelTokens =
|
|
62355
|
-
const
|
|
62356
|
-
const
|
|
62357
|
-
|
|
63516
|
+
const labelTokens = getUnvariantClassNameTokensWithImportantModifiers(classNameValue);
|
|
63517
|
+
const effectiveTextTransform = getEffectiveTailwindClassNameToken(labelTokens, (utility) => TEXT_TRANSFORM_UTILITIES.has(utility));
|
|
63518
|
+
const effectiveTracking = getEffectiveTailwindClassNameToken(labelTokens, (utility) => utility.startsWith("tracking-"));
|
|
63519
|
+
const isTrackedLabel = effectiveTextTransform === "uppercase" && effectiveTracking !== null && isNonNormalTrackingUtility(effectiveTracking);
|
|
63520
|
+
const effectiveRounding = getEffectiveTailwindClassNameToken(labelTokens, (utility) => BASE_ROUNDING_PATTERN.test(utility));
|
|
63521
|
+
const effectiveHorizontalPadding = getEffectiveTailwindClassNameToken(labelTokens, (utility) => HORIZONTAL_PADDING_PATTERN$1.test(utility));
|
|
63522
|
+
const isPillLabel = effectiveRounding === "rounded-full" && hasVisibleTailwindFillOrEdge(labelTokens) && effectiveHorizontalPadding !== null && hasPositivePillPadding(effectiveHorizontalPadding);
|
|
63523
|
+
const labelFontSizePx = getStaticTailwindFontSize(classNameValue);
|
|
63524
|
+
const hasDashPseudoElement = labelFontSizePx !== null && labelFontSizePx <= 14 && hasStaticDashPseudoElement(classNameValue);
|
|
63525
|
+
if (!isTrackedLabel && !isPillLabel && !hasDashPseudoElement) return;
|
|
62358
63526
|
const heading = getNextStaticJsxElementSibling(node);
|
|
62359
63527
|
if (!heading || !isNodeOfType(heading.openingElement.name, "JSXIdentifier") || heading.openingElement.name.name !== "h1") return;
|
|
62360
63528
|
const headingClassName = getStringFromClassNameAttr(heading.openingElement);
|
|
62361
|
-
|
|
63529
|
+
const effectiveHeadingSize = getEffectiveTailwindClassNameToken(headingClassName ? getUnvariantClassNameTokensWithImportantModifiers(headingClassName) : [], (utility) => parseStaticTailwindFontSize(utility) !== null);
|
|
63530
|
+
if (!effectiveHeadingSize || !HERO_HEADING_SIZE_CLASSES.has(effectiveHeadingSize)) return;
|
|
62362
63531
|
context.report({
|
|
62363
63532
|
node: node.openingElement,
|
|
62364
63533
|
message: "This small decorative label immediately above a display headline creates a generic hero scaffold. Fold the context into stronger content structure."
|
|
@@ -63154,11 +64323,53 @@ const noIconTileHeadingStack = defineRule({
|
|
|
63154
64323
|
} })
|
|
63155
64324
|
});
|
|
63156
64325
|
//#endregion
|
|
64326
|
+
//#region src/plugin/utils/tokenize-identifier-words.ts
|
|
64327
|
+
const IDENTIFIER_WORD_PATTERN = /[A-Z]+(?=[A-Z][a-z]|\b)|[A-Z]?[a-z]+|\d+/g;
|
|
64328
|
+
const tokenizeIdentifierWords = (identifierName) => {
|
|
64329
|
+
const words = identifierName.match(IDENTIFIER_WORD_PATTERN);
|
|
64330
|
+
if (!words) return [];
|
|
64331
|
+
return words.map((word) => word.toLowerCase());
|
|
64332
|
+
};
|
|
64333
|
+
//#endregion
|
|
63157
64334
|
//#region src/plugin/rules/design/no-image-hover-transform.ts
|
|
63158
64335
|
const HOVER_VARIANT_PATTERN = /^(?:(?:group|peer)-)?hover(?:\/[^:]+)?$/;
|
|
63159
64336
|
const IMAGE_TRANSFORM_PATTERN = /^-?(?:scale|rotate)-/;
|
|
63160
|
-
const NEUTRAL_SCALE_PATTERN = /^scale(?:-[xyz])?-(?:100|none)$/;
|
|
63161
|
-
const NEUTRAL_ROTATE_PATTERN = /^rotate(?:-[xyz])?-(?:0|none)$/;
|
|
64337
|
+
const NEUTRAL_SCALE_PATTERN = /^scale(?:-[xyz])?-(?:100|none|\[(?:1(?:\.0+)?|100(?:\.0+)?%)\])$/;
|
|
64338
|
+
const NEUTRAL_ROTATE_PATTERN = /^rotate(?:-[xyz])?-(?:0|none|\[0(?:\.0+)?(?:deg|grad|rad|turn)?\])$/;
|
|
64339
|
+
const MOTION_IMAGE_SCALE_PROPERTY_NAMES = [
|
|
64340
|
+
"scale",
|
|
64341
|
+
"scaleX",
|
|
64342
|
+
"scaleY"
|
|
64343
|
+
];
|
|
64344
|
+
const MOTION_IMAGE_ROTATION_PROPERTY_NAMES = [
|
|
64345
|
+
"rotate",
|
|
64346
|
+
"rotateX",
|
|
64347
|
+
"rotateY",
|
|
64348
|
+
"rotateZ"
|
|
64349
|
+
];
|
|
64350
|
+
const FUNCTIONAL_IMAGE_CONTEXT_PATTERN = /(?:^|[-_\s])(?:crop(?:per)?|gallery|image[-_\s]?viewer|lightbox|product[-_\s]?zoom|zoom)(?:$|[-_\s])/i;
|
|
64351
|
+
const FUNCTIONAL_IMAGE_CONTEXT_IDENTIFIER_WORDS = new Set([
|
|
64352
|
+
"crop",
|
|
64353
|
+
"cropper",
|
|
64354
|
+
"gallery",
|
|
64355
|
+
"lightbox",
|
|
64356
|
+
"zoom"
|
|
64357
|
+
]);
|
|
64358
|
+
const FUNCTIONAL_IMAGE_CONTEXT_VALUE_ATTRIBUTE_NAMES = new Set([
|
|
64359
|
+
"aria-label",
|
|
64360
|
+
"aria-roledescription",
|
|
64361
|
+
"className",
|
|
64362
|
+
"data-testid",
|
|
64363
|
+
"id",
|
|
64364
|
+
"title"
|
|
64365
|
+
]);
|
|
64366
|
+
const INACTIVE_CONTEXT_ATTRIBUTE_STRING_VALUES = new Set([
|
|
64367
|
+
"",
|
|
64368
|
+
"0",
|
|
64369
|
+
"false",
|
|
64370
|
+
"none",
|
|
64371
|
+
"off"
|
|
64372
|
+
]);
|
|
63162
64373
|
const removeNegativeModifier = (utility) => utility.startsWith("-") ? utility.slice(1) : utility;
|
|
63163
64374
|
const getHoverImageTransform = (classNameValue) => {
|
|
63164
64375
|
const rawTokens = splitTailwindClassName(classNameValue);
|
|
@@ -63176,6 +64387,87 @@ const getHoverImageTransform = (classNameValue) => {
|
|
|
63176
64387
|
}
|
|
63177
64388
|
return null;
|
|
63178
64389
|
};
|
|
64390
|
+
const getStaticIntrinsicFactoryTarget = (node) => {
|
|
64391
|
+
const candidate = stripParenExpression(node);
|
|
64392
|
+
if (isNodeOfType(candidate, "MemberExpression")) return getStaticPropertyName(candidate);
|
|
64393
|
+
if (!isNodeOfType(candidate, "CallExpression")) return null;
|
|
64394
|
+
const target = candidate.arguments[0];
|
|
64395
|
+
return target && !isNodeOfType(target, "SpreadElement") && isNodeOfType(target, "Literal") && typeof target.value === "string" ? target.value : null;
|
|
64396
|
+
};
|
|
64397
|
+
const isProvenMotionImage = (node, context) => {
|
|
64398
|
+
if (!isProvenFramerMotionJsxElement(node, context.scopes)) return false;
|
|
64399
|
+
if (isNodeOfType(node.name, "JSXMemberExpression")) return node.name.property.name === "img";
|
|
64400
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return false;
|
|
64401
|
+
const symbol = resolveConstIdentifierAlias(node.name, context.scopes);
|
|
64402
|
+
if (symbol?.kind === "import") return getImportedName(symbol.declarationNode) === "img";
|
|
64403
|
+
return Boolean(symbol?.kind === "const" && symbol.initializer && getStaticIntrinsicFactoryTarget(symbol.initializer) === "img");
|
|
64404
|
+
};
|
|
64405
|
+
const isKnownInactiveContextExpression = (node, context) => {
|
|
64406
|
+
const expression = stripParenExpression(node);
|
|
64407
|
+
if (isLiteralVoidExpression(expression)) return true;
|
|
64408
|
+
if (isNodeOfType(expression, "Identifier") && expression.name === "undefined" && context.scopes.isGlobalReference(expression)) return true;
|
|
64409
|
+
if (!isNodeOfType(expression, "Literal")) return false;
|
|
64410
|
+
if (expression.value === false || expression.value === null || expression.value === 0) return true;
|
|
64411
|
+
return typeof expression.value === "string" && INACTIVE_CONTEXT_ATTRIBUTE_STRING_VALUES.has(expression.value.trim().toLowerCase());
|
|
64412
|
+
};
|
|
64413
|
+
const isPotentiallyActiveContextAttribute = (attribute, context) => {
|
|
64414
|
+
if (!attribute.value) return true;
|
|
64415
|
+
if (isNodeOfType(attribute.value, "Literal")) return !isKnownInactiveContextExpression(attribute.value, context);
|
|
64416
|
+
return isNodeOfType(attribute.value, "JSXExpressionContainer") && !isKnownInactiveContextExpression(attribute.value.expression, context);
|
|
64417
|
+
};
|
|
64418
|
+
const isPotentiallyActiveEventHandler = (attribute, context) => {
|
|
64419
|
+
if (!attribute.value || !isNodeOfType(attribute.value, "JSXExpressionContainer") || isKnownInactiveContextExpression(attribute.value.expression, context)) return false;
|
|
64420
|
+
return !isNodeOfType(stripParenExpression(attribute.value.expression), "Literal");
|
|
64421
|
+
};
|
|
64422
|
+
const hasActiveDragAttribute = (node, context) => {
|
|
64423
|
+
for (const attributeName of ["drag", "draggable"]) {
|
|
64424
|
+
const attribute = getAuthoritativeJsxAttribute(node.attributes, attributeName);
|
|
64425
|
+
if (!attribute) continue;
|
|
64426
|
+
if (isPotentiallyActiveContextAttribute(attribute, context)) return true;
|
|
64427
|
+
}
|
|
64428
|
+
return false;
|
|
64429
|
+
};
|
|
64430
|
+
const hasFunctionalImageContextEvidence = (node, context) => {
|
|
64431
|
+
let currentNode = node;
|
|
64432
|
+
while (currentNode) {
|
|
64433
|
+
const openingElement = isNodeOfType(currentNode, "JSXOpeningElement") ? currentNode : isNodeOfType(currentNode, "JSXElement") ? currentNode.openingElement : null;
|
|
64434
|
+
if (openingElement) {
|
|
64435
|
+
if (hasActiveDragAttribute(openingElement, context)) return true;
|
|
64436
|
+
const elementName = resolveJsxElementName(openingElement);
|
|
64437
|
+
if (elementName && tokenizeIdentifierWords(elementName).some((word) => FUNCTIONAL_IMAGE_CONTEXT_IDENTIFIER_WORDS.has(word))) return true;
|
|
64438
|
+
for (const attribute of openingElement.attributes) {
|
|
64439
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
64440
|
+
const attributeName = getJsxAttributeName(attribute.name);
|
|
64441
|
+
if (!attributeName) continue;
|
|
64442
|
+
if (getAuthoritativeJsxAttribute(openingElement.attributes, attributeName) !== attribute) continue;
|
|
64443
|
+
if (FUNCTIONAL_IMAGE_CONTEXT_PATTERN.test(attributeName) && isPotentiallyActiveContextAttribute(attribute, context)) return true;
|
|
64444
|
+
if (/^onDrag/i.test(attributeName) && isPotentiallyActiveEventHandler(attribute, context)) return true;
|
|
64445
|
+
if (!FUNCTIONAL_IMAGE_CONTEXT_VALUE_ATTRIBUTE_NAMES.has(attributeName)) continue;
|
|
64446
|
+
if (getJsxPropStaticStringValues(attribute, context.scopes)?.some((value) => FUNCTIONAL_IMAGE_CONTEXT_PATTERN.test(value))) return true;
|
|
64447
|
+
}
|
|
64448
|
+
}
|
|
64449
|
+
currentNode = currentNode.parent;
|
|
64450
|
+
}
|
|
64451
|
+
return false;
|
|
64452
|
+
};
|
|
64453
|
+
const getMotionHoverTransformProperty = (node, context) => {
|
|
64454
|
+
if (!isProvenMotionImage(node, context) || hasFunctionalImageContextEvidence(node, context)) return null;
|
|
64455
|
+
const whileHoverObject = getStaticMotionPropObject(node, "whileHover", context.scopes);
|
|
64456
|
+
if (!whileHoverObject) return null;
|
|
64457
|
+
for (const propertyName of MOTION_IMAGE_SCALE_PROPERTY_NAMES) {
|
|
64458
|
+
const property = getEffectiveStyleProperty(whileHoverObject.properties, propertyName);
|
|
64459
|
+
if (!property) continue;
|
|
64460
|
+
const value = getStylePropertyNumberValue(property);
|
|
64461
|
+
if (value !== null && value !== 1) return propertyName;
|
|
64462
|
+
}
|
|
64463
|
+
for (const propertyName of MOTION_IMAGE_ROTATION_PROPERTY_NAMES) {
|
|
64464
|
+
const property = getEffectiveStyleProperty(whileHoverObject.properties, propertyName);
|
|
64465
|
+
if (!property) continue;
|
|
64466
|
+
const value = getStylePropertyNumberValue(property);
|
|
64467
|
+
if (value !== null && value !== 0) return propertyName;
|
|
64468
|
+
}
|
|
64469
|
+
return null;
|
|
64470
|
+
};
|
|
63179
64471
|
const noImageHoverTransform = defineRule({
|
|
63180
64472
|
id: "no-image-hover-transform",
|
|
63181
64473
|
title: "Image scales or rotates on hover",
|
|
@@ -63184,14 +64476,23 @@ const noImageHoverTransform = defineRule({
|
|
|
63184
64476
|
tags: ["design", "test-noise"],
|
|
63185
64477
|
recommendation: "Keep the image stable, or use a subtler hover response tied to an actual interaction affordance.",
|
|
63186
64478
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
63187
|
-
if (
|
|
63188
|
-
|
|
63189
|
-
|
|
63190
|
-
|
|
63191
|
-
|
|
64479
|
+
if (isNodeOfType(node.name, "JSXIdentifier") && node.name.name === "img") {
|
|
64480
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
64481
|
+
if (!classNameValue) return;
|
|
64482
|
+
const hoverTransform = getHoverImageTransform(classNameValue);
|
|
64483
|
+
if (!hoverTransform) return;
|
|
64484
|
+
if (hasFunctionalImageContextEvidence(node, context)) return;
|
|
64485
|
+
context.report({
|
|
64486
|
+
node,
|
|
64487
|
+
message: `The ${hoverTransform} treatment makes the image itself shift under the pointer. Use a steadier hover affordance.`
|
|
64488
|
+
});
|
|
64489
|
+
return;
|
|
64490
|
+
}
|
|
64491
|
+
const motionTransformProperty = getMotionHoverTransformProperty(node, context);
|
|
64492
|
+
if (!motionTransformProperty) return;
|
|
63192
64493
|
context.report({
|
|
63193
64494
|
node,
|
|
63194
|
-
message: `The ${
|
|
64495
|
+
message: `The whileHover ${motionTransformProperty} treatment makes the image itself shift under the pointer. Use a steadier hover affordance.`
|
|
63195
64496
|
});
|
|
63196
64497
|
} })
|
|
63197
64498
|
});
|
|
@@ -67307,6 +68608,9 @@ const UNRESOLVABLE = new Set([
|
|
|
67307
68608
|
"revert",
|
|
67308
68609
|
"none"
|
|
67309
68610
|
]);
|
|
68611
|
+
const GRADIENT_FUNCTION_PATTERN = /^(?:linear|radial|conic)-gradient\(/i;
|
|
68612
|
+
const GRADIENT_PRELUDE_PATTERN = /^(?:to\b|[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:deg|grad|rad|turn)\b|circle\b|ellipse\b|closest-|farthest-|\bat\b|from\b|in\b)/i;
|
|
68613
|
+
const GRADIENT_STOP_POSITION_PATTERN = /^(?:[+-]?0|[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:%|px|rem|em|deg|grad|rad|turn))(?:\s+(?:[+-]?0|[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:%|px|rem|em|deg|grad|rad|turn)))?$/i;
|
|
67310
68614
|
const resolveOpaqueColor = (raw) => {
|
|
67311
68615
|
const value = raw.trim().toLowerCase();
|
|
67312
68616
|
if (UNRESOLVABLE.has(value)) return null;
|
|
@@ -67330,6 +68634,49 @@ const resolveOpaqueColor = (raw) => {
|
|
|
67330
68634
|
}
|
|
67331
68635
|
return parseColorToRgb(value);
|
|
67332
68636
|
};
|
|
68637
|
+
const getFunctionalColorEndIndex = (value) => {
|
|
68638
|
+
const openingParenthesisIndex = value.indexOf("(");
|
|
68639
|
+
if (openingParenthesisIndex < 0) return null;
|
|
68640
|
+
let depth = 0;
|
|
68641
|
+
for (let characterIndex = openingParenthesisIndex; characterIndex < value.length; characterIndex += 1) {
|
|
68642
|
+
const character = value[characterIndex];
|
|
68643
|
+
if (character === "(") depth += 1;
|
|
68644
|
+
if (character === ")") depth -= 1;
|
|
68645
|
+
if (depth < 0) return null;
|
|
68646
|
+
if (depth === 0) return characterIndex + 1;
|
|
68647
|
+
}
|
|
68648
|
+
return null;
|
|
68649
|
+
};
|
|
68650
|
+
const parseOpaqueGradientStop = (stop) => {
|
|
68651
|
+
const trimmedStop = stop.trim();
|
|
68652
|
+
let colorEndIndex = 0;
|
|
68653
|
+
if (/^(?:rgb|hsl)a?\(/i.test(trimmedStop)) {
|
|
68654
|
+
const functionalColorEndIndex = getFunctionalColorEndIndex(trimmedStop);
|
|
68655
|
+
if (functionalColorEndIndex === null) return null;
|
|
68656
|
+
colorEndIndex = functionalColorEndIndex;
|
|
68657
|
+
} else {
|
|
68658
|
+
const colorMatch = trimmedStop.match(/^(?:#[\da-f]+|white|black)(?=\s|$)/i);
|
|
68659
|
+
if (!colorMatch) return null;
|
|
68660
|
+
colorEndIndex = colorMatch[0].length;
|
|
68661
|
+
}
|
|
68662
|
+
const color = resolveOpaqueColor(trimmedStop.slice(0, colorEndIndex));
|
|
68663
|
+
if (!color) return null;
|
|
68664
|
+
const position = trimmedStop.slice(colorEndIndex).trim();
|
|
68665
|
+
return !position || GRADIENT_STOP_POSITION_PATTERN.test(position) ? color : null;
|
|
68666
|
+
};
|
|
68667
|
+
const parseOpaqueGradientStops = (raw) => {
|
|
68668
|
+
const value = raw.trim();
|
|
68669
|
+
if (/var\(/i.test(value)) return null;
|
|
68670
|
+
if (!GRADIENT_FUNCTION_PATTERN.test(value)) return null;
|
|
68671
|
+
const contents = getCssFunctionContents(value);
|
|
68672
|
+
if (contents === null) return null;
|
|
68673
|
+
const parts = splitCssTopLevel(contents, ",");
|
|
68674
|
+
if (!parts || parts.length < 2) return null;
|
|
68675
|
+
const stopParts = parseOpaqueGradientStop(parts[0]) === null && GRADIENT_PRELUDE_PATTERN.test(parts[0]) ? parts.slice(1) : parts;
|
|
68676
|
+
if (stopParts.length < 2) return null;
|
|
68677
|
+
const stops = stopParts.map(parseOpaqueGradientStop);
|
|
68678
|
+
return stops.every((stop) => stop !== null) ? stops : null;
|
|
68679
|
+
};
|
|
67333
68680
|
const toPx = (property) => {
|
|
67334
68681
|
const numberValue = getStylePropertyNumberValue(property);
|
|
67335
68682
|
if (numberValue !== null) return numberValue;
|
|
@@ -67366,17 +68713,32 @@ const noLowContrastInlineStyle = defineRule({
|
|
|
67366
68713
|
let foreground = null;
|
|
67367
68714
|
let backgroundColorRaw = null;
|
|
67368
68715
|
let backgroundShorthandRaw = null;
|
|
68716
|
+
let backgroundImageRaw = null;
|
|
67369
68717
|
let backgroundColorIsUnknown = false;
|
|
67370
68718
|
let backgroundShorthandIsUnknown = false;
|
|
67371
|
-
let
|
|
68719
|
+
let backgroundImageIsUnknown = false;
|
|
68720
|
+
let backgroundClipRaw = null;
|
|
68721
|
+
let webkitBackgroundClipRaw = null;
|
|
68722
|
+
let backgroundClipIsUnknown = false;
|
|
68723
|
+
let webkitBackgroundClipIsUnknown = false;
|
|
67372
68724
|
let fontSizePx = null;
|
|
67373
68725
|
let isBold = null;
|
|
67374
68726
|
for (const property of properties) {
|
|
67375
68727
|
const key = getStylePropertyKey(property);
|
|
67376
68728
|
if (!key) continue;
|
|
67377
68729
|
if (key === "backgroundImage") {
|
|
67378
|
-
|
|
67379
|
-
|
|
68730
|
+
backgroundImageRaw = getStylePropertyStringValue(property);
|
|
68731
|
+
backgroundImageIsUnknown = backgroundImageRaw === null;
|
|
68732
|
+
continue;
|
|
68733
|
+
}
|
|
68734
|
+
if (key === "backgroundClip") {
|
|
68735
|
+
backgroundClipRaw = getStylePropertyStringValue(property);
|
|
68736
|
+
backgroundClipIsUnknown = backgroundClipRaw === null;
|
|
68737
|
+
continue;
|
|
68738
|
+
}
|
|
68739
|
+
if (key === "WebkitBackgroundClip") {
|
|
68740
|
+
webkitBackgroundClipRaw = getStylePropertyStringValue(property);
|
|
68741
|
+
webkitBackgroundClipIsUnknown = webkitBackgroundClipRaw === null;
|
|
67380
68742
|
continue;
|
|
67381
68743
|
}
|
|
67382
68744
|
if (key === "fontSize" && property.type === "Property") {
|
|
@@ -67397,13 +68759,29 @@ const noLowContrastInlineStyle = defineRule({
|
|
|
67397
68759
|
backgroundShorthandIsUnknown = stringValue === null;
|
|
67398
68760
|
}
|
|
67399
68761
|
}
|
|
67400
|
-
if (backgroundColorIsUnknown || backgroundShorthandIsUnknown ||
|
|
68762
|
+
if (backgroundColorIsUnknown || backgroundShorthandIsUnknown || backgroundImageIsUnknown) return;
|
|
67401
68763
|
if (backgroundColorRaw !== null && backgroundShorthandRaw !== null) return;
|
|
67402
|
-
|
|
67403
|
-
|
|
67404
|
-
|
|
68764
|
+
if (!foreground) return;
|
|
68765
|
+
let backgrounds = null;
|
|
68766
|
+
const hasPaintedBackgroundImage = backgroundImageRaw !== null && backgroundImageRaw.trim().toLowerCase() !== "none";
|
|
68767
|
+
if (hasPaintedBackgroundImage && backgroundShorthandRaw !== null) return;
|
|
68768
|
+
if (hasPaintedBackgroundImage || backgroundShorthandRaw !== null && backgroundImageRaw === null) {
|
|
68769
|
+
const gradientRaw = hasPaintedBackgroundImage ? backgroundImageRaw : backgroundShorthandRaw;
|
|
68770
|
+
backgrounds = gradientRaw === null ? null : parseOpaqueGradientStops(gradientRaw);
|
|
68771
|
+
if (hasPaintedBackgroundImage && !backgrounds) return;
|
|
68772
|
+
if (backgrounds) {
|
|
68773
|
+
const clipsBackgroundToText = [backgroundClipRaw, webkitBackgroundClipRaw].some((value) => value?.split(",").some((clipValue) => clipValue.trim().toLowerCase() === "text") === true);
|
|
68774
|
+
if (backgroundClipIsUnknown || webkitBackgroundClipIsUnknown || clipsBackgroundToText) return;
|
|
68775
|
+
}
|
|
68776
|
+
}
|
|
68777
|
+
if (!backgrounds) {
|
|
68778
|
+
const backgroundRaw = backgroundColorRaw ?? backgroundShorthandRaw;
|
|
68779
|
+
const background = backgroundRaw === null ? null : resolveOpaqueColor(backgroundRaw);
|
|
68780
|
+
if (!background) return;
|
|
68781
|
+
backgrounds = [background];
|
|
68782
|
+
}
|
|
67405
68783
|
const threshold = fontSizePx === null || fontSizePx >= 24 || isBold !== false && fontSizePx >= 18.66 ? 3 : WCAG_CONTRAST_NORMAL_MIN;
|
|
67406
|
-
const ratio = getWcagContrastRatio(foreground, background);
|
|
68784
|
+
const ratio = Math.min(...backgrounds.map((background) => getWcagContrastRatio(foreground, background)));
|
|
67407
68785
|
if (ratio < threshold) context.report({
|
|
67408
68786
|
node,
|
|
67409
68787
|
message: `Your users struggle to read this text: its contrast against the background is ${ratio.toFixed(2)}:1, below the ${threshold}:1 WCAG minimum, so darken or lighten one of the colors.`
|
|
@@ -67414,7 +68792,76 @@ const noLowContrastInlineStyle = defineRule({
|
|
|
67414
68792
|
//#region src/plugin/rules/design/no-manufactured-contrast-copy.ts
|
|
67415
68793
|
const NOT_THEN_ASSERTION_PATTERN = /\bnot\s+(?:just\s+)?[^.!?]{3,60}[.!?]\s+(?:it(?:'s| is)|we|you|a|an|the)\b/gi;
|
|
67416
68794
|
const NO_JUST_PATTERN = /\bno\s+[^.!?]{2,50}[.!?]\s+just\s+[^.!?]{2,60}(?:[.!?]|$)/gi;
|
|
67417
|
-
const
|
|
68795
|
+
const ASSERTION_THEN_RESTRICTION_PATTERN = /\b[^.!?]{3,60}\.\s+(?:no|just)\s+[^.!?]{2,60}(?:[.!?]|$)/gi;
|
|
68796
|
+
const LONG_FORM_CONTENT_PATH_PATTERN = /(?:^|[/\\])(?:blog|changelog|content|docs?|documentation|posts?)(?:[/\\]|$)/i;
|
|
68797
|
+
const EXCLUDED_INTRINSIC_COPY_ELEMENT_NAMES = new Set([
|
|
68798
|
+
"code",
|
|
68799
|
+
"kbd",
|
|
68800
|
+
"pre",
|
|
68801
|
+
"samp"
|
|
68802
|
+
]);
|
|
68803
|
+
const EXCLUDED_COPY_COMPONENT_NAME_PATTERN = /(?:Code|Console|Markdown|MDX|Mdx|Terminal)/;
|
|
68804
|
+
const STATIC_COPY_BOUNDARY = "?!";
|
|
68805
|
+
const CONTRAST_COPY_PATTERNS = [
|
|
68806
|
+
NOT_THEN_ASSERTION_PATTERN,
|
|
68807
|
+
NO_JUST_PATTERN,
|
|
68808
|
+
ASSERTION_THEN_RESTRICTION_PATTERN
|
|
68809
|
+
];
|
|
68810
|
+
const isExcludedCopyElement = (node) => {
|
|
68811
|
+
const elementName = resolveJsxElementName(node.openingElement);
|
|
68812
|
+
if (!elementName) return false;
|
|
68813
|
+
return EXCLUDED_INTRINSIC_COPY_ELEMENT_NAMES.has(elementName.toLowerCase()) || EXCLUDED_COPY_COMPONENT_NAME_PATTERN.test(elementName);
|
|
68814
|
+
};
|
|
68815
|
+
const getStaticCopyText = (node, context) => {
|
|
68816
|
+
if (!node) return null;
|
|
68817
|
+
if (isNodeOfType(node, "JSXText")) return node.value ?? "";
|
|
68818
|
+
if (isNodeOfType(node, "JSXEmptyExpression")) return "";
|
|
68819
|
+
if (isNodeOfType(node, "Literal")) {
|
|
68820
|
+
if (typeof node.value === "string") return node.value;
|
|
68821
|
+
if (node.value === null || typeof node.value === "boolean") return "";
|
|
68822
|
+
return STATIC_COPY_BOUNDARY;
|
|
68823
|
+
}
|
|
68824
|
+
if (isNodeOfType(node, "TemplateLiteral")) return node.expressions.length === 0 ? (node.quasis ?? []).map((quasi) => quasi.value?.raw ?? "").join("") : null;
|
|
68825
|
+
if (isNodeOfType(node, "JSXExpressionContainer")) return getStaticCopyText(node.expression, context);
|
|
68826
|
+
if (isNodeOfType(node, "JSXElement") || isNodeOfType(node, "JSXFragment")) {
|
|
68827
|
+
if (isNodeOfType(node, "JSXElement") && (isExcludedCopyElement(node) || isInsideStaticallyHiddenJsxSubtree$1(node, context))) return STATIC_COPY_BOUNDARY;
|
|
68828
|
+
const childTexts = [];
|
|
68829
|
+
for (const child of node.children ?? []) {
|
|
68830
|
+
const childText = getStaticCopyText(child, context);
|
|
68831
|
+
if (childText === null) return null;
|
|
68832
|
+
childTexts.push(childText);
|
|
68833
|
+
}
|
|
68834
|
+
return childTexts.join(" ");
|
|
68835
|
+
}
|
|
68836
|
+
return null;
|
|
68837
|
+
};
|
|
68838
|
+
const isInsideExcludedCopyContext = (node, context) => {
|
|
68839
|
+
if (isInsideStaticallyHiddenJsxSubtree$1(node, context)) return true;
|
|
68840
|
+
let ancestor = node.parent;
|
|
68841
|
+
while (ancestor) {
|
|
68842
|
+
if (isNodeOfType(ancestor, "JSXElement") && isExcludedCopyElement(ancestor)) return true;
|
|
68843
|
+
ancestor = ancestor.parent;
|
|
68844
|
+
}
|
|
68845
|
+
return false;
|
|
68846
|
+
};
|
|
68847
|
+
const countNonOverlappingPatternRanges = (text) => {
|
|
68848
|
+
const ranges = CONTRAST_COPY_PATTERNS.flatMap((pattern) => [...text.matchAll(pattern)].flatMap((match) => match.index === void 0 ? [] : [{
|
|
68849
|
+
start: match.index,
|
|
68850
|
+
end: match.index + match[0].length
|
|
68851
|
+
}])).sort((leftRange, rightRange) => leftRange.start - rightRange.start);
|
|
68852
|
+
let patternCount = 0;
|
|
68853
|
+
let previousRangeEnd = null;
|
|
68854
|
+
for (const range of ranges) {
|
|
68855
|
+
if (previousRangeEnd !== null && range.start < previousRangeEnd) continue;
|
|
68856
|
+
patternCount += 1;
|
|
68857
|
+
previousRangeEnd = range.end;
|
|
68858
|
+
}
|
|
68859
|
+
return patternCount;
|
|
68860
|
+
};
|
|
68861
|
+
const isLongFormContentPath = (context) => {
|
|
68862
|
+
const rootDirectory = getReactDoctorStringSetting(context.settings, "rootDirectory") ?? "";
|
|
68863
|
+
return LONG_FORM_CONTENT_PATH_PATTERN.test(`${rootDirectory}/${context.filename ?? ""}`);
|
|
68864
|
+
};
|
|
67418
68865
|
const noManufacturedContrastCopy = defineRule({
|
|
67419
68866
|
id: "no-manufactured-contrast-copy",
|
|
67420
68867
|
title: "Page repeatedly uses manufactured contrast copy",
|
|
@@ -67423,9 +68870,10 @@ const noManufacturedContrastCopy = defineRule({
|
|
|
67423
68870
|
tags: ["design", "test-noise"],
|
|
67424
68871
|
recommendation: "State the value directly instead of repeatedly contrasting it with a vague alternative.",
|
|
67425
68872
|
create: (context) => ({ JSXElement(node) {
|
|
67426
|
-
if (!isTopLevelPageCopyRoot(node)) return;
|
|
67427
|
-
const
|
|
67428
|
-
|
|
68873
|
+
if (isLongFormContentPath(context) || !isTopLevelPageCopyRoot(node) || isInsideExcludedCopyContext(node, context)) return;
|
|
68874
|
+
const staticCopyText = getStaticCopyText(node, context);
|
|
68875
|
+
if (staticCopyText === null) return;
|
|
68876
|
+
const patternCount = countNonOverlappingPatternRanges(staticCopyText.replace(/\s+/g, " ").trim());
|
|
67429
68877
|
if (patternCount < 3) return;
|
|
67430
68878
|
context.report({
|
|
67431
68879
|
node: node.openingElement,
|
|
@@ -67662,17 +69110,6 @@ const noMirrorPropEffect = defineRule({
|
|
|
67662
69110
|
}
|
|
67663
69111
|
});
|
|
67664
69112
|
//#endregion
|
|
67665
|
-
//#region src/plugin/constants/tailwind.ts
|
|
67666
|
-
const TAILWIND_NAMED_BREAKPOINTS = [
|
|
67667
|
-
"sm",
|
|
67668
|
-
"md",
|
|
67669
|
-
"lg",
|
|
67670
|
-
"xl",
|
|
67671
|
-
"2xl"
|
|
67672
|
-
];
|
|
67673
|
-
const TAILWIND_BREAKPOINT_NAMES = ["", ...TAILWIND_NAMED_BREAKPOINTS];
|
|
67674
|
-
const TAILWIND_BREAKPOINT_RANKS = new Map(TAILWIND_NAMED_BREAKPOINTS.map((breakpointName, breakpointIndex) => [breakpointName, breakpointIndex]));
|
|
67675
|
-
//#endregion
|
|
67676
69113
|
//#region src/plugin/rules/design/utils/get-css-transition-shorthand-evidence.ts
|
|
67677
69114
|
const CSS_TIME_PATTERN$3 = /^(-?\d*\.?\d+)(ms|s)$/i;
|
|
67678
69115
|
const CSS_TIMING_KEYWORDS = new Set([
|
|
@@ -67779,9 +69216,6 @@ const getCssTransitionShorthandEvidence = (value) => {
|
|
|
67779
69216
|
//#region src/plugin/utils/get-tailwind-arbitrary-utility-value.ts
|
|
67780
69217
|
const getTailwindArbitraryUtilityValue = (utility, prefix) => utility.startsWith(prefix) && utility.endsWith("]") ? utility.slice(prefix.length, -1) : null;
|
|
67781
69218
|
//#endregion
|
|
67782
|
-
//#region src/plugin/utils/normalize-tailwind-arbitrary-utility-value.ts
|
|
67783
|
-
const normalizeTailwindArbitraryUtilityValue = (value) => value.replace(/(?<!\\)_/g, " ");
|
|
67784
|
-
//#endregion
|
|
67785
69219
|
//#region src/plugin/utils/get-tailwind-transition-property-effect.ts
|
|
67786
69220
|
const KNOWN_TRANSITION_PROPERTY_EFFECTS = new Map([
|
|
67787
69221
|
["transition-none", {
|
|
@@ -69532,247 +70966,6 @@ const noMultipleMainLandmarks = defineRule({
|
|
|
69532
70966
|
}
|
|
69533
70967
|
});
|
|
69534
70968
|
//#endregion
|
|
69535
|
-
//#region src/plugin/utils/get-tailwind-visibility-effect.ts
|
|
69536
|
-
const DISPLAY_VISIBILITY_EFFECTS = new Map([
|
|
69537
|
-
["hidden", {
|
|
69538
|
-
isVisible: false,
|
|
69539
|
-
propertyName: "display"
|
|
69540
|
-
}],
|
|
69541
|
-
["block", {
|
|
69542
|
-
isVisible: true,
|
|
69543
|
-
propertyName: "display"
|
|
69544
|
-
}],
|
|
69545
|
-
["contents", {
|
|
69546
|
-
isVisible: true,
|
|
69547
|
-
propertyName: "display"
|
|
69548
|
-
}],
|
|
69549
|
-
["flex", {
|
|
69550
|
-
isVisible: true,
|
|
69551
|
-
propertyName: "display"
|
|
69552
|
-
}],
|
|
69553
|
-
["flow-root", {
|
|
69554
|
-
isVisible: true,
|
|
69555
|
-
propertyName: "display"
|
|
69556
|
-
}],
|
|
69557
|
-
["grid", {
|
|
69558
|
-
isVisible: true,
|
|
69559
|
-
propertyName: "display"
|
|
69560
|
-
}],
|
|
69561
|
-
["inline", {
|
|
69562
|
-
isVisible: true,
|
|
69563
|
-
propertyName: "display"
|
|
69564
|
-
}],
|
|
69565
|
-
["inline-block", {
|
|
69566
|
-
isVisible: true,
|
|
69567
|
-
propertyName: "display"
|
|
69568
|
-
}],
|
|
69569
|
-
["inline-flex", {
|
|
69570
|
-
isVisible: true,
|
|
69571
|
-
propertyName: "display"
|
|
69572
|
-
}],
|
|
69573
|
-
["inline-grid", {
|
|
69574
|
-
isVisible: true,
|
|
69575
|
-
propertyName: "display"
|
|
69576
|
-
}],
|
|
69577
|
-
["inline-table", {
|
|
69578
|
-
isVisible: true,
|
|
69579
|
-
propertyName: "display"
|
|
69580
|
-
}],
|
|
69581
|
-
["list-item", {
|
|
69582
|
-
isVisible: true,
|
|
69583
|
-
propertyName: "display"
|
|
69584
|
-
}],
|
|
69585
|
-
["table", {
|
|
69586
|
-
isVisible: true,
|
|
69587
|
-
propertyName: "display"
|
|
69588
|
-
}],
|
|
69589
|
-
["table-caption", {
|
|
69590
|
-
isVisible: true,
|
|
69591
|
-
propertyName: "display"
|
|
69592
|
-
}],
|
|
69593
|
-
["table-cell", {
|
|
69594
|
-
isVisible: true,
|
|
69595
|
-
propertyName: "display"
|
|
69596
|
-
}],
|
|
69597
|
-
["table-column", {
|
|
69598
|
-
isVisible: true,
|
|
69599
|
-
propertyName: "display"
|
|
69600
|
-
}],
|
|
69601
|
-
["table-column-group", {
|
|
69602
|
-
isVisible: true,
|
|
69603
|
-
propertyName: "display"
|
|
69604
|
-
}],
|
|
69605
|
-
["table-footer-group", {
|
|
69606
|
-
isVisible: true,
|
|
69607
|
-
propertyName: "display"
|
|
69608
|
-
}],
|
|
69609
|
-
["table-header-group", {
|
|
69610
|
-
isVisible: true,
|
|
69611
|
-
propertyName: "display"
|
|
69612
|
-
}],
|
|
69613
|
-
["table-row", {
|
|
69614
|
-
isVisible: true,
|
|
69615
|
-
propertyName: "display"
|
|
69616
|
-
}],
|
|
69617
|
-
["table-row-group", {
|
|
69618
|
-
isVisible: true,
|
|
69619
|
-
propertyName: "display"
|
|
69620
|
-
}]
|
|
69621
|
-
]);
|
|
69622
|
-
const VISIBILITY_VISIBILITY_EFFECTS = new Map([
|
|
69623
|
-
["collapse", {
|
|
69624
|
-
isVisible: false,
|
|
69625
|
-
propertyName: "visibility"
|
|
69626
|
-
}],
|
|
69627
|
-
["invisible", {
|
|
69628
|
-
isVisible: false,
|
|
69629
|
-
propertyName: "visibility"
|
|
69630
|
-
}],
|
|
69631
|
-
["visible", {
|
|
69632
|
-
isVisible: true,
|
|
69633
|
-
propertyName: "visibility"
|
|
69634
|
-
}]
|
|
69635
|
-
]);
|
|
69636
|
-
const VISIBLE_ARBITRARY_DISPLAY_VALUES = new Set([
|
|
69637
|
-
"block",
|
|
69638
|
-
"contents",
|
|
69639
|
-
"flex",
|
|
69640
|
-
"flow-root",
|
|
69641
|
-
"grid",
|
|
69642
|
-
"inline",
|
|
69643
|
-
"inline block",
|
|
69644
|
-
"inline flex",
|
|
69645
|
-
"inline flow-root",
|
|
69646
|
-
"inline grid",
|
|
69647
|
-
"inline table",
|
|
69648
|
-
"list-item",
|
|
69649
|
-
"table",
|
|
69650
|
-
"table-caption",
|
|
69651
|
-
"table-cell",
|
|
69652
|
-
"table-column",
|
|
69653
|
-
"table-column-group",
|
|
69654
|
-
"table-footer-group",
|
|
69655
|
-
"table-header-group",
|
|
69656
|
-
"table-row",
|
|
69657
|
-
"table-row-group"
|
|
69658
|
-
]);
|
|
69659
|
-
const getArbitraryCssPropertyValue = (utility, propertyName) => {
|
|
69660
|
-
const propertyPrefix = `[${propertyName}:`;
|
|
69661
|
-
return utility.toLowerCase().startsWith(propertyPrefix) && utility.endsWith("]") ? utility.slice(propertyPrefix.length, -1) : null;
|
|
69662
|
-
};
|
|
69663
|
-
const getTailwindVisibilityEffect = (utility) => {
|
|
69664
|
-
const knownEffect = DISPLAY_VISIBILITY_EFFECTS.get(utility) ?? VISIBILITY_VISIBILITY_EFFECTS.get(utility);
|
|
69665
|
-
if (knownEffect) return {
|
|
69666
|
-
...knownEffect,
|
|
69667
|
-
status: "known"
|
|
69668
|
-
};
|
|
69669
|
-
const arbitraryDisplayValue = getArbitraryCssPropertyValue(utility, "display");
|
|
69670
|
-
if (arbitraryDisplayValue !== null) {
|
|
69671
|
-
const displayValue = normalizeTailwindArbitraryUtilityValue(arbitraryDisplayValue).trim().toLowerCase();
|
|
69672
|
-
if (displayValue === "none") return {
|
|
69673
|
-
isVisible: false,
|
|
69674
|
-
propertyName: "display",
|
|
69675
|
-
status: "known"
|
|
69676
|
-
};
|
|
69677
|
-
return VISIBLE_ARBITRARY_DISPLAY_VALUES.has(displayValue) ? {
|
|
69678
|
-
isVisible: true,
|
|
69679
|
-
propertyName: "display",
|
|
69680
|
-
status: "known"
|
|
69681
|
-
} : {
|
|
69682
|
-
isVisible: null,
|
|
69683
|
-
propertyName: "display",
|
|
69684
|
-
status: "unknown"
|
|
69685
|
-
};
|
|
69686
|
-
}
|
|
69687
|
-
const arbitraryVisibilityValue = getArbitraryCssPropertyValue(utility, "visibility");
|
|
69688
|
-
if (arbitraryVisibilityValue === null) return {
|
|
69689
|
-
isVisible: null,
|
|
69690
|
-
propertyName: null,
|
|
69691
|
-
status: "not-relevant"
|
|
69692
|
-
};
|
|
69693
|
-
const visibilityValue = normalizeTailwindArbitraryUtilityValue(arbitraryVisibilityValue).trim().toLowerCase();
|
|
69694
|
-
if (visibilityValue === "visible") return {
|
|
69695
|
-
isVisible: true,
|
|
69696
|
-
propertyName: "visibility",
|
|
69697
|
-
status: "known"
|
|
69698
|
-
};
|
|
69699
|
-
return visibilityValue === "hidden" || visibilityValue === "collapse" ? {
|
|
69700
|
-
isVisible: false,
|
|
69701
|
-
propertyName: "visibility",
|
|
69702
|
-
status: "known"
|
|
69703
|
-
} : {
|
|
69704
|
-
isVisible: null,
|
|
69705
|
-
propertyName: "visibility",
|
|
69706
|
-
status: "unknown"
|
|
69707
|
-
};
|
|
69708
|
-
};
|
|
69709
|
-
//#endregion
|
|
69710
|
-
//#region src/plugin/utils/get-tailwind-visibility-at-breakpoints.ts
|
|
69711
|
-
const getResponsiveVariantScope = (variants) => {
|
|
69712
|
-
let minimumBreakpointIndex = 0;
|
|
69713
|
-
let maximumBreakpointIndex = TAILWIND_BREAKPOINT_NAMES.length;
|
|
69714
|
-
for (const variant of variants) {
|
|
69715
|
-
const minimumVariantIndex = TAILWIND_BREAKPOINT_NAMES.indexOf(variant);
|
|
69716
|
-
if (minimumVariantIndex > 0) {
|
|
69717
|
-
minimumBreakpointIndex = Math.max(minimumBreakpointIndex, minimumVariantIndex);
|
|
69718
|
-
continue;
|
|
69719
|
-
}
|
|
69720
|
-
if (variant.startsWith("max-")) {
|
|
69721
|
-
const maximumVariantIndex = TAILWIND_BREAKPOINT_NAMES.indexOf(variant.slice(4));
|
|
69722
|
-
if (maximumVariantIndex > 0) {
|
|
69723
|
-
maximumBreakpointIndex = Math.min(maximumBreakpointIndex, maximumVariantIndex);
|
|
69724
|
-
continue;
|
|
69725
|
-
}
|
|
69726
|
-
}
|
|
69727
|
-
return null;
|
|
69728
|
-
}
|
|
69729
|
-
return {
|
|
69730
|
-
maximumBreakpointIndex,
|
|
69731
|
-
minimumBreakpointIndex,
|
|
69732
|
-
specificity: variants.length
|
|
69733
|
-
};
|
|
69734
|
-
};
|
|
69735
|
-
const resolveVisibilityProperty = (scopedEffects, breakpointIndex, propertyName) => {
|
|
69736
|
-
const applicableEffects = scopedEffects.filter(({ effect, scope }) => effect.propertyName === propertyName && breakpointIndex >= scope.minimumBreakpointIndex && breakpointIndex < scope.maximumBreakpointIndex);
|
|
69737
|
-
if (applicableEffects.length === 0) return true;
|
|
69738
|
-
const highestImportanceEffects = applicableEffects.some(({ token }) => token.isImportant) ? applicableEffects.filter(({ token }) => token.isImportant) : applicableEffects;
|
|
69739
|
-
const maximumSpecificity = Math.max(...highestImportanceEffects.map(({ scope }) => scope.specificity));
|
|
69740
|
-
const highestSpecificityEffects = highestImportanceEffects.filter(({ scope }) => scope.specificity === maximumSpecificity);
|
|
69741
|
-
const maximumMinimumBreakpoint = Math.max(...highestSpecificityEffects.map(({ scope }) => scope.minimumBreakpointIndex));
|
|
69742
|
-
const latestMinimumEffects = highestSpecificityEffects.filter(({ scope }) => scope.minimumBreakpointIndex === maximumMinimumBreakpoint);
|
|
69743
|
-
const minimumMaximumBreakpoint = Math.min(...latestMinimumEffects.map(({ scope }) => scope.maximumBreakpointIndex));
|
|
69744
|
-
const highestPriorityStates = new Set(latestMinimumEffects.filter(({ scope }) => scope.maximumBreakpointIndex === minimumMaximumBreakpoint).map(({ effect }) => effect.isVisible));
|
|
69745
|
-
return highestPriorityStates.size === 1 ? highestPriorityStates.values().next().value ?? null : null;
|
|
69746
|
-
};
|
|
69747
|
-
const getTailwindVisibilityAtBreakpoints = (className) => {
|
|
69748
|
-
const scopedEffects = [];
|
|
69749
|
-
for (const token of splitTailwindClassName(className).map(parseTailwindClassNameToken)) {
|
|
69750
|
-
const resolution = getTailwindVisibilityEffect(token.utility);
|
|
69751
|
-
if (resolution.status === "not-relevant") continue;
|
|
69752
|
-
const scope = getResponsiveVariantScope(token.variants);
|
|
69753
|
-
if (scope === null) return null;
|
|
69754
|
-
if (!scope || scope.minimumBreakpointIndex >= scope.maximumBreakpointIndex) continue;
|
|
69755
|
-
if (resolution.status === "unknown" || resolution.propertyName === null || resolution.isVisible === null) return null;
|
|
69756
|
-
const effect = {
|
|
69757
|
-
isVisible: resolution.isVisible,
|
|
69758
|
-
propertyName: resolution.propertyName
|
|
69759
|
-
};
|
|
69760
|
-
scopedEffects.push({
|
|
69761
|
-
effect,
|
|
69762
|
-
scope,
|
|
69763
|
-
token
|
|
69764
|
-
});
|
|
69765
|
-
}
|
|
69766
|
-
const visibilityAtBreakpoints = [];
|
|
69767
|
-
for (let breakpointIndex = 0; breakpointIndex < TAILWIND_BREAKPOINT_NAMES.length; breakpointIndex += 1) {
|
|
69768
|
-
const displayVisibility = resolveVisibilityProperty(scopedEffects, breakpointIndex, "display");
|
|
69769
|
-
const visibilityVisibility = resolveVisibilityProperty(scopedEffects, breakpointIndex, "visibility");
|
|
69770
|
-
if (displayVisibility === null || visibilityVisibility === null) return null;
|
|
69771
|
-
visibilityAtBreakpoints.push(displayVisibility && visibilityVisibility);
|
|
69772
|
-
}
|
|
69773
|
-
return visibilityAtBreakpoints;
|
|
69774
|
-
};
|
|
69775
|
-
//#endregion
|
|
69776
70969
|
//#region src/plugin/rules/a11y/no-multiple-unlabeled-navigation-landmarks.ts
|
|
69777
70970
|
const getLandmarkName = (node) => {
|
|
69778
70971
|
for (const attributeName of ["aria-label", "aria-labelledby"]) {
|
|
@@ -73651,44 +74844,267 @@ const noNullishCoalescingArithmeticPrecedence = defineRule({
|
|
|
73651
74844
|
});
|
|
73652
74845
|
//#endregion
|
|
73653
74846
|
//#region src/plugin/rules/design/no-numbered-section-markers.ts
|
|
73654
|
-
const
|
|
74847
|
+
const NUMBERED_LABEL_ELEMENT_NAMES = new Set([
|
|
74848
|
+
"b",
|
|
74849
|
+
"div",
|
|
74850
|
+
"em",
|
|
74851
|
+
"p",
|
|
74852
|
+
"small",
|
|
74853
|
+
"span",
|
|
74854
|
+
"strong"
|
|
74855
|
+
]);
|
|
74856
|
+
const WRAPPED_HEADING_ELEMENT_NAMES = new Set(["div", "header"]);
|
|
74857
|
+
const ORDERED_CONTEXT_ELEMENT_NAMES = new Set([
|
|
74858
|
+
"article",
|
|
74859
|
+
"li",
|
|
74860
|
+
"menu",
|
|
74861
|
+
"nav",
|
|
74862
|
+
"ol",
|
|
74863
|
+
"table",
|
|
74864
|
+
"tbody",
|
|
74865
|
+
"td",
|
|
74866
|
+
"tfoot",
|
|
74867
|
+
"th",
|
|
74868
|
+
"thead",
|
|
74869
|
+
"time",
|
|
74870
|
+
"tr",
|
|
74871
|
+
"ul"
|
|
74872
|
+
]);
|
|
74873
|
+
const ORDERED_CONTEXT_ROLES = new Set([
|
|
74874
|
+
"list",
|
|
74875
|
+
"listitem",
|
|
74876
|
+
"navigation",
|
|
74877
|
+
"progressbar",
|
|
74878
|
+
"status"
|
|
74879
|
+
]);
|
|
73655
74880
|
const HEADING_ELEMENT_PATTERN$3 = /^h[1-6]$/;
|
|
73656
|
-
const
|
|
73657
|
-
|
|
74881
|
+
const ORDERED_CONTEXT_CLASS_SEGMENT_PATTERN = /(?:^|[-_:])(?:calendar|card|card-item|date|day|milestone|month|progress|step|stepper|steps|timeline|year)(?:$|[-_:])/i;
|
|
74882
|
+
const ORDERED_CONTEXT_COMPONENT_NAME_PATTERN = /(?:Calendar|Date|Day|Milestone|Month|Progress|Step|Stepper|Timeline|Year)/;
|
|
74883
|
+
const ORDERED_CONTEXT_LABEL_PATTERN = /\b(?:progress|step|steps)\b/i;
|
|
74884
|
+
const ORDERED_HEADING_PATTERN = /^(?:phase|stage|step)\b/i;
|
|
74885
|
+
const DATE_HEADING_PATTERN = /^(?:fri(?:day)?|mon(?:day)?|sat(?:urday)?|sun(?:day)?|thu(?:rsday)?|tue(?:sday)?|wed(?:nesday)?|jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sep(?:tember)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\b/i;
|
|
74886
|
+
const DATE_LIKE_LABEL_PATTERN = /\b(?:19|20)\d{2}\b|\b(?:jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sep(?:tember)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\b|\d{1,2}[./:-]\d{1,2}/i;
|
|
74887
|
+
const BARE_NUMBERED_LABEL_PATTERN = /^(\d{2})$/;
|
|
74888
|
+
const COMPOUND_NUMBERED_LABEL_PATTERN = /^(\d{1,2})\s*[/|·•—–]\s*\p{L}/u;
|
|
74889
|
+
const ACCENT_TEXT_CLASS_PATTERN = /^text-(?:amber|blue|cyan|emerald|fuchsia|green|indigo|lime|orange|pink|purple|red|rose|sky|teal|violet|yellow)-\d{2,3}$/;
|
|
74890
|
+
const ACCENT_ARBITRARY_TEXT_CLASS_PATTERN = /^text-\[(?:#|color:|hsl|oklch|rgb)/i;
|
|
74891
|
+
const BOLD_FONT_CLASS_NAMES = new Set([
|
|
74892
|
+
"font-black",
|
|
74893
|
+
"font-bold",
|
|
74894
|
+
"font-extrabold",
|
|
74895
|
+
"font-semibold"
|
|
74896
|
+
]);
|
|
74897
|
+
const getFullyStaticJsxText = (node) => {
|
|
74898
|
+
if (!node) return "";
|
|
74899
|
+
if (isNodeOfType(node, "JSXText")) return node.value ?? "";
|
|
74900
|
+
if (isNodeOfType(node, "Literal")) return typeof node.value === "string" || typeof node.value === "number" ? String(node.value) : null;
|
|
74901
|
+
if (isNodeOfType(node, "TemplateLiteral")) return node.expressions.length === 0 ? node.quasis[0]?.value.raw ?? "" : null;
|
|
74902
|
+
if (isNodeOfType(node, "JSXExpressionContainer")) return isNodeOfType(node.expression, "JSXEmptyExpression") ? "" : getFullyStaticJsxText(node.expression);
|
|
74903
|
+
if (isNodeOfType(node, "JSXElement") || isNodeOfType(node, "JSXFragment")) {
|
|
74904
|
+
let text = "";
|
|
74905
|
+
for (const child of node.children ?? []) {
|
|
74906
|
+
const childText = getFullyStaticJsxText(child);
|
|
74907
|
+
if (childText === null) return null;
|
|
74908
|
+
text += childText;
|
|
74909
|
+
}
|
|
74910
|
+
return text;
|
|
74911
|
+
}
|
|
74912
|
+
return null;
|
|
74913
|
+
};
|
|
74914
|
+
const getOutermostJsxRoot = (node) => {
|
|
74915
|
+
let outermostJsxRoot = node;
|
|
74916
|
+
let ancestor = node.parent;
|
|
74917
|
+
while (ancestor) {
|
|
74918
|
+
if (isNodeOfType(ancestor, "JSXElement") || isNodeOfType(ancestor, "JSXFragment")) outermostJsxRoot = ancestor;
|
|
74919
|
+
ancestor = ancestor.parent;
|
|
74920
|
+
}
|
|
74921
|
+
return outermostJsxRoot;
|
|
74922
|
+
};
|
|
74923
|
+
const hasConditionalOrLogicalAncestor = (node) => {
|
|
74924
|
+
let ancestor = node.parent;
|
|
74925
|
+
while (ancestor) {
|
|
74926
|
+
if (isNodeOfType(ancestor, "ConditionalExpression") || isNodeOfType(ancestor, "LogicalExpression")) return true;
|
|
74927
|
+
ancestor = ancestor.parent;
|
|
74928
|
+
}
|
|
74929
|
+
return false;
|
|
74930
|
+
};
|
|
74931
|
+
const getAncestorOpeningElements$1 = (node) => {
|
|
74932
|
+
const openingElements = [];
|
|
74933
|
+
let ancestor = node;
|
|
74934
|
+
while (ancestor) {
|
|
74935
|
+
if (isNodeOfType(ancestor, "JSXElement")) openingElements.push(ancestor.openingElement);
|
|
74936
|
+
ancestor = ancestor.parent;
|
|
74937
|
+
}
|
|
74938
|
+
return openingElements;
|
|
74939
|
+
};
|
|
74940
|
+
const hasUnresolvedJsxAttribute = (openingElement, attributeName) => {
|
|
74941
|
+
const attribute = getAuthoritativeJsxAttribute(openingElement.attributes, attributeName, false);
|
|
74942
|
+
if (!attribute?.value) return false;
|
|
74943
|
+
if (getStringLiteralAttributeValue(attribute) !== null) return false;
|
|
74944
|
+
return !(isNodeOfType(attribute.value, "JSXExpressionContainer") && isNodeOfType(attribute.value.expression, "Literal"));
|
|
74945
|
+
};
|
|
74946
|
+
const hasUnresolvedInlineRenderingStyle = (openingElement, scopes) => {
|
|
74947
|
+
const styleAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "style");
|
|
74948
|
+
if (!styleAttribute) return false;
|
|
74949
|
+
const styleExpression = getInlineStyleExpression(styleAttribute, scopes);
|
|
74950
|
+
if (!styleExpression) return true;
|
|
74951
|
+
if (styleExpression.properties.some((property) => getStylePropertyKey(property) === null)) return true;
|
|
74952
|
+
for (const propertyName of ["display", "visibility"]) {
|
|
74953
|
+
const property = getEffectiveStyleProperty(styleExpression.properties, propertyName);
|
|
74954
|
+
if (property && getStylePropertyStringValue(property) === null) return true;
|
|
74955
|
+
}
|
|
74956
|
+
return false;
|
|
74957
|
+
};
|
|
74958
|
+
const isHiddenOrRenderingUnknown = (openingElement, hasTailwind, scopes, settings) => {
|
|
74959
|
+
if (hasJsxSpreadAttribute(openingElement.attributes) || hasUnresolvedJsxAttribute(openingElement, "hidden") || hasUnresolvedJsxAttribute(openingElement, "aria-hidden") || hasUnresolvedInlineRenderingStyle(openingElement, scopes)) return true;
|
|
74960
|
+
const classNameAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "className");
|
|
74961
|
+
const classNameValue = getStringFromClassNameAttr(openingElement);
|
|
74962
|
+
if (classNameAttribute && classNameValue === null) return true;
|
|
74963
|
+
if (isHiddenFromScreenReader(openingElement, settings)) return true;
|
|
74964
|
+
const styleAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "style");
|
|
74965
|
+
const styleExpression = styleAttribute ? getInlineStyleExpression(styleAttribute, scopes) : null;
|
|
74966
|
+
if (styleExpression) {
|
|
74967
|
+
const displayProperty = getEffectiveStyleProperty(styleExpression.properties, "display");
|
|
74968
|
+
if (displayProperty && getStylePropertyStringValue(displayProperty)?.toLowerCase() === "none") return true;
|
|
74969
|
+
const visibilityProperty = getEffectiveStyleProperty(styleExpression.properties, "visibility");
|
|
74970
|
+
const visibilityValue = visibilityProperty ? getStylePropertyStringValue(visibilityProperty)?.toLowerCase() : null;
|
|
74971
|
+
if (visibilityValue === "hidden" || visibilityValue === "collapse") return true;
|
|
74972
|
+
}
|
|
74973
|
+
if (!hasTailwind || !classNameValue) return false;
|
|
74974
|
+
const visibilityAtBreakpoints = getTailwindVisibilityAtBreakpoints(classNameValue);
|
|
74975
|
+
return visibilityAtBreakpoints === null || visibilityAtBreakpoints.every((isVisible) => !isVisible);
|
|
74976
|
+
};
|
|
74977
|
+
const hasHiddenOrUnknownAncestor = (node, hasTailwind, scopes, settings) => getAncestorOpeningElements$1(node).some((openingElement) => isHiddenOrRenderingUnknown(openingElement, hasTailwind, scopes, settings));
|
|
74978
|
+
const parseNumberedSectionLabel = (text) => {
|
|
74979
|
+
const normalizedText = text.replace(/\s+/g, " ").trim();
|
|
74980
|
+
if (!normalizedText || normalizedText.length > 40 || DATE_LIKE_LABEL_PATTERN.test(normalizedText)) return null;
|
|
74981
|
+
const match = normalizedText.match(BARE_NUMBERED_LABEL_PATTERN) ?? normalizedText.match(COMPOUND_NUMBERED_LABEL_PATTERN);
|
|
73658
74982
|
if (!match) return null;
|
|
74983
|
+
const index = Number.parseInt(match[1], 10);
|
|
74984
|
+
return index > 0 && index <= 40 ? index : null;
|
|
74985
|
+
};
|
|
74986
|
+
const getHeadingFromElement = (element) => {
|
|
74987
|
+
if (!isNodeOfType(element.openingElement.name, "JSXIdentifier")) return null;
|
|
74988
|
+
if (HEADING_ELEMENT_PATTERN$3.test(element.openingElement.name.name)) return element;
|
|
74989
|
+
if (!WRAPPED_HEADING_ELEMENT_NAMES.has(element.openingElement.name.name)) return null;
|
|
74990
|
+
for (const child of element.children ?? []) {
|
|
74991
|
+
if (isNodeOfType(child, "JSXText") && child.value.trim() === "") continue;
|
|
74992
|
+
if (isNodeOfType(child, "JSXExpressionContainer") && isNodeOfType(child.expression, "JSXEmptyExpression")) continue;
|
|
74993
|
+
return isNodeOfType(child, "JSXElement") ? getHeadingFromElement(child) : null;
|
|
74994
|
+
}
|
|
74995
|
+
return null;
|
|
74996
|
+
};
|
|
74997
|
+
const getFollowingStaticHeading = (node) => {
|
|
73659
74998
|
const sibling = getNextStaticJsxElementSibling(node);
|
|
73660
|
-
if (!sibling
|
|
73661
|
-
|
|
74999
|
+
if (!sibling) return null;
|
|
75000
|
+
const heading = getHeadingFromElement(sibling);
|
|
75001
|
+
const headingText = getFullyStaticJsxText(heading);
|
|
75002
|
+
if (!heading || !headingText) return null;
|
|
75003
|
+
const normalizedHeadingText = headingText.replace(/\s+/g, " ").trim();
|
|
75004
|
+
if (!normalizedHeadingText || ORDERED_HEADING_PATTERN.test(normalizedHeadingText) || DATE_HEADING_PATTERN.test(normalizedHeadingText)) return null;
|
|
75005
|
+
return heading;
|
|
75006
|
+
};
|
|
75007
|
+
const hasOrderedOrUnresolvedContext = (node) => {
|
|
75008
|
+
let ancestor = node;
|
|
75009
|
+
while (ancestor) {
|
|
75010
|
+
if (isNodeOfType(ancestor, "JSXElement")) {
|
|
75011
|
+
const openingElement = ancestor.openingElement;
|
|
75012
|
+
if (hasJsxSpreadAttribute(openingElement.attributes)) return true;
|
|
75013
|
+
if (isNodeOfType(openingElement.name, "JSXIdentifier")) {
|
|
75014
|
+
const elementName = openingElement.name.name;
|
|
75015
|
+
if (ORDERED_CONTEXT_ELEMENT_NAMES.has(elementName)) return true;
|
|
75016
|
+
if (ORDERED_CONTEXT_COMPONENT_NAME_PATTERN.test(elementName)) return true;
|
|
75017
|
+
}
|
|
75018
|
+
const roleAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "role", false);
|
|
75019
|
+
const roleValue = roleAttribute ? getStringLiteralAttributeValue(roleAttribute) : null;
|
|
75020
|
+
if (roleAttribute && roleValue === null) return true;
|
|
75021
|
+
const role = roleValue?.toLowerCase();
|
|
75022
|
+
if (role && ORDERED_CONTEXT_ROLES.has(role)) return true;
|
|
75023
|
+
if (getAuthoritativeJsxAttribute(openingElement.attributes, "dateTime", false)) return true;
|
|
75024
|
+
const ariaLabelAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "aria-label", false);
|
|
75025
|
+
const ariaLabelValue = ariaLabelAttribute ? getStringLiteralAttributeValue(ariaLabelAttribute) : null;
|
|
75026
|
+
if (ariaLabelAttribute && ariaLabelValue === null) return true;
|
|
75027
|
+
const ariaLabel = ariaLabelValue ?? "";
|
|
75028
|
+
if (ariaLabel && ORDERED_CONTEXT_LABEL_PATTERN.test(ariaLabel)) return true;
|
|
75029
|
+
const classNameAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "className");
|
|
75030
|
+
const classNameValue = getStringFromClassNameAttr(openingElement);
|
|
75031
|
+
if (classNameAttribute && classNameValue === null) return true;
|
|
75032
|
+
if (classNameValue && ORDERED_CONTEXT_CLASS_SEGMENT_PATTERN.test(classNameValue)) return true;
|
|
75033
|
+
}
|
|
75034
|
+
ancestor = ancestor.parent;
|
|
75035
|
+
}
|
|
75036
|
+
return false;
|
|
75037
|
+
};
|
|
75038
|
+
const hasInlineMicroLabelTreatment = (openingElement, scopes) => {
|
|
75039
|
+
const styleAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "style");
|
|
75040
|
+
if (!styleAttribute) return false;
|
|
75041
|
+
const styleExpression = getInlineStyleExpression(styleAttribute, scopes);
|
|
75042
|
+
if (!styleExpression) return false;
|
|
75043
|
+
const fontFamily = getEffectiveStyleProperty(styleExpression.properties, "fontFamily");
|
|
75044
|
+
if (fontFamily && /mono/i.test(getStylePropertyStringValue(fontFamily) ?? "")) return true;
|
|
75045
|
+
const fontWeight = getEffectiveStyleProperty(styleExpression.properties, "fontWeight");
|
|
75046
|
+
if (fontWeight) {
|
|
75047
|
+
if ((getStylePropertyNumberValue(fontWeight) ?? Number.parseInt(getStylePropertyStringValue(fontWeight) ?? "", 10)) >= 600) return true;
|
|
75048
|
+
if (/^(?:bold|bolder)$/i.test(getStylePropertyStringValue(fontWeight) ?? "")) return true;
|
|
75049
|
+
}
|
|
75050
|
+
const letterSpacing = getEffectiveStyleProperty(styleExpression.properties, "letterSpacing");
|
|
75051
|
+
if (letterSpacing) {
|
|
75052
|
+
if ((getStylePropertyNumberValue(letterSpacing) ?? Number.parseFloat(getStylePropertyStringValue(letterSpacing) ?? "")) > 0) return true;
|
|
75053
|
+
}
|
|
75054
|
+
const textTransform = getEffectiveStyleProperty(styleExpression.properties, "textTransform");
|
|
75055
|
+
return Boolean(textTransform && getStylePropertyStringValue(textTransform)?.toLowerCase() === "uppercase");
|
|
75056
|
+
};
|
|
75057
|
+
const hasTailwindMicroLabelTreatment = (openingElement) => {
|
|
75058
|
+
const classNameValue = getStringFromClassNameAttr(openingElement);
|
|
75059
|
+
if (!classNameValue) return false;
|
|
75060
|
+
return getUnvariantClassNameTokens(classNameValue).some((classNameToken) => classNameToken === "font-mono" || classNameToken === "uppercase" || BOLD_FONT_CLASS_NAMES.has(classNameToken) || classNameToken.startsWith("tracking-") && classNameToken !== "tracking-normal" || ACCENT_TEXT_CLASS_PATTERN.test(classNameToken) || ACCENT_ARBITRARY_TEXT_CLASS_PATTERN.test(classNameToken));
|
|
75061
|
+
};
|
|
75062
|
+
const getSectionMarker = (node, hasTailwind, scopes, settings) => {
|
|
75063
|
+
const heading = getFollowingStaticHeading(node);
|
|
75064
|
+
if (!isNodeOfType(node.openingElement.name, "JSXIdentifier") || !NUMBERED_LABEL_ELEMENT_NAMES.has(node.openingElement.name.name) || !heading || hasConditionalOrLogicalAncestor(node) || hasOrderedOrUnresolvedContext(node) || hasOrderedOrUnresolvedContext(heading) || hasHiddenOrUnknownAncestor(node, hasTailwind, scopes, settings) || hasHiddenOrUnknownAncestor(heading, hasTailwind, scopes, settings)) return null;
|
|
75065
|
+
const text = getFullyStaticJsxText(node);
|
|
75066
|
+
if (text === null) return null;
|
|
75067
|
+
const index = parseNumberedSectionLabel(text);
|
|
75068
|
+
if (index === null) return null;
|
|
75069
|
+
const fontSize = getStaticEffectiveFontSize(node.openingElement, hasTailwind, scopes);
|
|
75070
|
+
if (fontSize === null || fontSize <= 0 || fontSize > 13) return null;
|
|
75071
|
+
if (!hasInlineMicroLabelTreatment(node.openingElement, scopes) && !(hasTailwind && hasTailwindMicroLabelTreatment(node.openingElement))) return null;
|
|
75072
|
+
return {
|
|
75073
|
+
index,
|
|
75074
|
+
openingElement: node.openingElement
|
|
75075
|
+
};
|
|
73662
75076
|
};
|
|
73663
75077
|
const noNumberedSectionMarkers = defineRule({
|
|
73664
75078
|
id: "no-numbered-section-markers",
|
|
73665
|
-
title: "
|
|
75079
|
+
title: "Styled numbers are used as section decoration",
|
|
73666
75080
|
severity: "warn",
|
|
73667
75081
|
defaultEnabled: false,
|
|
73668
75082
|
tags: ["design", "test-noise"],
|
|
73669
75083
|
recommendation: "Remove decorative section numbering unless the sequence communicates real progress or ordered steps.",
|
|
73670
75084
|
create: (context) => {
|
|
73671
|
-
const
|
|
75085
|
+
const hasTailwind = hasCapabilityOrUnspecified(context.settings, "tailwind");
|
|
75086
|
+
const markerBuckets = /* @__PURE__ */ new Map();
|
|
73672
75087
|
return {
|
|
73673
75088
|
JSXElement(node) {
|
|
73674
|
-
const marker = getSectionMarker(node);
|
|
73675
|
-
if (marker
|
|
73676
|
-
|
|
75089
|
+
const marker = getSectionMarker(node, hasTailwind, context.scopes, context.settings);
|
|
75090
|
+
if (!marker) return;
|
|
75091
|
+
const outermostJsxRoot = getOutermostJsxRoot(node);
|
|
75092
|
+
const existingBucket = markerBuckets.get(outermostJsxRoot);
|
|
75093
|
+
if (existingBucket) {
|
|
75094
|
+
existingBucket.set(marker.index, marker.openingElement);
|
|
75095
|
+
return;
|
|
75096
|
+
}
|
|
75097
|
+
markerBuckets.set(outermostJsxRoot, new Map([[marker.index, marker.openingElement]]));
|
|
73677
75098
|
},
|
|
73678
75099
|
"Program:exit"() {
|
|
73679
|
-
const
|
|
73680
|
-
|
|
73681
|
-
|
|
73682
|
-
if (
|
|
73683
|
-
|
|
73684
|
-
|
|
73685
|
-
|
|
73686
|
-
|
|
73687
|
-
message: "Several headings are prefixed with decorative sequence numbers. Keep numbering for genuinely ordered steps, not visual scaffolding."
|
|
73688
|
-
});
|
|
73689
|
-
return;
|
|
73690
|
-
}
|
|
73691
|
-
runStartIndex = markerIndex;
|
|
75100
|
+
for (const markers of markerBuckets.values()) {
|
|
75101
|
+
if (markers.size < 2) continue;
|
|
75102
|
+
const firstNode = markers.values().next().value;
|
|
75103
|
+
if (!firstNode) continue;
|
|
75104
|
+
context.report({
|
|
75105
|
+
node: firstNode,
|
|
75106
|
+
message: "Several headings are prefixed with styled numeric labels. Keep numbering for genuinely ordered steps, not visual scaffolding."
|
|
75107
|
+
});
|
|
73692
75108
|
}
|
|
73693
75109
|
}
|
|
73694
75110
|
};
|
|
@@ -74263,7 +75679,7 @@ const noOversizedLongHeading = defineRule({
|
|
|
74263
75679
|
});
|
|
74264
75680
|
//#endregion
|
|
74265
75681
|
//#region src/plugin/rules/design/no-overwide-text-measure.ts
|
|
74266
|
-
const TEXT_ELEMENT_NAMES = new Set([
|
|
75682
|
+
const TEXT_ELEMENT_NAMES$1 = new Set([
|
|
74267
75683
|
"blockquote",
|
|
74268
75684
|
"dd",
|
|
74269
75685
|
"figcaption",
|
|
@@ -74279,7 +75695,7 @@ const noOverwideTextMeasure = defineRule({
|
|
|
74279
75695
|
category: "Accessibility",
|
|
74280
75696
|
recommendation: "Constrain long-form text to a readable line length, usually between 60ch and 75ch.",
|
|
74281
75697
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
74282
|
-
if (!isNodeOfType(node.name, "JSXIdentifier") || !TEXT_ELEMENT_NAMES.has(node.name.name)) return;
|
|
75698
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || !TEXT_ELEMENT_NAMES$1.has(node.name.name)) return;
|
|
74283
75699
|
const classNameValue = getStringFromClassNameAttr(node);
|
|
74284
75700
|
if (classNameValue) {
|
|
74285
75701
|
const overwideToken = getClassNameTokens(classNameValue).find((token) => {
|
|
@@ -77298,6 +78714,118 @@ const noPropTypes = defineRule({
|
|
|
77298
78714
|
})
|
|
77299
78715
|
});
|
|
77300
78716
|
//#endregion
|
|
78717
|
+
//#region src/plugin/rules/design/no-pulsing-status-dot.ts
|
|
78718
|
+
const ANIMATION_UTILITY_PATTERN = /^animate-/;
|
|
78719
|
+
const ROUNDING_UTILITY_PATTERN = /^rounded(?:-|$)/;
|
|
78720
|
+
const WIDTH_UTILITY_PATTERN$1 = /^(?:size|w)-/;
|
|
78721
|
+
const HEIGHT_UTILITY_PATTERN$1 = /^(?:h|size)-/;
|
|
78722
|
+
const PULSING_ANIMATION_UTILITIES = new Set(["animate-ping", "animate-pulse"]);
|
|
78723
|
+
const LIVE_STATUS_ROLES = new Set([
|
|
78724
|
+
"alert",
|
|
78725
|
+
"progressbar",
|
|
78726
|
+
"status"
|
|
78727
|
+
]);
|
|
78728
|
+
const hasTinySquareSize = (tokens) => {
|
|
78729
|
+
const widthUtility = getEffectiveTailwindClassNameToken(tokens, (utility) => WIDTH_UTILITY_PATTERN$1.test(utility));
|
|
78730
|
+
const heightUtility = getEffectiveTailwindClassNameToken(tokens, (utility) => HEIGHT_UTILITY_PATTERN$1.test(utility));
|
|
78731
|
+
const widthPx = widthUtility ? parseStaticTailwindLengthPx(widthUtility, "size") ?? parseStaticTailwindLengthPx(widthUtility, "w") : null;
|
|
78732
|
+
const heightPx = heightUtility ? parseStaticTailwindLengthPx(heightUtility, "size") ?? parseStaticTailwindLengthPx(heightUtility, "h") : null;
|
|
78733
|
+
return Boolean(widthPx !== null && heightPx !== null && widthPx === heightPx && widthPx >= 2 && widthPx <= 16);
|
|
78734
|
+
};
|
|
78735
|
+
const readStaticBooleanValue = (value) => {
|
|
78736
|
+
if (typeof value !== "boolean" && typeof value !== "string") return null;
|
|
78737
|
+
const normalizedValue = value.toString().toLowerCase();
|
|
78738
|
+
if (normalizedValue === "true") return true;
|
|
78739
|
+
if (normalizedValue === "false") return false;
|
|
78740
|
+
return null;
|
|
78741
|
+
};
|
|
78742
|
+
const getStaticBooleanAttributeValue = (attribute) => {
|
|
78743
|
+
if (!attribute.value) return true;
|
|
78744
|
+
if (isNodeOfType(attribute.value, "Literal")) return readStaticBooleanValue(attribute.value.value);
|
|
78745
|
+
if (isNodeOfType(attribute.value, "JSXExpressionContainer") && isNodeOfType(attribute.value.expression, "Literal")) return readStaticBooleanValue(attribute.value.expression.value);
|
|
78746
|
+
return null;
|
|
78747
|
+
};
|
|
78748
|
+
const hasOnlyWhitespaceChildren = (element) => element.children.every((child) => isNodeOfType(child, "JSXText") && child.value.trim().length === 0);
|
|
78749
|
+
const hasLiveStatusSemanticExemption = (element) => {
|
|
78750
|
+
let ancestor = element;
|
|
78751
|
+
while (ancestor) {
|
|
78752
|
+
if (isNodeOfType(ancestor, "JSXElement")) {
|
|
78753
|
+
const openingElement = ancestor.openingElement;
|
|
78754
|
+
if (hasJsxSpreadAttribute(openingElement.attributes)) return true;
|
|
78755
|
+
const ariaBusyAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "aria-busy", false);
|
|
78756
|
+
if (ariaBusyAttribute && getStaticBooleanAttributeValue(ariaBusyAttribute) !== false) return true;
|
|
78757
|
+
const ariaLiveAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "aria-live", false);
|
|
78758
|
+
if (ariaLiveAttribute) {
|
|
78759
|
+
const ariaLiveValue = getStringLiteralAttributeValue(ariaLiveAttribute);
|
|
78760
|
+
if (!ariaLiveValue || ariaLiveValue.trim().toLowerCase() !== "off") return true;
|
|
78761
|
+
}
|
|
78762
|
+
const roleAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "role", false);
|
|
78763
|
+
if (roleAttribute) {
|
|
78764
|
+
const roleValue = getStringLiteralAttributeValue(roleAttribute);
|
|
78765
|
+
if (!roleValue || roleValue.trim().toLowerCase().split(/\s+/).some((role) => LIVE_STATUS_ROLES.has(role))) return true;
|
|
78766
|
+
}
|
|
78767
|
+
}
|
|
78768
|
+
ancestor = ancestor.parent;
|
|
78769
|
+
}
|
|
78770
|
+
return false;
|
|
78771
|
+
};
|
|
78772
|
+
const isHeaderContext = (node) => {
|
|
78773
|
+
let ancestor = node.parent;
|
|
78774
|
+
while (ancestor) {
|
|
78775
|
+
if (isNodeOfType(ancestor, "JSXElement") && isNodeOfType(ancestor.openingElement.name, "JSXIdentifier") && ancestor.openingElement.name.name === "header") return true;
|
|
78776
|
+
ancestor = ancestor.parent;
|
|
78777
|
+
}
|
|
78778
|
+
return false;
|
|
78779
|
+
};
|
|
78780
|
+
const isStrictNavigationContext = (node) => {
|
|
78781
|
+
let ancestor = node.parent;
|
|
78782
|
+
while (ancestor) {
|
|
78783
|
+
if (isNodeOfType(ancestor, "JSXElement")) {
|
|
78784
|
+
const openingElement = ancestor.openingElement;
|
|
78785
|
+
if (isNodeOfType(openingElement.name, "JSXIdentifier") && openingElement.name.name === "nav") return true;
|
|
78786
|
+
const roleAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "role", false);
|
|
78787
|
+
if ((roleAttribute ? getStringLiteralAttributeValue(roleAttribute) : null)?.trim().toLowerCase().split(/\s+/).some((role) => role === "navigation")) return true;
|
|
78788
|
+
}
|
|
78789
|
+
ancestor = ancestor.parent;
|
|
78790
|
+
}
|
|
78791
|
+
return false;
|
|
78792
|
+
};
|
|
78793
|
+
const isHeroContext = (node) => {
|
|
78794
|
+
let ancestor = node.parent;
|
|
78795
|
+
while (ancestor) {
|
|
78796
|
+
if (isNodeOfType(ancestor, "JSXElement") && isNodeOfType(ancestor.openingElement.name, "JSXIdentifier") && ancestor.openingElement.name.name === "section") return getStaticJsxDescendantOpeningElements(ancestor).some((openingElement) => isNodeOfType(openingElement.name, "JSXIdentifier") && openingElement.name.name === "h1");
|
|
78797
|
+
ancestor = ancestor.parent;
|
|
78798
|
+
}
|
|
78799
|
+
return false;
|
|
78800
|
+
};
|
|
78801
|
+
const noPulsingStatusDot = defineRule({
|
|
78802
|
+
id: "no-pulsing-status-dot",
|
|
78803
|
+
title: "Decorative status dot pulses continuously",
|
|
78804
|
+
severity: "warn",
|
|
78805
|
+
defaultEnabled: false,
|
|
78806
|
+
tags: [
|
|
78807
|
+
"design",
|
|
78808
|
+
"test-noise",
|
|
78809
|
+
"react-jsx-only"
|
|
78810
|
+
],
|
|
78811
|
+
requires: ["tailwind"],
|
|
78812
|
+
recommendation: "Use a static status indicator unless the element communicates real work in progress.",
|
|
78813
|
+
create: (context) => ({ JSXElement(node) {
|
|
78814
|
+
const openingElement = node.openingElement;
|
|
78815
|
+
if (!isProvenIntrinsicJsxElement(openingElement, context.scopes) || getAuthoritativeJsxAttribute(openingElement.attributes, "style") !== null || !hasOnlyWhitespaceChildren(node) || hasLiveStatusSemanticExemption(node) || isInsideStaticallyHiddenJsxSubtree$1(node, context) || !isStrictNavigationContext(node) && !isHeaderContext(node) && !isHeroContext(node)) return;
|
|
78816
|
+
const classNameValue = getStringFromClassNameAttr(openingElement);
|
|
78817
|
+
if (!classNameValue) return;
|
|
78818
|
+
const tokens = splitTailwindClassName(classNameValue);
|
|
78819
|
+
const animationUtility = getEffectiveTailwindClassNameToken(tokens, (utility) => ANIMATION_UTILITY_PATTERN.test(utility));
|
|
78820
|
+
if (!animationUtility || !PULSING_ANIMATION_UTILITIES.has(animationUtility)) return;
|
|
78821
|
+
if (getEffectiveTailwindClassNameToken(tokens, (utility) => ROUNDING_UTILITY_PATTERN.test(utility)) !== "rounded-full" || !hasTinySquareSize(tokens)) return;
|
|
78822
|
+
context.report({
|
|
78823
|
+
node: openingElement,
|
|
78824
|
+
message: "This tiny status dot pulses continuously without representing work in progress. Use a static indicator for passive availability or decoration."
|
|
78825
|
+
});
|
|
78826
|
+
} })
|
|
78827
|
+
});
|
|
78828
|
+
//#endregion
|
|
77301
78829
|
//#region src/plugin/rules/design/no-pure-black-background.ts
|
|
77302
78830
|
const noPureBlackBackground = defineRule({
|
|
77303
78831
|
id: "no-pure-black-background",
|
|
@@ -77383,6 +78911,130 @@ const noPureBlackShadow = defineRule({
|
|
|
77383
78911
|
})
|
|
77384
78912
|
});
|
|
77385
78913
|
//#endregion
|
|
78914
|
+
//#region src/plugin/rules/design/no-radial-halo.ts
|
|
78915
|
+
const HALO_SURFACE_ELEMENT_NAMES = new Set([
|
|
78916
|
+
"article",
|
|
78917
|
+
"aside",
|
|
78918
|
+
"div",
|
|
78919
|
+
"header",
|
|
78920
|
+
"main",
|
|
78921
|
+
"section"
|
|
78922
|
+
]);
|
|
78923
|
+
const BACKGROUND_IMAGE_STYLE_PROPERTY_NAMES = new Set(["background", "backgroundImage"]);
|
|
78924
|
+
const BACKGROUND_COLOR_STYLE_PROPERTY_NAMES = new Set(["background", "backgroundColor"]);
|
|
78925
|
+
const TAILWIND_ARBITRARY_IMAGE_VALUE_PATTERN = /^(?:image:|(?:radial-gradient|repeating-radial-gradient|linear-gradient|repeating-linear-gradient|conic-gradient|repeating-conic-gradient|url)\()/i;
|
|
78926
|
+
const TAILWIND_NON_COLOR_BACKGROUND_UTILITY_PATTERN = /^bg-(?:auto|bottom|center|clip-|contain|cover|fixed|left|local|no-repeat|none|origin-|repeat|right|scroll|top)/;
|
|
78927
|
+
const isTailwindBackgroundColorUtility = (utility) => {
|
|
78928
|
+
if (/^\[(?:background|background-color):[\s\S]+\]$/i.test(utility)) return true;
|
|
78929
|
+
if (utility.startsWith("bg-[") && utility.endsWith("]")) {
|
|
78930
|
+
const arbitraryValue = utility.slice(4, -1);
|
|
78931
|
+
return !TAILWIND_ARBITRARY_IMAGE_VALUE_PATTERN.test(arbitraryValue);
|
|
78932
|
+
}
|
|
78933
|
+
return utility.startsWith("bg-") && !TAILWIND_NON_COLOR_BACKGROUND_UTILITY_PATTERN.test(utility);
|
|
78934
|
+
};
|
|
78935
|
+
const getStaticTailwindBackgroundColor = (tokens) => {
|
|
78936
|
+
const resolution = resolveEffectiveTailwindClassNameToken(tokens, isTailwindBackgroundColorUtility, []);
|
|
78937
|
+
const utility = resolution.utility;
|
|
78938
|
+
if (!utility) return {
|
|
78939
|
+
isAmbiguous: resolution.isAmbiguous,
|
|
78940
|
+
isImportant: resolution.isImportant,
|
|
78941
|
+
value: null
|
|
78942
|
+
};
|
|
78943
|
+
let arbitraryValue = null;
|
|
78944
|
+
if (utility.startsWith("bg-[") && utility.endsWith("]")) arbitraryValue = utility.slice(4, -1).replace(/^color:/i, "");
|
|
78945
|
+
else arbitraryValue = utility.match(/^\[(?:background|background-color):([\s\S]+)\]$/i)?.[1] ?? null;
|
|
78946
|
+
return {
|
|
78947
|
+
isAmbiguous: resolution.isAmbiguous,
|
|
78948
|
+
isImportant: resolution.isImportant,
|
|
78949
|
+
value: arbitraryValue ? normalizeTailwindArbitraryUtilityValue(arbitraryValue) : null
|
|
78950
|
+
};
|
|
78951
|
+
};
|
|
78952
|
+
const getStaticHaloElementEvidence = (node, context) => {
|
|
78953
|
+
if (!isProvenIntrinsicJsxElement(node, context.scopes)) return null;
|
|
78954
|
+
const classNameAttribute = getAuthoritativeJsxAttribute(node.attributes, "className");
|
|
78955
|
+
const styleAttribute = getAuthoritativeJsxAttribute(node.attributes, "style");
|
|
78956
|
+
if (!classNameAttribute && hasJsxSpreadThatMayProvideAttribute(node.attributes, "className") || !styleAttribute && hasJsxSpreadThatMayProvideAttribute(node.attributes, "style")) return null;
|
|
78957
|
+
const className = classNameAttribute ? getStringFromClassNameAttr(node) : "";
|
|
78958
|
+
if (classNameAttribute && className === null) return null;
|
|
78959
|
+
const styleExpression = styleAttribute ? getInlineStyleExpression(styleAttribute, context.scopes) : null;
|
|
78960
|
+
if (styleAttribute && !styleExpression || styleExpression?.properties.some((property) => getStylePropertyKey(property) === null)) return null;
|
|
78961
|
+
return {
|
|
78962
|
+
styleExpression,
|
|
78963
|
+
tokens: className && hasCapabilityOrUnspecified(context.settings, "tailwind") ? splitTailwindClassName(className) : []
|
|
78964
|
+
};
|
|
78965
|
+
};
|
|
78966
|
+
const hasDarkBackground = (evidence) => {
|
|
78967
|
+
const tailwindBackground = getStaticTailwindBackgroundColor(evidence.tokens);
|
|
78968
|
+
if (tailwindBackground.isAmbiguous) return false;
|
|
78969
|
+
const inlineBackgroundProperty = getEffectiveStylePropertyAmong(evidence.styleExpression?.properties, BACKGROUND_COLOR_STYLE_PROPERTY_NAMES);
|
|
78970
|
+
const inlineBackgroundValue = inlineBackgroundProperty ? getStylePropertyStringValue(inlineBackgroundProperty) : null;
|
|
78971
|
+
const backgroundValue = inlineBackgroundProperty && !tailwindBackground.isImportant ? inlineBackgroundValue : tailwindBackground.value;
|
|
78972
|
+
if (!backgroundValue) return false;
|
|
78973
|
+
const color = parseStaticCssColorWithAlpha(backgroundValue);
|
|
78974
|
+
return Boolean(color && color.alpha >= .95 && color.red <= 35 && color.green <= 35 && color.blue <= 35);
|
|
78975
|
+
};
|
|
78976
|
+
const getStaticBackgroundImage = (evidence) => {
|
|
78977
|
+
const tailwindBackground = getStaticTailwindBackgroundImage(evidence.tokens);
|
|
78978
|
+
if (tailwindBackground.isAmbiguous) return null;
|
|
78979
|
+
const inlineBackgroundProperty = getEffectiveStylePropertyAmong(evidence.styleExpression?.properties, BACKGROUND_IMAGE_STYLE_PROPERTY_NAMES);
|
|
78980
|
+
const inlineBackgroundValue = inlineBackgroundProperty ? getStylePropertyStringValue(inlineBackgroundProperty) : null;
|
|
78981
|
+
return {
|
|
78982
|
+
property: inlineBackgroundProperty,
|
|
78983
|
+
value: inlineBackgroundProperty && !tailwindBackground.isImportant ? inlineBackgroundValue : tailwindBackground.value
|
|
78984
|
+
};
|
|
78985
|
+
};
|
|
78986
|
+
const isSmallPixelHalo = (stops) => {
|
|
78987
|
+
const positions = stops.flatMap((stop) => stop.positions);
|
|
78988
|
+
if (positions.length === 0) return false;
|
|
78989
|
+
let maximumPixelExtent = 0;
|
|
78990
|
+
for (const position of positions) {
|
|
78991
|
+
const numericValue = Number.parseFloat(position);
|
|
78992
|
+
if (!Number.isFinite(numericValue)) return false;
|
|
78993
|
+
if (numericValue === 0) continue;
|
|
78994
|
+
if (!position.toLowerCase().endsWith("px")) return false;
|
|
78995
|
+
maximumPixelExtent = Math.max(maximumPixelExtent, Math.abs(numericValue));
|
|
78996
|
+
}
|
|
78997
|
+
return maximumPixelExtent <= 24;
|
|
78998
|
+
};
|
|
78999
|
+
const hasSaturatedRadialHalo = (backgroundValue) => {
|
|
79000
|
+
const stops = parseStaticRadialGradient(backgroundValue);
|
|
79001
|
+
if (!stops || isSmallPixelHalo(stops)) return false;
|
|
79002
|
+
const finalStop = stops.at(-1);
|
|
79003
|
+
if (!finalStop || finalStop.color.alpha > .05) return false;
|
|
79004
|
+
const firstVisibleStop = stops.slice(0, -1).find((stop) => stop.color.alpha > RADIAL_HALO_TRANSPARENT_ALPHA_MAX);
|
|
79005
|
+
return Boolean(firstVisibleStop && firstVisibleStop.color.alpha >= .7 && hasColorChroma(firstVisibleStop.color));
|
|
79006
|
+
};
|
|
79007
|
+
const getStaticRootOpeningElement = (node) => {
|
|
79008
|
+
const root = getStaticJsxTreeRoot(node);
|
|
79009
|
+
return isNodeOfType(root, "JSXElement") ? root.openingElement : null;
|
|
79010
|
+
};
|
|
79011
|
+
const noRadialHalo = defineRule({
|
|
79012
|
+
id: "no-radial-halo",
|
|
79013
|
+
title: "Saturated radial halo on a dark surface",
|
|
79014
|
+
severity: "warn",
|
|
79015
|
+
defaultEnabled: false,
|
|
79016
|
+
tags: [
|
|
79017
|
+
"design",
|
|
79018
|
+
"test-noise",
|
|
79019
|
+
"react-jsx-only"
|
|
79020
|
+
],
|
|
79021
|
+
recommendation: "Use restrained surface contrast, product-specific imagery, or a subtler accent instead of a saturated radial halo on a dark page.",
|
|
79022
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
79023
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || !HALO_SURFACE_ELEMENT_NAMES.has(node.name.name) || isDataVisualizationContext(node, context.filename)) return;
|
|
79024
|
+
const evidence = getStaticHaloElementEvidence(node, context);
|
|
79025
|
+
if (!evidence) return;
|
|
79026
|
+
const backgroundImage = getStaticBackgroundImage(evidence);
|
|
79027
|
+
if (!backgroundImage?.value || !hasSaturatedRadialHalo(backgroundImage.value)) return;
|
|
79028
|
+
const rootOpeningElement = getStaticRootOpeningElement(node);
|
|
79029
|
+
const rootEvidence = rootOpeningElement && rootOpeningElement !== node ? getStaticHaloElementEvidence(rootOpeningElement, context) : null;
|
|
79030
|
+
if (!hasDarkBackground(evidence) && (!rootEvidence || !hasDarkBackground(rootEvidence))) return;
|
|
79031
|
+
context.report({
|
|
79032
|
+
node: backgroundImage.property ?? node,
|
|
79033
|
+
message: "This saturated radial halo adds a generic glow to a dark surface. Replace it with a more specific visual treatment or simplify the background."
|
|
79034
|
+
});
|
|
79035
|
+
} })
|
|
79036
|
+
});
|
|
79037
|
+
//#endregion
|
|
77386
79038
|
//#region src/plugin/rules/correctness/no-random-key.ts
|
|
77387
79039
|
const ALWAYS_FRESH_DIRECT_CALLEES = new Set([
|
|
77388
79040
|
"nanoid",
|
|
@@ -79235,6 +80887,238 @@ const noRenderReturnValue = defineRule({
|
|
|
79235
80887
|
} })
|
|
79236
80888
|
});
|
|
79237
80889
|
//#endregion
|
|
80890
|
+
//#region src/plugin/rules/design/no-repeated-container-text.ts
|
|
80891
|
+
const REPEATED_TEXT_CONTAINER_NAMES = new Set([
|
|
80892
|
+
"article",
|
|
80893
|
+
"aside",
|
|
80894
|
+
"div",
|
|
80895
|
+
"section"
|
|
80896
|
+
]);
|
|
80897
|
+
const REPEATED_TEXT_SKIPPED_NAMES = new Set([
|
|
80898
|
+
"a",
|
|
80899
|
+
"button",
|
|
80900
|
+
"canvas",
|
|
80901
|
+
"code",
|
|
80902
|
+
"datalist",
|
|
80903
|
+
"dd",
|
|
80904
|
+
"dl",
|
|
80905
|
+
"dt",
|
|
80906
|
+
"figure",
|
|
80907
|
+
"input",
|
|
80908
|
+
"kbd",
|
|
80909
|
+
"label",
|
|
80910
|
+
"menu",
|
|
80911
|
+
"nav",
|
|
80912
|
+
"ol",
|
|
80913
|
+
"option",
|
|
80914
|
+
"optgroup",
|
|
80915
|
+
"pre",
|
|
80916
|
+
"samp",
|
|
80917
|
+
"select",
|
|
80918
|
+
"summary",
|
|
80919
|
+
"svg",
|
|
80920
|
+
"table",
|
|
80921
|
+
"tbody",
|
|
80922
|
+
"td",
|
|
80923
|
+
"textarea",
|
|
80924
|
+
"tfoot",
|
|
80925
|
+
"th",
|
|
80926
|
+
"thead",
|
|
80927
|
+
"tr",
|
|
80928
|
+
"ul"
|
|
80929
|
+
]);
|
|
80930
|
+
const REPEATED_TEXT_SKIPPED_ROLES = new Set([
|
|
80931
|
+
"button",
|
|
80932
|
+
"cell",
|
|
80933
|
+
"grid",
|
|
80934
|
+
"gridcell",
|
|
80935
|
+
"graphics-document",
|
|
80936
|
+
"graphics-symbol",
|
|
80937
|
+
"img",
|
|
80938
|
+
"link",
|
|
80939
|
+
"list",
|
|
80940
|
+
"listbox",
|
|
80941
|
+
"listitem",
|
|
80942
|
+
"menu",
|
|
80943
|
+
"menubar",
|
|
80944
|
+
"navigation",
|
|
80945
|
+
"progressbar",
|
|
80946
|
+
"radiogroup",
|
|
80947
|
+
"row",
|
|
80948
|
+
"rowgroup",
|
|
80949
|
+
"table",
|
|
80950
|
+
"tablist",
|
|
80951
|
+
"tree",
|
|
80952
|
+
"treeitem",
|
|
80953
|
+
"diagram"
|
|
80954
|
+
]);
|
|
80955
|
+
const CONTENT_ATTRIBUTE_NAMES = new Set(["children", "dangerouslySetInnerHTML"]);
|
|
80956
|
+
const DATA_VISUALIZATION_CLASS_PATTERN = /(?:^|[-_\s])(?:chart|graph|heatmap|plot|visualization)(?:$|[-_\s])/i;
|
|
80957
|
+
const VISUALLY_HIDDEN_CLASS_NAMES$1 = new Set([
|
|
80958
|
+
"screen-reader-only",
|
|
80959
|
+
"sr-only",
|
|
80960
|
+
"visually-hidden"
|
|
80961
|
+
]);
|
|
80962
|
+
const VISUALLY_HIDDEN_UTILITY_NAMES = new Set(["not-sr-only", "sr-only"]);
|
|
80963
|
+
const TAILWIND_NAMED_BREAKPOINT_SET = new Set(TAILWIND_NAMED_BREAKPOINTS);
|
|
80964
|
+
const getNativeElementName = (openingElement) => {
|
|
80965
|
+
if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return null;
|
|
80966
|
+
const elementName = openingElement.name.name;
|
|
80967
|
+
return elementName === elementName.toLowerCase() ? elementName : null;
|
|
80968
|
+
};
|
|
80969
|
+
const hasContentAttribute = (openingElement) => openingElement.attributes.some((attribute) => isNodeOfType(attribute, "JSXAttribute") && CONTENT_ATTRIBUTE_NAMES.has(getJsxAttributeName(attribute.name) ?? ""));
|
|
80970
|
+
const isStaticallyHidden = (openingElement, classNameValue, context) => {
|
|
80971
|
+
const hiddenAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "hidden");
|
|
80972
|
+
if (hiddenAttribute) {
|
|
80973
|
+
if (!hiddenAttribute.value) return true;
|
|
80974
|
+
const hiddenValue = getStringLiteralAttributeValue(hiddenAttribute);
|
|
80975
|
+
if (hiddenValue === null || hiddenValue.toLowerCase() !== "false") return true;
|
|
80976
|
+
}
|
|
80977
|
+
const ariaHiddenAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "aria-hidden");
|
|
80978
|
+
if (ariaHiddenAttribute) {
|
|
80979
|
+
const ariaHiddenValue = getStringLiteralAttributeValue(ariaHiddenAttribute);
|
|
80980
|
+
if (ariaHiddenValue === null || ariaHiddenValue.toLowerCase() === "true") return true;
|
|
80981
|
+
}
|
|
80982
|
+
const styleAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "style");
|
|
80983
|
+
if (styleAttribute) {
|
|
80984
|
+
const styleExpression = getInlineStyleExpression(styleAttribute, context.scopes);
|
|
80985
|
+
if (!styleExpression) return false;
|
|
80986
|
+
const displayProperty = getEffectiveStyleProperty(styleExpression.properties, "display");
|
|
80987
|
+
const visibilityProperty = getEffectiveStyleProperty(styleExpression.properties, "visibility");
|
|
80988
|
+
if (displayProperty && getStylePropertyStringValue(displayProperty)?.trim().toLowerCase() === "none" || visibilityProperty && ["collapse", "hidden"].includes(getStylePropertyStringValue(visibilityProperty)?.trim().toLowerCase() ?? "")) return true;
|
|
80989
|
+
if (displayProperty && getStylePropertyStringValue(displayProperty) === null || visibilityProperty && getStylePropertyStringValue(visibilityProperty) === null) return true;
|
|
80990
|
+
}
|
|
80991
|
+
if (!classNameValue) return false;
|
|
80992
|
+
const classNameTokens = splitTailwindClassName(classNameValue);
|
|
80993
|
+
if (classNameTokens.some((classNameToken) => VISUALLY_HIDDEN_CLASS_NAMES$1.has(classNameToken))) return true;
|
|
80994
|
+
if (getEffectiveTailwindClassNameToken(classNameTokens, (utility) => VISUALLY_HIDDEN_UTILITY_NAMES.has(utility)) === "sr-only") return true;
|
|
80995
|
+
return getTailwindVisibilityAtBreakpoints(classNameValue)?.every((isVisible) => !isVisible) ?? false;
|
|
80996
|
+
};
|
|
80997
|
+
const hasResponsiveVisibility = (openingElement) => {
|
|
80998
|
+
const classNameValue = getStringFromClassNameAttr(openingElement);
|
|
80999
|
+
if (!classNameValue) return false;
|
|
81000
|
+
return splitTailwindClassName(classNameValue).map(parseTailwindClassNameToken).some(({ utility, variants }) => {
|
|
81001
|
+
if (!(getTailwindVisibilityEffect(utility).status !== "not-relevant" || VISUALLY_HIDDEN_UTILITY_NAMES.has(utility))) return false;
|
|
81002
|
+
return variants.some((variant) => TAILWIND_NAMED_BREAKPOINT_SET.has(variant) || variant.startsWith("max-") && TAILWIND_NAMED_BREAKPOINT_SET.has(variant.slice(4)) || /^(?:min|max)-\[.+\]$/.test(variant));
|
|
81003
|
+
});
|
|
81004
|
+
};
|
|
81005
|
+
const shouldSkipElement = (openingElement, isRoot, context) => {
|
|
81006
|
+
const elementName = getNativeElementName(openingElement);
|
|
81007
|
+
if (!elementName || REPEATED_TEXT_SKIPPED_NAMES.has(elementName)) return true;
|
|
81008
|
+
if (hasJsxSpreadAttribute(openingElement.attributes) || hasContentAttribute(openingElement)) return true;
|
|
81009
|
+
const classNameAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "className");
|
|
81010
|
+
const classNameValue = getStringFromClassNameAttr(openingElement);
|
|
81011
|
+
if (classNameAttribute && classNameValue === null) return true;
|
|
81012
|
+
if (classNameValue && DATA_VISUALIZATION_CLASS_PATTERN.test(classNameValue) || isStaticallyHidden(openingElement, classNameValue, context)) return true;
|
|
81013
|
+
const roleAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "role");
|
|
81014
|
+
if (roleAttribute) {
|
|
81015
|
+
const role = getStringLiteralAttributeValue(roleAttribute)?.toLowerCase();
|
|
81016
|
+
if (!role || REPEATED_TEXT_SKIPPED_ROLES.has(role)) return true;
|
|
81017
|
+
}
|
|
81018
|
+
return !isRoot && isTailwindCardSurface(openingElement);
|
|
81019
|
+
};
|
|
81020
|
+
const getStructuralPathSegment = (openingElement) => {
|
|
81021
|
+
const elementName = getNativeElementName(openingElement);
|
|
81022
|
+
if (!elementName) return null;
|
|
81023
|
+
const classNameValue = getStringFromClassNameAttr(openingElement);
|
|
81024
|
+
if (!classNameValue) return elementName;
|
|
81025
|
+
const normalizedClasses = splitTailwindClassName(classNameValue).sort().join(".");
|
|
81026
|
+
return normalizedClasses ? `${elementName}.${normalizedClasses}` : elementName;
|
|
81027
|
+
};
|
|
81028
|
+
const normalizeRepeatedText = (value) => {
|
|
81029
|
+
const text = value.replace(/\s+/g, " ").trim();
|
|
81030
|
+
if (text.length < 4 || text.length > 48 || !/\p{L}/u.test(text)) return null;
|
|
81031
|
+
return text;
|
|
81032
|
+
};
|
|
81033
|
+
const appendTextOccurrence = (collection, value, node, structuralPath) => {
|
|
81034
|
+
const text = normalizeRepeatedText(value);
|
|
81035
|
+
if (!text) return;
|
|
81036
|
+
const occurrences = collection.occurrencesByText.get(text) ?? [];
|
|
81037
|
+
occurrences.push({
|
|
81038
|
+
node,
|
|
81039
|
+
signature: structuralPath.join(">")
|
|
81040
|
+
});
|
|
81041
|
+
collection.occurrencesByText.set(text, occurrences);
|
|
81042
|
+
};
|
|
81043
|
+
const collectStaticChild = (node, structuralPath, collection, context) => {
|
|
81044
|
+
if (!collection.isStatic) return;
|
|
81045
|
+
if (isNodeOfType(node, "JSXText")) {
|
|
81046
|
+
appendTextOccurrence(collection, node.value ?? "", node, structuralPath);
|
|
81047
|
+
return;
|
|
81048
|
+
}
|
|
81049
|
+
if (isNodeOfType(node, "JSXElement")) {
|
|
81050
|
+
if (shouldSkipElement(node.openingElement, false, context)) return;
|
|
81051
|
+
collection.descendantCount += 1;
|
|
81052
|
+
if (collection.descendantCount > 250) {
|
|
81053
|
+
collection.isStatic = false;
|
|
81054
|
+
return;
|
|
81055
|
+
}
|
|
81056
|
+
if (hasResponsiveVisibility(node.openingElement)) {
|
|
81057
|
+
collection.isStatic = false;
|
|
81058
|
+
return;
|
|
81059
|
+
}
|
|
81060
|
+
const pathSegment = getStructuralPathSegment(node.openingElement);
|
|
81061
|
+
if (!pathSegment) return;
|
|
81062
|
+
const nextStructuralPath = [...structuralPath, pathSegment];
|
|
81063
|
+
for (const child of node.children) collectStaticChild(child, nextStructuralPath, collection, context);
|
|
81064
|
+
return;
|
|
81065
|
+
}
|
|
81066
|
+
if (isNodeOfType(node, "JSXFragment")) {
|
|
81067
|
+
for (const child of node.children) collectStaticChild(child, structuralPath, collection, context);
|
|
81068
|
+
return;
|
|
81069
|
+
}
|
|
81070
|
+
if (!isNodeOfType(node, "JSXExpressionContainer")) {
|
|
81071
|
+
collection.isStatic = false;
|
|
81072
|
+
return;
|
|
81073
|
+
}
|
|
81074
|
+
const expression = node.expression;
|
|
81075
|
+
if (isNodeOfType(expression, "Literal")) {
|
|
81076
|
+
if (typeof expression.value === "string") appendTextOccurrence(collection, expression.value, expression, structuralPath);
|
|
81077
|
+
return;
|
|
81078
|
+
}
|
|
81079
|
+
if (isNodeOfType(expression, "TemplateLiteral") && expression.expressions.length === 0 && expression.quasis.length === 1) {
|
|
81080
|
+
appendTextOccurrence(collection, expression.quasis[0].value.cooked ?? expression.quasis[0].value.raw, expression, structuralPath);
|
|
81081
|
+
return;
|
|
81082
|
+
}
|
|
81083
|
+
if (!isNodeOfType(expression, "JSXEmptyExpression")) collection.isStatic = false;
|
|
81084
|
+
};
|
|
81085
|
+
const collectRepeatedText = (container, context) => {
|
|
81086
|
+
if (hasResponsiveVisibility(container.openingElement) || shouldSkipElement(container.openingElement, true, context)) return null;
|
|
81087
|
+
const collection = {
|
|
81088
|
+
descendantCount: 0,
|
|
81089
|
+
isStatic: true,
|
|
81090
|
+
occurrencesByText: /* @__PURE__ */ new Map()
|
|
81091
|
+
};
|
|
81092
|
+
for (const child of container.children) collectStaticChild(child, [], collection, context);
|
|
81093
|
+
return collection.isStatic ? collection : null;
|
|
81094
|
+
};
|
|
81095
|
+
const noRepeatedContainerText = defineRule({
|
|
81096
|
+
id: "no-repeated-container-text",
|
|
81097
|
+
title: "A card repeats the same text in distinct slots",
|
|
81098
|
+
severity: "warn",
|
|
81099
|
+
defaultEnabled: false,
|
|
81100
|
+
tags: [
|
|
81101
|
+
"design",
|
|
81102
|
+
"test-noise",
|
|
81103
|
+
"react-jsx-only"
|
|
81104
|
+
],
|
|
81105
|
+
category: "Design",
|
|
81106
|
+
recommendation: "Keep repeated status or label copy in the one card slot where it carries the clearest meaning.",
|
|
81107
|
+
create: (context) => ({ JSXElement(node) {
|
|
81108
|
+
if (!hasCapabilityOrUnspecified(context.settings, "tailwind") || !isNodeOfType(node.openingElement.name, "JSXIdentifier") || !REPEATED_TEXT_CONTAINER_NAMES.has(node.openingElement.name.name) || !isTailwindCardSurface(node.openingElement)) return;
|
|
81109
|
+
const collection = collectRepeatedText(node, context);
|
|
81110
|
+
if (!collection) return;
|
|
81111
|
+
for (const [text, occurrences] of collection.occurrencesByText) {
|
|
81112
|
+
const distinctSignatures = new Set(occurrences.map((occurrence) => occurrence.signature));
|
|
81113
|
+
if (occurrences.length < 3 || distinctSignatures.size < 3) continue;
|
|
81114
|
+
context.report({
|
|
81115
|
+
node: occurrences[0].node,
|
|
81116
|
+
message: `The literal "${text}" appears in ${distinctSignatures.size} structurally different spots inside this card. Keep it in the one slot where it matters most.`
|
|
81117
|
+
});
|
|
81118
|
+
}
|
|
81119
|
+
} })
|
|
81120
|
+
});
|
|
81121
|
+
//#endregion
|
|
79238
81122
|
//#region src/plugin/rules/design/no-repeated-emoji-tiles.ts
|
|
79239
81123
|
const EMOJI_PATTERN = /\p{Extended_Pictographic}/u;
|
|
79240
81124
|
const EMOJI_SEQUENCE_PART_PATTERN = /\p{Extended_Pictographic}|\p{Emoji_Modifier}|\u200d|\ufe0f|\s/gu;
|
|
@@ -80733,14 +82617,6 @@ const classifySecretFileExposure = (filename, options = {}) => {
|
|
|
80733
82617
|
return "unknown";
|
|
80734
82618
|
};
|
|
80735
82619
|
//#endregion
|
|
80736
|
-
//#region src/plugin/utils/tokenize-identifier-words.ts
|
|
80737
|
-
const IDENTIFIER_WORD_PATTERN = /[A-Z]+(?=[A-Z][a-z]|\b)|[A-Z]?[a-z]+|\d+/g;
|
|
80738
|
-
const tokenizeIdentifierWords = (identifierName) => {
|
|
80739
|
-
const words = identifierName.match(IDENTIFIER_WORD_PATTERN);
|
|
80740
|
-
if (!words) return [];
|
|
80741
|
-
return words.map((word) => word.toLowerCase());
|
|
80742
|
-
};
|
|
80743
|
-
//#endregion
|
|
80744
82620
|
//#region src/plugin/utils/get-identifier-trailing-word.ts
|
|
80745
82621
|
const getIdentifierTrailingWord = (identifierName) => tokenizeIdentifierWords(identifierName).at(-1) ?? identifierName.toLowerCase();
|
|
80746
82622
|
//#endregion
|
|
@@ -81894,6 +83770,271 @@ const noSetStateInRender = defineRule({
|
|
|
81894
83770
|
}
|
|
81895
83771
|
});
|
|
81896
83772
|
//#endregion
|
|
83773
|
+
//#region src/plugin/constants/svg-tags.ts
|
|
83774
|
+
const SVG_TAGS = new Set([
|
|
83775
|
+
"a",
|
|
83776
|
+
"altGlyph",
|
|
83777
|
+
"altGlyphDef",
|
|
83778
|
+
"altGlyphItem",
|
|
83779
|
+
"animate",
|
|
83780
|
+
"animateColor",
|
|
83781
|
+
"animateMotion",
|
|
83782
|
+
"animateTransform",
|
|
83783
|
+
"circle",
|
|
83784
|
+
"clipPath",
|
|
83785
|
+
"cursor",
|
|
83786
|
+
"defs",
|
|
83787
|
+
"desc",
|
|
83788
|
+
"ellipse",
|
|
83789
|
+
"feBlend",
|
|
83790
|
+
"feColorMatrix",
|
|
83791
|
+
"feComponentTransfer",
|
|
83792
|
+
"feComposite",
|
|
83793
|
+
"feConvolveMatrix",
|
|
83794
|
+
"feDiffuseLighting",
|
|
83795
|
+
"feDisplacementMap",
|
|
83796
|
+
"feDistantLight",
|
|
83797
|
+
"feDropShadow",
|
|
83798
|
+
"feFlood",
|
|
83799
|
+
"feFuncA",
|
|
83800
|
+
"feFuncB",
|
|
83801
|
+
"feFuncG",
|
|
83802
|
+
"feFuncR",
|
|
83803
|
+
"feGaussianBlur",
|
|
83804
|
+
"feImage",
|
|
83805
|
+
"feMerge",
|
|
83806
|
+
"feMergeNode",
|
|
83807
|
+
"feMorphology",
|
|
83808
|
+
"feOffset",
|
|
83809
|
+
"fePointLight",
|
|
83810
|
+
"feSpecularLighting",
|
|
83811
|
+
"feSpotLight",
|
|
83812
|
+
"feTile",
|
|
83813
|
+
"feTurbulence",
|
|
83814
|
+
"filter",
|
|
83815
|
+
"foreignObject",
|
|
83816
|
+
"g",
|
|
83817
|
+
"glyph",
|
|
83818
|
+
"glyphRef",
|
|
83819
|
+
"image",
|
|
83820
|
+
"line",
|
|
83821
|
+
"linearGradient",
|
|
83822
|
+
"marker",
|
|
83823
|
+
"mask",
|
|
83824
|
+
"metadata",
|
|
83825
|
+
"mpath",
|
|
83826
|
+
"path",
|
|
83827
|
+
"pattern",
|
|
83828
|
+
"polygon",
|
|
83829
|
+
"polyline",
|
|
83830
|
+
"radialGradient",
|
|
83831
|
+
"rect",
|
|
83832
|
+
"set",
|
|
83833
|
+
"stop",
|
|
83834
|
+
"svg",
|
|
83835
|
+
"switch",
|
|
83836
|
+
"symbol",
|
|
83837
|
+
"text",
|
|
83838
|
+
"textPath",
|
|
83839
|
+
"title",
|
|
83840
|
+
"tref",
|
|
83841
|
+
"tspan",
|
|
83842
|
+
"use",
|
|
83843
|
+
"view"
|
|
83844
|
+
]);
|
|
83845
|
+
//#endregion
|
|
83846
|
+
//#region src/plugin/rules/design/no-shape-assembled-illustration.ts
|
|
83847
|
+
const PRIMITIVE_ELEMENT_NAMES = new Set([
|
|
83848
|
+
"rect",
|
|
83849
|
+
"circle",
|
|
83850
|
+
"ellipse",
|
|
83851
|
+
"polygon"
|
|
83852
|
+
]);
|
|
83853
|
+
const TEXT_ELEMENT_NAMES = new Set(["text", "tspan"]);
|
|
83854
|
+
const EXCLUDED_SUBTREE_ELEMENT_NAMES = new Set([
|
|
83855
|
+
"defs",
|
|
83856
|
+
"symbol",
|
|
83857
|
+
"mask",
|
|
83858
|
+
"clipPath"
|
|
83859
|
+
]);
|
|
83860
|
+
const TAILWIND_DIMENSION_UTILITY_PATTERN = /^(?:(?:size|w|h|max-w|max-h)-|\[(?:width|height|max-width|max-height):)/;
|
|
83861
|
+
const getIntrinsicElementName = (openingElement) => isNodeOfType(openingElement.name, "JSXIdentifier") ? openingElement.name.name : null;
|
|
83862
|
+
const getStrictStaticNumericValue = (value) => {
|
|
83863
|
+
if (!value) return null;
|
|
83864
|
+
let candidate = value;
|
|
83865
|
+
if (isNodeOfType(candidate, "JSXExpressionContainer")) candidate = stripParenExpression(candidate.expression);
|
|
83866
|
+
if (!isNodeOfType(candidate, "Literal")) return null;
|
|
83867
|
+
if (typeof candidate.value === "number") return Number.isFinite(candidate.value) ? candidate.value : null;
|
|
83868
|
+
if (typeof candidate.value !== "string") return null;
|
|
83869
|
+
const normalizedValue = candidate.value.trim().toLowerCase();
|
|
83870
|
+
if (!/^(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?(?:px)?$/.test(normalizedValue)) return null;
|
|
83871
|
+
const parsedValue = Number(normalizedValue.replace(/px$/, ""));
|
|
83872
|
+
return Number.isFinite(parsedValue) ? parsedValue : null;
|
|
83873
|
+
};
|
|
83874
|
+
const getStrictStaticStringAttributeValue = (attribute) => {
|
|
83875
|
+
const directValue = getStringLiteralAttributeValue(attribute);
|
|
83876
|
+
if (directValue !== null) return directValue;
|
|
83877
|
+
return isNodeOfType(attribute.value, "JSXExpressionContainer") ? getStaticStringExpression$1(attribute.value.expression) : null;
|
|
83878
|
+
};
|
|
83879
|
+
const getStaticSvgDimensions = (openingElement, context) => {
|
|
83880
|
+
if (openingElement.attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"))) return null;
|
|
83881
|
+
const widthAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "width");
|
|
83882
|
+
const heightAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "height");
|
|
83883
|
+
if (!widthAttribute || !heightAttribute) return null;
|
|
83884
|
+
const styleAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "style");
|
|
83885
|
+
if (styleAttribute) {
|
|
83886
|
+
const styleExpression = getInlineStyleExpression(styleAttribute, context.scopes);
|
|
83887
|
+
if (!styleExpression || getEffectiveStyleProperty(styleExpression.properties, "width") || getEffectiveStyleProperty(styleExpression.properties, "height") || getEffectiveStyleProperty(styleExpression.properties, "maxWidth") || getEffectiveStyleProperty(styleExpression.properties, "maxHeight")) return null;
|
|
83888
|
+
}
|
|
83889
|
+
if (getAuthoritativeJsxAttribute(openingElement.attributes, "className")) {
|
|
83890
|
+
const classNameValue = getStringFromClassNameAttr(openingElement);
|
|
83891
|
+
if (classNameValue === null || splitTailwindClassName(classNameValue).map(parseTailwindClassNameToken).some(({ utility }) => TAILWIND_DIMENSION_UTILITY_PATTERN.test(utility))) return null;
|
|
83892
|
+
}
|
|
83893
|
+
const width = getStrictStaticNumericValue(widthAttribute.value);
|
|
83894
|
+
const height = getStrictStaticNumericValue(heightAttribute.value);
|
|
83895
|
+
return width !== null && height !== null && width > 0 && height > 0 ? [width, height] : null;
|
|
83896
|
+
};
|
|
83897
|
+
const getCanonicalStaticPaintKey = (paint) => {
|
|
83898
|
+
if (paint === null) return null;
|
|
83899
|
+
const normalizedPaint = paint.trim().toLowerCase();
|
|
83900
|
+
const parseablePaint = normalizedPaint === "white" ? "#fff" : normalizedPaint === "black" ? "#000" : normalizedPaint;
|
|
83901
|
+
const parsedColor = parseColorToRgb(parseablePaint);
|
|
83902
|
+
const parsedColorWithAlpha = parseStaticCssColorWithAlpha(parseablePaint);
|
|
83903
|
+
if (!parsedColor || !parsedColorWithAlpha || parsedColorWithAlpha.alpha <= 0 || Object.values(parsedColor).some((channel) => !Number.isInteger(channel) || channel < 0 || channel > 255)) return null;
|
|
83904
|
+
return `${parsedColor.red},${parsedColor.green},${parsedColor.blue}`;
|
|
83905
|
+
};
|
|
83906
|
+
const getStaticOwnFill = (openingElement, context) => {
|
|
83907
|
+
if (openingElement.attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"))) return null;
|
|
83908
|
+
const styleAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "style");
|
|
83909
|
+
if (styleAttribute) {
|
|
83910
|
+
const styleExpression = getInlineStyleExpression(styleAttribute, context.scopes);
|
|
83911
|
+
if (!styleExpression) return null;
|
|
83912
|
+
const fillProperty = getEffectiveStyleProperty(styleExpression.properties, "fill");
|
|
83913
|
+
if (fillProperty) return getCanonicalStaticPaintKey(getStylePropertyStringValue(fillProperty));
|
|
83914
|
+
if (styleExpression.properties.some((property) => !isNodeOfType(property, "Property") || getStylePropertyKey(property) === null)) return null;
|
|
83915
|
+
}
|
|
83916
|
+
const fillAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "fill");
|
|
83917
|
+
return fillAttribute ? getCanonicalStaticPaintKey(getStrictStaticStringAttributeValue(fillAttribute)) : null;
|
|
83918
|
+
};
|
|
83919
|
+
const getStaticBooleanAttributeState = (openingElement, attributeName) => {
|
|
83920
|
+
const attribute = getAuthoritativeJsxAttribute(openingElement.attributes, attributeName, false);
|
|
83921
|
+
if (!attribute) return false;
|
|
83922
|
+
if (!attribute.value) return true;
|
|
83923
|
+
const value = attribute.value;
|
|
83924
|
+
if (isNodeOfType(value, "Literal")) return value.value === false || value.value === null ? false : true;
|
|
83925
|
+
if (!isNodeOfType(value, "JSXExpressionContainer")) return null;
|
|
83926
|
+
const expression = stripParenExpression(value.expression);
|
|
83927
|
+
return isNodeOfType(expression, "Literal") ? expression.value === false || expression.value === null ? false : true : null;
|
|
83928
|
+
};
|
|
83929
|
+
const getStaticPresentationAttributeValue = (openingElement, attributeName) => {
|
|
83930
|
+
const attribute = getAuthoritativeJsxAttribute(openingElement.attributes, attributeName, false);
|
|
83931
|
+
if (!attribute) return void 0;
|
|
83932
|
+
return getStrictStaticStringAttributeValue(attribute);
|
|
83933
|
+
};
|
|
83934
|
+
const getStaticStyleNumberValue = (property) => {
|
|
83935
|
+
const numberValue = getStylePropertyNumberValue(property);
|
|
83936
|
+
if (numberValue !== null) return numberValue;
|
|
83937
|
+
const stringValue = getStylePropertyStringValue(property);
|
|
83938
|
+
if (stringValue === null || stringValue.trim() === "") return null;
|
|
83939
|
+
const parsedValue = Number(stringValue);
|
|
83940
|
+
return Number.isFinite(parsedValue) ? parsedValue : null;
|
|
83941
|
+
};
|
|
83942
|
+
const getStaticRenderingState = (openingElement, context) => {
|
|
83943
|
+
if (openingElement.attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"))) return "unknown";
|
|
83944
|
+
const hiddenState = getStaticBooleanAttributeState(openingElement, "hidden");
|
|
83945
|
+
if (hiddenState === null) return "unknown";
|
|
83946
|
+
if (hiddenState) return "hidden";
|
|
83947
|
+
const displayValue = getStaticPresentationAttributeValue(openingElement, "display");
|
|
83948
|
+
const visibilityValue = getStaticPresentationAttributeValue(openingElement, "visibility");
|
|
83949
|
+
const opacityAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "opacity", false);
|
|
83950
|
+
const opacityValue = opacityAttribute ? getStrictStaticNumericValue(opacityAttribute.value) : void 0;
|
|
83951
|
+
if (displayValue === null || visibilityValue === null || opacityValue === null) return "unknown";
|
|
83952
|
+
if (displayValue?.trim().toLowerCase() === "none" || ["hidden", "collapse"].includes(visibilityValue?.trim().toLowerCase() ?? "") || opacityValue === 0) return "hidden";
|
|
83953
|
+
const styleAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "style");
|
|
83954
|
+
if (styleAttribute) {
|
|
83955
|
+
const styleExpression = getInlineStyleExpression(styleAttribute, context.scopes);
|
|
83956
|
+
if (!styleExpression || styleExpression.properties.some((property) => !isNodeOfType(property, "Property") || getStylePropertyKey(property) === null)) return "unknown";
|
|
83957
|
+
const displayProperty = getEffectiveStyleProperty(styleExpression.properties, "display");
|
|
83958
|
+
const visibilityProperty = getEffectiveStyleProperty(styleExpression.properties, "visibility");
|
|
83959
|
+
const opacityProperty = getEffectiveStyleProperty(styleExpression.properties, "opacity");
|
|
83960
|
+
const styleDisplayValue = displayProperty ? getStylePropertyStringValue(displayProperty) : void 0;
|
|
83961
|
+
const styleVisibilityValue = visibilityProperty ? getStylePropertyStringValue(visibilityProperty) : void 0;
|
|
83962
|
+
const styleOpacityValue = opacityProperty ? getStaticStyleNumberValue(opacityProperty) : void 0;
|
|
83963
|
+
if (styleDisplayValue === null || styleVisibilityValue === null || opacityProperty && !Number.isFinite(styleOpacityValue)) return "unknown";
|
|
83964
|
+
if (styleDisplayValue?.trim().toLowerCase() === "none" || ["hidden", "collapse"].includes(styleVisibilityValue?.trim().toLowerCase() ?? "") || styleOpacityValue === 0) return "hidden";
|
|
83965
|
+
}
|
|
83966
|
+
if (getAuthoritativeJsxAttribute(openingElement.attributes, "className")) {
|
|
83967
|
+
const classNameValue = getStringFromClassNameAttr(openingElement);
|
|
83968
|
+
if (classNameValue === null) return "unknown";
|
|
83969
|
+
const visibilityAtBreakpoints = getTailwindVisibilityAtBreakpoints(classNameValue);
|
|
83970
|
+
if (visibilityAtBreakpoints === null) return "unknown";
|
|
83971
|
+
if (visibilityAtBreakpoints.every((isVisible) => !isVisible)) return "hidden";
|
|
83972
|
+
}
|
|
83973
|
+
return "visible";
|
|
83974
|
+
};
|
|
83975
|
+
const isStaticallyEmptySvgExpression = (expression) => {
|
|
83976
|
+
const candidate = stripParenExpression(expression);
|
|
83977
|
+
if (isNodeOfType(candidate, "JSXEmptyExpression")) return true;
|
|
83978
|
+
if (isNodeOfType(candidate, "Literal")) return candidate.value === null || typeof candidate.value === "boolean" || typeof candidate.value === "string" && candidate.value.trim() === "";
|
|
83979
|
+
return isNodeOfType(candidate, "TemplateLiteral") && candidate.expressions.length === 0 && (candidate.quasis[0]?.value.cooked ?? candidate.quasis[0]?.value.raw ?? "").trim() === "";
|
|
83980
|
+
};
|
|
83981
|
+
const collectSvgIllustrationEvidence = (element, evidence, context, isExcludedByAncestor = false) => {
|
|
83982
|
+
const openingElement = element.openingElement;
|
|
83983
|
+
const elementName = getIntrinsicElementName(openingElement);
|
|
83984
|
+
if (!elementName || !SVG_TAGS.has(elementName)) return false;
|
|
83985
|
+
if (elementName === "pattern") evidence.hasPattern = true;
|
|
83986
|
+
const renderingState = getStaticRenderingState(openingElement, context);
|
|
83987
|
+
if (renderingState === "unknown") return false;
|
|
83988
|
+
const isExcluded = isExcludedByAncestor || renderingState === "hidden" || EXCLUDED_SUBTREE_ELEMENT_NAMES.has(elementName) || elementName === "pattern";
|
|
83989
|
+
if (!isExcluded && TEXT_ELEMENT_NAMES.has(elementName)) evidence.visibleTextElementCount += 1;
|
|
83990
|
+
if (!isExcluded && PRIMITIVE_ELEMENT_NAMES.has(elementName)) {
|
|
83991
|
+
const fillKey = getStaticOwnFill(openingElement, context);
|
|
83992
|
+
if (fillKey) evidence.primitiveFillKeys.push(fillKey);
|
|
83993
|
+
}
|
|
83994
|
+
for (const child of element.children) {
|
|
83995
|
+
if (isNodeOfType(child, "JSXText")) continue;
|
|
83996
|
+
if (isNodeOfType(child, "JSXElement")) {
|
|
83997
|
+
if (!collectSvgIllustrationEvidence(child, evidence, context, isExcluded)) return false;
|
|
83998
|
+
continue;
|
|
83999
|
+
}
|
|
84000
|
+
if (isNodeOfType(child, "JSXExpressionContainer")) {
|
|
84001
|
+
if (!isStaticallyEmptySvgExpression(child.expression)) return false;
|
|
84002
|
+
continue;
|
|
84003
|
+
}
|
|
84004
|
+
return false;
|
|
84005
|
+
}
|
|
84006
|
+
return true;
|
|
84007
|
+
};
|
|
84008
|
+
const noShapeAssembledIllustration = defineRule({
|
|
84009
|
+
id: "no-shape-assembled-illustration",
|
|
84010
|
+
title: "Large illustration is assembled from primitive shapes",
|
|
84011
|
+
severity: "warn",
|
|
84012
|
+
defaultEnabled: false,
|
|
84013
|
+
tags: ["design", "test-noise"],
|
|
84014
|
+
recommendation: "Replace the shape pile with deliberate artwork, a photograph, or a purpose-built graphic that supports the product story.",
|
|
84015
|
+
create: (context) => ({ JSXElement(node) {
|
|
84016
|
+
const openingElement = node.openingElement;
|
|
84017
|
+
if (getIntrinsicElementName(openingElement) !== "svg") return;
|
|
84018
|
+
const dimensions = getStaticSvgDimensions(openingElement, context);
|
|
84019
|
+
if (!dimensions || dimensions[0] < 200 || dimensions[1] < 200) return;
|
|
84020
|
+
const evidence = {
|
|
84021
|
+
hasPattern: false,
|
|
84022
|
+
primitiveFillKeys: [],
|
|
84023
|
+
visibleTextElementCount: 0
|
|
84024
|
+
};
|
|
84025
|
+
if (!collectSvgIllustrationEvidence(node, evidence, context) || evidence.hasPattern) return;
|
|
84026
|
+
if (evidence.visibleTextElementCount > 2) return;
|
|
84027
|
+
const primitiveCount = evidence.primitiveFillKeys.length;
|
|
84028
|
+
if (primitiveCount < 8) return;
|
|
84029
|
+
const distinctFills = new Set(evidence.primitiveFillKeys);
|
|
84030
|
+
if (distinctFills.size < 3) return;
|
|
84031
|
+
context.report({
|
|
84032
|
+
node: openingElement,
|
|
84033
|
+
message: `This ${Math.round(dimensions[0])}×${Math.round(dimensions[1])} SVG assembles a large illustration from ${primitiveCount} basic shapes and ${distinctFills.size} fills. Use deliberate artwork instead of placeholder clip art.`
|
|
84034
|
+
});
|
|
84035
|
+
} })
|
|
84036
|
+
});
|
|
84037
|
+
//#endregion
|
|
81897
84038
|
//#region src/plugin/rules/state-and-effects/no-side-effect-in-state-updater-function.ts
|
|
81898
84039
|
const MESSAGE$21 = "This side-effecting call runs inside a state updater, which React may invoke more than once. Move it outside the setter after computing the next state.";
|
|
81899
84040
|
const SYNCHRONOUS_CALLBACK_METHOD_NAMES = new Set([
|
|
@@ -82518,6 +84659,331 @@ const noSideEffectInStateUpdaterFunction = defineRule({
|
|
|
82518
84659
|
});
|
|
82519
84660
|
//#endregion
|
|
82520
84661
|
//#region src/plugin/rules/design/no-side-tab-border.ts
|
|
84662
|
+
const CHROMATIC_CSS_COLOR_KEYWORDS = new Set([
|
|
84663
|
+
"aqua",
|
|
84664
|
+
"blue",
|
|
84665
|
+
"coral",
|
|
84666
|
+
"crimson",
|
|
84667
|
+
"fuchsia",
|
|
84668
|
+
"gold",
|
|
84669
|
+
"green",
|
|
84670
|
+
"lime",
|
|
84671
|
+
"maroon",
|
|
84672
|
+
"navy",
|
|
84673
|
+
"olive",
|
|
84674
|
+
"orange",
|
|
84675
|
+
"pink",
|
|
84676
|
+
"purple",
|
|
84677
|
+
"rebeccapurple",
|
|
84678
|
+
"red",
|
|
84679
|
+
"teal",
|
|
84680
|
+
"yellow"
|
|
84681
|
+
]);
|
|
84682
|
+
const SHADOW_LENGTH_PATTERN = /^-?(?:\d+(?:\.\d+)?|\.\d+)(px)?$/i;
|
|
84683
|
+
const TAILWIND_SHADOW_GEOMETRY_PATTERN = /^shadow(?:-(?:2xl|inner|lg|md|none|sm|xl|xs|\[.+\]))?$/;
|
|
84684
|
+
const TAILWIND_ARBITRARY_SHADOW_PATTERN = /^shadow-\[(.+)\]$/;
|
|
84685
|
+
const ZERO_ALPHA_PATTERN = /^(?:0+(?:\.0*)?|\.0+)%?$/;
|
|
84686
|
+
const PSEUDO_ELEMENT_NAMES = ["before", "after"];
|
|
84687
|
+
const TAILWIND_BACKGROUND_COLOR_PATTERN = /^bg-(?:transparent|black|white|current|inherit|(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d+|\[(?!url\(|(?:image|length|position|size):).+\])(?:\/.+)?$/;
|
|
84688
|
+
const CHROMATIC_TAILWIND_BACKGROUND_PATTERN = /^bg-(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d+(?:\/.+)?$/;
|
|
84689
|
+
const PSEUDO_SIDE_TAB_CONTEXT_PATTERN = /(?:^|[-_:])(?:badge|callout|label|menu-item|nav-item|section-label|side-?tab|tab)(?:$|[-_:])/i;
|
|
84690
|
+
const PSEUDO_SIDE_TAB_ART_PATTERN = /(?:^|[-_:])(?:avatar|brand-mark|glyph|icon|logo|logo-mark|mark)(?:$|[-_:])/i;
|
|
84691
|
+
const PSEUDO_INTERACTION_OR_SELECTION_VARIANT_PATTERN = /(?:hover|focus|active|checked|target|selection|aria-(?:current|selected)|data-(?:active|current|selected))/i;
|
|
84692
|
+
const STATIC_SELECTED_OR_CURRENT_CLASS_PATTERN = /(?:^|[-_:])(?:active|current|selected)(?:$|[-_:])/i;
|
|
84693
|
+
const PSEUDO_DISPLAY_UTILITY_PATTERN = /^(?:block|contents|flex|grid|hidden|inline|inline-block|inline-flex|inline-grid)$/;
|
|
84694
|
+
const PSEUDO_VISIBILITY_UTILITY_PATTERN = /^(?:visible|invisible|collapse)$/;
|
|
84695
|
+
const PSEUDO_OPACITY_UTILITY_PATTERN = /^opacity-(.+)$/;
|
|
84696
|
+
const PSEUDO_SIDE_TAB_SAFE_ELEMENT_PATTERN = /(?:^|\.)(?:blockquote|code|hr|pre|table|tbody|td|tfoot|th|thead|tr)$/i;
|
|
84697
|
+
const splitShadowLayerTokens = (shadowValue) => {
|
|
84698
|
+
const tokens = [];
|
|
84699
|
+
let currentToken = "";
|
|
84700
|
+
let parenthesisDepth = 0;
|
|
84701
|
+
for (const character of shadowValue) {
|
|
84702
|
+
if (character === "(") {
|
|
84703
|
+
parenthesisDepth += 1;
|
|
84704
|
+
currentToken += character;
|
|
84705
|
+
continue;
|
|
84706
|
+
}
|
|
84707
|
+
if (character === ")") {
|
|
84708
|
+
if (parenthesisDepth === 0) return null;
|
|
84709
|
+
parenthesisDepth -= 1;
|
|
84710
|
+
currentToken += character;
|
|
84711
|
+
continue;
|
|
84712
|
+
}
|
|
84713
|
+
if (character === "," && parenthesisDepth === 0) return null;
|
|
84714
|
+
if (/\s/.test(character) && parenthesisDepth === 0) {
|
|
84715
|
+
if (currentToken) tokens.push(currentToken);
|
|
84716
|
+
currentToken = "";
|
|
84717
|
+
continue;
|
|
84718
|
+
}
|
|
84719
|
+
currentToken += character;
|
|
84720
|
+
}
|
|
84721
|
+
if (parenthesisDepth !== 0) return null;
|
|
84722
|
+
if (currentToken) tokens.push(currentToken);
|
|
84723
|
+
return tokens;
|
|
84724
|
+
};
|
|
84725
|
+
const parseShadowLength = (token) => {
|
|
84726
|
+
const match = token.match(SHADOW_LENGTH_PATTERN);
|
|
84727
|
+
if (!match) return null;
|
|
84728
|
+
return {
|
|
84729
|
+
hasPixelUnit: Boolean(match[1]),
|
|
84730
|
+
value: parseFloat(token)
|
|
84731
|
+
};
|
|
84732
|
+
};
|
|
84733
|
+
const isFullyTransparentColor = (color) => {
|
|
84734
|
+
const normalizedColor = color.trim().toLowerCase();
|
|
84735
|
+
if (normalizedColor === "transparent") return true;
|
|
84736
|
+
const hexDigits = normalizedColor.match(/^#([0-9a-f]{4}|[0-9a-f]{8})$/)?.[1];
|
|
84737
|
+
if (hexDigits?.length === 4 && hexDigits.endsWith("0")) return true;
|
|
84738
|
+
if (hexDigits?.length === 8 && hexDigits.endsWith("00")) return true;
|
|
84739
|
+
const functionArguments = normalizedColor.match(/^(?:rgb|hsl)a?\((.*)\)$/)?.[1];
|
|
84740
|
+
if (!functionArguments) return false;
|
|
84741
|
+
const slashAlpha = functionArguments.match(/\/\s*([^/]+)$/)?.[1];
|
|
84742
|
+
if (slashAlpha) return ZERO_ALPHA_PATTERN.test(slashAlpha.trim());
|
|
84743
|
+
const legacyArguments = functionArguments.split(",");
|
|
84744
|
+
return legacyArguments.length === 4 && ZERO_ALPHA_PATTERN.test(legacyArguments[legacyArguments.length - 1]?.trim() ?? "");
|
|
84745
|
+
};
|
|
84746
|
+
const hasResolvableColorChroma = (color) => {
|
|
84747
|
+
if (isFullyTransparentColor(color)) return false;
|
|
84748
|
+
const parsedColor = parseColorToRgb(color);
|
|
84749
|
+
if (parsedColor) return hasColorChroma(parsedColor);
|
|
84750
|
+
return CHROMATIC_CSS_COLOR_KEYWORDS.has(color.trim().toLowerCase());
|
|
84751
|
+
};
|
|
84752
|
+
const parseInsetSideTabShadow = (shadowValue) => {
|
|
84753
|
+
const tokens = splitShadowLayerTokens(shadowValue);
|
|
84754
|
+
if (!tokens) return null;
|
|
84755
|
+
if (tokens.filter((token) => token.toLowerCase() === "inset").length !== 1) return null;
|
|
84756
|
+
const shadowLengths = [];
|
|
84757
|
+
const colorTokens = [];
|
|
84758
|
+
for (const token of tokens) {
|
|
84759
|
+
if (token.toLowerCase() === "inset") continue;
|
|
84760
|
+
const shadowLength = parseShadowLength(token);
|
|
84761
|
+
if (shadowLength) shadowLengths.push(shadowLength);
|
|
84762
|
+
else colorTokens.push(token);
|
|
84763
|
+
}
|
|
84764
|
+
if (shadowLengths.length < 2 || shadowLengths.length > 4 || colorTokens.length !== 1) return null;
|
|
84765
|
+
const horizontalOffset = shadowLengths[0];
|
|
84766
|
+
const verticalOffset = shadowLengths[1];
|
|
84767
|
+
const blurRadius = shadowLengths[2]?.value ?? 0;
|
|
84768
|
+
const spreadRadius = shadowLengths[3]?.value ?? 0;
|
|
84769
|
+
if (!horizontalOffset || !verticalOffset || blurRadius !== 0 || spreadRadius !== 0) return null;
|
|
84770
|
+
if (horizontalOffset.value !== 0 && !horizontalOffset.hasPixelUnit || verticalOffset.value !== 0 && !verticalOffset.hasPixelUnit) return null;
|
|
84771
|
+
const horizontalWidth = Math.abs(horizontalOffset.value);
|
|
84772
|
+
const verticalWidth = Math.abs(verticalOffset.value);
|
|
84773
|
+
const isHorizontalEdge = horizontalWidth >= 3 && horizontalWidth <= 12 && verticalWidth === 0;
|
|
84774
|
+
if (!isHorizontalEdge && !(verticalWidth >= 3 && verticalWidth <= 12 && horizontalWidth === 0) || !hasResolvableColorChroma(colorTokens[0])) return null;
|
|
84775
|
+
if (isHorizontalEdge) return {
|
|
84776
|
+
edgeLabel: horizontalOffset.value > 0 ? "left" : "right",
|
|
84777
|
+
widthPx: horizontalWidth
|
|
84778
|
+
};
|
|
84779
|
+
return {
|
|
84780
|
+
edgeLabel: verticalOffset.value > 0 ? "top" : "bottom",
|
|
84781
|
+
widthPx: verticalWidth
|
|
84782
|
+
};
|
|
84783
|
+
};
|
|
84784
|
+
const isStaticallyFalseJsxAttribute = (attribute) => {
|
|
84785
|
+
const attributeValue = attribute.value;
|
|
84786
|
+
if (!attributeValue) return false;
|
|
84787
|
+
if (isNodeOfType(attributeValue, "Literal")) return attributeValue.value === false || attributeValue.value === "false";
|
|
84788
|
+
if (!isNodeOfType(attributeValue, "JSXExpressionContainer")) return false;
|
|
84789
|
+
return isNodeOfType(attributeValue.expression, "Literal") && (attributeValue.expression.value === false || attributeValue.expression.value === "false");
|
|
84790
|
+
};
|
|
84791
|
+
const isInteractiveOrSelectedIndicator = (openingElement) => {
|
|
84792
|
+
if (isInteractiveElement(resolveJsxElementType(openingElement), openingElement)) return true;
|
|
84793
|
+
const selectedAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "aria-selected");
|
|
84794
|
+
const currentAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "aria-current");
|
|
84795
|
+
if (selectedAttribute && !isStaticallyFalseJsxAttribute(selectedAttribute) || currentAttribute && !isStaticallyFalseJsxAttribute(currentAttribute)) return true;
|
|
84796
|
+
const roleAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "role");
|
|
84797
|
+
const role = roleAttribute ? getJsxPropStringValue(roleAttribute)?.trim().split(/\s+/)[0] : null;
|
|
84798
|
+
return Boolean(role && isInteractiveRole(role.toLowerCase()));
|
|
84799
|
+
};
|
|
84800
|
+
const getEffectiveTailwindShadowResolution = (tokens) => resolveEffectiveTailwindClassNameToken(tokens, (utility) => TAILWIND_SHADOW_GEOMETRY_PATTERN.test(utility));
|
|
84801
|
+
const getTailwindInsetSideTabShadow = (classNameTokens) => {
|
|
84802
|
+
const shadowResolution = getEffectiveTailwindShadowResolution(classNameTokens);
|
|
84803
|
+
if (shadowResolution.isAmbiguous || !shadowResolution.utility) return null;
|
|
84804
|
+
const arbitraryShadowValue = shadowResolution.utility.match(TAILWIND_ARBITRARY_SHADOW_PATTERN)?.[1];
|
|
84805
|
+
if (!arbitraryShadowValue) return null;
|
|
84806
|
+
const shadow = parseInsetSideTabShadow(arbitraryShadowValue.replace(/_/g, " "));
|
|
84807
|
+
return shadow ? {
|
|
84808
|
+
isImportant: shadowResolution.isImportant,
|
|
84809
|
+
shadow
|
|
84810
|
+
} : null;
|
|
84811
|
+
};
|
|
84812
|
+
const hasImportantTailwindShadow = (classNameTokens) => classNameTokens.some((classNameToken) => {
|
|
84813
|
+
const parsedToken = parseTailwindClassNameToken(classNameToken);
|
|
84814
|
+
return parsedToken.variants.length === 0 && parsedToken.isImportant && TAILWIND_SHADOW_GEOMETRY_PATTERN.test(parsedToken.utility);
|
|
84815
|
+
});
|
|
84816
|
+
const hasChromaticTailwindBackground = (utility) => {
|
|
84817
|
+
if (CHROMATIC_TAILWIND_BACKGROUND_PATTERN.test(utility)) {
|
|
84818
|
+
const opacityModifier = utility.match(/\/(.+)$/)?.[1]?.replace(/^\[|\]$/g, "");
|
|
84819
|
+
return !opacityModifier || !ZERO_ALPHA_PATTERN.test(opacityModifier);
|
|
84820
|
+
}
|
|
84821
|
+
const arbitraryColorMatch = utility.match(/^bg-\[(?:color:)?([^\]]+)\](?:\/(.+))?$/);
|
|
84822
|
+
const arbitraryColor = arbitraryColorMatch?.[1];
|
|
84823
|
+
const opacityModifier = arbitraryColorMatch?.[2]?.replace(/^\[|\]$/g, "");
|
|
84824
|
+
return Boolean(arbitraryColor && (!opacityModifier || !ZERO_ALPHA_PATTERN.test(opacityModifier)) && !isFullyTransparentColor(arbitraryColor) && hasResolvableColorChroma(arbitraryColor));
|
|
84825
|
+
};
|
|
84826
|
+
const getStaticPseudoUtilities = (classNameTokens, pseudoElementName) => {
|
|
84827
|
+
const utilities = [];
|
|
84828
|
+
for (const classNameToken of classNameTokens) {
|
|
84829
|
+
const parsedToken = parseTailwindClassNameToken(classNameToken);
|
|
84830
|
+
if (!parsedToken.variants.includes(pseudoElementName)) continue;
|
|
84831
|
+
if (parsedToken.variants.length !== 1 || parsedToken.variants[0] !== pseudoElementName || parsedToken.variants.some((variant) => PSEUDO_INTERACTION_OR_SELECTION_VARIANT_PATTERN.test(variant))) return null;
|
|
84832
|
+
utilities.push(parsedToken.isImportant ? `!${parsedToken.utility}` : parsedToken.utility);
|
|
84833
|
+
}
|
|
84834
|
+
return utilities;
|
|
84835
|
+
};
|
|
84836
|
+
const getTailwindDimensionResolution = (utilities, dimensionPrefix) => resolveEffectiveTailwindClassNameToken(utilities, (utility) => utility.startsWith(`${dimensionPrefix}-`) || utility.startsWith("size-"), []);
|
|
84837
|
+
const getTailwindDimensionPx = (utility, dimensionPrefix) => {
|
|
84838
|
+
if (!utility) return null;
|
|
84839
|
+
return utility.startsWith("size-") ? parseStaticTailwindLengthPx(utility, "size") : parseStaticTailwindLengthPx(utility, dimensionPrefix);
|
|
84840
|
+
};
|
|
84841
|
+
const isFullTailwindDimension = (utility, dimensionPrefix) => utility === `${dimensionPrefix}-full` || utility === "size-full";
|
|
84842
|
+
const getTailwindOffsetUtilityValue = (utility, sideName) => {
|
|
84843
|
+
const isNegative = utility.startsWith("-");
|
|
84844
|
+
const normalizedUtility = isNegative ? utility.slice(1) : utility;
|
|
84845
|
+
const axisPrefix = sideName === "left" || sideName === "right" ? "inset-x" : "inset-y";
|
|
84846
|
+
const matchingPrefix = normalizedUtility.startsWith(`${sideName}-`) ? sideName : normalizedUtility.startsWith(`${axisPrefix}-`) ? axisPrefix : /^inset-(?![xy]-)/.test(normalizedUtility) ? "inset" : null;
|
|
84847
|
+
if (!matchingPrefix) return void 0;
|
|
84848
|
+
if (normalizedUtility === `${matchingPrefix}-auto`) return null;
|
|
84849
|
+
const valuePx = parseStaticTailwindLengthPx(normalizedUtility, matchingPrefix);
|
|
84850
|
+
return valuePx === null ? null : valuePx * (isNegative ? -1 : 1);
|
|
84851
|
+
};
|
|
84852
|
+
const getTailwindPseudoOffsetResolution = (utilities, sideName) => {
|
|
84853
|
+
const relevantUtilities = utilities.map(parseTailwindClassNameToken).filter((parsedUtility) => getTailwindOffsetUtilityValue(parsedUtility.utility, sideName) !== void 0);
|
|
84854
|
+
const hasImportantUtility = relevantUtilities.some((parsedUtility) => parsedUtility.isImportant);
|
|
84855
|
+
const effectiveValues = new Set(relevantUtilities.filter((parsedUtility) => !hasImportantUtility || parsedUtility.isImportant).map((parsedUtility) => getTailwindOffsetUtilityValue(parsedUtility.utility, sideName)));
|
|
84856
|
+
if (effectiveValues.size !== 1) return {
|
|
84857
|
+
isAmbiguous: effectiveValues.size > 1,
|
|
84858
|
+
valuePx: null
|
|
84859
|
+
};
|
|
84860
|
+
const valuePx = effectiveValues.values().next().value ?? null;
|
|
84861
|
+
return {
|
|
84862
|
+
isAmbiguous: valuePx === null,
|
|
84863
|
+
valuePx
|
|
84864
|
+
};
|
|
84865
|
+
};
|
|
84866
|
+
const isNearlyFullPseudoAxis = (utilities, dimensionPrefix, startSide, endSide) => {
|
|
84867
|
+
const dimensionResolution = getTailwindDimensionResolution(utilities, dimensionPrefix);
|
|
84868
|
+
if (dimensionResolution.isAmbiguous) return false;
|
|
84869
|
+
if (dimensionResolution.utility) {
|
|
84870
|
+
const startOffset = getTailwindPseudoOffsetResolution(utilities, startSide);
|
|
84871
|
+
const endOffset = getTailwindPseudoOffsetResolution(utilities, endSide);
|
|
84872
|
+
return isFullTailwindDimension(dimensionResolution.utility, dimensionPrefix) && !startOffset.isAmbiguous && !endOffset.isAmbiguous && (startOffset.valuePx === null || startOffset.valuePx === 0) && (endOffset.valuePx === null || endOffset.valuePx === 0);
|
|
84873
|
+
}
|
|
84874
|
+
const startOffset = getTailwindPseudoOffsetResolution(utilities, startSide);
|
|
84875
|
+
const endOffset = getTailwindPseudoOffsetResolution(utilities, endSide);
|
|
84876
|
+
return !startOffset.isAmbiguous && !endOffset.isAmbiguous && startOffset.valuePx !== null && endOffset.valuePx !== null && startOffset.valuePx >= 0 && startOffset.valuePx <= 20 && endOffset.valuePx >= 0 && endOffset.valuePx <= 20;
|
|
84877
|
+
};
|
|
84878
|
+
const getAnchoredPseudoEdge = (utilities, startSide, endSide) => {
|
|
84879
|
+
const startOffset = getTailwindPseudoOffsetResolution(utilities, startSide);
|
|
84880
|
+
const endOffset = getTailwindPseudoOffsetResolution(utilities, endSide);
|
|
84881
|
+
if (startOffset.isAmbiguous || endOffset.isAmbiguous) return null;
|
|
84882
|
+
const isStartAnchored = startOffset.valuePx === 0;
|
|
84883
|
+
if (isStartAnchored === (endOffset.valuePx === 0)) return null;
|
|
84884
|
+
return isStartAnchored ? startSide : endSide;
|
|
84885
|
+
};
|
|
84886
|
+
const getTailwindPseudoSideTabStripe = (classNameTokens) => {
|
|
84887
|
+
for (const pseudoElementName of PSEUDO_ELEMENT_NAMES) {
|
|
84888
|
+
const utilities = getStaticPseudoUtilities(classNameTokens, pseudoElementName);
|
|
84889
|
+
if (!utilities?.length) continue;
|
|
84890
|
+
const positionResolution = resolveEffectiveTailwindClassNameToken(utilities, (utility) => [
|
|
84891
|
+
"absolute",
|
|
84892
|
+
"fixed",
|
|
84893
|
+
"relative",
|
|
84894
|
+
"static",
|
|
84895
|
+
"sticky"
|
|
84896
|
+
].includes(utility), []);
|
|
84897
|
+
const backgroundResolution = resolveEffectiveTailwindClassNameToken(utilities, (utility) => TAILWIND_BACKGROUND_COLOR_PATTERN.test(utility), []);
|
|
84898
|
+
const displayResolution = resolveEffectiveTailwindClassNameToken(utilities, (utility) => PSEUDO_DISPLAY_UTILITY_PATTERN.test(utility), []);
|
|
84899
|
+
const visibilityResolution = resolveEffectiveTailwindClassNameToken(utilities, (utility) => PSEUDO_VISIBILITY_UTILITY_PATTERN.test(utility), []);
|
|
84900
|
+
const opacityResolution = resolveEffectiveTailwindClassNameToken(utilities, (utility) => PSEUDO_OPACITY_UTILITY_PATTERN.test(utility), []);
|
|
84901
|
+
const backgroundOpacityResolution = resolveEffectiveTailwindClassNameToken(utilities, (utility) => utility.startsWith("bg-opacity-"), []);
|
|
84902
|
+
const contentResolution = resolveEffectiveTailwindClassNameToken(utilities, (utility) => utility.startsWith("content-") || utility.startsWith("[content:"), []);
|
|
84903
|
+
if (positionResolution.isAmbiguous || backgroundResolution.isAmbiguous || displayResolution.isAmbiguous || visibilityResolution.isAmbiguous || opacityResolution.isAmbiguous || backgroundOpacityResolution.isAmbiguous || contentResolution.isAmbiguous || positionResolution.utility !== "absolute" || !backgroundResolution.utility || !hasChromaticTailwindBackground(backgroundResolution.utility) || displayResolution.utility === "hidden" || displayResolution.utility === "contents" || visibilityResolution.utility === "invisible" || visibilityResolution.utility === "collapse" || contentResolution.utility === "content-none") continue;
|
|
84904
|
+
const opacityValue = opacityResolution.utility?.match(PSEUDO_OPACITY_UTILITY_PATTERN)?.[1];
|
|
84905
|
+
const backgroundOpacityValue = backgroundOpacityResolution.utility?.slice(11);
|
|
84906
|
+
if (opacityValue && ZERO_ALPHA_PATTERN.test(opacityValue.replace(/^\[|\]$/g, "")) || backgroundOpacityValue && ZERO_ALPHA_PATTERN.test(backgroundOpacityValue.replace(/^\[|\]$/g, ""))) continue;
|
|
84907
|
+
const widthResolution = getTailwindDimensionResolution(utilities, "w");
|
|
84908
|
+
const heightResolution = getTailwindDimensionResolution(utilities, "h");
|
|
84909
|
+
if (widthResolution.isAmbiguous || heightResolution.isAmbiguous) continue;
|
|
84910
|
+
const widthPx = getTailwindDimensionPx(widthResolution.utility, "w");
|
|
84911
|
+
const heightPx = getTailwindDimensionPx(heightResolution.utility, "h");
|
|
84912
|
+
const isVerticalStripe = widthPx !== null && widthPx >= 3 && widthPx <= 12 && isNearlyFullPseudoAxis(utilities, "h", "top", "bottom");
|
|
84913
|
+
if (isVerticalStripe === (heightPx !== null && heightPx >= 3 && heightPx <= 12 && isNearlyFullPseudoAxis(utilities, "w", "left", "right"))) continue;
|
|
84914
|
+
const edgeLabel = isVerticalStripe ? getAnchoredPseudoEdge(utilities, "left", "right") : getAnchoredPseudoEdge(utilities, "top", "bottom");
|
|
84915
|
+
if (!edgeLabel) continue;
|
|
84916
|
+
const stripeWidthPx = isVerticalStripe ? widthPx : heightPx;
|
|
84917
|
+
if (stripeWidthPx === null) continue;
|
|
84918
|
+
return {
|
|
84919
|
+
edgeLabel,
|
|
84920
|
+
pseudoElementName,
|
|
84921
|
+
widthPx: stripeWidthPx
|
|
84922
|
+
};
|
|
84923
|
+
}
|
|
84924
|
+
return null;
|
|
84925
|
+
};
|
|
84926
|
+
const hasActualSelectedOrCurrentState = (openingElement) => {
|
|
84927
|
+
const selectedAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "aria-selected");
|
|
84928
|
+
const currentAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "aria-current");
|
|
84929
|
+
const dataSelectedAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "data-selected");
|
|
84930
|
+
const dataStateAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "data-state");
|
|
84931
|
+
const dataStateValue = dataStateAttribute ? getJsxPropStringValue(dataStateAttribute)?.toLowerCase() : null;
|
|
84932
|
+
return Boolean(selectedAttribute && !isStaticallyFalseJsxAttribute(selectedAttribute) || currentAttribute && !isStaticallyFalseJsxAttribute(currentAttribute) || dataSelectedAttribute && !isStaticallyFalseJsxAttribute(dataSelectedAttribute) || dataStateAttribute && (!dataStateValue || [
|
|
84933
|
+
"active",
|
|
84934
|
+
"current",
|
|
84935
|
+
"selected"
|
|
84936
|
+
].includes(dataStateValue)));
|
|
84937
|
+
};
|
|
84938
|
+
const isDynamicJsxContent = (node) => {
|
|
84939
|
+
if (isNodeOfType(node, "JSXExpressionContainer")) return !(isNodeOfType(node.expression, "Literal") || isNodeOfType(node.expression, "TemplateLiteral") && node.expression.expressions.length === 0);
|
|
84940
|
+
if (isNodeOfType(node, "JSXElement") || isNodeOfType(node, "JSXFragment")) return node.children.some(isDynamicJsxContent);
|
|
84941
|
+
return false;
|
|
84942
|
+
};
|
|
84943
|
+
const hasStaticSideTabLabelContext = (openingElement, className) => {
|
|
84944
|
+
if (splitTailwindClassName(className).some((token) => PSEUDO_SIDE_TAB_CONTEXT_PATTERN.test(token))) return true;
|
|
84945
|
+
const element = isNodeOfType(openingElement.parent, "JSXElement") ? openingElement.parent : null;
|
|
84946
|
+
if (!element || element.children.some(isDynamicJsxContent)) return false;
|
|
84947
|
+
const labelText = getStaticJsxText(element).replace(/\s+/g, " ").trim();
|
|
84948
|
+
return labelText.length > 0 && labelText.length <= 32 && /[\p{L}\p{N}]/u.test(labelText);
|
|
84949
|
+
};
|
|
84950
|
+
const isGlyphOrLogoContext = (openingElement, classNameTokens) => {
|
|
84951
|
+
const elementType = resolveJsxElementType(openingElement);
|
|
84952
|
+
if (/^(?:canvas|img|path|picture|svg|use)$/i.test(elementType) || /(?:avatar|glyph|icon|logo|mark)$/i.test(elementType)) return true;
|
|
84953
|
+
if (classNameTokens.some((classNameToken) => {
|
|
84954
|
+
const parsedToken = parseTailwindClassNameToken(classNameToken);
|
|
84955
|
+
return parsedToken.variants.length === 0 && PSEUDO_SIDE_TAB_ART_PATTERN.test(parsedToken.utility);
|
|
84956
|
+
})) return true;
|
|
84957
|
+
const widthResolution = getTailwindDimensionResolution(classNameTokens, "w");
|
|
84958
|
+
const heightResolution = getTailwindDimensionResolution(classNameTokens, "h");
|
|
84959
|
+
const widthPx = getTailwindDimensionPx(widthResolution.utility, "w");
|
|
84960
|
+
const heightPx = getTailwindDimensionPx(heightResolution.utility, "h");
|
|
84961
|
+
const staticLabelText = getStaticJsxText(isNodeOfType(openingElement.parent, "JSXElement") ? openingElement.parent : null).replace(/\s+/g, " ").trim();
|
|
84962
|
+
return !widthResolution.isAmbiguous && !heightResolution.isAmbiguous && widthPx !== null && heightPx !== null && widthPx <= 40 && heightPx <= 40 && staticLabelText.length === 0;
|
|
84963
|
+
};
|
|
84964
|
+
const isHorizontalUnderlineHost = (openingElement) => {
|
|
84965
|
+
const elementType = resolveJsxElementType(openingElement);
|
|
84966
|
+
return /^(?:a|area|button)$/i.test(elementType) || /(?:button|link)$/i.test(elementType);
|
|
84967
|
+
};
|
|
84968
|
+
const hasStaticSelectedOrCurrentClass = (classNameTokens) => classNameTokens.some((classNameToken) => {
|
|
84969
|
+
const parsedToken = parseTailwindClassNameToken(classNameToken);
|
|
84970
|
+
return parsedToken.variants.length === 0 && STATIC_SELECTED_OR_CURRENT_CLASS_PATTERN.test(parsedToken.utility);
|
|
84971
|
+
});
|
|
84972
|
+
const hasStaticPseudoPositioningContext = (classNameTokens) => {
|
|
84973
|
+
const positionResolution = resolveEffectiveTailwindClassNameToken(classNameTokens, (utility) => [
|
|
84974
|
+
"absolute",
|
|
84975
|
+
"fixed",
|
|
84976
|
+
"relative",
|
|
84977
|
+
"static",
|
|
84978
|
+
"sticky"
|
|
84979
|
+
].includes(utility));
|
|
84980
|
+
return !positionResolution.isAmbiguous && Boolean(positionResolution.utility && [
|
|
84981
|
+
"absolute",
|
|
84982
|
+
"fixed",
|
|
84983
|
+
"relative",
|
|
84984
|
+
"sticky"
|
|
84985
|
+
].includes(positionResolution.utility));
|
|
84986
|
+
};
|
|
82521
84987
|
const isNeutralBorderColor = (value) => {
|
|
82522
84988
|
const trimmed = value.trim().toLowerCase();
|
|
82523
84989
|
if ([
|
|
@@ -82600,11 +85066,11 @@ const getTailwindBorderColorNeutrality = (utility, expectedSide) => {
|
|
|
82600
85066
|
};
|
|
82601
85067
|
const noSideTabBorder = defineRule({
|
|
82602
85068
|
id: "no-side-tab-border",
|
|
82603
|
-
title: "Thick one-sided
|
|
85069
|
+
title: "Thick one-sided stripe",
|
|
82604
85070
|
tags: ["design", "test-noise"],
|
|
82605
85071
|
severity: "warn",
|
|
82606
85072
|
defaultEnabled: false,
|
|
82607
|
-
recommendation: "Use a
|
|
85073
|
+
recommendation: "Use a background change, a thin neutral edge, or a subtler surface treatment instead of a thick one-sided stripe.",
|
|
82608
85074
|
create: (context) => ({
|
|
82609
85075
|
JSXAttribute(node) {
|
|
82610
85076
|
const expression = getInlineStyleExpression(node);
|
|
@@ -82613,6 +85079,14 @@ const noSideTabBorder = defineRule({
|
|
|
82613
85079
|
const className = openingElement ? getStringFromClassNameAttr(openingElement) : null;
|
|
82614
85080
|
if (className && hasSpinnerClass(className)) return;
|
|
82615
85081
|
const classNameTokens = className ? splitTailwindClassName(className) : [];
|
|
85082
|
+
const shadowProperty = getEffectiveStyleProperty(expression.properties, "boxShadow");
|
|
85083
|
+
const shadowValue = shadowProperty ? getStylePropertyStringValue(shadowProperty) : null;
|
|
85084
|
+
const insetSideTabShadow = shadowValue ? parseInsetSideTabShadow(shadowValue) : null;
|
|
85085
|
+
const isImportantTailwindShadowEffective = hasCapabilityOrUnspecified(context.settings, "tailwind") && hasImportantTailwindShadow(classNameTokens);
|
|
85086
|
+
if (shadowProperty && insetSideTabShadow && openingElement && !isImportantTailwindShadowEffective && !isInteractiveOrSelectedIndicator(openingElement)) context.report({
|
|
85087
|
+
node: shadowProperty,
|
|
85088
|
+
message: `Your users see an off, dated inset stripe on one side (${insetSideTabShadow.edgeLabel}: ${insetSideTabShadow.widthPx}px), so use a subtler surface treatment or drop it.`
|
|
85089
|
+
});
|
|
82616
85090
|
let hasBorderRadius = false;
|
|
82617
85091
|
const borderRadiusProperty = getEffectiveStyleProperty(expression.properties, "borderRadius");
|
|
82618
85092
|
if (borderRadiusProperty) {
|
|
@@ -82672,6 +85146,23 @@ const noSideTabBorder = defineRule({
|
|
|
82672
85146
|
if (!classStr) return;
|
|
82673
85147
|
if (hasSpinnerClass(classStr)) return;
|
|
82674
85148
|
const classNameTokens = splitTailwindClassName(classStr);
|
|
85149
|
+
const tailwindInsetSideTabShadow = getTailwindInsetSideTabShadow(classNameTokens);
|
|
85150
|
+
if (tailwindInsetSideTabShadow && !isInteractiveOrSelectedIndicator(node)) {
|
|
85151
|
+
const styleAttribute = getAuthoritativeJsxAttribute(node.attributes, "style");
|
|
85152
|
+
const styleExpression = styleAttribute ? getInlineStyleExpression(styleAttribute) : null;
|
|
85153
|
+
const hasUnknownInlineStyleProperty = Boolean(styleExpression?.properties.some((property) => getStylePropertyKey(property) === null));
|
|
85154
|
+
const hasInlineShadowOverride = Boolean(styleAttribute && (!styleExpression || hasUnknownInlineStyleProperty || getEffectiveStyleProperty(styleExpression.properties, "boxShadow")));
|
|
85155
|
+
if (tailwindInsetSideTabShadow.isImportant || !hasInlineShadowOverride) context.report({
|
|
85156
|
+
node,
|
|
85157
|
+
message: `Your users see an off, dated inset stripe on one side (${tailwindInsetSideTabShadow.shadow.edgeLabel}: ${tailwindInsetSideTabShadow.shadow.widthPx}px), so use a subtler surface treatment or drop it.`
|
|
85158
|
+
});
|
|
85159
|
+
}
|
|
85160
|
+
const pseudoSideTabStripe = getTailwindPseudoSideTabStripe(classNameTokens);
|
|
85161
|
+
const elementType = resolveJsxElementType(node);
|
|
85162
|
+
if (pseudoSideTabStripe && !PSEUDO_SIDE_TAB_SAFE_ELEMENT_PATTERN.test(elementType) && !hasActualSelectedOrCurrentState(node) && !hasStaticSelectedOrCurrentClass(classNameTokens) && hasStaticSideTabLabelContext(node, classStr) && hasStaticPseudoPositioningContext(classNameTokens) && !isGlyphOrLogoContext(node, classNameTokens) && !((pseudoSideTabStripe.edgeLabel === "top" || pseudoSideTabStripe.edgeLabel === "bottom") && isHorizontalUnderlineHost(node))) context.report({
|
|
85163
|
+
node,
|
|
85164
|
+
message: `Your users see an off, dated ${pseudoSideTabStripe.widthPx}px ${pseudoSideTabStripe.pseudoElementName} stripe on the ${pseudoSideTabStripe.edgeLabel} edge, so use a subtler surface treatment or drop it.`
|
|
85165
|
+
});
|
|
82675
85166
|
const hasBaseRoundingUtility = classNameTokens.some((classNameToken) => {
|
|
82676
85167
|
const parsedToken = parseTailwindClassNameToken(classNameToken);
|
|
82677
85168
|
return parsedToken.variants.length === 0 && ROUNDING_PATTERN.test(parsedToken.utility);
|
|
@@ -84279,79 +86770,6 @@ const noStringRefs = defineRule({
|
|
|
84279
86770
|
}
|
|
84280
86771
|
});
|
|
84281
86772
|
//#endregion
|
|
84282
|
-
//#region src/plugin/constants/svg-tags.ts
|
|
84283
|
-
const SVG_TAGS = new Set([
|
|
84284
|
-
"a",
|
|
84285
|
-
"altGlyph",
|
|
84286
|
-
"altGlyphDef",
|
|
84287
|
-
"altGlyphItem",
|
|
84288
|
-
"animate",
|
|
84289
|
-
"animateColor",
|
|
84290
|
-
"animateMotion",
|
|
84291
|
-
"animateTransform",
|
|
84292
|
-
"circle",
|
|
84293
|
-
"clipPath",
|
|
84294
|
-
"cursor",
|
|
84295
|
-
"defs",
|
|
84296
|
-
"desc",
|
|
84297
|
-
"ellipse",
|
|
84298
|
-
"feBlend",
|
|
84299
|
-
"feColorMatrix",
|
|
84300
|
-
"feComponentTransfer",
|
|
84301
|
-
"feComposite",
|
|
84302
|
-
"feConvolveMatrix",
|
|
84303
|
-
"feDiffuseLighting",
|
|
84304
|
-
"feDisplacementMap",
|
|
84305
|
-
"feDistantLight",
|
|
84306
|
-
"feDropShadow",
|
|
84307
|
-
"feFlood",
|
|
84308
|
-
"feFuncA",
|
|
84309
|
-
"feFuncB",
|
|
84310
|
-
"feFuncG",
|
|
84311
|
-
"feFuncR",
|
|
84312
|
-
"feGaussianBlur",
|
|
84313
|
-
"feImage",
|
|
84314
|
-
"feMerge",
|
|
84315
|
-
"feMergeNode",
|
|
84316
|
-
"feMorphology",
|
|
84317
|
-
"feOffset",
|
|
84318
|
-
"fePointLight",
|
|
84319
|
-
"feSpecularLighting",
|
|
84320
|
-
"feSpotLight",
|
|
84321
|
-
"feTile",
|
|
84322
|
-
"feTurbulence",
|
|
84323
|
-
"filter",
|
|
84324
|
-
"foreignObject",
|
|
84325
|
-
"g",
|
|
84326
|
-
"glyph",
|
|
84327
|
-
"glyphRef",
|
|
84328
|
-
"image",
|
|
84329
|
-
"line",
|
|
84330
|
-
"linearGradient",
|
|
84331
|
-
"marker",
|
|
84332
|
-
"mask",
|
|
84333
|
-
"metadata",
|
|
84334
|
-
"mpath",
|
|
84335
|
-
"path",
|
|
84336
|
-
"pattern",
|
|
84337
|
-
"polygon",
|
|
84338
|
-
"polyline",
|
|
84339
|
-
"radialGradient",
|
|
84340
|
-
"rect",
|
|
84341
|
-
"set",
|
|
84342
|
-
"stop",
|
|
84343
|
-
"svg",
|
|
84344
|
-
"switch",
|
|
84345
|
-
"symbol",
|
|
84346
|
-
"text",
|
|
84347
|
-
"textPath",
|
|
84348
|
-
"title",
|
|
84349
|
-
"tref",
|
|
84350
|
-
"tspan",
|
|
84351
|
-
"use",
|
|
84352
|
-
"view"
|
|
84353
|
-
]);
|
|
84354
|
-
//#endregion
|
|
84355
86773
|
//#region src/plugin/rules/design/no-svg-currentcolor-with-fill-class.ts
|
|
84356
86774
|
const NON_COLOR_PAINT_VALUES = new Set([
|
|
84357
86775
|
"current",
|
|
@@ -84727,6 +87145,24 @@ const noTightDisplayTracking = defineRule({
|
|
|
84727
87145
|
} })
|
|
84728
87146
|
});
|
|
84729
87147
|
//#endregion
|
|
87148
|
+
//#region src/plugin/rules/design/utils/get-effective-nonzero-tailwind-tracking.ts
|
|
87149
|
+
const TRACKING_UTILITY_PATTERN = /^-?tracking-/;
|
|
87150
|
+
const STATIC_TRACKING_UTILITIES = new Set([
|
|
87151
|
+
"tracking-tight",
|
|
87152
|
+
"tracking-tighter",
|
|
87153
|
+
"tracking-wide",
|
|
87154
|
+
"tracking-wider",
|
|
87155
|
+
"tracking-widest"
|
|
87156
|
+
]);
|
|
87157
|
+
const NONZERO_ARBITRARY_TRACKING_PATTERN = /^-?tracking-\[(?:length:)?(-?(?:\d+(?:\.\d*)?|\.\d+))(?:cap|ch|cm|dvh|dvw|em|ex|ic|in|lh|lvh|lvw|mm|pc|pt|px|q|rcap|rch|rem|rex|ric|rlh|svh|svw|vb|vh|vi|vmax|vmin|vw)\]$/i;
|
|
87158
|
+
const getEffectiveNonzeroTailwindTracking = (tokens) => {
|
|
87159
|
+
const effectiveTracking = getEffectiveTailwindClassNameToken(tokens, (utility) => TRACKING_UTILITY_PATTERN.test(utility));
|
|
87160
|
+
if (!effectiveTracking) return null;
|
|
87161
|
+
if (STATIC_TRACKING_UTILITIES.has(effectiveTracking)) return effectiveTracking;
|
|
87162
|
+
const arbitraryTracking = effectiveTracking.match(NONZERO_ARBITRARY_TRACKING_PATTERN);
|
|
87163
|
+
return arbitraryTracking && Number.parseFloat(arbitraryTracking[1]) !== 0 ? effectiveTracking : null;
|
|
87164
|
+
};
|
|
87165
|
+
//#endregion
|
|
84730
87166
|
//#region src/plugin/rules/design/no-tiny-text.ts
|
|
84731
87167
|
const LETTER_OR_DIGIT_PATTERN = /[\p{L}\p{N}]/u;
|
|
84732
87168
|
const NAMED_GLYPH_ENTITY_CHARS = {
|
|
@@ -84750,12 +87186,22 @@ const NAMED_GLYPH_ENTITY_CHARS = {
|
|
|
84750
87186
|
};
|
|
84751
87187
|
const decodeHtmlEntities = (text) => text.replace(/&#x([0-9a-f]+);/gi, (_, hexCode) => String.fromCodePoint(Number.parseInt(hexCode, 16))).replace(/&#(\d+);/g, (_, decimalCode) => String.fromCodePoint(Number.parseInt(decimalCode, 10))).replace(/&([a-z]+);/gi, (match, entityName) => NAMED_GLYPH_ENTITY_CHARS[entityName.toLowerCase()] ?? match);
|
|
84752
87188
|
const collectStaticExpressionText = (node) => {
|
|
84753
|
-
if (!node) return
|
|
84754
|
-
if (isNodeOfType(node, "Literal"))
|
|
84755
|
-
|
|
84756
|
-
|
|
87189
|
+
if (!node) return null;
|
|
87190
|
+
if (isNodeOfType(node, "Literal")) {
|
|
87191
|
+
if (node.value === null || typeof node.value === "boolean") return "";
|
|
87192
|
+
return typeof node.value === "string" || typeof node.value === "number" ? String(node.value) : null;
|
|
87193
|
+
}
|
|
87194
|
+
if (isNodeOfType(node, "TemplateLiteral")) {
|
|
87195
|
+
if (node.expressions.length > 0) return null;
|
|
87196
|
+
return (node.quasis ?? []).map((quasi) => quasi.value?.raw ?? "").join("");
|
|
87197
|
+
}
|
|
87198
|
+
if (isNodeOfType(node, "ConditionalExpression")) {
|
|
87199
|
+
const consequentText = collectStaticExpressionText(node.consequent);
|
|
87200
|
+
const alternateText = collectStaticExpressionText(node.alternate);
|
|
87201
|
+
return consequentText === null || alternateText === null ? null : consequentText + alternateText;
|
|
87202
|
+
}
|
|
84757
87203
|
if (isNodeOfType(node, "LogicalExpression")) return collectStaticExpressionText(node.right);
|
|
84758
|
-
return
|
|
87204
|
+
return null;
|
|
84759
87205
|
};
|
|
84760
87206
|
const ICON_IDENTIFIER_NAME_PATTERN = /icon|glyph/i;
|
|
84761
87207
|
const isIconIdentifierExpression = (node) => {
|
|
@@ -84767,9 +87213,7 @@ const isIconIdentifierExpression = (node) => {
|
|
|
84767
87213
|
if (isNodeOfType(node, "LogicalExpression")) return isIconIdentifierExpression(node.right);
|
|
84768
87214
|
return false;
|
|
84769
87215
|
};
|
|
84770
|
-
const hasOnlyIconIdentifierChildren = (
|
|
84771
|
-
const jsxElement = styleAttribute.parent?.parent;
|
|
84772
|
-
if (!isNodeOfType(jsxElement, "JSXElement")) return false;
|
|
87216
|
+
const hasOnlyIconIdentifierChildren = (jsxElement) => {
|
|
84773
87217
|
let expressionChildCount = 0;
|
|
84774
87218
|
for (const child of jsxElement.children ?? []) {
|
|
84775
87219
|
if (isNodeOfType(child, "JSXText")) {
|
|
@@ -84784,9 +87228,7 @@ const hasOnlyIconIdentifierChildren = (styleAttribute) => {
|
|
|
84784
87228
|
};
|
|
84785
87229
|
const REACT_ICONS_COMPONENT_NAME_PATTERN = /^(?:Fa|Md|Io|Bs|Bi|Ri|Gi|Hi|Lu|Tb|Fi|Ai|Cg|Di|Gr|Im|Pi|Si|Sl|Ti|Vsc|Wi)[A-Z0-9]/;
|
|
84786
87230
|
const ICON_WORD_PATTERN = /icon/i;
|
|
84787
|
-
const isChildlessIconComponent = (
|
|
84788
|
-
const openingElement = styleAttribute.parent;
|
|
84789
|
-
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
|
|
87231
|
+
const isChildlessIconComponent = (openingElement) => {
|
|
84790
87232
|
if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return false;
|
|
84791
87233
|
const elementName = openingElement.name.name;
|
|
84792
87234
|
if (!/^[A-Z]/.test(elementName)) return false;
|
|
@@ -84795,16 +87237,184 @@ const isChildlessIconComponent = (styleAttribute) => {
|
|
|
84795
87237
|
if (!isNodeOfType(jsxElement, "JSXElement")) return true;
|
|
84796
87238
|
return (jsxElement.children ?? []).every((child) => isNodeOfType(child, "JSXText") && (child.value ?? "").trim() === "");
|
|
84797
87239
|
};
|
|
84798
|
-
const hasGlyphOnlyContent = (
|
|
84799
|
-
const jsxElement = styleAttribute.parent?.parent;
|
|
84800
|
-
if (!isNodeOfType(jsxElement, "JSXElement")) return false;
|
|
87240
|
+
const hasGlyphOnlyContent = (jsxElement) => {
|
|
84801
87241
|
let staticText = "";
|
|
84802
87242
|
for (const child of jsxElement.children ?? []) if (isNodeOfType(child, "JSXText")) staticText += typeof child.value === "string" ? child.value : "";
|
|
84803
|
-
else if (isNodeOfType(child, "JSXExpressionContainer"))
|
|
87243
|
+
else if (isNodeOfType(child, "JSXExpressionContainer")) {
|
|
87244
|
+
if (isIconIdentifierExpression(child.expression)) continue;
|
|
87245
|
+
const expressionText = collectStaticExpressionText(child.expression);
|
|
87246
|
+
if (expressionText === null) return false;
|
|
87247
|
+
staticText += expressionText;
|
|
87248
|
+
}
|
|
84804
87249
|
const trimmedText = decodeHtmlEntities(staticText.trim());
|
|
84805
87250
|
return trimmedText.length > 0 && !LETTER_OR_DIGIT_PATTERN.test(trimmedText);
|
|
84806
87251
|
};
|
|
84807
|
-
const
|
|
87252
|
+
const PREFORMATTED_ELEMENT_NAMES$3 = new Set([
|
|
87253
|
+
"code",
|
|
87254
|
+
"head",
|
|
87255
|
+
"kbd",
|
|
87256
|
+
"noscript",
|
|
87257
|
+
"option",
|
|
87258
|
+
"pre",
|
|
87259
|
+
"samp",
|
|
87260
|
+
"script",
|
|
87261
|
+
"style",
|
|
87262
|
+
"sub",
|
|
87263
|
+
"sup",
|
|
87264
|
+
"svg",
|
|
87265
|
+
"template",
|
|
87266
|
+
"title",
|
|
87267
|
+
"var"
|
|
87268
|
+
]);
|
|
87269
|
+
const FUNCTIONAL_ELEMENT_NAMES = new Set([
|
|
87270
|
+
"a",
|
|
87271
|
+
"button",
|
|
87272
|
+
"caption",
|
|
87273
|
+
"dd",
|
|
87274
|
+
"dt",
|
|
87275
|
+
"figcaption",
|
|
87276
|
+
"footer",
|
|
87277
|
+
"label",
|
|
87278
|
+
"nav",
|
|
87279
|
+
"summary",
|
|
87280
|
+
"td",
|
|
87281
|
+
"th",
|
|
87282
|
+
"time"
|
|
87283
|
+
]);
|
|
87284
|
+
const FUNCTIONAL_ROLE_NAMES = new Set([
|
|
87285
|
+
"button",
|
|
87286
|
+
"cell",
|
|
87287
|
+
"checkbox",
|
|
87288
|
+
"columnheader",
|
|
87289
|
+
"gridcell",
|
|
87290
|
+
"link",
|
|
87291
|
+
"menuitem",
|
|
87292
|
+
"menuitemcheckbox",
|
|
87293
|
+
"menuitemradio",
|
|
87294
|
+
"navigation",
|
|
87295
|
+
"option",
|
|
87296
|
+
"radio",
|
|
87297
|
+
"rowheader",
|
|
87298
|
+
"switch",
|
|
87299
|
+
"tab",
|
|
87300
|
+
"treeitem"
|
|
87301
|
+
]);
|
|
87302
|
+
const FUNCTIONAL_CLASS_NAME_PATTERN = /^(?:badge|breadcrumb|caption|category|chip|eyebrow|kicker|label|meta|nav|pill|tag|timestamp)(?:$|-)/i;
|
|
87303
|
+
const PREFORMATTED_CLASS_NAME_PATTERN = /^(?:code|console|diff|editor|syntax|terminal)(?:$|-)/i;
|
|
87304
|
+
const PREFORMATTED_COMPONENT_NAME_PATTERN = /^(?:(?:Code|Console|Diff|Editor|Syntax|Terminal)(?:Block|Output|Pane|Renderer|View|Viewer)?|SyntaxHighlighter)$/;
|
|
87305
|
+
const VISUALLY_HIDDEN_CLASS_NAMES = new Set([
|
|
87306
|
+
"a11y-hidden",
|
|
87307
|
+
"hidden-visually",
|
|
87308
|
+
"offscreen",
|
|
87309
|
+
"screen-reader",
|
|
87310
|
+
"screen-reader-only",
|
|
87311
|
+
"screenreader",
|
|
87312
|
+
"sr-only",
|
|
87313
|
+
"visually-hidden",
|
|
87314
|
+
"visuallyhidden"
|
|
87315
|
+
]);
|
|
87316
|
+
const CASE_TOKENS$2 = new Set([
|
|
87317
|
+
"capitalize",
|
|
87318
|
+
"lowercase",
|
|
87319
|
+
"normal-case",
|
|
87320
|
+
"uppercase"
|
|
87321
|
+
]);
|
|
87322
|
+
const getAncestorOpeningElements = (jsxElement) => {
|
|
87323
|
+
const openingElements = [];
|
|
87324
|
+
let currentNode = jsxElement;
|
|
87325
|
+
while (currentNode) {
|
|
87326
|
+
if (isNodeOfType(currentNode, "JSXElement")) openingElements.push(currentNode.openingElement);
|
|
87327
|
+
currentNode = currentNode.parent;
|
|
87328
|
+
}
|
|
87329
|
+
return openingElements;
|
|
87330
|
+
};
|
|
87331
|
+
const hasUnresolvedVisibilityAttribute = (openingElement) => {
|
|
87332
|
+
for (const attributeName of ["hidden", "aria-hidden"]) {
|
|
87333
|
+
const attribute = getAuthoritativeJsxAttribute(openingElement.attributes, attributeName, false);
|
|
87334
|
+
if (attribute?.value && isNodeOfType(attribute.value, "JSXExpressionContainer") && !isNodeOfType(attribute.value.expression, "Literal") && (!isNodeOfType(attribute.value.expression, "TemplateLiteral") || attribute.value.expression.expressions.length > 0)) return true;
|
|
87335
|
+
}
|
|
87336
|
+
return false;
|
|
87337
|
+
};
|
|
87338
|
+
const hasUnresolvedClassName = (openingElement) => Boolean(getAuthoritativeJsxAttribute(openingElement.attributes, "className") && getStringFromClassNameAttr(openingElement) === null);
|
|
87339
|
+
const hasUnresolvedInlineVisibility = (openingElement) => {
|
|
87340
|
+
const styleAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "style");
|
|
87341
|
+
if (!styleAttribute) return false;
|
|
87342
|
+
const expression = getInlineStyleExpression(styleAttribute);
|
|
87343
|
+
if (!expression) return true;
|
|
87344
|
+
if (expression.properties.some((property) => getStylePropertyKey(property) === null)) return true;
|
|
87345
|
+
for (const propertyName of ["display", "visibility"]) {
|
|
87346
|
+
const property = getEffectiveStyleProperty(expression.properties, propertyName);
|
|
87347
|
+
if (property && getStylePropertyStringValue(property) === null) return true;
|
|
87348
|
+
}
|
|
87349
|
+
return false;
|
|
87350
|
+
};
|
|
87351
|
+
const hasUnresolvedRenderingState = (openingElement) => hasJsxSpreadAttribute(openingElement.attributes) || hasUnresolvedVisibilityAttribute(openingElement) || hasUnresolvedClassName(openingElement) || hasUnresolvedInlineVisibility(openingElement);
|
|
87352
|
+
const isStaticallyNonRendered = (openingElement, hasTailwind) => {
|
|
87353
|
+
const hiddenAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "hidden", false);
|
|
87354
|
+
if (hiddenAttribute && getStringLiteralAttributeValue(hiddenAttribute) !== null) return true;
|
|
87355
|
+
if (isHiddenFromScreenReader(openingElement, void 0)) return true;
|
|
87356
|
+
const styleAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "style");
|
|
87357
|
+
const expression = styleAttribute ? getInlineStyleExpression(styleAttribute) : null;
|
|
87358
|
+
if (expression) {
|
|
87359
|
+
const displayProperty = getEffectiveStyleProperty(expression.properties, "display");
|
|
87360
|
+
if (displayProperty && getStylePropertyStringValue(displayProperty)?.toLowerCase() === "none") return true;
|
|
87361
|
+
const visibilityProperty = getEffectiveStyleProperty(expression.properties, "visibility");
|
|
87362
|
+
const visibilityValue = visibilityProperty ? getStylePropertyStringValue(visibilityProperty)?.toLowerCase() : null;
|
|
87363
|
+
if (visibilityValue === "hidden" || visibilityValue === "collapse") return true;
|
|
87364
|
+
}
|
|
87365
|
+
if (!hasTailwind) return false;
|
|
87366
|
+
const classNameValue = getStringFromClassNameAttr(openingElement);
|
|
87367
|
+
if (!classNameValue) return false;
|
|
87368
|
+
const visibilityAtBreakpoints = getTailwindVisibilityAtBreakpoints(classNameValue);
|
|
87369
|
+
return Boolean(visibilityAtBreakpoints && visibilityAtBreakpoints.every((isVisible) => !isVisible));
|
|
87370
|
+
};
|
|
87371
|
+
const isVisuallyHidden = (openingElement) => {
|
|
87372
|
+
const classNameValue = getStringFromClassNameAttr(openingElement);
|
|
87373
|
+
if (!classNameValue) return false;
|
|
87374
|
+
const tokens = splitTailwindClassName(classNameValue).map(parseTailwindClassNameToken);
|
|
87375
|
+
const hasImportantScreenReaderOnly = tokens.some((token) => token.utility === "sr-only" && token.isImportant);
|
|
87376
|
+
const hasImportantVisibleOverride = tokens.some((token) => token.utility === "not-sr-only" && token.isImportant);
|
|
87377
|
+
if (hasImportantScreenReaderOnly && !hasImportantVisibleOverride) return true;
|
|
87378
|
+
if (tokens.some((token) => token.utility === "not-sr-only")) return false;
|
|
87379
|
+
return tokens.some((token) => VISUALLY_HIDDEN_CLASS_NAMES.has(token.utility.toLowerCase()));
|
|
87380
|
+
};
|
|
87381
|
+
const isPreformattedContext = (openingElement) => {
|
|
87382
|
+
const elementName = resolveJsxElementType(openingElement);
|
|
87383
|
+
if (PREFORMATTED_ELEMENT_NAMES$3.has(elementName.toLowerCase()) || PREFORMATTED_COMPONENT_NAME_PATTERN.test(elementName)) return true;
|
|
87384
|
+
const classNameValue = getStringFromClassNameAttr(openingElement);
|
|
87385
|
+
return Boolean(classNameValue && splitTailwindClassName(classNameValue).map(parseTailwindClassNameToken).some((token) => PREFORMATTED_CLASS_NAME_PATTERN.test(token.utility)));
|
|
87386
|
+
};
|
|
87387
|
+
const isFunctionalTextContext = (openingElements) => openingElements.some((openingElement) => {
|
|
87388
|
+
const elementName = resolveJsxElementType(openingElement).toLowerCase();
|
|
87389
|
+
if (FUNCTIONAL_ELEMENT_NAMES.has(elementName)) return true;
|
|
87390
|
+
const roleAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "role", false);
|
|
87391
|
+
const roleValue = roleAttribute ? getStringLiteralAttributeValue(roleAttribute) : null;
|
|
87392
|
+
if (roleValue && FUNCTIONAL_ROLE_NAMES.has(roleValue.toLowerCase())) return true;
|
|
87393
|
+
const classNameValue = getStringFromClassNameAttr(openingElement);
|
|
87394
|
+
return Boolean(classNameValue && splitTailwindClassName(classNameValue).map(parseTailwindClassNameToken).some((token) => FUNCTIONAL_CLASS_NAME_PATTERN.test(token.utility)));
|
|
87395
|
+
});
|
|
87396
|
+
const hasNonzeroInlineLetterSpacing = (expression) => {
|
|
87397
|
+
const property = getEffectiveStyleProperty(expression.properties, "letterSpacing");
|
|
87398
|
+
if (!property) return false;
|
|
87399
|
+
const numberValue = getStylePropertyNumberValue(property);
|
|
87400
|
+
if (numberValue !== null) return numberValue !== 0;
|
|
87401
|
+
const stringValue = getStylePropertyStringValue(property);
|
|
87402
|
+
if (!stringValue || stringValue === "normal") return false;
|
|
87403
|
+
const parsedValue = Number.parseFloat(stringValue);
|
|
87404
|
+
return Number.isFinite(parsedValue) && parsedValue !== 0;
|
|
87405
|
+
};
|
|
87406
|
+
const isUppercaseTrackedMicroLabel = (openingElement) => {
|
|
87407
|
+
const styleAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "style");
|
|
87408
|
+
const expression = styleAttribute ? getInlineStyleExpression(styleAttribute) : null;
|
|
87409
|
+
if (expression) {
|
|
87410
|
+
const textTransformProperty = getEffectiveStyleProperty(expression.properties, "textTransform");
|
|
87411
|
+
if (textTransformProperty && getStylePropertyStringValue(textTransformProperty) === "uppercase" && hasNonzeroInlineLetterSpacing(expression)) return true;
|
|
87412
|
+
}
|
|
87413
|
+
const classNameValue = getStringFromClassNameAttr(openingElement);
|
|
87414
|
+
if (!classNameValue) return false;
|
|
87415
|
+
const tokens = getUnvariantClassNameTokensWithImportantModifiers(classNameValue);
|
|
87416
|
+
return getEffectiveTailwindClassNameToken(tokens, (utility) => CASE_TOKENS$2.has(utility)) === "uppercase" && Boolean(getEffectiveNonzeroTailwindTracking(tokens));
|
|
87417
|
+
};
|
|
84808
87418
|
const noTinyText = defineRule({
|
|
84809
87419
|
id: "no-tiny-text",
|
|
84810
87420
|
title: "Text is too small",
|
|
@@ -84814,55 +87424,23 @@ const noTinyText = defineRule({
|
|
|
84814
87424
|
recommendation: "Use at least 12px for body text, and 16px is best. Small text is hard to read, especially on phones.",
|
|
84815
87425
|
create: (context) => {
|
|
84816
87426
|
const reportedPxValues = /* @__PURE__ */ new Set();
|
|
84817
|
-
|
|
84818
|
-
|
|
84819
|
-
|
|
84820
|
-
|
|
84821
|
-
|
|
84822
|
-
|
|
84823
|
-
|
|
84824
|
-
|
|
84825
|
-
|
|
84826
|
-
|
|
84827
|
-
|
|
84828
|
-
|
|
84829
|
-
|
|
84830
|
-
if (remMatch) pxValue = parseFloat(remMatch[1]) * 16;
|
|
84831
|
-
}
|
|
84832
|
-
if (pxValue === null || pxValue <= 0 || pxValue >= 12) continue;
|
|
84833
|
-
if (reportedPxValues.has(pxValue)) continue;
|
|
84834
|
-
if (isUppercaseMicroLabel(expression)) continue;
|
|
84835
|
-
if (hasGlyphOnlyContent(node)) continue;
|
|
84836
|
-
if (hasOnlyIconIdentifierChildren(node)) continue;
|
|
84837
|
-
if (isChildlessIconComponent(node)) continue;
|
|
84838
|
-
reportedPxValues.add(pxValue);
|
|
84839
|
-
context.report({
|
|
84840
|
-
node: property,
|
|
84841
|
-
message: `Your users strain to read ${pxValue}px text, so use at least 12px for body text, & 16px is best.`
|
|
84842
|
-
});
|
|
84843
|
-
}
|
|
87427
|
+
const hasTailwind = hasCapabilityOrUnspecified(context.settings, "tailwind");
|
|
87428
|
+
return { JSXElement(node) {
|
|
87429
|
+
const openingElement = node.openingElement;
|
|
87430
|
+
const openingElements = getAncestorOpeningElements(node);
|
|
87431
|
+
if (openingElements.some(hasUnresolvedRenderingState)) return;
|
|
87432
|
+
if (openingElements.some((ancestorOpeningElement) => isStaticallyNonRendered(ancestorOpeningElement, hasTailwind) || isVisuallyHidden(ancestorOpeningElement) || isPreformattedContext(ancestorOpeningElement))) return;
|
|
87433
|
+
const pxValue = getStaticEffectiveFontSize(openingElement, hasTailwind);
|
|
87434
|
+
if (pxValue === null || pxValue <= 0 || pxValue >= 12 || reportedPxValues.has(pxValue) || hasGlyphOnlyContent(node) || hasOnlyIconIdentifierChildren(node) || isChildlessIconComponent(openingElement) || isUppercaseTrackedMicroLabel(openingElement) && !isFunctionalTextContext(openingElements)) return;
|
|
87435
|
+
reportedPxValues.add(pxValue);
|
|
87436
|
+
context.report({
|
|
87437
|
+
node: openingElement,
|
|
87438
|
+
message: `Your users strain to read ${pxValue}px text, so use at least 12px for readable interface text, & 16px is best.`
|
|
87439
|
+
});
|
|
84844
87440
|
} };
|
|
84845
87441
|
}
|
|
84846
87442
|
});
|
|
84847
87443
|
//#endregion
|
|
84848
|
-
//#region src/plugin/rules/design/utils/get-effective-nonzero-tailwind-tracking.ts
|
|
84849
|
-
const TRACKING_UTILITY_PATTERN = /^-?tracking-/;
|
|
84850
|
-
const STATIC_TRACKING_UTILITIES = new Set([
|
|
84851
|
-
"tracking-tight",
|
|
84852
|
-
"tracking-tighter",
|
|
84853
|
-
"tracking-wide",
|
|
84854
|
-
"tracking-wider",
|
|
84855
|
-
"tracking-widest"
|
|
84856
|
-
]);
|
|
84857
|
-
const NONZERO_ARBITRARY_TRACKING_PATTERN = /^-?tracking-\[(?:length:)?(-?(?:\d+(?:\.\d*)?|\.\d+))(?:cap|ch|cm|dvh|dvw|em|ex|ic|in|lh|lvh|lvw|mm|pc|pt|px|q|rcap|rch|rem|rex|ric|rlh|svh|svw|vb|vh|vi|vmax|vmin|vw)\]$/i;
|
|
84858
|
-
const getEffectiveNonzeroTailwindTracking = (tokens) => {
|
|
84859
|
-
const effectiveTracking = getEffectiveTailwindClassNameToken(tokens, (utility) => TRACKING_UTILITY_PATTERN.test(utility));
|
|
84860
|
-
if (!effectiveTracking) return null;
|
|
84861
|
-
if (STATIC_TRACKING_UTILITIES.has(effectiveTracking)) return effectiveTracking;
|
|
84862
|
-
const arbitraryTracking = effectiveTracking.match(NONZERO_ARBITRARY_TRACKING_PATTERN);
|
|
84863
|
-
return arbitraryTracking && Number.parseFloat(arbitraryTracking[1]) !== 0 ? effectiveTracking : null;
|
|
84864
|
-
};
|
|
84865
|
-
//#endregion
|
|
84866
87444
|
//#region src/plugin/rules/design/utils/is-technical-label-text.ts
|
|
84867
87445
|
const UPPERCASE_TECHNICAL_TOKEN_PATTERN = /^[A-Z0-9][A-Z0-9_.:/-]*$/;
|
|
84868
87446
|
const TECHNICAL_TOKEN_PATTERN = /^[A-Za-z0-9]+(?:[-_./:][A-Za-z0-9]+)+$/;
|
|
@@ -85641,12 +88219,6 @@ const isIconOnlyButton = (node) => {
|
|
|
85641
88219
|
}
|
|
85642
88220
|
return iconCount === 1;
|
|
85643
88221
|
};
|
|
85644
|
-
const parseTailwindLength = (token, prefix) => {
|
|
85645
|
-
const arbitraryMatch = token.match(new RegExp(`^${prefix}-\\[([\\d.]+)px\\]$`));
|
|
85646
|
-
if (arbitraryMatch) return Number.parseFloat(arbitraryMatch[1]);
|
|
85647
|
-
const scaleMatch = token.match(new RegExp(`^${prefix}-([\\d.]+)$`));
|
|
85648
|
-
return scaleMatch ? Number.parseFloat(scaleMatch[1]) * 4 : null;
|
|
85649
|
-
};
|
|
85650
88222
|
const WIDTH_UTILITY_PATTERN = /^(?:size|w)-/;
|
|
85651
88223
|
const HEIGHT_UTILITY_PATTERN = /^(?:h|size)-/;
|
|
85652
88224
|
const HORIZONTAL_PADDING_UTILITY_PATTERN = /^p(?:[xlr])?-/;
|
|
@@ -85673,8 +88245,8 @@ const getTailwindTargetSize = (node) => {
|
|
|
85673
88245
|
})) return null;
|
|
85674
88246
|
const effectiveWidth = getEffectiveTailwindClassNameToken(tokens, (utility) => WIDTH_UTILITY_PATTERN.test(utility));
|
|
85675
88247
|
const effectiveHeight = getEffectiveTailwindClassNameToken(tokens, (utility) => HEIGHT_UTILITY_PATTERN.test(utility));
|
|
85676
|
-
const width = effectiveWidth ?
|
|
85677
|
-
const height = effectiveHeight ?
|
|
88248
|
+
const width = effectiveWidth ? parseStaticTailwindLengthPx(effectiveWidth, "size") ?? parseStaticTailwindLengthPx(effectiveWidth, "w") : null;
|
|
88249
|
+
const height = effectiveHeight ? parseStaticTailwindLengthPx(effectiveHeight, "size") ?? parseStaticTailwindLengthPx(effectiveHeight, "h") : null;
|
|
85678
88250
|
return width !== null && height !== null ? [width, height] : null;
|
|
85679
88251
|
};
|
|
85680
88252
|
const getInlineTargetSize = (node) => {
|
|
@@ -127742,6 +130314,18 @@ const reactDoctorRules = [
|
|
|
127742
130314
|
requires: [...new Set(["react", ...noAsyncEventHandlerWithoutReentryGuard.requires ?? []])]
|
|
127743
130315
|
}
|
|
127744
130316
|
},
|
|
130317
|
+
{
|
|
130318
|
+
key: "react-doctor/no-auto-scrolling-content",
|
|
130319
|
+
id: "no-auto-scrolling-content",
|
|
130320
|
+
source: "react-doctor",
|
|
130321
|
+
originallyExternal: false,
|
|
130322
|
+
rule: {
|
|
130323
|
+
...noAutoScrollingContent,
|
|
130324
|
+
framework: "global",
|
|
130325
|
+
category: "Maintainability",
|
|
130326
|
+
tags: [...new Set(["design", ...noAutoScrollingContent.tags ?? []])]
|
|
130327
|
+
}
|
|
130328
|
+
},
|
|
127745
130329
|
{
|
|
127746
130330
|
key: "react-doctor/no-autofocus",
|
|
127747
130331
|
id: "no-autofocus",
|
|
@@ -128100,6 +130684,18 @@ const reactDoctorRules = [
|
|
|
128100
130684
|
tags: [...new Set(["design", ...noDecorativePulse.tags ?? []])]
|
|
128101
130685
|
}
|
|
128102
130686
|
},
|
|
130687
|
+
{
|
|
130688
|
+
key: "react-doctor/no-decorative-radial-spotlight",
|
|
130689
|
+
id: "no-decorative-radial-spotlight",
|
|
130690
|
+
source: "react-doctor",
|
|
130691
|
+
originallyExternal: false,
|
|
130692
|
+
rule: {
|
|
130693
|
+
...noDecorativeRadialSpotlight,
|
|
130694
|
+
framework: "global",
|
|
130695
|
+
category: "Maintainability",
|
|
130696
|
+
tags: [...new Set(["design", ...noDecorativeRadialSpotlight.tags ?? []])]
|
|
130697
|
+
}
|
|
130698
|
+
},
|
|
128103
130699
|
{
|
|
128104
130700
|
key: "react-doctor/no-default-props",
|
|
128105
130701
|
id: "no-default-props",
|
|
@@ -129828,6 +132424,18 @@ const reactDoctorRules = [
|
|
|
129828
132424
|
category: "Maintainability"
|
|
129829
132425
|
}
|
|
129830
132426
|
},
|
|
132427
|
+
{
|
|
132428
|
+
key: "react-doctor/no-pulsing-status-dot",
|
|
132429
|
+
id: "no-pulsing-status-dot",
|
|
132430
|
+
source: "react-doctor",
|
|
132431
|
+
originallyExternal: false,
|
|
132432
|
+
rule: {
|
|
132433
|
+
...noPulsingStatusDot,
|
|
132434
|
+
framework: "global",
|
|
132435
|
+
category: "Maintainability",
|
|
132436
|
+
tags: [...new Set(["design", ...noPulsingStatusDot.tags ?? []])]
|
|
132437
|
+
}
|
|
132438
|
+
},
|
|
129831
132439
|
{
|
|
129832
132440
|
key: "react-doctor/no-pure-black-background",
|
|
129833
132441
|
id: "no-pure-black-background",
|
|
@@ -129852,6 +132460,18 @@ const reactDoctorRules = [
|
|
|
129852
132460
|
tags: [...new Set(["design", ...noPureBlackShadow.tags ?? []])]
|
|
129853
132461
|
}
|
|
129854
132462
|
},
|
|
132463
|
+
{
|
|
132464
|
+
key: "react-doctor/no-radial-halo",
|
|
132465
|
+
id: "no-radial-halo",
|
|
132466
|
+
source: "react-doctor",
|
|
132467
|
+
originallyExternal: false,
|
|
132468
|
+
rule: {
|
|
132469
|
+
...noRadialHalo,
|
|
132470
|
+
framework: "global",
|
|
132471
|
+
category: "Maintainability",
|
|
132472
|
+
tags: [...new Set(["design", ...noRadialHalo.tags ?? []])]
|
|
132473
|
+
}
|
|
132474
|
+
},
|
|
129855
132475
|
{
|
|
129856
132476
|
key: "react-doctor/no-random-key",
|
|
129857
132477
|
id: "no-random-key",
|
|
@@ -130015,6 +132635,18 @@ const reactDoctorRules = [
|
|
|
130015
132635
|
requires: [...new Set(["react", ...noRenderReturnValue.requires ?? []])]
|
|
130016
132636
|
}
|
|
130017
132637
|
},
|
|
132638
|
+
{
|
|
132639
|
+
key: "react-doctor/no-repeated-container-text",
|
|
132640
|
+
id: "no-repeated-container-text",
|
|
132641
|
+
source: "react-doctor",
|
|
132642
|
+
originallyExternal: false,
|
|
132643
|
+
rule: {
|
|
132644
|
+
...noRepeatedContainerText,
|
|
132645
|
+
framework: "global",
|
|
132646
|
+
category: "Maintainability",
|
|
132647
|
+
tags: [...new Set(["design", ...noRepeatedContainerText.tags ?? []])]
|
|
132648
|
+
}
|
|
132649
|
+
},
|
|
130018
132650
|
{
|
|
130019
132651
|
key: "react-doctor/no-repeated-emoji-tiles",
|
|
130020
132652
|
id: "no-repeated-emoji-tiles",
|
|
@@ -130206,6 +132838,18 @@ const reactDoctorRules = [
|
|
|
130206
132838
|
requires: [...new Set(["react", ...noSetStateInRender.requires ?? []])]
|
|
130207
132839
|
}
|
|
130208
132840
|
},
|
|
132841
|
+
{
|
|
132842
|
+
key: "react-doctor/no-shape-assembled-illustration",
|
|
132843
|
+
id: "no-shape-assembled-illustration",
|
|
132844
|
+
source: "react-doctor",
|
|
132845
|
+
originallyExternal: false,
|
|
132846
|
+
rule: {
|
|
132847
|
+
...noShapeAssembledIllustration,
|
|
132848
|
+
framework: "global",
|
|
132849
|
+
category: "Maintainability",
|
|
132850
|
+
tags: [...new Set(["design", ...noShapeAssembledIllustration.tags ?? []])]
|
|
132851
|
+
}
|
|
132852
|
+
},
|
|
130209
132853
|
{
|
|
130210
132854
|
key: "react-doctor/no-side-effect-in-state-updater-function",
|
|
130211
132855
|
id: "no-side-effect-in-state-updater-function",
|