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.
Files changed (3) hide show
  1. package/dist/index.d.ts +288 -0
  2. package/dist/index.js +3240 -596
  3. 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
- "supercharge your workflow",
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 LONG_FORM_CONTENT_PATH_PATTERN = /(?:^|[/\\])(?:articles?|blog|changelog|content|docs?|posts?)(?:[/\\]|$)/i;
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) => ({ JSXText(jsxTextNode) {
15689
- if (context.filename && LONG_FORM_CONTENT_PATH_PATTERN.test(context.filename)) return;
15690
- const textValue = typeof jsxTextNode.value === "string" ? jsxTextNode.value : "";
15691
- if (!textValue.includes(EM_DASH)) return;
15692
- if (!PROSE_EM_DASH_PATTERN.test(textValue)) return;
15693
- if (isInsideExcludedTypographyAncestor(jsxTextNode)) return;
15694
- context.report({
15695
- node: jsxTextNode,
15696
- message: "Em dash (—) in UI text reads like AI output to your users."
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 isDecorativeGridValue = (value) => {
53429
- return [...value.replace(/repeating-linear-gradient/gi, "").matchAll(/linear-gradient\(/gi)].length >= 2 && /(?:^|[^\d.])1px(?:[^\d.]|$)/i.test(value) && /transparent/i.test(value);
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
- const classNameValue = getStringFromClassNameAttr(node);
53441
- if (classNameValue && isDecorativeGridValue(classNameValue)) {
53442
- context.report({
53443
- node,
53444
- message: "This layered one-pixel grid is decorative rather than functional. Simplify the surface or tie the grid to spatial content."
53445
- });
53446
- return;
53447
- }
53448
- for (const attribute of node.attributes ?? []) {
53449
- if (!isNodeOfType(attribute, "JSXAttribute")) continue;
53450
- const styleExpression = getInlineStyleExpression(attribute);
53451
- if (!styleExpression) continue;
53452
- for (const propertyName of ["background", "backgroundImage"]) {
53453
- const property = getEffectiveStyleProperty(styleExpression.properties, propertyName);
53454
- if (!property) continue;
53455
- const propertyValue = getStylePropertyStringValue(property);
53456
- if (!propertyValue || !isDecorativeGridValue(propertyValue)) continue;
53457
- context.report({
53458
- node: property,
53459
- message: "This background draws a decorative coordinate grid. Use it only when the grid conveys spatial information."
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. Use hierarchy and static contrast for announcements and feature labels.",
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 || !getUnvariantClassNameTokens(classNameValue).includes("animate-pulse")) return;
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$1 = /^px-(?:px|[\d.]+|\[[^\]]+\])$/;
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$1.test(token)) && hasVisibleTailwindFillOrEdge(tokens);
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 pageText = getStaticJsxText(node).replace(/\s+/g, " ").toLowerCase();
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 = new Set(getUnvariantClassNameTokens(classNameValue));
62355
- const isTrackedLabel = labelTokens.has("uppercase") && [...labelTokens].some((token) => token.startsWith("tracking-"));
62356
- const isPillLabel = labelTokens.has("rounded-full") && hasVisibleTailwindFillOrEdge([...labelTokens]) && [...labelTokens].some(hasPositivePillPadding);
62357
- if (!isTrackedLabel && !isPillLabel) return;
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
- if (!headingClassName || !getUnvariantClassNameTokens(headingClassName).some((token) => HERO_HEADING_SIZE_CLASSES.has(token))) return;
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 (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
63188
- const classNameValue = getStringFromClassNameAttr(node);
63189
- if (!classNameValue) return;
63190
- const hoverTransform = getHoverImageTransform(classNameValue);
63191
- if (!hoverTransform) return;
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 ${hoverTransform} treatment makes the image itself shift under the pointer. Use a steadier hover affordance.`
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 backgroundImagePreventsEvaluation = false;
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
- const backgroundImageValue = getStylePropertyStringValue(property);
67379
- backgroundImagePreventsEvaluation = backgroundImageValue === null || backgroundImageValue.trim().toLowerCase() !== "none";
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 || backgroundImagePreventsEvaluation) return;
68762
+ if (backgroundColorIsUnknown || backgroundShorthandIsUnknown || backgroundImageIsUnknown) return;
67401
68763
  if (backgroundColorRaw !== null && backgroundShorthandRaw !== null) return;
67402
- const backgroundRaw = backgroundColorRaw ?? backgroundShorthandRaw;
67403
- const background = backgroundRaw === null ? null : resolveOpaqueColor(backgroundRaw);
67404
- if (!foreground || !background) return;
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 countMatches = (text, pattern) => [...text.matchAll(pattern)].length;
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 pageText = getStaticJsxText(node).replace(/\s+/g, " ").trim();
67428
- const patternCount = countMatches(pageText, NOT_THEN_ASSERTION_PATTERN) + countMatches(pageText, NO_JUST_PATTERN);
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 NUMBERED_MARKER_PATTERN = /^(0[1-9]|1[0-2])$/;
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 getSectionMarker = (node) => {
73657
- const match = getStaticJsxText(node).trim().match(NUMBERED_MARKER_PATTERN);
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 || !isNodeOfType(sibling.openingElement.name, "JSXIdentifier") || !HEADING_ELEMENT_PATTERN$3.test(sibling.openingElement.name.name)) return null;
73661
- return parseInt(match[1], 10);
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: "Sequential numbers are used as section decoration",
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 markers = /* @__PURE__ */ new Map();
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 === null) return;
73676
- markers.set(marker, node.openingElement);
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 sortedMarkers = [...markers.keys()].sort((left, right) => left - right);
73680
- let runStartIndex = 0;
73681
- for (let markerIndex = 1; markerIndex <= sortedMarkers.length; markerIndex += 1) {
73682
- if (markerIndex < sortedMarkers.length && sortedMarkers[markerIndex] === sortedMarkers[markerIndex - 1] + 1) continue;
73683
- if (markerIndex - runStartIndex >= 3) {
73684
- const firstNode = markers.get(sortedMarkers[runStartIndex]);
73685
- if (firstNode) context.report({
73686
- node: firstNode,
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 border",
85069
+ title: "Thick one-sided stripe",
82604
85070
  tags: ["design", "test-noise"],
82605
85071
  severity: "warn",
82606
85072
  defaultEnabled: false,
82607
- recommendation: "Use a softer accent like an inset box-shadow, a background, or a thin border-bottom instead of a thick one-sided border.",
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")) return typeof node.value === "string" ? node.value : "";
84755
- if (isNodeOfType(node, "TemplateLiteral")) return (node.quasis ?? []).map((quasi) => quasi.value?.raw ?? "").join("");
84756
- if (isNodeOfType(node, "ConditionalExpression")) return collectStaticExpressionText(node.consequent) + collectStaticExpressionText(node.alternate);
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 = (styleAttribute) => {
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 = (styleAttribute) => {
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 = (styleAttribute) => {
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")) staticText += collectStaticExpressionText(child.expression);
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 isUppercaseMicroLabel = (expression) => (expression.properties ?? []).some((property) => getStylePropertyKey(property) === "textTransform" && getStylePropertyStringValue(property) === "uppercase");
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
- return { JSXAttribute(node) {
84818
- const expression = getInlineStyleExpression(node);
84819
- if (!expression) return;
84820
- for (const property of expression.properties ?? []) {
84821
- if (getStylePropertyKey(property) !== "fontSize") continue;
84822
- let pxValue = null;
84823
- const numValue = getStylePropertyNumberValue(property);
84824
- const strValue = getStylePropertyStringValue(property);
84825
- if (numValue !== null) pxValue = numValue;
84826
- else if (strValue !== null) {
84827
- const pxMatch = strValue.match(/^([\d.]+)px$/);
84828
- if (pxMatch) pxValue = parseFloat(pxMatch[1]);
84829
- const remMatch = strValue.match(/^([\d.]+)rem$/);
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 ? parseTailwindLength(effectiveWidth, "size") ?? parseTailwindLength(effectiveWidth, "w") : null;
85677
- const height = effectiveHeight ? parseTailwindLength(effectiveHeight, "size") ?? parseTailwindLength(effectiveHeight, "h") : null;
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",