@empline/preflight 1.1.11 → 1.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/checks/consolidated/auth-storage-state.d.ts +3 -0
- package/dist/checks/consolidated/auth-storage-state.d.ts.map +1 -0
- package/dist/checks/consolidated/auth-storage-state.js +146 -0
- package/dist/checks/consolidated/auth-storage-state.js.map +1 -0
- package/dist/checks/consolidated/business.d.ts +50 -0
- package/dist/checks/consolidated/business.d.ts.map +1 -0
- package/dist/checks/consolidated/business.js +252 -0
- package/dist/checks/consolidated/business.js.map +1 -0
- package/dist/checks/consolidated/caching-strategy.d.ts +104 -0
- package/dist/checks/consolidated/caching-strategy.d.ts.map +1 -0
- package/dist/checks/consolidated/caching-strategy.js +725 -0
- package/dist/checks/consolidated/caching-strategy.js.map +1 -0
- package/dist/checks/consolidated/code-quality.d.ts +83 -0
- package/dist/checks/consolidated/code-quality.d.ts.map +1 -0
- package/dist/checks/consolidated/code-quality.js +445 -0
- package/dist/checks/consolidated/code-quality.js.map +1 -0
- package/dist/checks/consolidated/console-statements.d.ts +32 -0
- package/dist/checks/consolidated/console-statements.d.ts.map +1 -0
- package/dist/checks/consolidated/console-statements.js +304 -0
- package/dist/checks/consolidated/console-statements.js.map +1 -0
- package/dist/checks/consolidated/css-advanced-validation.d.ts +24 -0
- package/dist/checks/consolidated/css-advanced-validation.d.ts.map +1 -0
- package/dist/checks/consolidated/css-advanced-validation.js +415 -0
- package/dist/checks/consolidated/css-advanced-validation.js.map +1 -0
- package/dist/checks/consolidated/css-organization.d.ts +14 -0
- package/dist/checks/consolidated/css-organization.d.ts.map +1 -0
- package/dist/checks/consolidated/css-organization.js +432 -0
- package/dist/checks/consolidated/css-organization.js.map +1 -0
- package/dist/checks/consolidated/css-runtime-validation.d.ts +22 -0
- package/dist/checks/consolidated/css-runtime-validation.d.ts.map +1 -0
- package/dist/checks/consolidated/css-runtime-validation.js +330 -0
- package/dist/checks/consolidated/css-runtime-validation.js.map +1 -0
- package/dist/checks/consolidated/css-variable-validation.d.ts +17 -0
- package/dist/checks/consolidated/css-variable-validation.d.ts.map +1 -0
- package/dist/checks/consolidated/css-variable-validation.js +412 -0
- package/dist/checks/consolidated/css-variable-validation.js.map +1 -0
- package/dist/checks/consolidated/dark-mode-consistency.d.ts +23 -0
- package/dist/checks/consolidated/dark-mode-consistency.d.ts.map +1 -0
- package/dist/checks/consolidated/dark-mode-consistency.js +291 -0
- package/dist/checks/consolidated/dark-mode-consistency.js.map +1 -0
- package/dist/checks/consolidated/database.d.ts +95 -0
- package/dist/checks/consolidated/database.d.ts.map +1 -0
- package/dist/checks/consolidated/database.js +427 -0
- package/dist/checks/consolidated/database.js.map +1 -0
- package/dist/checks/consolidated/e2e-checks.d.ts +52 -0
- package/dist/checks/consolidated/e2e-checks.d.ts.map +1 -0
- package/dist/checks/consolidated/e2e-checks.js +157 -0
- package/dist/checks/consolidated/e2e-checks.js.map +1 -0
- package/dist/checks/consolidated/e2e-regression-coverage.d.ts +14 -0
- package/dist/checks/consolidated/e2e-regression-coverage.d.ts.map +1 -0
- package/dist/checks/consolidated/e2e-regression-coverage.js +151 -0
- package/dist/checks/consolidated/e2e-regression-coverage.js.map +1 -0
- package/dist/checks/consolidated/e2e-validation.d.ts +137 -0
- package/dist/checks/consolidated/e2e-validation.d.ts.map +1 -0
- package/dist/checks/consolidated/e2e-validation.js +1001 -0
- package/dist/checks/consolidated/e2e-validation.js.map +1 -0
- package/dist/checks/consolidated/enterprise-baseline.d.ts +9 -0
- package/dist/checks/consolidated/enterprise-baseline.d.ts.map +1 -0
- package/dist/checks/consolidated/enterprise-baseline.js +277 -0
- package/dist/checks/consolidated/enterprise-baseline.js.map +1 -0
- package/dist/checks/consolidated/generate-pageload-config.d.ts +6 -0
- package/dist/checks/consolidated/generate-pageload-config.d.ts.map +1 -0
- package/dist/checks/consolidated/generate-pageload-config.js +161 -0
- package/dist/checks/consolidated/generate-pageload-config.js.map +1 -0
- package/dist/checks/consolidated/hardened-checks.d.ts +276 -0
- package/dist/checks/consolidated/hardened-checks.d.ts.map +1 -0
- package/dist/checks/consolidated/hardened-checks.js +3056 -0
- package/dist/checks/consolidated/hardened-checks.js.map +1 -0
- package/dist/checks/consolidated/homepage-ux.d.ts +12 -0
- package/dist/checks/consolidated/homepage-ux.d.ts.map +1 -0
- package/dist/checks/consolidated/homepage-ux.js +242 -0
- package/dist/checks/consolidated/homepage-ux.js.map +1 -0
- package/dist/checks/consolidated/images.d.ts +76 -0
- package/dist/checks/consolidated/images.d.ts.map +1 -0
- package/dist/checks/consolidated/images.js +311 -0
- package/dist/checks/consolidated/images.js.map +1 -0
- package/dist/checks/consolidated/import-cycles.d.ts +63 -0
- package/dist/checks/consolidated/import-cycles.d.ts.map +1 -0
- package/dist/checks/consolidated/import-cycles.js +291 -0
- package/dist/checks/consolidated/import-cycles.js.map +1 -0
- package/dist/checks/consolidated/imports.d.ts +112 -0
- package/dist/checks/consolidated/imports.d.ts.map +1 -0
- package/dist/checks/consolidated/imports.js +977 -0
- package/dist/checks/consolidated/imports.js.map +1 -0
- package/dist/checks/consolidated/inline-style-conflicts.d.ts +21 -0
- package/dist/checks/consolidated/inline-style-conflicts.d.ts.map +1 -0
- package/dist/checks/consolidated/inline-style-conflicts.js +300 -0
- package/dist/checks/consolidated/inline-style-conflicts.js.map +1 -0
- package/dist/checks/consolidated/lib-organization.d.ts +12 -0
- package/dist/checks/consolidated/lib-organization.d.ts.map +1 -0
- package/dist/checks/consolidated/lib-organization.js +419 -0
- package/dist/checks/consolidated/lib-organization.js.map +1 -0
- package/dist/checks/consolidated/n-plus-one.d.ts +63 -0
- package/dist/checks/consolidated/n-plus-one.d.ts.map +1 -0
- package/dist/checks/consolidated/n-plus-one.js +331 -0
- package/dist/checks/consolidated/n-plus-one.js.map +1 -0
- package/dist/checks/consolidated/nextjs.d.ts +51 -0
- package/dist/checks/consolidated/nextjs.d.ts.map +1 -0
- package/dist/checks/consolidated/nextjs.js +205 -0
- package/dist/checks/consolidated/nextjs.js.map +1 -0
- package/dist/checks/consolidated/organization.d.ts +54 -0
- package/dist/checks/consolidated/organization.d.ts.map +1 -0
- package/dist/checks/consolidated/organization.js +158 -0
- package/dist/checks/consolidated/organization.js.map +1 -0
- package/dist/checks/consolidated/pageload.d.ts +12 -0
- package/dist/checks/consolidated/pageload.d.ts.map +1 -0
- package/dist/checks/consolidated/pageload.js +138 -0
- package/dist/checks/consolidated/pageload.js.map +1 -0
- package/dist/checks/consolidated/performance.d.ts +112 -0
- package/dist/checks/consolidated/performance.d.ts.map +1 -0
- package/dist/checks/consolidated/performance.js +1546 -0
- package/dist/checks/consolidated/performance.js.map +1 -0
- package/dist/checks/consolidated/quality.d.ts +52 -0
- package/dist/checks/consolidated/quality.d.ts.map +1 -0
- package/dist/checks/consolidated/quality.js +253 -0
- package/dist/checks/consolidated/quality.js.map +1 -0
- package/dist/checks/consolidated/react.d.ts +48 -0
- package/dist/checks/consolidated/react.d.ts.map +1 -0
- package/dist/checks/consolidated/react.js +203 -0
- package/dist/checks/consolidated/react.js.map +1 -0
- package/dist/checks/consolidated/regression-hygiene.d.ts +17 -0
- package/dist/checks/consolidated/regression-hygiene.d.ts.map +1 -0
- package/dist/checks/consolidated/regression-hygiene.js +242 -0
- package/dist/checks/consolidated/regression-hygiene.js.map +1 -0
- package/dist/checks/consolidated/regression.d.ts +20 -0
- package/dist/checks/consolidated/regression.d.ts.map +1 -0
- package/dist/checks/consolidated/regression.js +121 -0
- package/dist/checks/consolidated/regression.js.map +1 -0
- package/dist/checks/consolidated/runtime.d.ts +53 -0
- package/dist/checks/consolidated/runtime.d.ts.map +1 -0
- package/dist/checks/consolidated/runtime.js +160 -0
- package/dist/checks/consolidated/runtime.js.map +1 -0
- package/dist/checks/consolidated/script-performance.d.ts +17 -0
- package/dist/checks/consolidated/script-performance.d.ts.map +1 -0
- package/dist/checks/consolidated/script-performance.js +137 -0
- package/dist/checks/consolidated/script-performance.js.map +1 -0
- package/dist/checks/consolidated/security.d.ts +78 -0
- package/dist/checks/consolidated/security.d.ts.map +1 -0
- package/dist/checks/consolidated/security.js +404 -0
- package/dist/checks/consolidated/security.js.map +1 -0
- package/dist/checks/consolidated/seo.d.ts +31 -0
- package/dist/checks/consolidated/seo.d.ts.map +1 -0
- package/dist/checks/consolidated/seo.js +1438 -0
- package/dist/checks/consolidated/seo.js.map +1 -0
- package/dist/checks/consolidated/sx-prop-deprecation.d.ts +22 -0
- package/dist/checks/consolidated/sx-prop-deprecation.d.ts.map +1 -0
- package/dist/checks/consolidated/sx-prop-deprecation.js +280 -0
- package/dist/checks/consolidated/sx-prop-deprecation.js.map +1 -0
- package/dist/checks/consolidated/tailwind-class-validation.d.ts +25 -0
- package/dist/checks/consolidated/tailwind-class-validation.d.ts.map +1 -0
- package/dist/checks/consolidated/tailwind-class-validation.js +533 -0
- package/dist/checks/consolidated/tailwind-class-validation.js.map +1 -0
- package/dist/checks/consolidated/testing.d.ts +54 -0
- package/dist/checks/consolidated/testing.d.ts.map +1 -0
- package/dist/checks/consolidated/testing.js +163 -0
- package/dist/checks/consolidated/testing.js.map +1 -0
- package/dist/checks/consolidated/typescript.d.ts +3 -0
- package/dist/checks/consolidated/typescript.d.ts.map +1 -0
- package/dist/checks/consolidated/typescript.js +31 -0
- package/dist/checks/consolidated/typescript.js.map +1 -0
- package/dist/checks/consolidated/ui-accessibility-advanced.d.ts +104 -0
- package/dist/checks/consolidated/ui-accessibility-advanced.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-accessibility-advanced.js +689 -0
- package/dist/checks/consolidated/ui-accessibility-advanced.js.map +1 -0
- package/dist/checks/consolidated/ui-accessibility.d.ts +121 -0
- package/dist/checks/consolidated/ui-accessibility.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-accessibility.js +776 -0
- package/dist/checks/consolidated/ui-accessibility.js.map +1 -0
- package/dist/checks/consolidated/ui-advanced-spacing.d.ts +142 -0
- package/dist/checks/consolidated/ui-advanced-spacing.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-advanced-spacing.js +1220 -0
- package/dist/checks/consolidated/ui-advanced-spacing.js.map +1 -0
- package/dist/checks/consolidated/ui-animation-duration.d.ts +108 -0
- package/dist/checks/consolidated/ui-animation-duration.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-animation-duration.js +531 -0
- package/dist/checks/consolidated/ui-animation-duration.js.map +1 -0
- package/dist/checks/consolidated/ui-border-radius.d.ts +90 -0
- package/dist/checks/consolidated/ui-border-radius.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-border-radius.js +519 -0
- package/dist/checks/consolidated/ui-border-radius.js.map +1 -0
- package/dist/checks/consolidated/ui-buttons.d.ts +32 -0
- package/dist/checks/consolidated/ui-buttons.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-buttons.js +481 -0
- package/dist/checks/consolidated/ui-buttons.js.map +1 -0
- package/dist/checks/consolidated/ui-cards.d.ts +29 -0
- package/dist/checks/consolidated/ui-cards.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-cards.js +504 -0
- package/dist/checks/consolidated/ui-cards.js.map +1 -0
- package/dist/checks/consolidated/ui-checks.d.ts +48 -0
- package/dist/checks/consolidated/ui-checks.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-checks.js +264 -0
- package/dist/checks/consolidated/ui-checks.js.map +1 -0
- package/dist/checks/consolidated/ui-cleanup.d.ts +81 -0
- package/dist/checks/consolidated/ui-cleanup.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-cleanup.js +650 -0
- package/dist/checks/consolidated/ui-cleanup.js.map +1 -0
- package/dist/checks/consolidated/ui-components.d.ts +255 -0
- package/dist/checks/consolidated/ui-components.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-components.js +2008 -0
- package/dist/checks/consolidated/ui-components.js.map +1 -0
- package/dist/checks/consolidated/ui-consistency-advanced.d.ts +130 -0
- package/dist/checks/consolidated/ui-consistency-advanced.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-consistency-advanced.js +982 -0
- package/dist/checks/consolidated/ui-consistency-advanced.js.map +1 -0
- package/dist/checks/consolidated/ui-consistency-comprehensive.d.ts +30 -0
- package/dist/checks/consolidated/ui-consistency-comprehensive.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-consistency-comprehensive.js +1018 -0
- package/dist/checks/consolidated/ui-consistency-comprehensive.js.map +1 -0
- package/dist/checks/consolidated/ui-consistency-extended.d.ts +26 -0
- package/dist/checks/consolidated/ui-consistency-extended.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-consistency-extended.js +606 -0
- package/dist/checks/consolidated/ui-consistency-extended.js.map +1 -0
- package/dist/checks/consolidated/ui-data-display.d.ts +103 -0
- package/dist/checks/consolidated/ui-data-display.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-data-display.js +740 -0
- package/dist/checks/consolidated/ui-data-display.js.map +1 -0
- package/dist/checks/consolidated/ui-deprecated.d.ts +22 -0
- package/dist/checks/consolidated/ui-deprecated.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-deprecated.js +336 -0
- package/dist/checks/consolidated/ui-deprecated.js.map +1 -0
- package/dist/checks/consolidated/ui-empty-null-states.d.ts +90 -0
- package/dist/checks/consolidated/ui-empty-null-states.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-empty-null-states.js +511 -0
- package/dist/checks/consolidated/ui-empty-null-states.js.map +1 -0
- package/dist/checks/consolidated/ui-error-states.d.ts +99 -0
- package/dist/checks/consolidated/ui-error-states.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-error-states.js +694 -0
- package/dist/checks/consolidated/ui-error-states.js.map +1 -0
- package/dist/checks/consolidated/ui-feedback-confirmations.d.ts +90 -0
- package/dist/checks/consolidated/ui-feedback-confirmations.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-feedback-confirmations.js +596 -0
- package/dist/checks/consolidated/ui-feedback-confirmations.js.map +1 -0
- package/dist/checks/consolidated/ui-forms.d.ts +32 -0
- package/dist/checks/consolidated/ui-forms.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-forms.js +568 -0
- package/dist/checks/consolidated/ui-forms.js.map +1 -0
- package/dist/checks/consolidated/ui-gradient-shadow.d.ts +90 -0
- package/dist/checks/consolidated/ui-gradient-shadow.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-gradient-shadow.js +568 -0
- package/dist/checks/consolidated/ui-gradient-shadow.js.map +1 -0
- package/dist/checks/consolidated/ui-grid-responsive.d.ts +27 -0
- package/dist/checks/consolidated/ui-grid-responsive.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-grid-responsive.js +441 -0
- package/dist/checks/consolidated/ui-grid-responsive.js.map +1 -0
- package/dist/checks/consolidated/ui-icon-size-tokens.d.ts +104 -0
- package/dist/checks/consolidated/ui-icon-size-tokens.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-icon-size-tokens.js +514 -0
- package/dist/checks/consolidated/ui-icon-size-tokens.js.map +1 -0
- package/dist/checks/consolidated/ui-iconography.d.ts +90 -0
- package/dist/checks/consolidated/ui-iconography.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-iconography.js +565 -0
- package/dist/checks/consolidated/ui-iconography.js.map +1 -0
- package/dist/checks/consolidated/ui-interactive-states.d.ts +240 -0
- package/dist/checks/consolidated/ui-interactive-states.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-interactive-states.js +2474 -0
- package/dist/checks/consolidated/ui-interactive-states.js.map +1 -0
- package/dist/checks/consolidated/ui-layout.d.ts +256 -0
- package/dist/checks/consolidated/ui-layout.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-layout.js +1371 -0
- package/dist/checks/consolidated/ui-layout.js.map +1 -0
- package/dist/checks/consolidated/ui-loading-skeletons.d.ts +11 -0
- package/dist/checks/consolidated/ui-loading-skeletons.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-loading-skeletons.js +145 -0
- package/dist/checks/consolidated/ui-loading-skeletons.js.map +1 -0
- package/dist/checks/consolidated/ui-loading-state-skeletons.d.ts +9 -0
- package/dist/checks/consolidated/ui-loading-state-skeletons.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-loading-state-skeletons.js +125 -0
- package/dist/checks/consolidated/ui-loading-state-skeletons.js.map +1 -0
- package/dist/checks/consolidated/ui-media.d.ts +74 -0
- package/dist/checks/consolidated/ui-media.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-media.js +408 -0
- package/dist/checks/consolidated/ui-media.js.map +1 -0
- package/dist/checks/consolidated/ui-micro-interactions.d.ts +107 -0
- package/dist/checks/consolidated/ui-micro-interactions.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-micro-interactions.js +825 -0
- package/dist/checks/consolidated/ui-micro-interactions.js.map +1 -0
- package/dist/checks/consolidated/ui-microcopy-consistency.d.ts +114 -0
- package/dist/checks/consolidated/ui-microcopy-consistency.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-microcopy-consistency.js +566 -0
- package/dist/checks/consolidated/ui-microcopy-consistency.js.map +1 -0
- package/dist/checks/consolidated/ui-mobile-ux.d.ts +251 -0
- package/dist/checks/consolidated/ui-mobile-ux.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-mobile-ux.js +2212 -0
- package/dist/checks/consolidated/ui-mobile-ux.js.map +1 -0
- package/dist/checks/consolidated/ui-motion-accessibility.d.ts +93 -0
- package/dist/checks/consolidated/ui-motion-accessibility.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-motion-accessibility.js +450 -0
- package/dist/checks/consolidated/ui-motion-accessibility.js.map +1 -0
- package/dist/checks/consolidated/ui-navigation.d.ts +85 -0
- package/dist/checks/consolidated/ui-navigation.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-navigation.js +673 -0
- package/dist/checks/consolidated/ui-navigation.js.map +1 -0
- package/dist/checks/consolidated/ui-patterns.d.ts +174 -0
- package/dist/checks/consolidated/ui-patterns.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-patterns.js +1532 -0
- package/dist/checks/consolidated/ui-patterns.js.map +1 -0
- package/dist/checks/consolidated/ui-responsive.d.ts +89 -0
- package/dist/checks/consolidated/ui-responsive.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-responsive.js +588 -0
- package/dist/checks/consolidated/ui-responsive.js.map +1 -0
- package/dist/checks/consolidated/ui-spacing-standards.d.ts +43 -0
- package/dist/checks/consolidated/ui-spacing-standards.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-spacing-standards.js +874 -0
- package/dist/checks/consolidated/ui-spacing-standards.js.map +1 -0
- package/dist/checks/consolidated/ui-spacing.d.ts +751 -0
- package/dist/checks/consolidated/ui-spacing.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-spacing.js +4996 -0
- package/dist/checks/consolidated/ui-spacing.js.map +1 -0
- package/dist/checks/consolidated/ui-standards-auto-fixer.d.ts +70 -0
- package/dist/checks/consolidated/ui-standards-auto-fixer.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-standards-auto-fixer.js +429 -0
- package/dist/checks/consolidated/ui-standards-auto-fixer.js.map +1 -0
- package/dist/checks/consolidated/ui-standards-enforcement.d.ts +100 -0
- package/dist/checks/consolidated/ui-standards-enforcement.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-standards-enforcement.js +935 -0
- package/dist/checks/consolidated/ui-standards-enforcement.js.map +1 -0
- package/dist/checks/consolidated/ui-state-consistency.d.ts +90 -0
- package/dist/checks/consolidated/ui-state-consistency.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-state-consistency.js +659 -0
- package/dist/checks/consolidated/ui-state-consistency.js.map +1 -0
- package/dist/checks/consolidated/ui-style-validation.d.ts +74 -0
- package/dist/checks/consolidated/ui-style-validation.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-style-validation.js +403 -0
- package/dist/checks/consolidated/ui-style-validation.js.map +1 -0
- package/dist/checks/consolidated/ui-tokens.d.ts +110 -0
- package/dist/checks/consolidated/ui-tokens.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-tokens.js +990 -0
- package/dist/checks/consolidated/ui-tokens.js.map +1 -0
- package/dist/checks/consolidated/ui-typography.d.ts +77 -0
- package/dist/checks/consolidated/ui-typography.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-typography.js +416 -0
- package/dist/checks/consolidated/ui-typography.js.map +1 -0
- package/dist/checks/consolidated/ui-visual-hierarchy.d.ts +90 -0
- package/dist/checks/consolidated/ui-visual-hierarchy.d.ts.map +1 -0
- package/dist/checks/consolidated/ui-visual-hierarchy.js +562 -0
- package/dist/checks/consolidated/ui-visual-hierarchy.js.map +1 -0
- package/dist/checks/consolidated/woocommerce.d.ts +50 -0
- package/dist/checks/consolidated/woocommerce.d.ts.map +1 -0
- package/dist/checks/consolidated/woocommerce.js +198 -0
- package/dist/checks/consolidated/woocommerce.js.map +1 -0
- package/dist/checks/core/api-route-protection.d.ts +2 -0
- package/dist/checks/core/api-route-protection.d.ts.map +1 -0
- package/dist/checks/core/api-route-protection.js +101 -0
- package/dist/checks/core/api-route-protection.js.map +1 -0
- package/dist/checks/core/critical.d.ts +8 -0
- package/dist/checks/core/critical.d.ts.map +1 -0
- package/dist/checks/core/critical.js +200 -0
- package/dist/checks/core/critical.js.map +1 -0
- package/dist/checks/core/database.d.ts +8 -0
- package/dist/checks/core/database.d.ts.map +1 -0
- package/dist/checks/core/database.js +699 -0
- package/dist/checks/core/database.js.map +1 -0
- package/dist/checks/core/development.d.ts +8 -0
- package/dist/checks/core/development.d.ts.map +1 -0
- package/dist/checks/core/development.js +417 -0
- package/dist/checks/core/development.js.map +1 -0
- package/dist/checks/core/hydration-mismatch-check.d.ts +38 -0
- package/dist/checks/core/hydration-mismatch-check.d.ts.map +1 -0
- package/dist/checks/core/hydration-mismatch-check.js +411 -0
- package/dist/checks/core/hydration-mismatch-check.js.map +1 -0
- package/dist/checks/core/performance.d.ts +8 -0
- package/dist/checks/core/performance.d.ts.map +1 -0
- package/dist/checks/core/performance.js +474 -0
- package/dist/checks/core/performance.js.map +1 -0
- package/dist/checks/core/security.d.ts +8 -0
- package/dist/checks/core/security.d.ts.map +1 -0
- package/dist/checks/core/security.js +275 -0
- package/dist/checks/core/security.js.map +1 -0
- package/dist/checks/core/standardized-error-handling.d.ts +43 -0
- package/dist/checks/core/standardized-error-handling.d.ts.map +1 -0
- package/dist/checks/core/standardized-error-handling.js +384 -0
- package/dist/checks/core/standardized-error-handling.js.map +1 -0
- package/dist/checks/core/supercatch.d.ts +8 -0
- package/dist/checks/core/supercatch.d.ts.map +1 -0
- package/dist/checks/core/supercatch.js +750 -0
- package/dist/checks/core/supercatch.js.map +1 -0
- package/dist/checks/core/suppression-check.d.ts +2 -0
- package/dist/checks/core/suppression-check.d.ts.map +1 -0
- package/dist/checks/core/suppression-check.js +129 -0
- package/dist/checks/core/suppression-check.js.map +1 -0
- package/dist/checks/core/ui-quality.d.ts +8 -0
- package/dist/checks/core/ui-quality.d.ts.map +1 -0
- package/dist/checks/core/ui-quality.js +1736 -0
- package/dist/checks/core/ui-quality.js.map +1 -0
- package/dist/checks/core/unused-assets-check.d.ts +2 -0
- package/dist/checks/core/unused-assets-check.d.ts.map +1 -0
- package/dist/checks/core/unused-assets-check.js +112 -0
- package/dist/checks/core/unused-assets-check.js.map +1 -0
- package/dist/checks/core/use-status-ssr-safety.d.ts +34 -0
- package/dist/checks/core/use-status-ssr-safety.d.ts.map +1 -0
- package/dist/checks/core/use-status-ssr-safety.js +283 -0
- package/dist/checks/core/use-status-ssr-safety.js.map +1 -0
- package/dist/checks/email/email-flow-validation.d.ts +23 -0
- package/dist/checks/email/email-flow-validation.d.ts.map +1 -0
- package/dist/checks/email/email-flow-validation.js +468 -0
- package/dist/checks/email/email-flow-validation.js.map +1 -0
- package/dist/checks/email/email-template-db-verification.d.ts +20 -0
- package/dist/checks/email/email-template-db-verification.d.ts.map +1 -0
- package/dist/checks/email/email-template-db-verification.js +46 -0
- package/dist/checks/email/email-template-db-verification.js.map +1 -0
- package/dist/checks/email/email-template-validation.d.ts +24 -0
- package/dist/checks/email/email-template-validation.d.ts.map +1 -0
- package/dist/checks/email/email-template-validation.js +688 -0
- package/dist/checks/email/email-template-validation.js.map +1 -0
- package/dist/checks/jsx/comment-placement.d.ts +45 -0
- package/dist/checks/jsx/comment-placement.d.ts.map +1 -0
- package/dist/checks/jsx/comment-placement.js +316 -0
- package/dist/checks/jsx/comment-placement.js.map +1 -0
- package/dist/checks/specialized/admin-layout-check.d.ts +19 -0
- package/dist/checks/specialized/admin-layout-check.d.ts.map +1 -0
- package/dist/checks/specialized/admin-layout-check.js +166 -0
- package/dist/checks/specialized/admin-layout-check.js.map +1 -0
- package/dist/checks/specialized/client-server-separation.d.ts +14 -0
- package/dist/checks/specialized/client-server-separation.d.ts.map +1 -0
- package/dist/checks/specialized/client-server-separation.js +197 -0
- package/dist/checks/specialized/client-server-separation.js.map +1 -0
- package/dist/checks/specialized/cost-optimization.d.ts +18 -0
- package/dist/checks/specialized/cost-optimization.d.ts.map +1 -0
- package/dist/checks/specialized/cost-optimization.js +78 -0
- package/dist/checks/specialized/cost-optimization.js.map +1 -0
- package/dist/checks/specialized/database-migration-sync.d.ts +21 -0
- package/dist/checks/specialized/database-migration-sync.d.ts.map +1 -0
- package/dist/checks/specialized/database-migration-sync.js +150 -0
- package/dist/checks/specialized/database-migration-sync.js.map +1 -0
- package/dist/checks/specialized/database-model-validation.d.ts +15 -0
- package/dist/checks/specialized/database-model-validation.d.ts.map +1 -0
- package/dist/checks/specialized/database-model-validation.js +35 -0
- package/dist/checks/specialized/database-model-validation.js.map +1 -0
- package/dist/checks/specialized/database-schema-migrations-diff.d.ts +27 -0
- package/dist/checks/specialized/database-schema-migrations-diff.d.ts.map +1 -0
- package/dist/checks/specialized/database-schema-migrations-diff.js +177 -0
- package/dist/checks/specialized/database-schema-migrations-diff.js.map +1 -0
- package/dist/checks/specialized/database-schema-sync.d.ts +23 -0
- package/dist/checks/specialized/database-schema-sync.d.ts.map +1 -0
- package/dist/checks/specialized/database-schema-sync.js +77 -0
- package/dist/checks/specialized/database-schema-sync.js.map +1 -0
- package/dist/checks/specialized/decimal-serialization.d.ts +24 -0
- package/dist/checks/specialized/decimal-serialization.d.ts.map +1 -0
- package/dist/checks/specialized/decimal-serialization.js +400 -0
- package/dist/checks/specialized/decimal-serialization.js.map +1 -0
- package/dist/checks/specialized/detect-router-issues.d.ts +14 -0
- package/dist/checks/specialized/detect-router-issues.d.ts.map +1 -0
- package/dist/checks/specialized/detect-router-issues.js +96 -0
- package/dist/checks/specialized/detect-router-issues.js.map +1 -0
- package/dist/checks/specialized/enum-validation.d.ts +15 -0
- package/dist/checks/specialized/enum-validation.d.ts.map +1 -0
- package/dist/checks/specialized/enum-validation.js +35 -0
- package/dist/checks/specialized/enum-validation.js.map +1 -0
- package/dist/checks/specialized/hash-collision.d.ts +18 -0
- package/dist/checks/specialized/hash-collision.d.ts.map +1 -0
- package/dist/checks/specialized/hash-collision.js +78 -0
- package/dist/checks/specialized/hash-collision.js.map +1 -0
- package/dist/checks/specialized/id-generation-enforcement.d.ts +16 -0
- package/dist/checks/specialized/id-generation-enforcement.d.ts.map +1 -0
- package/dist/checks/specialized/id-generation-enforcement.js +307 -0
- package/dist/checks/specialized/id-generation-enforcement.js.map +1 -0
- package/dist/checks/specialized/image-data-integrity.d.ts +15 -0
- package/dist/checks/specialized/image-data-integrity.d.ts.map +1 -0
- package/dist/checks/specialized/image-data-integrity.js +79 -0
- package/dist/checks/specialized/image-data-integrity.js.map +1 -0
- package/dist/checks/specialized/image-health.d.ts +14 -0
- package/dist/checks/specialized/image-health.d.ts.map +1 -0
- package/dist/checks/specialized/image-health.js +122 -0
- package/dist/checks/specialized/image-health.js.map +1 -0
- package/dist/checks/specialized/image-metadata-validation.d.ts +14 -0
- package/dist/checks/specialized/image-metadata-validation.d.ts.map +1 -0
- package/dist/checks/specialized/image-metadata-validation.js +95 -0
- package/dist/checks/specialized/image-metadata-validation.js.map +1 -0
- package/dist/checks/specialized/image-optimization.d.ts +16 -0
- package/dist/checks/specialized/image-optimization.d.ts.map +1 -0
- package/dist/checks/specialized/image-optimization.js +86 -0
- package/dist/checks/specialized/image-optimization.js.map +1 -0
- package/dist/checks/specialized/invalid-module-imports.d.ts +24 -0
- package/dist/checks/specialized/invalid-module-imports.d.ts.map +1 -0
- package/dist/checks/specialized/invalid-module-imports.js +209 -0
- package/dist/checks/specialized/invalid-module-imports.js.map +1 -0
- package/dist/checks/specialized/lint-validation.d.ts +26 -0
- package/dist/checks/specialized/lint-validation.d.ts.map +1 -0
- package/dist/checks/specialized/lint-validation.js +193 -0
- package/dist/checks/specialized/lint-validation.js.map +1 -0
- package/dist/checks/specialized/listing-workflow.d.ts +19 -0
- package/dist/checks/specialized/listing-workflow.d.ts.map +1 -0
- package/dist/checks/specialized/listing-workflow.js +89 -0
- package/dist/checks/specialized/listing-workflow.js.map +1 -0
- package/dist/checks/specialized/mui-imports-validation.d.ts +18 -0
- package/dist/checks/specialized/mui-imports-validation.d.ts.map +1 -0
- package/dist/checks/specialized/mui-imports-validation.js +134 -0
- package/dist/checks/specialized/mui-imports-validation.js.map +1 -0
- package/dist/checks/specialized/nextauth-v5-compliance.d.ts +16 -0
- package/dist/checks/specialized/nextauth-v5-compliance.d.ts.map +1 -0
- package/dist/checks/specialized/nextauth-v5-compliance.js +164 -0
- package/dist/checks/specialized/nextauth-v5-compliance.js.map +1 -0
- package/dist/checks/specialized/nextjs-params-check.d.ts +14 -0
- package/dist/checks/specialized/nextjs-params-check.d.ts.map +1 -0
- package/dist/checks/specialized/nextjs-params-check.js +140 -0
- package/dist/checks/specialized/nextjs-params-check.js.map +1 -0
- package/dist/checks/specialized/no-legacy-catalog-aliases-validation.d.ts +16 -0
- package/dist/checks/specialized/no-legacy-catalog-aliases-validation.d.ts.map +1 -0
- package/dist/checks/specialized/no-legacy-catalog-aliases-validation.js +36 -0
- package/dist/checks/specialized/no-legacy-catalog-aliases-validation.js.map +1 -0
- package/dist/checks/specialized/no-wata-cardgraded-validation.d.ts +22 -0
- package/dist/checks/specialized/no-wata-cardgraded-validation.d.ts.map +1 -0
- package/dist/checks/specialized/no-wata-cardgraded-validation.js +97 -0
- package/dist/checks/specialized/no-wata-cardgraded-validation.js.map +1 -0
- package/dist/checks/specialized/parameter-consistency-check.d.ts +20 -0
- package/dist/checks/specialized/parameter-consistency-check.d.ts.map +1 -0
- package/dist/checks/specialized/parameter-consistency-check.js +115 -0
- package/dist/checks/specialized/parameter-consistency-check.js.map +1 -0
- package/dist/checks/specialized/prisma-field-names-validation.d.ts +15 -0
- package/dist/checks/specialized/prisma-field-names-validation.d.ts.map +1 -0
- package/dist/checks/specialized/prisma-field-names-validation.js +35 -0
- package/dist/checks/specialized/prisma-field-names-validation.js.map +1 -0
- package/dist/checks/specialized/prisma-null-syntax.d.ts +34 -0
- package/dist/checks/specialized/prisma-null-syntax.d.ts.map +1 -0
- package/dist/checks/specialized/prisma-null-syntax.js +330 -0
- package/dist/checks/specialized/prisma-null-syntax.js.map +1 -0
- package/dist/checks/specialized/prisma-query-validation.d.ts +15 -0
- package/dist/checks/specialized/prisma-query-validation.d.ts.map +1 -0
- package/dist/checks/specialized/prisma-query-validation.js +35 -0
- package/dist/checks/specialized/prisma-query-validation.js.map +1 -0
- package/dist/checks/specialized/product-type-validation.d.ts +17 -0
- package/dist/checks/specialized/product-type-validation.d.ts.map +1 -0
- package/dist/checks/specialized/product-type-validation.js +129 -0
- package/dist/checks/specialized/product-type-validation.js.map +1 -0
- package/dist/checks/specialized/responsive-image-validation.d.ts +14 -0
- package/dist/checks/specialized/responsive-image-validation.d.ts.map +1 -0
- package/dist/checks/specialized/responsive-image-validation.js +101 -0
- package/dist/checks/specialized/responsive-image-validation.js.map +1 -0
- package/dist/checks/specialized/root-cleanliness.d.ts +21 -0
- package/dist/checks/specialized/root-cleanliness.d.ts.map +1 -0
- package/dist/checks/specialized/root-cleanliness.js +251 -0
- package/dist/checks/specialized/root-cleanliness.js.map +1 -0
- package/dist/checks/specialized/rotation-detection-validation.d.ts +16 -0
- package/dist/checks/specialized/rotation-detection-validation.d.ts.map +1 -0
- package/dist/checks/specialized/rotation-detection-validation.js +113 -0
- package/dist/checks/specialized/rotation-detection-validation.js.map +1 -0
- package/dist/checks/specialized/script-organization.d.ts +17 -0
- package/dist/checks/specialized/script-organization.d.ts.map +1 -0
- package/dist/checks/specialized/script-organization.js +487 -0
- package/dist/checks/specialized/script-organization.js.map +1 -0
- package/dist/checks/specialized/shared-components-migration.d.ts +137 -0
- package/dist/checks/specialized/shared-components-migration.d.ts.map +1 -0
- package/dist/checks/specialized/shared-components-migration.js +1288 -0
- package/dist/checks/specialized/shared-components-migration.js.map +1 -0
- package/dist/checks/specialized/store-specialties-normalization.d.ts +10 -0
- package/dist/checks/specialized/store-specialties-normalization.d.ts.map +1 -0
- package/dist/checks/specialized/store-specialties-normalization.js +126 -0
- package/dist/checks/specialized/store-specialties-normalization.js.map +1 -0
- package/dist/checks/specialized/two-stage-trim-validation.d.ts +16 -0
- package/dist/checks/specialized/two-stage-trim-validation.d.ts.map +1 -0
- package/dist/checks/specialized/two-stage-trim-validation.js +115 -0
- package/dist/checks/specialized/two-stage-trim-validation.js.map +1 -0
- package/dist/checks/specialized/underscore-variable-audit.d.ts +26 -0
- package/dist/checks/specialized/underscore-variable-audit.d.ts.map +1 -0
- package/dist/checks/specialized/underscore-variable-audit.js +219 -0
- package/dist/checks/specialized/underscore-variable-audit.js.map +1 -0
- package/dist/checks/specialized/unified-badge-consistency.d.ts +16 -0
- package/dist/checks/specialized/unified-badge-consistency.d.ts.map +1 -0
- package/dist/checks/specialized/unified-badge-consistency.js +284 -0
- package/dist/checks/specialized/unified-badge-consistency.js.map +1 -0
- package/dist/checks/specialized/validate-integration-enums.d.ts +15 -0
- package/dist/checks/specialized/validate-integration-enums.d.ts.map +1 -0
- package/dist/checks/specialized/validate-integration-enums.js +131 -0
- package/dist/checks/specialized/validate-integration-enums.js.map +1 -0
- package/dist/checks/testing/action-regression.d.ts +23 -0
- package/dist/checks/testing/action-regression.d.ts.map +1 -0
- package/dist/checks/testing/action-regression.js +192 -0
- package/dist/checks/testing/action-regression.js.map +1 -0
- package/dist/checks/testing/critical-api-coverage.d.ts +21 -0
- package/dist/checks/testing/critical-api-coverage.d.ts.map +1 -0
- package/dist/checks/testing/critical-api-coverage.js +158 -0
- package/dist/checks/testing/critical-api-coverage.js.map +1 -0
- package/dist/checks/testing/data-entry-regression-required.d.ts +24 -0
- package/dist/checks/testing/data-entry-regression-required.d.ts.map +1 -0
- package/dist/checks/testing/data-entry-regression-required.js +378 -0
- package/dist/checks/testing/data-entry-regression-required.js.map +1 -0
- package/dist/checks/testing/e2e-best-practices.d.ts +24 -0
- package/dist/checks/testing/e2e-best-practices.d.ts.map +1 -0
- package/dist/checks/testing/e2e-best-practices.js +791 -0
- package/dist/checks/testing/e2e-best-practices.js.map +1 -0
- package/dist/checks/testing/e2e-flake-patterns.d.ts +26 -0
- package/dist/checks/testing/e2e-flake-patterns.d.ts.map +1 -0
- package/dist/checks/testing/e2e-flake-patterns.js +305 -0
- package/dist/checks/testing/e2e-flake-patterns.js.map +1 -0
- package/dist/checks/testing/e2e-redundant-visibility-checks.d.ts +25 -0
- package/dist/checks/testing/e2e-redundant-visibility-checks.d.ts.map +1 -0
- package/dist/checks/testing/e2e-redundant-visibility-checks.js +613 -0
- package/dist/checks/testing/e2e-redundant-visibility-checks.js.map +1 -0
- package/dist/checks/testing/e2e-slow-tests.d.ts +9 -0
- package/dist/checks/testing/e2e-slow-tests.d.ts.map +1 -0
- package/dist/checks/testing/e2e-slow-tests.js +142 -0
- package/dist/checks/testing/e2e-slow-tests.js.map +1 -0
- package/dist/checks/testing/e2e-timeouts.d.ts +9 -0
- package/dist/checks/testing/e2e-timeouts.d.ts.map +1 -0
- package/dist/checks/testing/e2e-timeouts.js +82 -0
- package/dist/checks/testing/e2e-timeouts.js.map +1 -0
- package/dist/checks/testing/integration-e2e-depth.d.ts +20 -0
- package/dist/checks/testing/integration-e2e-depth.d.ts.map +1 -0
- package/dist/checks/testing/integration-e2e-depth.js +575 -0
- package/dist/checks/testing/integration-e2e-depth.js.map +1 -0
- package/dist/checks/testing/playwright-feature-coverage-gaps.d.ts +31 -0
- package/dist/checks/testing/playwright-feature-coverage-gaps.d.ts.map +1 -0
- package/dist/checks/testing/playwright-feature-coverage-gaps.js +1582 -0
- package/dist/checks/testing/playwright-feature-coverage-gaps.js.map +1 -0
- package/dist/checks/testing/playwright-mock-inventory.d.ts +24 -0
- package/dist/checks/testing/playwright-mock-inventory.d.ts.map +1 -0
- package/dist/checks/testing/playwright-mock-inventory.js +380 -0
- package/dist/checks/testing/playwright-mock-inventory.js.map +1 -0
- package/dist/checks/testing/test-coverage-threshold.d.ts +25 -0
- package/dist/checks/testing/test-coverage-threshold.d.ts.map +1 -0
- package/dist/checks/testing/test-coverage-threshold.js +166 -0
- package/dist/checks/testing/test-coverage-threshold.js.map +1 -0
- package/dist/checks/testing/test-flakiness-score.d.ts +27 -0
- package/dist/checks/testing/test-flakiness-score.d.ts.map +1 -0
- package/dist/checks/testing/test-flakiness-score.js +358 -0
- package/dist/checks/testing/test-flakiness-score.js.map +1 -0
- package/dist/checks/testing/test-patterns.d.ts +16 -0
- package/dist/checks/testing/test-patterns.d.ts.map +1 -0
- package/dist/checks/testing/test-patterns.js +156 -0
- package/dist/checks/testing/test-patterns.js.map +1 -0
- package/dist/checks/workflows/a-plus-rating-validation.d.ts +42 -0
- package/dist/checks/workflows/a-plus-rating-validation.d.ts.map +1 -0
- package/dist/checks/workflows/a-plus-rating-validation.js +527 -0
- package/dist/checks/workflows/a-plus-rating-validation.js.map +1 -0
- package/dist/checks/workflows/affected.d.ts +14 -0
- package/dist/checks/workflows/affected.d.ts.map +1 -0
- package/dist/checks/workflows/affected.js +126 -0
- package/dist/checks/workflows/affected.js.map +1 -0
- package/dist/checks/workflows/ai.d.ts +6 -0
- package/dist/checks/workflows/ai.d.ts.map +1 -0
- package/dist/checks/workflows/ai.js +42 -0
- package/dist/checks/workflows/ai.js.map +1 -0
- package/dist/checks/workflows/all.d.ts +31 -0
- package/dist/checks/workflows/all.d.ts.map +1 -0
- package/dist/checks/workflows/all.js +2688 -0
- package/dist/checks/workflows/all.js.map +1 -0
- package/dist/checks/workflows/commit.d.ts +19 -0
- package/dist/checks/workflows/commit.d.ts.map +1 -0
- package/dist/checks/workflows/commit.js +207 -0
- package/dist/checks/workflows/commit.js.map +1 -0
- package/dist/checks/workflows/critical.d.ts +9 -0
- package/dist/checks/workflows/critical.d.ts.map +1 -0
- package/dist/checks/workflows/critical.js +213 -0
- package/dist/checks/workflows/critical.js.map +1 -0
- package/dist/checks/workflows/database-id-validation.d.ts +9 -0
- package/dist/checks/workflows/database-id-validation.d.ts.map +1 -0
- package/dist/checks/workflows/database-id-validation.js +13 -0
- package/dist/checks/workflows/database-id-validation.js.map +1 -0
- package/dist/checks/workflows/deploy.d.ts +20 -0
- package/dist/checks/workflows/deploy.d.ts.map +1 -0
- package/dist/checks/workflows/deploy.js +107 -0
- package/dist/checks/workflows/deploy.js.map +1 -0
- package/dist/checks/workflows/deployment-readiness.d.ts +12 -0
- package/dist/checks/workflows/deployment-readiness.d.ts.map +1 -0
- package/dist/checks/workflows/deployment-readiness.js +403 -0
- package/dist/checks/workflows/deployment-readiness.js.map +1 -0
- package/dist/checks/workflows/dev.d.ts +19 -0
- package/dist/checks/workflows/dev.d.ts.map +1 -0
- package/dist/checks/workflows/dev.js +88 -0
- package/dist/checks/workflows/dev.js.map +1 -0
- package/dist/checks/workflows/development.d.ts +9 -0
- package/dist/checks/workflows/development.d.ts.map +1 -0
- package/dist/checks/workflows/development.js +65 -0
- package/dist/checks/workflows/development.js.map +1 -0
- package/dist/checks/workflows/enterprise.d.ts +10 -0
- package/dist/checks/workflows/enterprise.d.ts.map +1 -0
- package/dist/checks/workflows/enterprise.js +359 -0
- package/dist/checks/workflows/enterprise.js.map +1 -0
- package/dist/checks/workflows/images.d.ts +6 -0
- package/dist/checks/workflows/images.d.ts.map +1 -0
- package/dist/checks/workflows/images.js +58 -0
- package/dist/checks/workflows/images.js.map +1 -0
- package/dist/checks/workflows/naming.d.ts +19 -0
- package/dist/checks/workflows/naming.d.ts.map +1 -0
- package/dist/checks/workflows/naming.js +42 -0
- package/dist/checks/workflows/naming.js.map +1 -0
- package/dist/checks/workflows/performance.d.ts +8 -0
- package/dist/checks/workflows/performance.d.ts.map +1 -0
- package/dist/checks/workflows/performance.js +77 -0
- package/dist/checks/workflows/performance.js.map +1 -0
- package/dist/checks/workflows/pre-deploy.d.ts +6 -0
- package/dist/checks/workflows/pre-deploy.d.ts.map +1 -0
- package/dist/checks/workflows/pre-deploy.js +41 -0
- package/dist/checks/workflows/pre-deploy.js.map +1 -0
- package/dist/checks/workflows/security.d.ts +8 -0
- package/dist/checks/workflows/security.d.ts.map +1 -0
- package/dist/checks/workflows/security.js +71 -0
- package/dist/checks/workflows/security.js.map +1 -0
- package/dist/checks/workflows/supercatch.d.ts +8 -0
- package/dist/checks/workflows/supercatch.d.ts.map +1 -0
- package/dist/checks/workflows/supercatch.js +127 -0
- package/dist/checks/workflows/supercatch.js.map +1 -0
- package/dist/checks/workflows/ui-quality.d.ts +9 -0
- package/dist/checks/workflows/ui-quality.d.ts.map +1 -0
- package/dist/checks/workflows/ui-quality.js +264 -0
- package/dist/checks/workflows/ui-quality.js.map +1 -0
- package/dist/checks/workflows/ui-uniformity.d.ts +18 -0
- package/dist/checks/workflows/ui-uniformity.d.ts.map +1 -0
- package/dist/checks/workflows/ui-uniformity.js +265 -0
- package/dist/checks/workflows/ui-uniformity.js.map +1 -0
- package/dist/checks/workflows/vercel.d.ts +16 -0
- package/dist/checks/workflows/vercel.d.ts.map +1 -0
- package/dist/checks/workflows/vercel.js +173 -0
- package/dist/checks/workflows/vercel.js.map +1 -0
- package/dist/utils/validation-helpers.d.ts +43 -0
- package/dist/utils/validation-helpers.d.ts.map +1 -0
- package/dist/utils/validation-helpers.js +370 -0
- package/dist/utils/validation-helpers.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,2474 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* UI Interactive States Preflight (BLOCKING)
|
|
5
|
+
*
|
|
6
|
+
* Google/Meta/X-level interactive state consistency.
|
|
7
|
+
* Ensures hover, active, focus, and disabled states are consistent across all components.
|
|
8
|
+
*
|
|
9
|
+
* Checks:
|
|
10
|
+
* 1. Hover state consistency - Same hover tokens across similar components
|
|
11
|
+
* 2. Active/pressed state coverage - All buttons have active states
|
|
12
|
+
* 3. Focus ring consistency - Same focus style across components
|
|
13
|
+
* 4. Disabled state styling - Consistent disabled appearance
|
|
14
|
+
* 5. Cursor consistency - Correct cursor for each state
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* pnpm preflight:ui-interactive-states # All checks
|
|
18
|
+
* pnpm preflight:ui-interactive-states hover # Hover states only
|
|
19
|
+
* pnpm preflight:ui-interactive-states active # Active states only
|
|
20
|
+
* pnpm preflight:ui-interactive-states focus # Focus states only
|
|
21
|
+
* pnpm preflight:ui-interactive-states disabled # Disabled states only
|
|
22
|
+
* pnpm preflight:ui-interactive-states cursor # Cursor consistency only
|
|
23
|
+
*/
|
|
24
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
25
|
+
if (k2 === undefined) k2 = k;
|
|
26
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
27
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
28
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
29
|
+
}
|
|
30
|
+
Object.defineProperty(o, k2, desc);
|
|
31
|
+
}) : (function(o, m, k, k2) {
|
|
32
|
+
if (k2 === undefined) k2 = k;
|
|
33
|
+
o[k2] = m[k];
|
|
34
|
+
}));
|
|
35
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
36
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
37
|
+
}) : function(o, v) {
|
|
38
|
+
o["default"] = v;
|
|
39
|
+
});
|
|
40
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
41
|
+
var ownKeys = function(o) {
|
|
42
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
43
|
+
var ar = [];
|
|
44
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
45
|
+
return ar;
|
|
46
|
+
};
|
|
47
|
+
return ownKeys(o);
|
|
48
|
+
};
|
|
49
|
+
return function (mod) {
|
|
50
|
+
if (mod && mod.__esModule) return mod;
|
|
51
|
+
var result = {};
|
|
52
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
53
|
+
__setModuleDefault(result, mod);
|
|
54
|
+
return result;
|
|
55
|
+
};
|
|
56
|
+
})();
|
|
57
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
58
|
+
exports.UIInteractiveStatesModule = exports.STANDARD_HOVER_TOKENS = exports.STANDARD_FOCUS_STYLES = void 0;
|
|
59
|
+
const fs = __importStar(require("fs"));
|
|
60
|
+
const path = __importStar(require("path"));
|
|
61
|
+
const console_chars_1 = require("../../utils/console-chars");
|
|
62
|
+
const file_cache_1 = require("../../shared/file-cache");
|
|
63
|
+
const glob_patterns_1 = require("../../shared/glob-patterns");
|
|
64
|
+
const universal_progress_reporter_1 = require("../system/universal-progress-reporter");
|
|
65
|
+
const concurrency_config_1 = require("../../shared/concurrency-config");
|
|
66
|
+
const EXCLUDED = [...glob_patterns_1.STANDARD_EXCLUDES, "**/*.test.tsx", "**/*.spec.tsx", "**/*.stories.tsx"];
|
|
67
|
+
// INTERACTIVE STATE STANDARDS
|
|
68
|
+
/**
|
|
69
|
+
* Standard hover tokens - should be consistent across components
|
|
70
|
+
*/
|
|
71
|
+
const STANDARD_HOVER_TOKENS = {
|
|
72
|
+
// Background hovers
|
|
73
|
+
"bg-tertiary": "var(--bg-tertiary)",
|
|
74
|
+
"overlay-light": "var(--overlay-light)",
|
|
75
|
+
"overlay-medium": "var(--overlay-medium)",
|
|
76
|
+
"primary-hover": "var(--primary-hover)",
|
|
77
|
+
// Tailwind equivalents
|
|
78
|
+
"bg-tertiary-tw": "hover:bg-[var(--bg-tertiary)]",
|
|
79
|
+
"overlay-light-tw": "hover:bg-[var(--overlay-light)]",
|
|
80
|
+
};
|
|
81
|
+
exports.STANDARD_HOVER_TOKENS = STANDARD_HOVER_TOKENS;
|
|
82
|
+
/**
|
|
83
|
+
* Standard focus ring styles
|
|
84
|
+
*/
|
|
85
|
+
const STANDARD_FOCUS_STYLES = {
|
|
86
|
+
ring: "focus-visible:ring-2",
|
|
87
|
+
ringColor: "focus-visible:ring-primary",
|
|
88
|
+
outline: "focus-visible:outline-none",
|
|
89
|
+
};
|
|
90
|
+
exports.STANDARD_FOCUS_STYLES = STANDARD_FOCUS_STYLES;
|
|
91
|
+
/**
|
|
92
|
+
* Interactive elements that should have hover states
|
|
93
|
+
*/
|
|
94
|
+
const INTERACTIVE_ELEMENTS = [
|
|
95
|
+
"Button",
|
|
96
|
+
"IconButton",
|
|
97
|
+
"Link",
|
|
98
|
+
"a",
|
|
99
|
+
"MenuItem",
|
|
100
|
+
"ListItemButton",
|
|
101
|
+
"Tab",
|
|
102
|
+
"Chip",
|
|
103
|
+
"Card", // When clickable
|
|
104
|
+
];
|
|
105
|
+
// Get concurrency from shared config (respects PREFLIGHT_CONCURRENCY env var)
|
|
106
|
+
const concurrencyConfig = (0, concurrency_config_1.getConcurrencyConfig)();
|
|
107
|
+
class UIInteractiveStatesModule {
|
|
108
|
+
verbose;
|
|
109
|
+
parallel = false;
|
|
110
|
+
constructor(options = {}) {
|
|
111
|
+
this.verbose = options.verbose || false;
|
|
112
|
+
this.parallel = options.parallel || concurrencyConfig.parallel;
|
|
113
|
+
}
|
|
114
|
+
async getTsxFiles() {
|
|
115
|
+
return file_cache_1.fileCache.getAppAndComponentsTSX();
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* 1. Hover State Consistency
|
|
119
|
+
* Ensure consistent hover background tokens
|
|
120
|
+
*/
|
|
121
|
+
async checkHoverStateConsistency() {
|
|
122
|
+
const startTime = Date.now();
|
|
123
|
+
const issues = [];
|
|
124
|
+
const files = await this.getTsxFiles();
|
|
125
|
+
// Track hover patterns used across codebase
|
|
126
|
+
const hoverPatterns = new Map();
|
|
127
|
+
for (const file of files) {
|
|
128
|
+
const content = fs.readFileSync(file, "utf8");
|
|
129
|
+
const lines = content.split("\n");
|
|
130
|
+
for (let i = 0; i < lines.length; i++) {
|
|
131
|
+
const line = lines[i];
|
|
132
|
+
// Skip lines with preflight-ignore comment (current line or up to 5 lines before)
|
|
133
|
+
const contextLines = lines.slice(Math.max(0, i - 5), i + 1).join("\n");
|
|
134
|
+
if (contextLines.includes("preflight-ignore")) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
// Track hover:bg-* patterns
|
|
138
|
+
const hoverBgMatch = line.match(/hover:bg-\[([^\]]+)\]/);
|
|
139
|
+
if (hoverBgMatch) {
|
|
140
|
+
const pattern = hoverBgMatch[1];
|
|
141
|
+
const existing = hoverPatterns.get(pattern) || { count: 0, files: [] };
|
|
142
|
+
existing.count++;
|
|
143
|
+
if (!existing.files.includes(file))
|
|
144
|
+
existing.files.push(file);
|
|
145
|
+
hoverPatterns.set(pattern, existing);
|
|
146
|
+
}
|
|
147
|
+
// Check for hardcoded hover colors (not using tokens)
|
|
148
|
+
if (/hover:bg-(?:gray|slate|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d{2,3}/.test(line)) {
|
|
149
|
+
issues.push({
|
|
150
|
+
file,
|
|
151
|
+
line: i + 1,
|
|
152
|
+
type: "hardcoded-hover-color",
|
|
153
|
+
severity: "warning",
|
|
154
|
+
message: "Hardcoded Tailwind color for hover state",
|
|
155
|
+
suggestion: "Use hover:bg-[var(--bg-tertiary)] or hover:bg-[var(--overlay-light)]",
|
|
156
|
+
snippet: line.trim().substring(0, 80),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
// Check for inline hover styles (should use Tailwind)
|
|
160
|
+
if (/:hover/.test(line) && /style=/.test(line)) {
|
|
161
|
+
issues.push({
|
|
162
|
+
file,
|
|
163
|
+
line: i + 1,
|
|
164
|
+
type: "inline-hover-style",
|
|
165
|
+
severity: "warning",
|
|
166
|
+
message: "Inline CSS hover style (prefer Tailwind)",
|
|
167
|
+
suggestion: "Use Tailwind hover: classes instead of inline :hover styles",
|
|
168
|
+
snippet: line.trim().substring(0, 80),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Report inconsistent hover patterns
|
|
174
|
+
if (hoverPatterns.size > 3) {
|
|
175
|
+
const sorted = [...hoverPatterns.entries()].sort((a, b) => b[1].count - a[1].count);
|
|
176
|
+
const summary = sorted
|
|
177
|
+
.slice(0, 5)
|
|
178
|
+
.map(([p, d]) => `${p} (${d.count}x)`)
|
|
179
|
+
.join(", ");
|
|
180
|
+
issues.push({
|
|
181
|
+
file: "codebase-wide",
|
|
182
|
+
line: 0,
|
|
183
|
+
type: "hover-pattern-inconsistency",
|
|
184
|
+
severity: "info",
|
|
185
|
+
message: `${hoverPatterns.size} different hover background patterns detected`,
|
|
186
|
+
suggestion: `Standardize on 2-3 hover tokens. Top patterns: ${summary}`,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
name: "Hover State Consistency",
|
|
191
|
+
passed: issues.filter((i) => i.severity === "error").length === 0,
|
|
192
|
+
blocking: false,
|
|
193
|
+
issues,
|
|
194
|
+
duration: Date.now() - startTime,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* 2. Active/Pressed State Coverage
|
|
199
|
+
* Ensure interactive elements have active states
|
|
200
|
+
*/
|
|
201
|
+
async checkActiveStateCoverage() {
|
|
202
|
+
const startTime = Date.now();
|
|
203
|
+
const issues = [];
|
|
204
|
+
const files = await this.getTsxFiles();
|
|
205
|
+
for (const file of files) {
|
|
206
|
+
const content = fs.readFileSync(file, "utf8");
|
|
207
|
+
const lines = content.split("\n");
|
|
208
|
+
for (let i = 0; i < lines.length; i++) {
|
|
209
|
+
const line = lines[i];
|
|
210
|
+
const contextBlock = lines.slice(i, Math.min(i + 10, lines.length)).join("\n");
|
|
211
|
+
// Check buttons with hover but no active state
|
|
212
|
+
if (/<Button/.test(line) || /className=["'][^"']*hover:/.test(line)) {
|
|
213
|
+
const hasHover = /hover:/.test(contextBlock);
|
|
214
|
+
const hasActive = /active:|:active/.test(contextBlock);
|
|
215
|
+
// Only flag if it has custom hover but no active
|
|
216
|
+
if (hasHover && !hasActive && /className=/.test(line)) {
|
|
217
|
+
const hasBuiltInButton = /<Button|<IconButton|<LoadingButton|<AnimatedButton/.test(line);
|
|
218
|
+
if (!hasBuiltInButton) {
|
|
219
|
+
issues.push({
|
|
220
|
+
file,
|
|
221
|
+
line: i + 1,
|
|
222
|
+
type: "hover-no-active",
|
|
223
|
+
severity: "info",
|
|
224
|
+
message: "Interactive element has hover state but no active state",
|
|
225
|
+
suggestion: "Add active:scale-[0.98] or active:bg-[var(--overlay-medium)] for feedback",
|
|
226
|
+
snippet: line.trim().substring(0, 80),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// Check custom clickable elements
|
|
232
|
+
if (/<div[^>]*onClick|<Box[^>]*onClick/.test(line)) {
|
|
233
|
+
const hasActive = /active:|:active/.test(contextBlock);
|
|
234
|
+
const hasCursor = /cursor-pointer/.test(contextBlock);
|
|
235
|
+
if (hasCursor && !hasActive) {
|
|
236
|
+
issues.push({
|
|
237
|
+
file,
|
|
238
|
+
line: i + 1,
|
|
239
|
+
type: "clickable-no-active",
|
|
240
|
+
severity: "info",
|
|
241
|
+
message: "Clickable element without active/pressed state",
|
|
242
|
+
suggestion: "Add active:scale-[0.98] or active:opacity-90 for tactile feedback",
|
|
243
|
+
snippet: line.trim().substring(0, 80),
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
name: "Active State Coverage",
|
|
251
|
+
passed: true, // Info only
|
|
252
|
+
blocking: false,
|
|
253
|
+
issues,
|
|
254
|
+
duration: Date.now() - startTime,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* 3. Focus Ring Consistency
|
|
259
|
+
* Ensure focus states use consistent styling
|
|
260
|
+
*/
|
|
261
|
+
async checkFocusRingConsistency() {
|
|
262
|
+
const startTime = Date.now();
|
|
263
|
+
const issues = [];
|
|
264
|
+
const files = await this.getTsxFiles();
|
|
265
|
+
// Track focus ring patterns
|
|
266
|
+
const focusPatterns = new Map();
|
|
267
|
+
for (const file of files) {
|
|
268
|
+
const content = fs.readFileSync(file, "utf8");
|
|
269
|
+
const lines = content.split("\n");
|
|
270
|
+
for (let i = 0; i < lines.length; i++) {
|
|
271
|
+
const line = lines[i];
|
|
272
|
+
// Track focus ring color patterns
|
|
273
|
+
const focusRingMatch = line.match(/focus(?:-visible)?:ring-(\w+)/);
|
|
274
|
+
if (focusRingMatch) {
|
|
275
|
+
const color = focusRingMatch[1];
|
|
276
|
+
focusPatterns.set(color, (focusPatterns.get(color) || 0) + 1);
|
|
277
|
+
}
|
|
278
|
+
// Check for focus:outline-none without ring
|
|
279
|
+
if (/focus:outline-none/.test(line) && !/focus(?:-visible)?:ring/.test(line)) {
|
|
280
|
+
const contextBlock = lines.slice(i, Math.min(i + 5, lines.length)).join("\n");
|
|
281
|
+
const hasOtherFocusIndicator = /focus:border|focus:shadow|focus:bg/.test(contextBlock);
|
|
282
|
+
if (!hasOtherFocusIndicator) {
|
|
283
|
+
issues.push({
|
|
284
|
+
file,
|
|
285
|
+
line: i + 1,
|
|
286
|
+
type: "outline-none-no-alternative",
|
|
287
|
+
severity: "warning",
|
|
288
|
+
message: "focus:outline-none without alternative focus indicator",
|
|
289
|
+
suggestion: "Add focus-visible:ring-2 focus-visible:ring-primary",
|
|
290
|
+
snippet: line.trim().substring(0, 80),
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Check for focus: instead of focus-visible: (accessibility issue)
|
|
295
|
+
if (/\bfocus:(?!visible)/.test(line) && /ring|outline|border|shadow/.test(line)) {
|
|
296
|
+
// Skip if focus-visible is also present
|
|
297
|
+
if (!/focus-visible:/.test(line)) {
|
|
298
|
+
issues.push({
|
|
299
|
+
file,
|
|
300
|
+
line: i + 1,
|
|
301
|
+
type: "focus-instead-of-focus-visible",
|
|
302
|
+
severity: "info",
|
|
303
|
+
message: "Using focus: instead of focus-visible: for visual indicators",
|
|
304
|
+
suggestion: "Use focus-visible: to only show focus ring for keyboard navigation",
|
|
305
|
+
snippet: line.trim().substring(0, 80),
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// Report focus ring color inconsistencies
|
|
312
|
+
if (focusPatterns.size > 2) {
|
|
313
|
+
const sorted = [...focusPatterns.entries()].sort((a, b) => b[1] - a[1]);
|
|
314
|
+
issues.push({
|
|
315
|
+
file: "codebase-wide",
|
|
316
|
+
line: 0,
|
|
317
|
+
type: "focus-ring-color-inconsistency",
|
|
318
|
+
severity: "info",
|
|
319
|
+
message: `${focusPatterns.size} different focus ring colors detected`,
|
|
320
|
+
suggestion: `Standardize on primary ring color. Usage: ${sorted.map(([c, n]) => `${c}(${n})`).join(", ")}`,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
name: "Focus Ring Consistency",
|
|
325
|
+
passed: issues.filter((i) => i.severity === "error").length === 0,
|
|
326
|
+
blocking: false,
|
|
327
|
+
issues,
|
|
328
|
+
duration: Date.now() - startTime,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* 4. Disabled State Styling
|
|
333
|
+
* Ensure disabled states are consistent
|
|
334
|
+
*/
|
|
335
|
+
async checkDisabledStateStyling() {
|
|
336
|
+
const startTime = Date.now();
|
|
337
|
+
const issues = [];
|
|
338
|
+
const files = await this.getTsxFiles();
|
|
339
|
+
for (const file of files) {
|
|
340
|
+
const content = fs.readFileSync(file, "utf8");
|
|
341
|
+
const lines = content.split("\n");
|
|
342
|
+
for (let i = 0; i < lines.length; i++) {
|
|
343
|
+
const line = lines[i];
|
|
344
|
+
const contextBlock = lines.slice(i, Math.min(i + 10, lines.length)).join("\n");
|
|
345
|
+
// Check for disabled prop without cursor-not-allowed
|
|
346
|
+
if (/disabled[=:]/.test(line)) {
|
|
347
|
+
const hasNotAllowed = /cursor-not-allowed|pointer-events-none/.test(contextBlock);
|
|
348
|
+
const hasDisabledStyles = /disabled:/.test(contextBlock);
|
|
349
|
+
const isBuiltInComponent = /<Button|<IconButton|<TextField|<Select|<Checkbox|<Radio/.test(line);
|
|
350
|
+
if (!isBuiltInComponent && !hasNotAllowed && !hasDisabledStyles) {
|
|
351
|
+
issues.push({
|
|
352
|
+
file,
|
|
353
|
+
line: i + 1,
|
|
354
|
+
type: "disabled-no-cursor",
|
|
355
|
+
severity: "info",
|
|
356
|
+
message: "Disabled element without cursor-not-allowed",
|
|
357
|
+
suggestion: "Add disabled:cursor-not-allowed disabled:opacity-50",
|
|
358
|
+
snippet: line.trim().substring(0, 80),
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
// Check for hardcoded disabled opacity
|
|
363
|
+
if (/disabled.*opacity-(?!50)(\d+)/.test(line)) {
|
|
364
|
+
const opacityMatch = line.match(/opacity-(\d+)/);
|
|
365
|
+
if (opacityMatch && !["50", "60"].includes(opacityMatch[1])) {
|
|
366
|
+
issues.push({
|
|
367
|
+
file,
|
|
368
|
+
line: i + 1,
|
|
369
|
+
type: "inconsistent-disabled-opacity",
|
|
370
|
+
severity: "info",
|
|
371
|
+
message: `Disabled opacity-${opacityMatch[1]} (standard is opacity-50)`,
|
|
372
|
+
suggestion: "Use disabled:opacity-50 for consistency",
|
|
373
|
+
snippet: line.trim().substring(0, 80),
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
name: "Disabled State Styling",
|
|
381
|
+
passed: true, // Info only
|
|
382
|
+
blocking: false,
|
|
383
|
+
issues,
|
|
384
|
+
duration: Date.now() - startTime,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* 5. Cursor Consistency
|
|
389
|
+
* Ensure correct cursor for each element type
|
|
390
|
+
*/
|
|
391
|
+
async checkCursorConsistency() {
|
|
392
|
+
const startTime = Date.now();
|
|
393
|
+
const issues = [];
|
|
394
|
+
const files = await this.getTsxFiles();
|
|
395
|
+
for (const file of files) {
|
|
396
|
+
const content = fs.readFileSync(file, "utf8");
|
|
397
|
+
const lines = content.split("\n");
|
|
398
|
+
for (let i = 0; i < lines.length; i++) {
|
|
399
|
+
const line = lines[i];
|
|
400
|
+
const contextBlock = lines.slice(i, Math.min(i + 5, lines.length)).join("\n");
|
|
401
|
+
// Check for onClick without cursor-pointer
|
|
402
|
+
if (/onClick=/.test(line)) {
|
|
403
|
+
const isInteractiveElement = /<(Button|IconButton|a|Link|MenuItem|Tab|Chip)/.test(line);
|
|
404
|
+
const hasCursor = /cursor-/.test(contextBlock);
|
|
405
|
+
const isDisabled = /disabled/.test(contextBlock);
|
|
406
|
+
if (!isInteractiveElement && !hasCursor && !isDisabled) {
|
|
407
|
+
issues.push({
|
|
408
|
+
file,
|
|
409
|
+
line: i + 1,
|
|
410
|
+
type: "onclick-no-cursor",
|
|
411
|
+
severity: "info",
|
|
412
|
+
message: "Clickable element without cursor-pointer",
|
|
413
|
+
suggestion: "Add cursor-pointer to indicate clickability",
|
|
414
|
+
snippet: line.trim().substring(0, 80),
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
// Check for cursor-pointer on non-interactive elements
|
|
419
|
+
if (/cursor-pointer/.test(line) &&
|
|
420
|
+
!/(onClick|href|to=|role=["']button)/.test(contextBlock)) {
|
|
421
|
+
issues.push({
|
|
422
|
+
file,
|
|
423
|
+
line: i + 1,
|
|
424
|
+
type: "cursor-pointer-not-interactive",
|
|
425
|
+
severity: "info",
|
|
426
|
+
message: "cursor-pointer on non-interactive element",
|
|
427
|
+
suggestion: "Only use cursor-pointer on actually clickable elements",
|
|
428
|
+
snippet: line.trim().substring(0, 80),
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
// Check for loading states without cursor-wait
|
|
432
|
+
if (/(isLoading|loading|isPending)\s*&&/.test(line)) {
|
|
433
|
+
const hasCursorWait = /cursor-wait/.test(contextBlock);
|
|
434
|
+
// This is optional, so just info
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
name: "Cursor Consistency",
|
|
440
|
+
passed: true, // Info only
|
|
441
|
+
blocking: false,
|
|
442
|
+
issues,
|
|
443
|
+
duration: Date.now() - startTime,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* 6. Scale Feedback Consistency
|
|
448
|
+
* Ensure active:scale values are consistent across components
|
|
449
|
+
*/
|
|
450
|
+
async checkScaleFeedbackConsistency() {
|
|
451
|
+
const startTime = Date.now();
|
|
452
|
+
const issues = [];
|
|
453
|
+
const files = await this.getTsxFiles();
|
|
454
|
+
// Track scale values used
|
|
455
|
+
const scaleValues = new Map();
|
|
456
|
+
const STANDARD_SCALES = ["scale-95", "scale-[0.98]", "scale-[0.97]", "scale-100"];
|
|
457
|
+
for (const file of files) {
|
|
458
|
+
const content = fs.readFileSync(file, "utf8");
|
|
459
|
+
const lines = content.split("\n");
|
|
460
|
+
for (let i = 0; i < lines.length; i++) {
|
|
461
|
+
const line = lines[i];
|
|
462
|
+
// Track active:scale-* patterns
|
|
463
|
+
const scaleMatch = line.match(/active:scale-\[?([^\s\]"']+)\]?/);
|
|
464
|
+
if (scaleMatch) {
|
|
465
|
+
const value = scaleMatch[1];
|
|
466
|
+
const existing = scaleValues.get(value) || { count: 0, files: [] };
|
|
467
|
+
existing.count++;
|
|
468
|
+
if (!existing.files.includes(file))
|
|
469
|
+
existing.files.push(file);
|
|
470
|
+
scaleValues.set(value, existing);
|
|
471
|
+
}
|
|
472
|
+
// Check for non-standard scale values
|
|
473
|
+
const nonStandardScale = line.match(/active:scale-\[([0-9.]+)\]/);
|
|
474
|
+
if (nonStandardScale) {
|
|
475
|
+
const value = parseFloat(nonStandardScale[1]);
|
|
476
|
+
if (value < 0.95 || value > 0.99) {
|
|
477
|
+
issues.push({
|
|
478
|
+
file,
|
|
479
|
+
line: i + 1,
|
|
480
|
+
type: "unusual-scale-value",
|
|
481
|
+
severity: "info",
|
|
482
|
+
message: `Unusual scale value: ${value} (standard is 0.97-0.98)`,
|
|
483
|
+
suggestion: "Use active:scale-[0.98] or active:scale-95 for consistency",
|
|
484
|
+
snippet: line.trim().substring(0, 80),
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
// Report scale inconsistencies
|
|
491
|
+
if (scaleValues.size > 2) {
|
|
492
|
+
const sorted = [...scaleValues.entries()].sort((a, b) => b[1].count - a[1].count);
|
|
493
|
+
issues.push({
|
|
494
|
+
file: "codebase-wide",
|
|
495
|
+
line: 0,
|
|
496
|
+
type: "scale-inconsistency",
|
|
497
|
+
severity: "info",
|
|
498
|
+
message: `${scaleValues.size} different active:scale values detected`,
|
|
499
|
+
suggestion: `Standardize on 1-2 scale values. Usage: ${sorted.map(([v, d]) => `${v}(${d.count})`).join(", ")}`,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
name: "Scale Feedback Consistency",
|
|
504
|
+
passed: true, // Info only
|
|
505
|
+
blocking: false,
|
|
506
|
+
issues,
|
|
507
|
+
duration: Date.now() - startTime,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* 7. Loading Spinner Consistency
|
|
512
|
+
* Ensure loading spinners use consistent sizing and patterns
|
|
513
|
+
*/
|
|
514
|
+
async checkLoadingSpinnerConsistency() {
|
|
515
|
+
const startTime = Date.now();
|
|
516
|
+
const issues = [];
|
|
517
|
+
const files = await this.getTsxFiles();
|
|
518
|
+
// Track spinner sizes
|
|
519
|
+
const spinnerSizes = new Map();
|
|
520
|
+
const STANDARD_SIZES = [16, 20, 24, 32, 40];
|
|
521
|
+
for (const file of files) {
|
|
522
|
+
const content = fs.readFileSync(file, "utf8");
|
|
523
|
+
const lines = content.split("\n");
|
|
524
|
+
for (let i = 0; i < lines.length; i++) {
|
|
525
|
+
const line = lines[i];
|
|
526
|
+
// Check CircularProgress/Spinner size
|
|
527
|
+
const sizeMatch = line.match(/<(?:CircularProgress|Spinner)[^>]*size[=:][\s{]*(\d+)/);
|
|
528
|
+
if (sizeMatch) {
|
|
529
|
+
const size = parseInt(sizeMatch[1]);
|
|
530
|
+
spinnerSizes.set(String(size), (spinnerSizes.get(String(size)) || 0) + 1);
|
|
531
|
+
if (!STANDARD_SIZES.includes(size)) {
|
|
532
|
+
issues.push({
|
|
533
|
+
file,
|
|
534
|
+
line: i + 1,
|
|
535
|
+
type: "non-standard-spinner-size",
|
|
536
|
+
severity: "info",
|
|
537
|
+
message: `Non-standard spinner size: ${size}px`,
|
|
538
|
+
suggestion: `Use standard sizes: ${STANDARD_SIZES.join(", ")}px`,
|
|
539
|
+
snippet: line.trim().substring(0, 80),
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
// Check for loading without visual indicator
|
|
544
|
+
if (/isLoading\s*\?\s*[^:]*:[^<]*<Button/.test(line) || /loading\s*\?\s*[^:]*:[^<]*<Button/.test(line)) {
|
|
545
|
+
const hasSpinner = /CircularProgress|Spinner|animate-spin/.test(content.substring(Math.max(0, content.indexOf(line) - 200), content.indexOf(line) + 200));
|
|
546
|
+
if (!hasSpinner) {
|
|
547
|
+
issues.push({
|
|
548
|
+
file,
|
|
549
|
+
line: i + 1,
|
|
550
|
+
type: "loading-no-spinner",
|
|
551
|
+
severity: "info",
|
|
552
|
+
message: "Loading state without visual spinner indicator",
|
|
553
|
+
suggestion: "Add CircularProgress or Spinner component during loading",
|
|
554
|
+
snippet: line.trim().substring(0, 80),
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return {
|
|
561
|
+
name: "Loading Spinner Consistency",
|
|
562
|
+
passed: true, // Info only
|
|
563
|
+
blocking: false,
|
|
564
|
+
issues,
|
|
565
|
+
duration: Date.now() - startTime,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* 8. Reduced Motion Support (Accessibility)
|
|
570
|
+
* Check for prefers-reduced-motion support in animations
|
|
571
|
+
*/
|
|
572
|
+
async checkReducedMotionSupport() {
|
|
573
|
+
const startTime = Date.now();
|
|
574
|
+
const issues = [];
|
|
575
|
+
const files = await this.getTsxFiles();
|
|
576
|
+
let hasReducedMotionSupport = false;
|
|
577
|
+
let animationCount = 0;
|
|
578
|
+
for (const file of files) {
|
|
579
|
+
const content = fs.readFileSync(file, "utf8");
|
|
580
|
+
// Check for reduced motion support
|
|
581
|
+
if (/prefers-reduced-motion|motion-reduce|motion-safe/.test(content)) {
|
|
582
|
+
hasReducedMotionSupport = true;
|
|
583
|
+
}
|
|
584
|
+
// Count animation usage
|
|
585
|
+
const animations = (content.match(/animate-|transition-|@keyframes/g) || []).length;
|
|
586
|
+
animationCount += animations;
|
|
587
|
+
const lines = content.split("\n");
|
|
588
|
+
for (let i = 0; i < lines.length; i++) {
|
|
589
|
+
const line = lines[i];
|
|
590
|
+
// Check for complex animations without reduced motion alternative
|
|
591
|
+
if (/animate-(?:bounce|pulse|ping|spin)/.test(line)) {
|
|
592
|
+
const contextBlock = content.substring(Math.max(0, content.indexOf(line) - 100), content.indexOf(line) + line.length + 100);
|
|
593
|
+
if (!/motion-reduce|prefers-reduced-motion/.test(contextBlock)) {
|
|
594
|
+
issues.push({
|
|
595
|
+
file,
|
|
596
|
+
line: i + 1,
|
|
597
|
+
type: "animation-no-reduced-motion",
|
|
598
|
+
severity: "info",
|
|
599
|
+
message: "Animation without reduced-motion alternative",
|
|
600
|
+
suggestion: "Add motion-reduce:animate-none for accessibility",
|
|
601
|
+
snippet: line.trim().substring(0, 80),
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
// Global check: if we have many animations but no reduced motion support
|
|
608
|
+
if (animationCount > 20 && !hasReducedMotionSupport) {
|
|
609
|
+
issues.push({
|
|
610
|
+
file: "codebase-wide",
|
|
611
|
+
line: 0,
|
|
612
|
+
type: "no-reduced-motion-global",
|
|
613
|
+
severity: "warning",
|
|
614
|
+
message: `${animationCount} animations found but no prefers-reduced-motion support detected`,
|
|
615
|
+
suggestion: "Add global CSS: @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; } }",
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
return {
|
|
619
|
+
name: "Reduced Motion Support",
|
|
620
|
+
passed: issues.filter((i) => i.severity === "error").length === 0,
|
|
621
|
+
blocking: false,
|
|
622
|
+
issues,
|
|
623
|
+
duration: Date.now() - startTime,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* 9. Async Operation Feedback
|
|
628
|
+
* Check that async operations provide success/error feedback
|
|
629
|
+
*/
|
|
630
|
+
async checkAsyncOperationFeedback() {
|
|
631
|
+
const startTime = Date.now();
|
|
632
|
+
const issues = [];
|
|
633
|
+
const files = await this.getTsxFiles();
|
|
634
|
+
for (const file of files) {
|
|
635
|
+
const content = fs.readFileSync(file, "utf8");
|
|
636
|
+
const lines = content.split("\n");
|
|
637
|
+
// Check for fetch/mutation without success feedback
|
|
638
|
+
for (let i = 0; i < lines.length; i++) {
|
|
639
|
+
const line = lines[i];
|
|
640
|
+
// Look for async handlers that might need feedback
|
|
641
|
+
if (/async.*=>\s*\{/.test(line) && /fetch\(|\.mutate|\.mutateAsync/.test(content)) {
|
|
642
|
+
const fnBlock = content.substring(content.indexOf(line), Math.min(content.indexOf(line) + 500, content.length));
|
|
643
|
+
// Check if there's success feedback
|
|
644
|
+
const hasSuccessFeedback = /success\(|toast\.|showToast|setSuccess|onSuccess/.test(fnBlock);
|
|
645
|
+
const hasErrorFeedback = /catch|\.catch|error\(|showError|onError/.test(fnBlock);
|
|
646
|
+
// Only flag if it has error handling but no success feedback (intentional pattern check)
|
|
647
|
+
if (hasErrorFeedback && !hasSuccessFeedback && /response\.ok|\.then/.test(fnBlock)) {
|
|
648
|
+
// Skip if it's clearly a background operation
|
|
649
|
+
if (!/sync|refresh|poll|interval/i.test(fnBlock)) {
|
|
650
|
+
issues.push({
|
|
651
|
+
file,
|
|
652
|
+
line: i + 1,
|
|
653
|
+
type: "async-no-success-feedback",
|
|
654
|
+
severity: "info",
|
|
655
|
+
message: "Async operation has error handling but no visible success feedback",
|
|
656
|
+
suggestion: "Add success toast/notification after successful operations",
|
|
657
|
+
snippet: line.trim().substring(0, 60),
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return {
|
|
665
|
+
name: "Async Operation Feedback",
|
|
666
|
+
passed: true, // Info only
|
|
667
|
+
blocking: false,
|
|
668
|
+
issues,
|
|
669
|
+
duration: Date.now() - startTime,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* 10. Transition Property Consistency
|
|
674
|
+
* Ensure transition properties are consistent (all vs specific)
|
|
675
|
+
*/
|
|
676
|
+
async checkTransitionConsistency() {
|
|
677
|
+
const startTime = Date.now();
|
|
678
|
+
const issues = [];
|
|
679
|
+
const files = await this.getTsxFiles();
|
|
680
|
+
const transitionTypes = new Map();
|
|
681
|
+
for (const file of files) {
|
|
682
|
+
const content = fs.readFileSync(file, "utf8");
|
|
683
|
+
const lines = content.split("\n");
|
|
684
|
+
for (let i = 0; i < lines.length; i++) {
|
|
685
|
+
const line = lines[i];
|
|
686
|
+
// Track transition-all vs specific transitions
|
|
687
|
+
if (/transition-all/.test(line)) {
|
|
688
|
+
transitionTypes.set("all", (transitionTypes.get("all") || 0) + 1);
|
|
689
|
+
}
|
|
690
|
+
if (/transition-(?:colors|opacity|shadow|transform)/.test(line)) {
|
|
691
|
+
const match = line.match(/transition-(colors|opacity|shadow|transform)/);
|
|
692
|
+
if (match) {
|
|
693
|
+
transitionTypes.set(match[1], (transitionTypes.get(match[1]) || 0) + 1);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
// Check for transition-all on performance-critical elements
|
|
697
|
+
if (/transition-all/.test(line) && /<(?:img|Image|video|canvas)/i.test(line)) {
|
|
698
|
+
issues.push({
|
|
699
|
+
file,
|
|
700
|
+
line: i + 1,
|
|
701
|
+
type: "transition-all-media",
|
|
702
|
+
severity: "warning",
|
|
703
|
+
message: "transition-all on media element may cause performance issues",
|
|
704
|
+
suggestion: "Use specific transitions: transition-opacity or transition-transform",
|
|
705
|
+
snippet: line.trim().substring(0, 80),
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
// Report if mixed patterns
|
|
711
|
+
if (transitionTypes.size > 3) {
|
|
712
|
+
const sorted = [...transitionTypes.entries()].sort((a, b) => b[1] - a[1]);
|
|
713
|
+
issues.push({
|
|
714
|
+
file: "codebase-wide",
|
|
715
|
+
line: 0,
|
|
716
|
+
type: "transition-inconsistency",
|
|
717
|
+
severity: "info",
|
|
718
|
+
message: `Multiple transition patterns detected`,
|
|
719
|
+
suggestion: `Consider standardizing. Usage: ${sorted.map(([t, c]) => `${t}(${c})`).join(", ")}`,
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
return {
|
|
723
|
+
name: "Transition Consistency",
|
|
724
|
+
passed: issues.filter((i) => i.severity === "error").length === 0,
|
|
725
|
+
blocking: false,
|
|
726
|
+
issues,
|
|
727
|
+
duration: Date.now() - startTime,
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* 11. Touch/Gesture Feedback
|
|
732
|
+
* Check for mobile touch states and gesture feedback
|
|
733
|
+
*/
|
|
734
|
+
async checkTouchGestureFeedback() {
|
|
735
|
+
const startTime = Date.now();
|
|
736
|
+
const issues = [];
|
|
737
|
+
const files = await this.getTsxFiles();
|
|
738
|
+
for (const file of files) {
|
|
739
|
+
const content = fs.readFileSync(file, "utf8");
|
|
740
|
+
const lines = content.split("\n");
|
|
741
|
+
for (let i = 0; i < lines.length; i++) {
|
|
742
|
+
const line = lines[i];
|
|
743
|
+
// Check for touch-action usage consistency
|
|
744
|
+
if (/touch-action/.test(line)) {
|
|
745
|
+
const touchActionMatch = line.match(/touch-action[:\s]+["']?(\w+)/);
|
|
746
|
+
if (touchActionMatch) {
|
|
747
|
+
// touch-action: none can cause accessibility issues
|
|
748
|
+
if (touchActionMatch[1] === "none") {
|
|
749
|
+
issues.push({
|
|
750
|
+
file,
|
|
751
|
+
line: i + 1,
|
|
752
|
+
type: "touch-action-none",
|
|
753
|
+
severity: "warning",
|
|
754
|
+
message: "touch-action: none disables all touch gestures",
|
|
755
|
+
suggestion: "Use touch-action: pan-x, pan-y, or manipulation instead for better UX",
|
|
756
|
+
snippet: line.trim().substring(0, 80),
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
// Check for tap-highlight removal without alternative
|
|
762
|
+
if (/-webkit-tap-highlight-color:\s*transparent/.test(line)) {
|
|
763
|
+
const contextBlock = lines.slice(i, Math.min(i + 5, lines.length)).join("\n");
|
|
764
|
+
const hasActiveFeedback = /active:|:active/.test(contextBlock);
|
|
765
|
+
if (!hasActiveFeedback) {
|
|
766
|
+
issues.push({
|
|
767
|
+
file,
|
|
768
|
+
line: i + 1,
|
|
769
|
+
type: "tap-highlight-no-alternative",
|
|
770
|
+
severity: "info",
|
|
771
|
+
message: "Tap highlight removed without alternative touch feedback",
|
|
772
|
+
suggestion: "Add active:bg-* or active:scale-* for touch feedback",
|
|
773
|
+
snippet: line.trim().substring(0, 80),
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
// Check for swipeable elements without visual affordance
|
|
778
|
+
if (/onTouchStart|onSwipe|swipeable|Swipeable/.test(line)) {
|
|
779
|
+
const contextBlock = lines.slice(Math.max(0, i - 5), Math.min(i + 10, lines.length)).join("\n");
|
|
780
|
+
const hasSwipeIndicator = /swipe|drag|arrow|chevron|indicator/i.test(contextBlock);
|
|
781
|
+
if (!hasSwipeIndicator) {
|
|
782
|
+
issues.push({
|
|
783
|
+
file,
|
|
784
|
+
line: i + 1,
|
|
785
|
+
type: "swipe-no-affordance",
|
|
786
|
+
severity: "info",
|
|
787
|
+
message: "Swipeable element may lack visual affordance",
|
|
788
|
+
suggestion: "Add visual indicators (arrows, dots) to show swipe capability",
|
|
789
|
+
snippet: line.trim().substring(0, 80),
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
return {
|
|
796
|
+
name: "Touch/Gesture Feedback",
|
|
797
|
+
passed: issues.filter((i) => i.severity === "error").length === 0,
|
|
798
|
+
blocking: false,
|
|
799
|
+
issues,
|
|
800
|
+
duration: Date.now() - startTime,
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* 12. Scroll Snap Consistency
|
|
805
|
+
* Check for consistent scroll-snap patterns
|
|
806
|
+
*/
|
|
807
|
+
async checkScrollSnapConsistency() {
|
|
808
|
+
const startTime = Date.now();
|
|
809
|
+
const issues = [];
|
|
810
|
+
const files = await this.getTsxFiles();
|
|
811
|
+
const scrollSnapTypes = new Map();
|
|
812
|
+
for (const file of files) {
|
|
813
|
+
const content = fs.readFileSync(file, "utf8");
|
|
814
|
+
const lines = content.split("\n");
|
|
815
|
+
for (let i = 0; i < lines.length; i++) {
|
|
816
|
+
const line = lines[i];
|
|
817
|
+
// Track scroll-snap-type patterns
|
|
818
|
+
const snapTypeMatch = line.match(/scroll-snap-type[:\s]+["']?([^;"'\s}]+)/);
|
|
819
|
+
if (snapTypeMatch) {
|
|
820
|
+
const type = snapTypeMatch[1];
|
|
821
|
+
scrollSnapTypes.set(type, (scrollSnapTypes.get(type) || 0) + 1);
|
|
822
|
+
}
|
|
823
|
+
// Check for snap container without snap children
|
|
824
|
+
if (/snap-(?:x|y|both|mandatory|proximity)/.test(line)) {
|
|
825
|
+
const contextBlock = lines.slice(i, Math.min(i + 15, lines.length)).join("\n");
|
|
826
|
+
const hasSnapChildren = /snap-(?:start|center|end|align)/.test(contextBlock);
|
|
827
|
+
if (!hasSnapChildren) {
|
|
828
|
+
issues.push({
|
|
829
|
+
file,
|
|
830
|
+
line: i + 1,
|
|
831
|
+
type: "snap-container-no-children",
|
|
832
|
+
severity: "info",
|
|
833
|
+
message: "Scroll snap container may lack snap-aligned children",
|
|
834
|
+
suggestion: "Add snap-start, snap-center, or snap-end to child elements",
|
|
835
|
+
snippet: line.trim().substring(0, 80),
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
// Check for scroll-behavior: smooth
|
|
840
|
+
if (/scroll-behavior:\s*smooth|scroll-smooth/.test(line)) {
|
|
841
|
+
const contextBlock = content.substring(Math.max(0, content.indexOf(line) - 200), content.indexOf(line) + 200);
|
|
842
|
+
// Should respect reduced motion
|
|
843
|
+
if (!/motion-reduce|prefers-reduced-motion/.test(contextBlock)) {
|
|
844
|
+
issues.push({
|
|
845
|
+
file,
|
|
846
|
+
line: i + 1,
|
|
847
|
+
type: "smooth-scroll-no-motion-check",
|
|
848
|
+
severity: "info",
|
|
849
|
+
message: "Smooth scroll without reduced-motion consideration",
|
|
850
|
+
suggestion: "Add motion-reduce:scroll-auto for accessibility",
|
|
851
|
+
snippet: line.trim().substring(0, 80),
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
// Report inconsistent snap types
|
|
858
|
+
if (scrollSnapTypes.size > 2) {
|
|
859
|
+
const sorted = [...scrollSnapTypes.entries()].sort((a, b) => b[1] - a[1]);
|
|
860
|
+
issues.push({
|
|
861
|
+
file: "codebase-wide",
|
|
862
|
+
line: 0,
|
|
863
|
+
type: "scroll-snap-inconsistency",
|
|
864
|
+
severity: "info",
|
|
865
|
+
message: `${scrollSnapTypes.size} different scroll-snap-type values detected`,
|
|
866
|
+
suggestion: `Standardize snap behavior. Usage: ${sorted.map(([t, c]) => `${t}(${c})`).join(", ")}`,
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
return {
|
|
870
|
+
name: "Scroll Snap Consistency",
|
|
871
|
+
passed: true, // Info only
|
|
872
|
+
blocking: false,
|
|
873
|
+
issues,
|
|
874
|
+
duration: Date.now() - startTime,
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* 13. Drag and Drop Feedback
|
|
879
|
+
* Check for DnD visual indicators and feedback
|
|
880
|
+
*/
|
|
881
|
+
async checkDragDropFeedback() {
|
|
882
|
+
const startTime = Date.now();
|
|
883
|
+
const issues = [];
|
|
884
|
+
const files = await this.getTsxFiles();
|
|
885
|
+
for (const file of files) {
|
|
886
|
+
const content = fs.readFileSync(file, "utf8");
|
|
887
|
+
const lines = content.split("\n");
|
|
888
|
+
for (let i = 0; i < lines.length; i++) {
|
|
889
|
+
const line = lines[i];
|
|
890
|
+
// Check for draggable without visual feedback
|
|
891
|
+
if (/draggable[=:]|onDragStart|useDrag|Draggable/.test(line)) {
|
|
892
|
+
const contextBlock = lines.slice(i, Math.min(i + 15, lines.length)).join("\n");
|
|
893
|
+
// Check for drag cursor
|
|
894
|
+
const hasDragCursor = /cursor-(?:grab|move|grabbing)/.test(contextBlock);
|
|
895
|
+
if (!hasDragCursor) {
|
|
896
|
+
issues.push({
|
|
897
|
+
file,
|
|
898
|
+
line: i + 1,
|
|
899
|
+
type: "draggable-no-cursor",
|
|
900
|
+
severity: "info",
|
|
901
|
+
message: "Draggable element without grab cursor",
|
|
902
|
+
suggestion: "Add cursor-grab and active:cursor-grabbing for drag feedback",
|
|
903
|
+
snippet: line.trim().substring(0, 80),
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
// Check for drag visual feedback (opacity, scale, shadow)
|
|
907
|
+
const hasDragVisual = /dragging|isDragging|opacity|shadow|scale/.test(contextBlock);
|
|
908
|
+
if (!hasDragVisual) {
|
|
909
|
+
issues.push({
|
|
910
|
+
file,
|
|
911
|
+
line: i + 1,
|
|
912
|
+
type: "draggable-no-visual",
|
|
913
|
+
severity: "info",
|
|
914
|
+
message: "Draggable element may lack visual drag state",
|
|
915
|
+
suggestion: "Add opacity-50 or shadow-lg during drag for visual feedback",
|
|
916
|
+
snippet: line.trim().substring(0, 80),
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
// Check for drop zone visual feedback
|
|
921
|
+
if (/onDrop|onDragOver|onDragEnter|useDrop|Droppable/.test(line)) {
|
|
922
|
+
const contextBlock = lines.slice(i, Math.min(i + 15, lines.length)).join("\n");
|
|
923
|
+
const hasDropVisual = /isOver|isDragOver|dragOver|drop.*highlight|border.*dashed/.test(contextBlock);
|
|
924
|
+
if (!hasDropVisual) {
|
|
925
|
+
issues.push({
|
|
926
|
+
file,
|
|
927
|
+
line: i + 1,
|
|
928
|
+
type: "dropzone-no-visual",
|
|
929
|
+
severity: "info",
|
|
930
|
+
message: "Drop zone may lack visual feedback on drag over",
|
|
931
|
+
suggestion: "Add border highlight or background change when dragging over",
|
|
932
|
+
snippet: line.trim().substring(0, 80),
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
return {
|
|
939
|
+
name: "Drag and Drop Feedback",
|
|
940
|
+
passed: true, // Info only
|
|
941
|
+
blocking: false,
|
|
942
|
+
issues,
|
|
943
|
+
duration: Date.now() - startTime,
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* 14. Input Validation Feedback
|
|
948
|
+
* Check for real-time validation visual feedback
|
|
949
|
+
*/
|
|
950
|
+
async checkInputValidationFeedback() {
|
|
951
|
+
const startTime = Date.now();
|
|
952
|
+
const issues = [];
|
|
953
|
+
const files = await this.getTsxFiles();
|
|
954
|
+
for (const file of files) {
|
|
955
|
+
const content = fs.readFileSync(file, "utf8");
|
|
956
|
+
const lines = content.split("\n");
|
|
957
|
+
for (let i = 0; i < lines.length; i++) {
|
|
958
|
+
const line = lines[i];
|
|
959
|
+
// Check for form validation without visual feedback
|
|
960
|
+
if (/(?:errors|error)\[["']?\w+["']?\]|formState\.errors|fieldState\.error/.test(line)) {
|
|
961
|
+
const contextBlock = lines.slice(i, Math.min(i + 10, lines.length)).join("\n");
|
|
962
|
+
// Check for error visual styling
|
|
963
|
+
const hasErrorStyling = /error|destructive|red|border-.*error|text-.*error|ring-.*error/.test(contextBlock);
|
|
964
|
+
if (!hasErrorStyling) {
|
|
965
|
+
issues.push({
|
|
966
|
+
file,
|
|
967
|
+
line: i + 1,
|
|
968
|
+
type: "validation-no-visual",
|
|
969
|
+
severity: "info",
|
|
970
|
+
message: "Form validation error without visual feedback",
|
|
971
|
+
suggestion: "Add red border or text color for validation errors",
|
|
972
|
+
snippet: line.trim().substring(0, 80),
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
// Check for error shake animation
|
|
976
|
+
const hasShakeAnimation = /shake|animate-shake|wiggle/.test(contextBlock);
|
|
977
|
+
// This is optional but nice to have - not flagging as issue
|
|
978
|
+
}
|
|
979
|
+
// Check for success validation feedback (e.g., checkmark)
|
|
980
|
+
if (/isValid|isValidating|validation.*success/.test(line)) {
|
|
981
|
+
const contextBlock = lines.slice(i, Math.min(i + 10, lines.length)).join("\n");
|
|
982
|
+
const hasSuccessIndicator = /check|success|green|valid.*icon/.test(contextBlock);
|
|
983
|
+
// Optional - good to have but not required
|
|
984
|
+
}
|
|
985
|
+
// Check for TextField/Input with error prop
|
|
986
|
+
if (/<(?:TextField|Input)[^>]*error[=:]/.test(line)) {
|
|
987
|
+
const contextBlock = lines.slice(i, Math.min(i + 5, lines.length)).join("\n");
|
|
988
|
+
const hasHelperText = /helperText|FormHelperText|error.*message/.test(contextBlock);
|
|
989
|
+
if (!hasHelperText) {
|
|
990
|
+
issues.push({
|
|
991
|
+
file,
|
|
992
|
+
line: i + 1,
|
|
993
|
+
type: "input-error-no-message",
|
|
994
|
+
severity: "info",
|
|
995
|
+
message: "Input with error state but no helper text",
|
|
996
|
+
suggestion: "Add helperText to explain the validation error",
|
|
997
|
+
snippet: line.trim().substring(0, 80),
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
return {
|
|
1004
|
+
name: "Input Validation Feedback",
|
|
1005
|
+
passed: true, // Info only
|
|
1006
|
+
blocking: false,
|
|
1007
|
+
issues,
|
|
1008
|
+
duration: Date.now() - startTime,
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* 15. Toast/Notification Timing
|
|
1013
|
+
* Check for consistent notification durations
|
|
1014
|
+
*/
|
|
1015
|
+
async checkToastNotificationTiming() {
|
|
1016
|
+
const startTime = Date.now();
|
|
1017
|
+
const issues = [];
|
|
1018
|
+
const files = await this.getTsxFiles();
|
|
1019
|
+
// Standard durations (in ms)
|
|
1020
|
+
const STANDARD_DURATIONS = [3000, 5000, 7000, 10000];
|
|
1021
|
+
const toastDurations = new Map();
|
|
1022
|
+
for (const file of files) {
|
|
1023
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1024
|
+
const lines = content.split("\n");
|
|
1025
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1026
|
+
const line = lines[i];
|
|
1027
|
+
// Check for toast/notification duration
|
|
1028
|
+
const durationMatch = line.match(/(?:duration|autoHideDuration|timeout)[=:]\s*\{?\s*(\d+)/);
|
|
1029
|
+
if (durationMatch && /toast|notification|snackbar|alert/i.test(content.substring(Math.max(0, content.indexOf(line) - 100), content.indexOf(line) + 100))) {
|
|
1030
|
+
const duration = parseInt(durationMatch[1]);
|
|
1031
|
+
toastDurations.set(duration, (toastDurations.get(duration) || 0) + 1);
|
|
1032
|
+
// Check for very short durations (< 2s)
|
|
1033
|
+
if (duration < 2000) {
|
|
1034
|
+
issues.push({
|
|
1035
|
+
file,
|
|
1036
|
+
line: i + 1,
|
|
1037
|
+
type: "toast-too-short",
|
|
1038
|
+
severity: "warning",
|
|
1039
|
+
message: `Toast duration ${duration}ms is too short to read`,
|
|
1040
|
+
suggestion: "Use at least 3000ms (3s) for readable notifications",
|
|
1041
|
+
snippet: line.trim().substring(0, 80),
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
// Check for very long durations (> 15s) without dismiss
|
|
1045
|
+
if (duration > 15000) {
|
|
1046
|
+
const contextBlock = lines.slice(i, Math.min(i + 10, lines.length)).join("\n");
|
|
1047
|
+
const hasDismiss = /dismiss|close|onClose/.test(contextBlock);
|
|
1048
|
+
if (!hasDismiss) {
|
|
1049
|
+
issues.push({
|
|
1050
|
+
file,
|
|
1051
|
+
line: i + 1,
|
|
1052
|
+
type: "toast-too-long-no-dismiss",
|
|
1053
|
+
severity: "info",
|
|
1054
|
+
message: `Toast duration ${duration}ms is long - ensure user can dismiss`,
|
|
1055
|
+
suggestion: "Add dismiss button for long-duration notifications",
|
|
1056
|
+
snippet: line.trim().substring(0, 80),
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
// Check for non-standard durations
|
|
1061
|
+
if (!STANDARD_DURATIONS.includes(duration) && duration >= 2000 && duration <= 15000) {
|
|
1062
|
+
issues.push({
|
|
1063
|
+
file,
|
|
1064
|
+
line: i + 1,
|
|
1065
|
+
type: "toast-non-standard-duration",
|
|
1066
|
+
severity: "info",
|
|
1067
|
+
message: `Non-standard toast duration: ${duration}ms`,
|
|
1068
|
+
suggestion: `Use standard durations: ${STANDARD_DURATIONS.join(", ")}ms`,
|
|
1069
|
+
snippet: line.trim().substring(0, 80),
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
// Check for success toast after action
|
|
1074
|
+
if (/success\(|showSuccess|toast\.success/.test(line)) {
|
|
1075
|
+
// Good - has success feedback
|
|
1076
|
+
}
|
|
1077
|
+
// Check for error toast persistence
|
|
1078
|
+
if (/error\(|showError|toast\.error/.test(line)) {
|
|
1079
|
+
const contextBlock = lines.slice(i, Math.min(i + 5, lines.length)).join("\n");
|
|
1080
|
+
const hasLongerDuration = /duration.*[5-9]\d{3}|duration.*1\d{4}|persistent|autoHide.*false/.test(contextBlock);
|
|
1081
|
+
if (!hasLongerDuration) {
|
|
1082
|
+
issues.push({
|
|
1083
|
+
file,
|
|
1084
|
+
line: i + 1,
|
|
1085
|
+
type: "error-toast-short",
|
|
1086
|
+
severity: "info",
|
|
1087
|
+
message: "Error notification may auto-dismiss too quickly",
|
|
1088
|
+
suggestion: "Error toasts should stay longer (5-7s) or be manually dismissable",
|
|
1089
|
+
snippet: line.trim().substring(0, 80),
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
// Report inconsistent durations
|
|
1096
|
+
if (toastDurations.size > 3) {
|
|
1097
|
+
const sorted = [...toastDurations.entries()].sort((a, b) => b[1] - a[1]);
|
|
1098
|
+
issues.push({
|
|
1099
|
+
file: "codebase-wide",
|
|
1100
|
+
line: 0,
|
|
1101
|
+
type: "toast-duration-inconsistency",
|
|
1102
|
+
severity: "info",
|
|
1103
|
+
message: `${toastDurations.size} different toast durations detected`,
|
|
1104
|
+
suggestion: `Standardize on 2-3 durations. Usage: ${sorted.map(([d, c]) => `${d}ms(${c})`).join(", ")}`,
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
return {
|
|
1108
|
+
name: "Toast/Notification Timing",
|
|
1109
|
+
passed: issues.filter((i) => i.severity === "error").length === 0,
|
|
1110
|
+
blocking: false,
|
|
1111
|
+
issues,
|
|
1112
|
+
duration: Date.now() - startTime,
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* 16. Skeleton-to-Content Match
|
|
1117
|
+
* Check that skeleton sizes match actual loaded content
|
|
1118
|
+
*/
|
|
1119
|
+
async checkSkeletonContentMatch() {
|
|
1120
|
+
const startTime = Date.now();
|
|
1121
|
+
const issues = [];
|
|
1122
|
+
const files = await this.getTsxFiles();
|
|
1123
|
+
const skeletonDimensions = new Map();
|
|
1124
|
+
for (const file of files) {
|
|
1125
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1126
|
+
const lines = content.split("\n");
|
|
1127
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1128
|
+
const line = lines[i];
|
|
1129
|
+
if (/<Skeleton\b/.test(line)) {
|
|
1130
|
+
const contextBlock = lines.slice(i, Math.min(i + 5, lines.length)).join("\n");
|
|
1131
|
+
const widthMatch = contextBlock.match(/(?:width|w)=["'{]\s*(\d+|[\w-]+)/);
|
|
1132
|
+
const heightMatch = contextBlock.match(/(?:height|h)=["'{]\s*(\d+|[\w-]+)/);
|
|
1133
|
+
if (widthMatch || heightMatch) {
|
|
1134
|
+
const dim = `${widthMatch?.[1] || "auto"}x${heightMatch?.[1] || "auto"}`;
|
|
1135
|
+
const existing = skeletonDimensions.get(dim) || [];
|
|
1136
|
+
existing.push({ file, line: i + 1 });
|
|
1137
|
+
skeletonDimensions.set(dim, existing);
|
|
1138
|
+
}
|
|
1139
|
+
if (!widthMatch && !heightMatch && !/className=["'][^"']*(?:w-|h-)/.test(contextBlock)) {
|
|
1140
|
+
issues.push({
|
|
1141
|
+
file,
|
|
1142
|
+
line: i + 1,
|
|
1143
|
+
type: "skeleton-no-dimensions",
|
|
1144
|
+
severity: "info",
|
|
1145
|
+
message: "Skeleton without explicit dimensions may cause layout shift",
|
|
1146
|
+
suggestion: "Add width/height props or Tailwind w-/h- classes matching content size",
|
|
1147
|
+
snippet: line.trim().substring(0, 80),
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
if (skeletonDimensions.size > 10) {
|
|
1154
|
+
issues.push({
|
|
1155
|
+
file: "codebase-wide",
|
|
1156
|
+
line: 0,
|
|
1157
|
+
type: "many-skeleton-dimensions",
|
|
1158
|
+
severity: "info",
|
|
1159
|
+
message: `${skeletonDimensions.size} different skeleton dimension combinations detected`,
|
|
1160
|
+
suggestion: "Consider standardizing skeleton sizes for consistency",
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
return {
|
|
1164
|
+
name: "Skeleton-to-Content Match",
|
|
1165
|
+
passed: true,
|
|
1166
|
+
blocking: false,
|
|
1167
|
+
issues,
|
|
1168
|
+
duration: Date.now() - startTime,
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* 17. Empty State Patterns
|
|
1173
|
+
* Check for consistent empty state component usage
|
|
1174
|
+
*/
|
|
1175
|
+
async checkEmptyStatePatterns() {
|
|
1176
|
+
const startTime = Date.now();
|
|
1177
|
+
const issues = [];
|
|
1178
|
+
const files = await this.getTsxFiles();
|
|
1179
|
+
const emptyStatePatterns = new Map();
|
|
1180
|
+
for (const file of files) {
|
|
1181
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1182
|
+
const lines = content.split("\n");
|
|
1183
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1184
|
+
const line = lines[i];
|
|
1185
|
+
if (/(?:data|items|results|list)(?:\?)?\.length\s*===?\s*0|isEmpty|noData|noResults/i.test(line)) {
|
|
1186
|
+
const contextBlock = lines.slice(i, Math.min(i + 10, lines.length)).join("\n");
|
|
1187
|
+
const usesEmptyStateComponent = /<EmptyState|<NoData|<NoResults|<Empty\b/i.test(contextBlock);
|
|
1188
|
+
const hasInlineEmpty = /No\s+\w+\s+(?:found|available|yet)|Nothing\s+to\s+show|Empty/i.test(contextBlock);
|
|
1189
|
+
const hasIcon = /<(?:Icon|SvgIcon|\w+Icon)|📭|🔍|📋/.test(contextBlock);
|
|
1190
|
+
const hasAction = /<Button|onClick|href=/.test(contextBlock);
|
|
1191
|
+
if (!usesEmptyStateComponent && hasInlineEmpty) {
|
|
1192
|
+
emptyStatePatterns.set("inline-text", (emptyStatePatterns.get("inline-text") || 0) + 1);
|
|
1193
|
+
if (!hasIcon) {
|
|
1194
|
+
issues.push({
|
|
1195
|
+
file,
|
|
1196
|
+
line: i + 1,
|
|
1197
|
+
type: "empty-state-no-icon",
|
|
1198
|
+
severity: "info",
|
|
1199
|
+
message: "Empty state without visual icon",
|
|
1200
|
+
suggestion: "Add an icon to make empty states more visually distinct",
|
|
1201
|
+
snippet: line.trim().substring(0, 80),
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
if (!hasAction) {
|
|
1205
|
+
issues.push({
|
|
1206
|
+
file,
|
|
1207
|
+
line: i + 1,
|
|
1208
|
+
type: "empty-state-no-action",
|
|
1209
|
+
severity: "info",
|
|
1210
|
+
message: "Empty state without call-to-action",
|
|
1211
|
+
suggestion: "Consider adding a button or link to help users take action",
|
|
1212
|
+
snippet: line.trim().substring(0, 80),
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
else if (usesEmptyStateComponent) {
|
|
1217
|
+
emptyStatePatterns.set("component", (emptyStatePatterns.get("component") || 0) + 1);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
if (emptyStatePatterns.get("inline-text") && emptyStatePatterns.get("component")) {
|
|
1223
|
+
issues.push({
|
|
1224
|
+
file: "codebase-wide",
|
|
1225
|
+
line: 0,
|
|
1226
|
+
type: "mixed-empty-state-patterns",
|
|
1227
|
+
severity: "info",
|
|
1228
|
+
message: `Mixed empty state patterns: ${emptyStatePatterns.get("component")} components, ${emptyStatePatterns.get("inline-text")} inline`,
|
|
1229
|
+
suggestion: "Standardize on EmptyState component for consistency",
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
return {
|
|
1233
|
+
name: "Empty State Patterns",
|
|
1234
|
+
passed: true,
|
|
1235
|
+
blocking: false,
|
|
1236
|
+
issues,
|
|
1237
|
+
duration: Date.now() - startTime,
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* 18. Selection Feedback
|
|
1242
|
+
* Check for multi-select and checkbox visual states
|
|
1243
|
+
*/
|
|
1244
|
+
async checkSelectionFeedback() {
|
|
1245
|
+
const startTime = Date.now();
|
|
1246
|
+
const issues = [];
|
|
1247
|
+
const files = await this.getTsxFiles();
|
|
1248
|
+
for (const file of files) {
|
|
1249
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1250
|
+
const lines = content.split("\n");
|
|
1251
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1252
|
+
const line = lines[i];
|
|
1253
|
+
if (/selectedItems|selectedRows|selection|isSelected|onSelect/i.test(line)) {
|
|
1254
|
+
const contextBlock = lines.slice(i, Math.min(i + 15, lines.length)).join("\n");
|
|
1255
|
+
const hasSelectedStyle = /selected.*bg-|bg-.*selected|ring.*selected|border.*selected|data-\[selected\]/i.test(contextBlock);
|
|
1256
|
+
const hasCheckbox = /<Checkbox|<input.*type=["']checkbox/.test(contextBlock);
|
|
1257
|
+
if (!hasSelectedStyle && !hasCheckbox) {
|
|
1258
|
+
issues.push({
|
|
1259
|
+
file,
|
|
1260
|
+
line: i + 1,
|
|
1261
|
+
type: "selection-no-visual",
|
|
1262
|
+
severity: "info",
|
|
1263
|
+
message: "Selection logic without clear visual feedback",
|
|
1264
|
+
suggestion: "Add background color or border change for selected items",
|
|
1265
|
+
snippet: line.trim().substring(0, 80),
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
const hasMultiSelect = /selectedItems|selectedRows|Set<|new Set/.test(contextBlock);
|
|
1269
|
+
const hasSelectAll = /selectAll|toggleAll|Select All/i.test(content);
|
|
1270
|
+
if (hasMultiSelect && !hasSelectAll) {
|
|
1271
|
+
issues.push({
|
|
1272
|
+
file,
|
|
1273
|
+
line: i + 1,
|
|
1274
|
+
type: "multi-select-no-select-all",
|
|
1275
|
+
severity: "info",
|
|
1276
|
+
message: "Multi-select without Select All option",
|
|
1277
|
+
suggestion: "Add Select All checkbox for bulk operations",
|
|
1278
|
+
snippet: line.trim().substring(0, 80),
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
if (/<(?:tr|TableRow)[^>]*onClick/.test(line)) {
|
|
1283
|
+
const contextBlock = lines.slice(i, Math.min(i + 5, lines.length)).join("\n");
|
|
1284
|
+
if (!/hover:|cursor-pointer/.test(contextBlock)) {
|
|
1285
|
+
issues.push({
|
|
1286
|
+
file,
|
|
1287
|
+
line: i + 1,
|
|
1288
|
+
type: "clickable-row-no-hover",
|
|
1289
|
+
severity: "info",
|
|
1290
|
+
message: "Clickable table row without hover feedback",
|
|
1291
|
+
suggestion: "Add hover:bg-* and cursor-pointer for clickable rows",
|
|
1292
|
+
snippet: line.trim().substring(0, 80),
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
return {
|
|
1299
|
+
name: "Selection Feedback",
|
|
1300
|
+
passed: true,
|
|
1301
|
+
blocking: false,
|
|
1302
|
+
issues,
|
|
1303
|
+
duration: Date.now() - startTime,
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* 19. Accordion Animation
|
|
1308
|
+
* Check for consistent expand/collapse animations
|
|
1309
|
+
*/
|
|
1310
|
+
async checkAccordionAnimation() {
|
|
1311
|
+
const startTime = Date.now();
|
|
1312
|
+
const issues = [];
|
|
1313
|
+
const files = await this.getTsxFiles();
|
|
1314
|
+
const animationPatterns = new Map();
|
|
1315
|
+
for (const file of files) {
|
|
1316
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1317
|
+
const lines = content.split("\n");
|
|
1318
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1319
|
+
const line = lines[i];
|
|
1320
|
+
if (/<(?:Accordion|Collapsible|Disclosure|Expandable)\b/i.test(line) ||
|
|
1321
|
+
/isOpen|isExpanded|expanded|collapsed/i.test(line)) {
|
|
1322
|
+
const contextBlock = lines.slice(i, Math.min(i + 20, lines.length)).join("\n");
|
|
1323
|
+
const hasTransition = /transition-|animate-/.test(contextBlock);
|
|
1324
|
+
const hasMaxHeight = /max-h-|maxHeight/.test(contextBlock);
|
|
1325
|
+
const hasFramerMotion = /motion\.|AnimatePresence|variants/.test(contextBlock);
|
|
1326
|
+
const hasRadixAnimation = /data-\[state=/.test(contextBlock);
|
|
1327
|
+
if (hasTransition)
|
|
1328
|
+
animationPatterns.set("tailwind-transition", (animationPatterns.get("tailwind-transition") || 0) + 1);
|
|
1329
|
+
if (hasFramerMotion)
|
|
1330
|
+
animationPatterns.set("framer-motion", (animationPatterns.get("framer-motion") || 0) + 1);
|
|
1331
|
+
if (hasRadixAnimation)
|
|
1332
|
+
animationPatterns.set("radix-data-state", (animationPatterns.get("radix-data-state") || 0) + 1);
|
|
1333
|
+
if (/height.*transition|transition.*height/.test(contextBlock) && !hasMaxHeight) {
|
|
1334
|
+
issues.push({
|
|
1335
|
+
file,
|
|
1336
|
+
line: i + 1,
|
|
1337
|
+
type: "height-animation-no-max",
|
|
1338
|
+
severity: "info",
|
|
1339
|
+
message: "Height animation may need max-height for smooth transitions",
|
|
1340
|
+
suggestion: "Use max-h-0/max-h-[value] with overflow-hidden for smooth accordion",
|
|
1341
|
+
snippet: line.trim().substring(0, 80),
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
const hasConditionalRender = /\{.*&&|isOpen\s*\?|\{.*\?.*:/.test(contextBlock);
|
|
1345
|
+
if (hasConditionalRender && !hasTransition && !hasFramerMotion && !hasRadixAnimation) {
|
|
1346
|
+
issues.push({
|
|
1347
|
+
file,
|
|
1348
|
+
line: i + 1,
|
|
1349
|
+
type: "accordion-no-animation",
|
|
1350
|
+
severity: "info",
|
|
1351
|
+
message: "Collapsible content without animation",
|
|
1352
|
+
suggestion: "Add transition animation for smoother UX",
|
|
1353
|
+
snippet: line.trim().substring(0, 80),
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
if (animationPatterns.size > 1) {
|
|
1360
|
+
const sorted = [...animationPatterns.entries()].sort((a, b) => b[1] - a[1]);
|
|
1361
|
+
issues.push({
|
|
1362
|
+
file: "codebase-wide",
|
|
1363
|
+
line: 0,
|
|
1364
|
+
type: "mixed-accordion-animations",
|
|
1365
|
+
severity: "info",
|
|
1366
|
+
message: `Multiple accordion animation patterns detected`,
|
|
1367
|
+
suggestion: `Consider standardizing. Usage: ${sorted.map(([p, c]) => `${p}(${c})`).join(", ")}`,
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
return {
|
|
1371
|
+
name: "Accordion Animation",
|
|
1372
|
+
passed: true,
|
|
1373
|
+
blocking: false,
|
|
1374
|
+
issues,
|
|
1375
|
+
duration: Date.now() - startTime,
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* 20. Modal Entry/Exit Animation
|
|
1380
|
+
* Check for consistent modal animation patterns
|
|
1381
|
+
*/
|
|
1382
|
+
async checkModalAnimation() {
|
|
1383
|
+
const startTime = Date.now();
|
|
1384
|
+
const issues = [];
|
|
1385
|
+
const files = await this.getTsxFiles();
|
|
1386
|
+
const modalAnimations = new Map();
|
|
1387
|
+
for (const file of files) {
|
|
1388
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1389
|
+
const lines = content.split("\n");
|
|
1390
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1391
|
+
const line = lines[i];
|
|
1392
|
+
if (/<(?:Dialog|Modal|Sheet|Drawer|AlertDialog)\b/i.test(line)) {
|
|
1393
|
+
const contextBlock = lines.slice(i, Math.min(i + 30, lines.length)).join("\n");
|
|
1394
|
+
const hasTransition = /transition-|animate-/.test(contextBlock);
|
|
1395
|
+
const hasFramerMotion = /motion\.|AnimatePresence/.test(contextBlock);
|
|
1396
|
+
const hasRadixAnimation = /data-\[state=/.test(contextBlock);
|
|
1397
|
+
if (hasFramerMotion)
|
|
1398
|
+
modalAnimations.set("framer-motion", (modalAnimations.get("framer-motion") || 0) + 1);
|
|
1399
|
+
if (hasRadixAnimation)
|
|
1400
|
+
modalAnimations.set("radix", (modalAnimations.get("radix") || 0) + 1);
|
|
1401
|
+
if (hasTransition && !hasFramerMotion && !hasRadixAnimation) {
|
|
1402
|
+
modalAnimations.set("tailwind", (modalAnimations.get("tailwind") || 0) + 1);
|
|
1403
|
+
}
|
|
1404
|
+
const hasBackdrop = /backdrop|overlay|scrim/i.test(contextBlock);
|
|
1405
|
+
const hasBackdropAnimation = /backdrop.*(?:opacity|fade|transition)|(?:opacity|fade|transition).*backdrop/i.test(contextBlock);
|
|
1406
|
+
if (hasBackdrop && !hasBackdropAnimation && !hasRadixAnimation) {
|
|
1407
|
+
issues.push({
|
|
1408
|
+
file,
|
|
1409
|
+
line: i + 1,
|
|
1410
|
+
type: "modal-backdrop-no-animation",
|
|
1411
|
+
severity: "info",
|
|
1412
|
+
message: "Modal backdrop without fade animation",
|
|
1413
|
+
suggestion: "Add opacity transition to backdrop for polished feel",
|
|
1414
|
+
snippet: line.trim().substring(0, 80),
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
if (!hasRadixAnimation && !hasFramerMotion) {
|
|
1418
|
+
const hasExitAnimation = /exit|leave|closing|animate-out/i.test(contextBlock);
|
|
1419
|
+
if (hasTransition && !hasExitAnimation) {
|
|
1420
|
+
issues.push({
|
|
1421
|
+
file,
|
|
1422
|
+
line: i + 1,
|
|
1423
|
+
type: "modal-no-exit-animation",
|
|
1424
|
+
severity: "info",
|
|
1425
|
+
message: "Modal may lack exit animation",
|
|
1426
|
+
suggestion: "Add exit animation for smooth close transition",
|
|
1427
|
+
snippet: line.trim().substring(0, 80),
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
if (modalAnimations.size > 1) {
|
|
1435
|
+
const sorted = [...modalAnimations.entries()].sort((a, b) => b[1] - a[1]);
|
|
1436
|
+
issues.push({
|
|
1437
|
+
file: "codebase-wide",
|
|
1438
|
+
line: 0,
|
|
1439
|
+
type: "mixed-modal-animations",
|
|
1440
|
+
severity: "info",
|
|
1441
|
+
message: `Multiple modal animation patterns detected`,
|
|
1442
|
+
suggestion: `Consider standardizing. Usage: ${sorted.map(([p, c]) => `${p}(${c})`).join(", ")}`,
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
return {
|
|
1446
|
+
name: "Modal Entry/Exit Animation",
|
|
1447
|
+
passed: true,
|
|
1448
|
+
blocking: false,
|
|
1449
|
+
issues,
|
|
1450
|
+
duration: Date.now() - startTime,
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1454
|
+
* 21. Copy-to-Clipboard Feedback
|
|
1455
|
+
* Check for success indication after clipboard operations
|
|
1456
|
+
*/
|
|
1457
|
+
async checkCopyFeedback() {
|
|
1458
|
+
const startTime = Date.now();
|
|
1459
|
+
const issues = [];
|
|
1460
|
+
const files = await this.getTsxFiles();
|
|
1461
|
+
for (const file of files) {
|
|
1462
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1463
|
+
const lines = content.split("\n");
|
|
1464
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1465
|
+
const line = lines[i];
|
|
1466
|
+
if (/navigator\.clipboard|writeText|copyToClipboard|useCopy|copy\(/i.test(line)) {
|
|
1467
|
+
const contextBlock = lines.slice(i, Math.min(i + 15, lines.length)).join("\n");
|
|
1468
|
+
const hasToast = /toast|notification|snackbar/i.test(contextBlock);
|
|
1469
|
+
const hasIconChange = /copied|check|success|CheckIcon|ClipboardCheck/i.test(contextBlock);
|
|
1470
|
+
const hasTooltip = /tooltip|Tooltip|title=["']Copied/i.test(contextBlock);
|
|
1471
|
+
const hasStateChange = /setCopied|isCopied|copied.*true/.test(contextBlock);
|
|
1472
|
+
if (!hasToast && !hasIconChange && !hasTooltip && !hasStateChange) {
|
|
1473
|
+
issues.push({
|
|
1474
|
+
file,
|
|
1475
|
+
line: i + 1,
|
|
1476
|
+
type: "copy-no-feedback",
|
|
1477
|
+
severity: "warning",
|
|
1478
|
+
message: "Clipboard copy without visual feedback",
|
|
1479
|
+
suggestion: "Add toast, icon change, or tooltip to confirm copy action",
|
|
1480
|
+
snippet: line.trim().substring(0, 80),
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
if (hasStateChange) {
|
|
1484
|
+
const hasTimeout = /setTimeout|useTimeout|delay/.test(contextBlock);
|
|
1485
|
+
if (!hasTimeout) {
|
|
1486
|
+
issues.push({
|
|
1487
|
+
file,
|
|
1488
|
+
line: i + 1,
|
|
1489
|
+
type: "copy-state-no-reset",
|
|
1490
|
+
severity: "info",
|
|
1491
|
+
message: "Copy state may not reset after showing 'Copied'",
|
|
1492
|
+
suggestion: "Reset copied state after 2-3 seconds with setTimeout",
|
|
1493
|
+
snippet: line.trim().substring(0, 80),
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
return {
|
|
1501
|
+
name: "Copy-to-Clipboard Feedback",
|
|
1502
|
+
passed: issues.filter((i) => i.severity === "error").length === 0,
|
|
1503
|
+
blocking: false,
|
|
1504
|
+
issues,
|
|
1505
|
+
duration: Date.now() - startTime,
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* 22. Infinite Scroll Loading
|
|
1510
|
+
* Check for loading indicators at scroll boundaries
|
|
1511
|
+
*/
|
|
1512
|
+
async checkInfiniteScrollLoading() {
|
|
1513
|
+
const startTime = Date.now();
|
|
1514
|
+
const issues = [];
|
|
1515
|
+
const files = await this.getTsxFiles();
|
|
1516
|
+
for (const file of files) {
|
|
1517
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1518
|
+
const lines = content.split("\n");
|
|
1519
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1520
|
+
const line = lines[i];
|
|
1521
|
+
if (/useInfiniteQuery|InfiniteScroll|useInfiniteScroll|fetchNextPage|hasNextPage/i.test(line)) {
|
|
1522
|
+
const contextBlock = lines.slice(i, Math.min(i + 30, lines.length)).join("\n");
|
|
1523
|
+
const hasLoadingIndicator = /isFetchingNextPage|loadingMore|isLoadingMore|loading.*spinner|CircularProgress|Spinner/i.test(contextBlock);
|
|
1524
|
+
const hasLoadMoreButton = /Load\s*More|Show\s*More|loadMore/i.test(contextBlock);
|
|
1525
|
+
if (!hasLoadingIndicator && !hasLoadMoreButton) {
|
|
1526
|
+
issues.push({
|
|
1527
|
+
file,
|
|
1528
|
+
line: i + 1,
|
|
1529
|
+
type: "infinite-scroll-no-loading",
|
|
1530
|
+
severity: "warning",
|
|
1531
|
+
message: "Infinite scroll without visible loading indicator",
|
|
1532
|
+
suggestion: "Show spinner or skeleton when fetching next page",
|
|
1533
|
+
snippet: line.trim().substring(0, 80),
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
const hasEndIndicator = /hasNextPage.*false|no.*more|end.*of.*list|reached.*end/i.test(contextBlock);
|
|
1537
|
+
if (!hasEndIndicator) {
|
|
1538
|
+
issues.push({
|
|
1539
|
+
file,
|
|
1540
|
+
line: i + 1,
|
|
1541
|
+
type: "infinite-scroll-no-end",
|
|
1542
|
+
severity: "info",
|
|
1543
|
+
message: "Infinite scroll may not indicate when all items are loaded",
|
|
1544
|
+
suggestion: "Show 'No more items' message when hasNextPage is false",
|
|
1545
|
+
snippet: line.trim().substring(0, 80),
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
return {
|
|
1552
|
+
name: "Infinite Scroll Loading",
|
|
1553
|
+
passed: issues.filter((i) => i.severity === "error").length === 0,
|
|
1554
|
+
blocking: false,
|
|
1555
|
+
issues,
|
|
1556
|
+
duration: Date.now() - startTime,
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
/**
|
|
1560
|
+
* 23. Filter Active State
|
|
1561
|
+
* Check for visual feedback when filters are applied
|
|
1562
|
+
*/
|
|
1563
|
+
async checkFilterActiveState() {
|
|
1564
|
+
const startTime = Date.now();
|
|
1565
|
+
const issues = [];
|
|
1566
|
+
const files = await this.getTsxFiles();
|
|
1567
|
+
for (const file of files) {
|
|
1568
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1569
|
+
const lines = content.split("\n");
|
|
1570
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1571
|
+
const line = lines[i];
|
|
1572
|
+
if (/filters?State|activeFilters?|appliedFilters?|filterValues?/i.test(line)) {
|
|
1573
|
+
const contextBlock = lines.slice(i, Math.min(i + 30, lines.length)).join("\n");
|
|
1574
|
+
const hasFilterCount = /filterCount|activeCount|badge|\.length|\.size/i.test(contextBlock);
|
|
1575
|
+
const hasBadgeComponent = /<Badge|<Chip|<Tag|count=/i.test(contextBlock);
|
|
1576
|
+
if (hasFilterCount && !hasBadgeComponent) {
|
|
1577
|
+
issues.push({
|
|
1578
|
+
file,
|
|
1579
|
+
line: i + 1,
|
|
1580
|
+
type: "filter-count-no-badge",
|
|
1581
|
+
severity: "info",
|
|
1582
|
+
message: "Filter count tracked but may not be visually displayed",
|
|
1583
|
+
suggestion: "Show badge with active filter count on filter button",
|
|
1584
|
+
snippet: line.trim().substring(0, 80),
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
const hasClearAll = /clearAll|resetFilters?|clearFilters?|remove.*all/i.test(contextBlock);
|
|
1588
|
+
if (!hasClearAll) {
|
|
1589
|
+
issues.push({
|
|
1590
|
+
file,
|
|
1591
|
+
line: i + 1,
|
|
1592
|
+
type: "no-clear-all-filters",
|
|
1593
|
+
severity: "info",
|
|
1594
|
+
message: "Filter management without Clear All option",
|
|
1595
|
+
suggestion: "Add 'Clear All' button when filters are active",
|
|
1596
|
+
snippet: line.trim().substring(0, 80),
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
if (/<(?:Filter|FilterMenu|FilterPopover|FilterDropdown)\b/i.test(line)) {
|
|
1601
|
+
const contextBlock = lines.slice(i, Math.min(i + 15, lines.length)).join("\n");
|
|
1602
|
+
const hasActiveStyle = /active|selected|bg-primary|variant=["']default/.test(contextBlock);
|
|
1603
|
+
if (!hasActiveStyle) {
|
|
1604
|
+
issues.push({
|
|
1605
|
+
file,
|
|
1606
|
+
line: i + 1,
|
|
1607
|
+
type: "filter-trigger-no-active-state",
|
|
1608
|
+
severity: "info",
|
|
1609
|
+
message: "Filter trigger may not show when filters are active",
|
|
1610
|
+
suggestion: "Change button style or add badge when filters are applied",
|
|
1611
|
+
snippet: line.trim().substring(0, 80),
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
if (/FilterChip|FilterTag|activeFilter/i.test(line)) {
|
|
1616
|
+
const contextBlock = lines.slice(i, Math.min(i + 10, lines.length)).join("\n");
|
|
1617
|
+
const hasRemove = /onRemove|onDelete|onClear|×|✕|close|CloseIcon/i.test(contextBlock);
|
|
1618
|
+
if (!hasRemove) {
|
|
1619
|
+
issues.push({
|
|
1620
|
+
file,
|
|
1621
|
+
line: i + 1,
|
|
1622
|
+
type: "filter-chip-no-remove",
|
|
1623
|
+
severity: "info",
|
|
1624
|
+
message: "Filter chip without remove/clear button",
|
|
1625
|
+
suggestion: "Add X button to allow removing individual filters",
|
|
1626
|
+
snippet: line.trim().substring(0, 80),
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
return {
|
|
1633
|
+
name: "Filter Active State",
|
|
1634
|
+
passed: true,
|
|
1635
|
+
blocking: false,
|
|
1636
|
+
issues,
|
|
1637
|
+
duration: Date.now() - startTime,
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
/**
|
|
1641
|
+
* 24. Optimistic Updates
|
|
1642
|
+
* Check for optimistic UI patterns in mutations
|
|
1643
|
+
*/
|
|
1644
|
+
async checkOptimisticUpdates() {
|
|
1645
|
+
const startTime = Date.now();
|
|
1646
|
+
const issues = [];
|
|
1647
|
+
const files = await this.getTsxFiles();
|
|
1648
|
+
for (const file of files) {
|
|
1649
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1650
|
+
const lines = content.split("\n");
|
|
1651
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1652
|
+
const line = lines[i];
|
|
1653
|
+
// Check for mutations that could benefit from optimistic updates
|
|
1654
|
+
if (/useMutation|mutateAsync|\.mutate\(/.test(line)) {
|
|
1655
|
+
const contextBlock = lines.slice(Math.max(0, i - 5), Math.min(i + 20, lines.length)).join("\n");
|
|
1656
|
+
const hasOptimistic = /optimistic|onMutate.*return|previousData|rollback/i.test(contextBlock);
|
|
1657
|
+
const isLikeOrToggle = /like|favorite|bookmark|toggle|follow|vote/i.test(contextBlock);
|
|
1658
|
+
// Toggle-type actions should use optimistic updates
|
|
1659
|
+
if (isLikeOrToggle && !hasOptimistic) {
|
|
1660
|
+
issues.push({
|
|
1661
|
+
file,
|
|
1662
|
+
line: i + 1,
|
|
1663
|
+
type: "toggle-no-optimistic",
|
|
1664
|
+
severity: "info",
|
|
1665
|
+
message: "Toggle action without optimistic update",
|
|
1666
|
+
suggestion: "Add optimistic update for instant feedback on like/toggle actions",
|
|
1667
|
+
snippet: line.trim().substring(0, 80),
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
// Check for error rollback
|
|
1671
|
+
if (hasOptimistic && !/onError.*previous|rollback|setQueryData/i.test(contextBlock)) {
|
|
1672
|
+
issues.push({
|
|
1673
|
+
file,
|
|
1674
|
+
line: i + 1,
|
|
1675
|
+
type: "optimistic-no-rollback",
|
|
1676
|
+
severity: "warning",
|
|
1677
|
+
message: "Optimistic update may lack error rollback",
|
|
1678
|
+
suggestion: "Add onError handler to rollback optimistic changes on failure",
|
|
1679
|
+
snippet: line.trim().substring(0, 80),
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
// Check for direct state updates that should wait for server
|
|
1684
|
+
if (/setItems|setData|setState/.test(line) && /delete|remove/i.test(line)) {
|
|
1685
|
+
const contextBlock = lines.slice(i, Math.min(i + 10, lines.length)).join("\n");
|
|
1686
|
+
const hasAwait = /await|\.then|onSuccess/.test(contextBlock);
|
|
1687
|
+
if (!hasAwait) {
|
|
1688
|
+
issues.push({
|
|
1689
|
+
file,
|
|
1690
|
+
line: i + 1,
|
|
1691
|
+
type: "delete-before-confirm",
|
|
1692
|
+
severity: "info",
|
|
1693
|
+
message: "State update before server confirmation",
|
|
1694
|
+
suggestion: "Consider waiting for server response or implementing optimistic update pattern",
|
|
1695
|
+
snippet: line.trim().substring(0, 80),
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
return {
|
|
1702
|
+
name: "Optimistic Updates",
|
|
1703
|
+
passed: issues.filter((i) => i.severity === "error").length === 0,
|
|
1704
|
+
blocking: false,
|
|
1705
|
+
issues,
|
|
1706
|
+
duration: Date.now() - startTime,
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
/**
|
|
1710
|
+
* 25. Undo/Redo Patterns
|
|
1711
|
+
* Check for undo functionality on destructive actions
|
|
1712
|
+
*/
|
|
1713
|
+
async checkUndoRedoPatterns() {
|
|
1714
|
+
const startTime = Date.now();
|
|
1715
|
+
const issues = [];
|
|
1716
|
+
const files = await this.getTsxFiles();
|
|
1717
|
+
let hasUndoSystem = false;
|
|
1718
|
+
for (const file of files) {
|
|
1719
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1720
|
+
const lines = content.split("\n");
|
|
1721
|
+
// Check for undo system presence
|
|
1722
|
+
if (/useUndo|undo.*redo|history.*stack|command.*pattern/i.test(content)) {
|
|
1723
|
+
hasUndoSystem = true;
|
|
1724
|
+
}
|
|
1725
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1726
|
+
const line = lines[i];
|
|
1727
|
+
// Check for delete actions without undo
|
|
1728
|
+
if (/onDelete|handleDelete|deleteItem|removeItem/i.test(line)) {
|
|
1729
|
+
const contextBlock = lines.slice(i, Math.min(i + 20, lines.length)).join("\n");
|
|
1730
|
+
const hasUndo = /undo|Undo|restore|Restore|unarchive/i.test(contextBlock);
|
|
1731
|
+
const hasSoftDelete = /soft.*delete|archive|trash|isDeleted/i.test(contextBlock);
|
|
1732
|
+
const hasConfirmation = /confirm|Confirm|Dialog|Modal/i.test(contextBlock);
|
|
1733
|
+
if (!hasUndo && !hasSoftDelete && !hasConfirmation) {
|
|
1734
|
+
issues.push({
|
|
1735
|
+
file,
|
|
1736
|
+
line: i + 1,
|
|
1737
|
+
type: "delete-no-undo",
|
|
1738
|
+
severity: "info",
|
|
1739
|
+
message: "Delete action without undo option",
|
|
1740
|
+
suggestion: "Add undo toast, soft delete, or confirmation dialog for destructive actions",
|
|
1741
|
+
snippet: line.trim().substring(0, 80),
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
// Check for bulk actions without undo
|
|
1746
|
+
if (/bulkDelete|deleteSelected|removeAll|deleteMany/i.test(line)) {
|
|
1747
|
+
const contextBlock = lines.slice(i, Math.min(i + 15, lines.length)).join("\n");
|
|
1748
|
+
const hasUndo = /undo|restore/i.test(contextBlock);
|
|
1749
|
+
if (!hasUndo) {
|
|
1750
|
+
issues.push({
|
|
1751
|
+
file,
|
|
1752
|
+
line: i + 1,
|
|
1753
|
+
type: "bulk-delete-no-undo",
|
|
1754
|
+
severity: "warning",
|
|
1755
|
+
message: "Bulk delete without undo option",
|
|
1756
|
+
suggestion: "Bulk deletes should have undo capability or require extra confirmation",
|
|
1757
|
+
snippet: line.trim().substring(0, 80),
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
// Check for toast with undo action
|
|
1762
|
+
if (/toast\(|showToast|notification\(/i.test(line)) {
|
|
1763
|
+
const contextBlock = lines.slice(i, Math.min(i + 10, lines.length)).join("\n");
|
|
1764
|
+
const isDeletion = /delete|remove|archive/i.test(contextBlock);
|
|
1765
|
+
const hasUndoAction = /action.*undo|undo.*action|Undo/i.test(contextBlock);
|
|
1766
|
+
if (isDeletion && !hasUndoAction) {
|
|
1767
|
+
issues.push({
|
|
1768
|
+
file,
|
|
1769
|
+
line: i + 1,
|
|
1770
|
+
type: "delete-toast-no-undo",
|
|
1771
|
+
severity: "info",
|
|
1772
|
+
message: "Deletion toast without undo action",
|
|
1773
|
+
suggestion: "Add 'Undo' action button to deletion confirmation toast",
|
|
1774
|
+
snippet: line.trim().substring(0, 80),
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
return {
|
|
1781
|
+
name: "Undo/Redo Patterns",
|
|
1782
|
+
passed: issues.filter((i) => i.severity === "error").length === 0,
|
|
1783
|
+
blocking: false,
|
|
1784
|
+
issues,
|
|
1785
|
+
duration: Date.now() - startTime,
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
/**
|
|
1789
|
+
* 26. Keyboard Shortcuts
|
|
1790
|
+
* Check for keyboard navigation and shortcut support
|
|
1791
|
+
*/
|
|
1792
|
+
async checkKeyboardShortcuts() {
|
|
1793
|
+
const startTime = Date.now();
|
|
1794
|
+
const issues = [];
|
|
1795
|
+
const files = await this.getTsxFiles();
|
|
1796
|
+
let hasKeyboardShortcuts = false;
|
|
1797
|
+
let hasShortcutHelp = false;
|
|
1798
|
+
for (const file of files) {
|
|
1799
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1800
|
+
const lines = content.split("\n");
|
|
1801
|
+
// Check for keyboard shortcut system
|
|
1802
|
+
if (/useHotkeys|useKeyboard|onKeyDown|KeyboardEvent|Mousetrap|hotkeys-js/i.test(content)) {
|
|
1803
|
+
hasKeyboardShortcuts = true;
|
|
1804
|
+
}
|
|
1805
|
+
if (/shortcut.*help|keyboard.*shortcuts|hotkeys.*modal|shortcut.*dialog|\?.*shortcuts/i.test(content)) {
|
|
1806
|
+
hasShortcutHelp = true;
|
|
1807
|
+
}
|
|
1808
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1809
|
+
const line = lines[i];
|
|
1810
|
+
// Check for actions that commonly need shortcuts
|
|
1811
|
+
if (/<(?:SearchInput|SearchField|Search)\b/i.test(line)) {
|
|
1812
|
+
const contextBlock = content;
|
|
1813
|
+
const hasSearchShortcut = /(?:cmd|ctrl|meta)\+k|\/.*search|focus.*search/i.test(contextBlock);
|
|
1814
|
+
if (!hasSearchShortcut) {
|
|
1815
|
+
issues.push({
|
|
1816
|
+
file,
|
|
1817
|
+
line: i + 1,
|
|
1818
|
+
type: "search-no-shortcut",
|
|
1819
|
+
severity: "info",
|
|
1820
|
+
message: "Search input without keyboard shortcut",
|
|
1821
|
+
suggestion: "Add Cmd/Ctrl+K shortcut to focus search (standard pattern)",
|
|
1822
|
+
snippet: line.trim().substring(0, 80),
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
// Check for keyboard event handlers
|
|
1827
|
+
if (/onKeyDown|onKeyUp|onKeyPress/.test(line)) {
|
|
1828
|
+
const contextBlock = lines.slice(i, Math.min(i + 10, lines.length)).join("\n");
|
|
1829
|
+
// Check for Escape key handling in modals/popups
|
|
1830
|
+
if (/<(?:Dialog|Modal|Popover|Menu)\b/i.test(content.substring(Math.max(0, content.indexOf(line) - 200), content.indexOf(line) + 200))) {
|
|
1831
|
+
const hasEscape = /Escape|key.*27|e\.key\s*===?\s*["']Escape/i.test(contextBlock);
|
|
1832
|
+
if (!hasEscape) {
|
|
1833
|
+
issues.push({
|
|
1834
|
+
file,
|
|
1835
|
+
line: i + 1,
|
|
1836
|
+
type: "modal-no-escape",
|
|
1837
|
+
severity: "info",
|
|
1838
|
+
message: "Keyboard handler may not handle Escape key",
|
|
1839
|
+
suggestion: "Add Escape key to close modals/popups",
|
|
1840
|
+
snippet: line.trim().substring(0, 80),
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
// Check for arrow key navigation in lists
|
|
1845
|
+
if (/list|menu|dropdown|options/i.test(content.substring(Math.max(0, content.indexOf(line) - 100), content.indexOf(line) + 100))) {
|
|
1846
|
+
const hasArrows = /ArrowUp|ArrowDown|Arrow(?:Left|Right)|key.*(?:38|40)/i.test(contextBlock);
|
|
1847
|
+
if (!hasArrows) {
|
|
1848
|
+
issues.push({
|
|
1849
|
+
file,
|
|
1850
|
+
line: i + 1,
|
|
1851
|
+
type: "list-no-arrow-nav",
|
|
1852
|
+
severity: "info",
|
|
1853
|
+
message: "List keyboard handler without arrow navigation",
|
|
1854
|
+
suggestion: "Add ArrowUp/ArrowDown for keyboard navigation",
|
|
1855
|
+
snippet: line.trim().substring(0, 80),
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
// Check for Enter key on clickable elements
|
|
1861
|
+
if (/<div[^>]*onClick|role=["']button/.test(line)) {
|
|
1862
|
+
const contextBlock = lines.slice(i, Math.min(i + 5, lines.length)).join("\n");
|
|
1863
|
+
const hasKeyboard = /onKeyDown|onKeyPress|onKeyUp/.test(contextBlock);
|
|
1864
|
+
const hasTabIndex = /tabIndex/.test(contextBlock);
|
|
1865
|
+
if (!hasKeyboard && hasTabIndex) {
|
|
1866
|
+
issues.push({
|
|
1867
|
+
file,
|
|
1868
|
+
line: i + 1,
|
|
1869
|
+
type: "clickable-no-enter",
|
|
1870
|
+
severity: "info",
|
|
1871
|
+
message: "Focusable clickable element without keyboard handler",
|
|
1872
|
+
suggestion: "Add onKeyDown to handle Enter/Space for keyboard activation",
|
|
1873
|
+
snippet: line.trim().substring(0, 80),
|
|
1874
|
+
});
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
// Global check for shortcut documentation
|
|
1880
|
+
if (hasKeyboardShortcuts && !hasShortcutHelp) {
|
|
1881
|
+
issues.push({
|
|
1882
|
+
file: "codebase-wide",
|
|
1883
|
+
line: 0,
|
|
1884
|
+
type: "shortcuts-no-help",
|
|
1885
|
+
severity: "info",
|
|
1886
|
+
message: "Keyboard shortcuts exist but no help/documentation found",
|
|
1887
|
+
suggestion: "Add keyboard shortcut help modal (often triggered by '?')",
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
return {
|
|
1891
|
+
name: "Keyboard Shortcuts",
|
|
1892
|
+
passed: true,
|
|
1893
|
+
blocking: false,
|
|
1894
|
+
issues,
|
|
1895
|
+
duration: Date.now() - startTime,
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
/**
|
|
1899
|
+
* 27. Search Debounce
|
|
1900
|
+
* Check for debouncing on search/filter inputs
|
|
1901
|
+
*/
|
|
1902
|
+
async checkSearchDebounce() {
|
|
1903
|
+
const startTime = Date.now();
|
|
1904
|
+
const issues = [];
|
|
1905
|
+
const files = await this.getTsxFiles();
|
|
1906
|
+
for (const file of files) {
|
|
1907
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1908
|
+
const lines = content.split("\n");
|
|
1909
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1910
|
+
const line = lines[i];
|
|
1911
|
+
// Check for search inputs without debounce
|
|
1912
|
+
if (/search|filter|query/i.test(line) && /onChange|onInput/.test(line)) {
|
|
1913
|
+
const contextBlock = lines.slice(Math.max(0, i - 10), Math.min(i + 10, lines.length)).join("\n");
|
|
1914
|
+
const hasDebounce = /debounce|useDebounce|useDeferredValue|setTimeout|lodash.*debounce/i.test(contextBlock);
|
|
1915
|
+
const hasThrottle = /throttle|useThrottle/i.test(contextBlock);
|
|
1916
|
+
if (!hasDebounce && !hasThrottle) {
|
|
1917
|
+
// Check if it triggers an API call
|
|
1918
|
+
const triggersApi = /fetch|api|query|refetch|search.*\(/i.test(contextBlock);
|
|
1919
|
+
if (triggersApi) {
|
|
1920
|
+
issues.push({
|
|
1921
|
+
file,
|
|
1922
|
+
line: i + 1,
|
|
1923
|
+
type: "search-no-debounce",
|
|
1924
|
+
severity: "warning",
|
|
1925
|
+
message: "Search input triggers API without debouncing",
|
|
1926
|
+
suggestion: "Add debounce (300-500ms) to prevent excessive API calls",
|
|
1927
|
+
snippet: line.trim().substring(0, 80),
|
|
1928
|
+
});
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
// Check for useEffect with search dependency
|
|
1933
|
+
if (/useEffect/.test(line)) {
|
|
1934
|
+
const effectBlock = lines.slice(i, Math.min(i + 15, lines.length)).join("\n");
|
|
1935
|
+
const hasSearchDep = /\[.*(?:search|query|filter).*\]/i.test(effectBlock);
|
|
1936
|
+
const hasApi = /fetch|api|\.get|\.post/i.test(effectBlock);
|
|
1937
|
+
if (hasSearchDep && hasApi) {
|
|
1938
|
+
const hasDebounce = /debounce|setTimeout|useDeferredValue/i.test(effectBlock);
|
|
1939
|
+
if (!hasDebounce) {
|
|
1940
|
+
issues.push({
|
|
1941
|
+
file,
|
|
1942
|
+
line: i + 1,
|
|
1943
|
+
type: "effect-search-no-debounce",
|
|
1944
|
+
severity: "warning",
|
|
1945
|
+
message: "useEffect with search dependency may fire too frequently",
|
|
1946
|
+
suggestion: "Debounce the search value or use useDeferredValue",
|
|
1947
|
+
snippet: line.trim().substring(0, 80),
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
// Check debounce timing
|
|
1953
|
+
const debounceMatch = line.match(/debounce[^,]*,\s*(\d+)/i);
|
|
1954
|
+
if (debounceMatch) {
|
|
1955
|
+
const delay = parseInt(debounceMatch[1]);
|
|
1956
|
+
if (delay < 150) {
|
|
1957
|
+
issues.push({
|
|
1958
|
+
file,
|
|
1959
|
+
line: i + 1,
|
|
1960
|
+
type: "debounce-too-short",
|
|
1961
|
+
severity: "info",
|
|
1962
|
+
message: `Debounce delay ${delay}ms may be too short`,
|
|
1963
|
+
suggestion: "Use 300-500ms for search debouncing",
|
|
1964
|
+
snippet: line.trim().substring(0, 80),
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
if (delay > 1000) {
|
|
1968
|
+
issues.push({
|
|
1969
|
+
file,
|
|
1970
|
+
line: i + 1,
|
|
1971
|
+
type: "debounce-too-long",
|
|
1972
|
+
severity: "info",
|
|
1973
|
+
message: `Debounce delay ${delay}ms may feel sluggish`,
|
|
1974
|
+
suggestion: "300-500ms is usually optimal for search",
|
|
1975
|
+
snippet: line.trim().substring(0, 80),
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
return {
|
|
1982
|
+
name: "Search Debounce",
|
|
1983
|
+
passed: issues.filter((i) => i.severity === "error").length === 0,
|
|
1984
|
+
blocking: false,
|
|
1985
|
+
issues,
|
|
1986
|
+
duration: Date.now() - startTime,
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
1989
|
+
/**
|
|
1990
|
+
* 28. Confirmation Dialogs
|
|
1991
|
+
* Check for confirmation on destructive actions
|
|
1992
|
+
*/
|
|
1993
|
+
async checkConfirmationDialogs() {
|
|
1994
|
+
const startTime = Date.now();
|
|
1995
|
+
const issues = [];
|
|
1996
|
+
const files = await this.getTsxFiles();
|
|
1997
|
+
for (const file of files) {
|
|
1998
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1999
|
+
const lines = content.split("\n");
|
|
2000
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2001
|
+
const line = lines[i];
|
|
2002
|
+
// Check for destructive actions without confirmation
|
|
2003
|
+
if (/handleDelete|onDelete|deleteItem|removeItem|handleRemove/i.test(line)) {
|
|
2004
|
+
const contextBlock = lines.slice(i, Math.min(i + 20, lines.length)).join("\n");
|
|
2005
|
+
const hasConfirm = /confirm|Confirm|Dialog|Modal|AlertDialog|window\.confirm/i.test(contextBlock);
|
|
2006
|
+
const hasUndo = /undo|restore/i.test(contextBlock);
|
|
2007
|
+
if (!hasConfirm && !hasUndo) {
|
|
2008
|
+
issues.push({
|
|
2009
|
+
file,
|
|
2010
|
+
line: i + 1,
|
|
2011
|
+
type: "delete-no-confirmation",
|
|
2012
|
+
severity: "warning",
|
|
2013
|
+
message: "Delete action without confirmation dialog",
|
|
2014
|
+
suggestion: "Add confirmation dialog for destructive actions",
|
|
2015
|
+
snippet: line.trim().substring(0, 80),
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
// Check for dangerous operations
|
|
2020
|
+
if (/reset.*data|clear.*all|wipe|purge|truncate/i.test(line)) {
|
|
2021
|
+
const contextBlock = lines.slice(i, Math.min(i + 15, lines.length)).join("\n");
|
|
2022
|
+
const hasDoubleConfirm = /type.*confirm|enter.*name|type.*DELETE/i.test(contextBlock);
|
|
2023
|
+
if (!hasDoubleConfirm) {
|
|
2024
|
+
issues.push({
|
|
2025
|
+
file,
|
|
2026
|
+
line: i + 1,
|
|
2027
|
+
type: "dangerous-no-double-confirm",
|
|
2028
|
+
severity: "info",
|
|
2029
|
+
message: "High-risk operation without extra confirmation",
|
|
2030
|
+
suggestion: "Require typing 'DELETE' or item name for dangerous operations",
|
|
2031
|
+
snippet: line.trim().substring(0, 80),
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
// Check confirmation dialog patterns
|
|
2036
|
+
if (/<(?:AlertDialog|ConfirmDialog|ConfirmationModal)\b/i.test(line)) {
|
|
2037
|
+
const contextBlock = lines.slice(i, Math.min(i + 20, lines.length)).join("\n");
|
|
2038
|
+
// Check for clear destructive action button
|
|
2039
|
+
const hasDestructiveButton = /variant=["']destructive|color=["']error|danger|red/i.test(contextBlock);
|
|
2040
|
+
if (!hasDestructiveButton) {
|
|
2041
|
+
issues.push({
|
|
2042
|
+
file,
|
|
2043
|
+
line: i + 1,
|
|
2044
|
+
type: "confirm-no-destructive-style",
|
|
2045
|
+
severity: "info",
|
|
2046
|
+
message: "Confirmation dialog without destructive button styling",
|
|
2047
|
+
suggestion: "Use red/destructive variant for delete confirmation button",
|
|
2048
|
+
snippet: line.trim().substring(0, 80),
|
|
2049
|
+
});
|
|
2050
|
+
}
|
|
2051
|
+
// Check for clear cancel option
|
|
2052
|
+
const hasCancelButton = /cancel|Cancel|onCancel|close|Close|dismiss/i.test(contextBlock);
|
|
2053
|
+
if (!hasCancelButton) {
|
|
2054
|
+
issues.push({
|
|
2055
|
+
file,
|
|
2056
|
+
line: i + 1,
|
|
2057
|
+
type: "confirm-no-cancel",
|
|
2058
|
+
severity: "warning",
|
|
2059
|
+
message: "Confirmation dialog may lack clear cancel option",
|
|
2060
|
+
suggestion: "Always provide obvious cancel/close option",
|
|
2061
|
+
snippet: line.trim().substring(0, 80),
|
|
2062
|
+
});
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
return {
|
|
2068
|
+
name: "Confirmation Dialogs",
|
|
2069
|
+
passed: issues.filter((i) => i.severity === "error").length === 0,
|
|
2070
|
+
blocking: false,
|
|
2071
|
+
issues,
|
|
2072
|
+
duration: Date.now() - startTime,
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
/**
|
|
2076
|
+
* 29. Progress Indicators
|
|
2077
|
+
* Check for progress tracking on multi-step operations
|
|
2078
|
+
*/
|
|
2079
|
+
async checkProgressIndicators() {
|
|
2080
|
+
const startTime = Date.now();
|
|
2081
|
+
const issues = [];
|
|
2082
|
+
const files = await this.getTsxFiles();
|
|
2083
|
+
const progressPatterns = new Map();
|
|
2084
|
+
for (const file of files) {
|
|
2085
|
+
const content = fs.readFileSync(file, "utf8");
|
|
2086
|
+
const lines = content.split("\n");
|
|
2087
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2088
|
+
const line = lines[i];
|
|
2089
|
+
// Check for multi-step processes
|
|
2090
|
+
if (/step|Step|currentStep|activeStep/i.test(line)) {
|
|
2091
|
+
const contextBlock = lines.slice(i, Math.min(i + 20, lines.length)).join("\n");
|
|
2092
|
+
const hasStepper = /<Stepper|<Steps|<ProgressSteps|StepIndicator/i.test(contextBlock);
|
|
2093
|
+
const hasProgress = /progress|Progress|step.*of.*total/i.test(contextBlock);
|
|
2094
|
+
if (!hasStepper && !hasProgress) {
|
|
2095
|
+
issues.push({
|
|
2096
|
+
file,
|
|
2097
|
+
line: i + 1,
|
|
2098
|
+
type: "steps-no-indicator",
|
|
2099
|
+
severity: "info",
|
|
2100
|
+
message: "Multi-step process without progress indicator",
|
|
2101
|
+
suggestion: "Add Stepper component to show progress through steps",
|
|
2102
|
+
snippet: line.trim().substring(0, 80),
|
|
2103
|
+
});
|
|
2104
|
+
}
|
|
2105
|
+
// Track stepper pattern
|
|
2106
|
+
if (hasStepper) {
|
|
2107
|
+
progressPatterns.set("stepper", (progressPatterns.get("stepper") || 0) + 1);
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
// Check for file uploads
|
|
2111
|
+
if (/upload|Upload|handleUpload|onUpload/i.test(line)) {
|
|
2112
|
+
const contextBlock = lines.slice(i, Math.min(i + 25, lines.length)).join("\n");
|
|
2113
|
+
const hasProgress = /progress|Progress|percent|loaded.*total/i.test(contextBlock);
|
|
2114
|
+
const hasProgressBar = /<Progress|<LinearProgress|ProgressBar/i.test(contextBlock);
|
|
2115
|
+
if (!hasProgress && !hasProgressBar) {
|
|
2116
|
+
issues.push({
|
|
2117
|
+
file,
|
|
2118
|
+
line: i + 1,
|
|
2119
|
+
type: "upload-no-progress",
|
|
2120
|
+
severity: "warning",
|
|
2121
|
+
message: "File upload without progress indicator",
|
|
2122
|
+
suggestion: "Show upload progress bar with percentage",
|
|
2123
|
+
snippet: line.trim().substring(0, 80),
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
// Check for bulk operations
|
|
2128
|
+
if (/bulk|batch|process.*items|import.*data/i.test(line)) {
|
|
2129
|
+
const contextBlock = lines.slice(i, Math.min(i + 20, lines.length)).join("\n");
|
|
2130
|
+
const hasProgress = /progress|processed|current.*total|\d+.*of.*\d+/i.test(contextBlock);
|
|
2131
|
+
if (!hasProgress) {
|
|
2132
|
+
issues.push({
|
|
2133
|
+
file,
|
|
2134
|
+
line: i + 1,
|
|
2135
|
+
type: "bulk-no-progress",
|
|
2136
|
+
severity: "info",
|
|
2137
|
+
message: "Bulk operation without progress tracking",
|
|
2138
|
+
suggestion: "Show 'X of Y processed' indicator for bulk operations",
|
|
2139
|
+
snippet: line.trim().substring(0, 80),
|
|
2140
|
+
});
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
// Check progress bar accessibility
|
|
2144
|
+
if (/<Progress|<LinearProgress|<CircularProgress/i.test(line)) {
|
|
2145
|
+
const contextBlock = lines.slice(i, Math.min(i + 5, lines.length)).join("\n");
|
|
2146
|
+
const hasAriaLabel = /aria-label|aria-valuetext|aria-valuenow/i.test(contextBlock);
|
|
2147
|
+
if (!hasAriaLabel) {
|
|
2148
|
+
issues.push({
|
|
2149
|
+
file,
|
|
2150
|
+
line: i + 1,
|
|
2151
|
+
type: "progress-no-aria",
|
|
2152
|
+
severity: "info",
|
|
2153
|
+
message: "Progress indicator may lack screen reader support",
|
|
2154
|
+
suggestion: "Add aria-label or aria-valuetext for accessibility",
|
|
2155
|
+
snippet: line.trim().substring(0, 80),
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
return {
|
|
2162
|
+
name: "Progress Indicators",
|
|
2163
|
+
passed: issues.filter((i) => i.severity === "error").length === 0,
|
|
2164
|
+
blocking: false,
|
|
2165
|
+
issues,
|
|
2166
|
+
duration: Date.now() - startTime,
|
|
2167
|
+
};
|
|
2168
|
+
}
|
|
2169
|
+
/**
|
|
2170
|
+
* 30. Error Recovery
|
|
2171
|
+
* Check for retry mechanisms and error recovery UI
|
|
2172
|
+
*/
|
|
2173
|
+
async checkErrorRecovery() {
|
|
2174
|
+
const startTime = Date.now();
|
|
2175
|
+
const issues = [];
|
|
2176
|
+
const files = await this.getTsxFiles();
|
|
2177
|
+
for (const file of files) {
|
|
2178
|
+
const content = fs.readFileSync(file, "utf8");
|
|
2179
|
+
const lines = content.split("\n");
|
|
2180
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2181
|
+
const line = lines[i];
|
|
2182
|
+
// Check for error states without retry
|
|
2183
|
+
if (/isError|error\s*&&|\.error\s*\?|hasError/i.test(line)) {
|
|
2184
|
+
const contextBlock = lines.slice(i, Math.min(i + 20, lines.length)).join("\n");
|
|
2185
|
+
const hasRetry = /retry|Retry|refetch|try.*again|reload/i.test(contextBlock);
|
|
2186
|
+
const hasErrorMessage = /error.*message|message.*error|Error:|display.*error/i.test(contextBlock);
|
|
2187
|
+
if (!hasRetry) {
|
|
2188
|
+
issues.push({
|
|
2189
|
+
file,
|
|
2190
|
+
line: i + 1,
|
|
2191
|
+
type: "error-no-retry",
|
|
2192
|
+
severity: "info",
|
|
2193
|
+
message: "Error state without retry option",
|
|
2194
|
+
suggestion: "Add 'Try Again' or 'Retry' button for recoverable errors",
|
|
2195
|
+
snippet: line.trim().substring(0, 80),
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
2198
|
+
if (!hasErrorMessage) {
|
|
2199
|
+
issues.push({
|
|
2200
|
+
file,
|
|
2201
|
+
line: i + 1,
|
|
2202
|
+
type: "error-no-message",
|
|
2203
|
+
severity: "warning",
|
|
2204
|
+
message: "Error state without clear error message",
|
|
2205
|
+
suggestion: "Display user-friendly error message explaining what went wrong",
|
|
2206
|
+
snippet: line.trim().substring(0, 80),
|
|
2207
|
+
});
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
// Check for network error handling
|
|
2211
|
+
if (/catch|\.catch|onError/i.test(line)) {
|
|
2212
|
+
const contextBlock = lines.slice(i, Math.min(i + 15, lines.length)).join("\n");
|
|
2213
|
+
const hasOfflineCheck = /offline|navigator\.onLine|network.*error/i.test(contextBlock);
|
|
2214
|
+
const hasTimeoutCheck = /timeout|TIMEOUT|timed.*out/i.test(contextBlock);
|
|
2215
|
+
// Check if it distinguishes network errors
|
|
2216
|
+
if (/fetch|api|axios/i.test(contextBlock) && !hasOfflineCheck) {
|
|
2217
|
+
issues.push({
|
|
2218
|
+
file,
|
|
2219
|
+
line: i + 1,
|
|
2220
|
+
type: "no-offline-handling",
|
|
2221
|
+
severity: "info",
|
|
2222
|
+
message: "API error handling may not detect offline state",
|
|
2223
|
+
suggestion: "Check navigator.onLine and show appropriate offline message",
|
|
2224
|
+
snippet: line.trim().substring(0, 80),
|
|
2225
|
+
});
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
// Check for ErrorBoundary usage
|
|
2229
|
+
if (/<ErrorBoundary/i.test(line)) {
|
|
2230
|
+
const contextBlock = lines.slice(i, Math.min(i + 15, lines.length)).join("\n");
|
|
2231
|
+
const hasFallback = /fallback|Fallback|renderError|FallbackComponent/i.test(contextBlock);
|
|
2232
|
+
const hasRetry = /retry|reset|recover/i.test(contextBlock);
|
|
2233
|
+
if (!hasFallback) {
|
|
2234
|
+
issues.push({
|
|
2235
|
+
file,
|
|
2236
|
+
line: i + 1,
|
|
2237
|
+
type: "boundary-no-fallback",
|
|
2238
|
+
severity: "warning",
|
|
2239
|
+
message: "ErrorBoundary without fallback UI",
|
|
2240
|
+
suggestion: "Add fallback prop with user-friendly error UI",
|
|
2241
|
+
snippet: line.trim().substring(0, 80),
|
|
2242
|
+
});
|
|
2243
|
+
}
|
|
2244
|
+
if (!hasRetry) {
|
|
2245
|
+
issues.push({
|
|
2246
|
+
file,
|
|
2247
|
+
line: i + 1,
|
|
2248
|
+
type: "boundary-no-retry",
|
|
2249
|
+
severity: "info",
|
|
2250
|
+
message: "ErrorBoundary without retry mechanism",
|
|
2251
|
+
suggestion: "Add reset/retry functionality to recover from errors",
|
|
2252
|
+
snippet: line.trim().substring(0, 80),
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
return {
|
|
2259
|
+
name: "Error Recovery",
|
|
2260
|
+
passed: issues.filter((i) => i.severity === "error").length === 0,
|
|
2261
|
+
blocking: false,
|
|
2262
|
+
issues,
|
|
2263
|
+
duration: Date.now() - startTime,
|
|
2264
|
+
};
|
|
2265
|
+
}
|
|
2266
|
+
/**
|
|
2267
|
+
* 31. Success Celebrations
|
|
2268
|
+
* Check for micro-animations and feedback on successful actions
|
|
2269
|
+
*/
|
|
2270
|
+
async checkSuccessCelebrations() {
|
|
2271
|
+
const startTime = Date.now();
|
|
2272
|
+
const issues = [];
|
|
2273
|
+
const files = await this.getTsxFiles();
|
|
2274
|
+
let hasConfetti = false;
|
|
2275
|
+
let hasSuccessAnimation = false;
|
|
2276
|
+
for (const file of files) {
|
|
2277
|
+
const content = fs.readFileSync(file, "utf8");
|
|
2278
|
+
const lines = content.split("\n");
|
|
2279
|
+
// Check for celebration libraries
|
|
2280
|
+
if (/confetti|canvas-confetti|lottie|celebrate/i.test(content)) {
|
|
2281
|
+
hasConfetti = true;
|
|
2282
|
+
}
|
|
2283
|
+
if (/success.*animate|animate.*success|pulse|bounce.*success/i.test(content)) {
|
|
2284
|
+
hasSuccessAnimation = true;
|
|
2285
|
+
}
|
|
2286
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2287
|
+
const line = lines[i];
|
|
2288
|
+
// Check for significant success events
|
|
2289
|
+
if (/onSuccess|success\(|showSuccess|isSuccess/i.test(line)) {
|
|
2290
|
+
const contextBlock = lines.slice(i, Math.min(i + 15, lines.length)).join("\n");
|
|
2291
|
+
// Check for completion/milestone events
|
|
2292
|
+
const isMilestone = /complete|finish|checkout|payment|submit.*order|upgrade/i.test(contextBlock);
|
|
2293
|
+
const hasSpecialFeedback = /confetti|celebrate|animation|animate|party|🎉|✨/i.test(contextBlock);
|
|
2294
|
+
if (isMilestone && !hasSpecialFeedback) {
|
|
2295
|
+
issues.push({
|
|
2296
|
+
file,
|
|
2297
|
+
line: i + 1,
|
|
2298
|
+
type: "milestone-no-celebration",
|
|
2299
|
+
severity: "info",
|
|
2300
|
+
message: "Milestone event without celebratory feedback",
|
|
2301
|
+
suggestion: "Consider adding confetti or special animation for completed purchases/signups",
|
|
2302
|
+
snippet: line.trim().substring(0, 80),
|
|
2303
|
+
});
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
// Check for checkmark animations
|
|
2307
|
+
if (/<Check|<SuccessIcon|CheckCircle|CheckIcon/i.test(line)) {
|
|
2308
|
+
const contextBlock = lines.slice(i, Math.min(i + 5, lines.length)).join("\n");
|
|
2309
|
+
const hasAnimation = /animate|motion|transition/i.test(contextBlock);
|
|
2310
|
+
// This is nice-to-have, just track
|
|
2311
|
+
}
|
|
2312
|
+
// Check for form submission success
|
|
2313
|
+
if (/onSubmit.*success|handleSubmit.*success|form.*success/i.test(line)) {
|
|
2314
|
+
const contextBlock = lines.slice(i, Math.min(i + 20, lines.length)).join("\n");
|
|
2315
|
+
const hasSuccessFeedback = /toast|notification|success.*message|✓|check/i.test(contextBlock);
|
|
2316
|
+
const hasRedirect = /navigate|redirect|router\.push|window\.location/i.test(contextBlock);
|
|
2317
|
+
if (!hasSuccessFeedback && !hasRedirect) {
|
|
2318
|
+
issues.push({
|
|
2319
|
+
file,
|
|
2320
|
+
line: i + 1,
|
|
2321
|
+
type: "form-success-no-feedback",
|
|
2322
|
+
severity: "info",
|
|
2323
|
+
message: "Form submission success without clear feedback",
|
|
2324
|
+
suggestion: "Show success toast or checkmark animation after submission",
|
|
2325
|
+
snippet: line.trim().substring(0, 80),
|
|
2326
|
+
});
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
// Check for save operations
|
|
2330
|
+
if (/save|Save|handleSave|onSave/i.test(line)) {
|
|
2331
|
+
const contextBlock = lines.slice(i, Math.min(i + 15, lines.length)).join("\n");
|
|
2332
|
+
const hasSuccessFeedback = /success|saved|toast|notification|check/i.test(contextBlock);
|
|
2333
|
+
const hasAutoSave = /auto.*save|autosave|debounce.*save/i.test(contextBlock);
|
|
2334
|
+
if (!hasSuccessFeedback && !hasAutoSave) {
|
|
2335
|
+
issues.push({
|
|
2336
|
+
file,
|
|
2337
|
+
line: i + 1,
|
|
2338
|
+
type: "save-no-feedback",
|
|
2339
|
+
severity: "info",
|
|
2340
|
+
message: "Save action without success confirmation",
|
|
2341
|
+
suggestion: "Show 'Saved!' indicator or checkmark briefly after save",
|
|
2342
|
+
snippet: line.trim().substring(0, 80),
|
|
2343
|
+
});
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
return {
|
|
2349
|
+
name: "Success Celebrations",
|
|
2350
|
+
passed: true,
|
|
2351
|
+
blocking: false,
|
|
2352
|
+
issues,
|
|
2353
|
+
duration: Date.now() - startTime,
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
2356
|
+
/**
|
|
2357
|
+
* Run all checks
|
|
2358
|
+
*/
|
|
2359
|
+
async runAll(filter) {
|
|
2360
|
+
const startTime = Date.now();
|
|
2361
|
+
const checks = [];
|
|
2362
|
+
const checkMap = {
|
|
2363
|
+
hover: () => this.checkHoverStateConsistency(),
|
|
2364
|
+
active: () => this.checkActiveStateCoverage(),
|
|
2365
|
+
focus: () => this.checkFocusRingConsistency(),
|
|
2366
|
+
disabled: () => this.checkDisabledStateStyling(),
|
|
2367
|
+
cursor: () => this.checkCursorConsistency(),
|
|
2368
|
+
scale: () => this.checkScaleFeedbackConsistency(),
|
|
2369
|
+
spinner: () => this.checkLoadingSpinnerConsistency(),
|
|
2370
|
+
motion: () => this.checkReducedMotionSupport(),
|
|
2371
|
+
feedback: () => this.checkAsyncOperationFeedback(),
|
|
2372
|
+
transition: () => this.checkTransitionConsistency(),
|
|
2373
|
+
touch: () => this.checkTouchGestureFeedback(),
|
|
2374
|
+
scroll: () => this.checkScrollSnapConsistency(),
|
|
2375
|
+
dnd: () => this.checkDragDropFeedback(),
|
|
2376
|
+
validation: () => this.checkInputValidationFeedback(),
|
|
2377
|
+
toast: () => this.checkToastNotificationTiming(),
|
|
2378
|
+
skeleton: () => this.checkSkeletonContentMatch(),
|
|
2379
|
+
empty: () => this.checkEmptyStatePatterns(),
|
|
2380
|
+
selection: () => this.checkSelectionFeedback(),
|
|
2381
|
+
accordion: () => this.checkAccordionAnimation(),
|
|
2382
|
+
modal: () => this.checkModalAnimation(),
|
|
2383
|
+
copy: () => this.checkCopyFeedback(),
|
|
2384
|
+
infinite: () => this.checkInfiniteScrollLoading(),
|
|
2385
|
+
filter: () => this.checkFilterActiveState(),
|
|
2386
|
+
optimistic: () => this.checkOptimisticUpdates(),
|
|
2387
|
+
undo: () => this.checkUndoRedoPatterns(),
|
|
2388
|
+
keyboard: () => this.checkKeyboardShortcuts(),
|
|
2389
|
+
debounce: () => this.checkSearchDebounce(),
|
|
2390
|
+
confirm: () => this.checkConfirmationDialogs(),
|
|
2391
|
+
progress: () => this.checkProgressIndicators(),
|
|
2392
|
+
recovery: () => this.checkErrorRecovery(),
|
|
2393
|
+
celebrate: () => this.checkSuccessCelebrations(),
|
|
2394
|
+
};
|
|
2395
|
+
if (filter && checkMap[filter]) {
|
|
2396
|
+
checks.push(await checkMap[filter]());
|
|
2397
|
+
}
|
|
2398
|
+
else {
|
|
2399
|
+
for (const check of Object.values(checkMap)) {
|
|
2400
|
+
checks.push(await check());
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
const errors = checks.reduce((sum, c) => sum + c.issues.filter((i) => i.severity === "error").length, 0);
|
|
2404
|
+
const warnings = checks.reduce((sum, c) => sum + c.issues.filter((i) => i.severity === "warning").length, 0);
|
|
2405
|
+
return {
|
|
2406
|
+
module: "UI Interactive States",
|
|
2407
|
+
passed: errors === 0,
|
|
2408
|
+
totalDuration: Date.now() - startTime,
|
|
2409
|
+
checks,
|
|
2410
|
+
summary: {
|
|
2411
|
+
total: checks.length,
|
|
2412
|
+
passed: checks.filter((c) => c.passed).length,
|
|
2413
|
+
failed: checks.filter((c) => !c.passed).length,
|
|
2414
|
+
errors,
|
|
2415
|
+
warnings,
|
|
2416
|
+
},
|
|
2417
|
+
};
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
exports.UIInteractiveStatesModule = UIInteractiveStatesModule;
|
|
2421
|
+
// CLI ENTRY POINT
|
|
2422
|
+
async function main() {
|
|
2423
|
+
const args = process.argv.slice(2);
|
|
2424
|
+
const filter = args.find((a) => !a.startsWith("-"));
|
|
2425
|
+
const verbose = args.includes("--verbose") || args.includes("-v");
|
|
2426
|
+
const parallel = args.includes("--parallel") || args.includes("-p");
|
|
2427
|
+
const reporter = (0, universal_progress_reporter_1.createUniversalProgressReporter)(path.basename(__filename, ".ts"));
|
|
2428
|
+
const module = new UIInteractiveStatesModule({ verbose, parallel });
|
|
2429
|
+
const result = await module.runAll(filter);
|
|
2430
|
+
console.log((0, console_chars_1.createDivider)(80, "heavy"));
|
|
2431
|
+
console.log(`${console_chars_1.emoji.target} UI INTERACTIVE STATES CHECK`);
|
|
2432
|
+
console.log((0, console_chars_1.createDivider)(80, "heavy"));
|
|
2433
|
+
console.log();
|
|
2434
|
+
for (const check of result.checks) {
|
|
2435
|
+
const icon = check.passed
|
|
2436
|
+
? console_chars_1.emoji.success
|
|
2437
|
+
: check.issues.some((i) => i.severity === "error")
|
|
2438
|
+
? console_chars_1.emoji.error
|
|
2439
|
+
: console_chars_1.emoji.warning;
|
|
2440
|
+
console.log(`${icon} ${check.name} (${check.duration}ms)`);
|
|
2441
|
+
if (check.issues.length > 0 && (verbose || check.issues.some((i) => i.severity !== "info"))) {
|
|
2442
|
+
for (const issue of check.issues.slice(0, verbose ? 100 : 5)) {
|
|
2443
|
+
const sevIcon = issue.severity === "error"
|
|
2444
|
+
? console_chars_1.emoji.error
|
|
2445
|
+
: issue.severity === "warning"
|
|
2446
|
+
? console_chars_1.emoji.warning
|
|
2447
|
+
: console_chars_1.emoji.info;
|
|
2448
|
+
const relPath = path.relative(process.cwd(), issue.file).replace(/\\/g, "/");
|
|
2449
|
+
console.log(` ${sevIcon} ${relPath}:${issue.line}`);
|
|
2450
|
+
console.log(` ${issue.message}`);
|
|
2451
|
+
console.log(` ${console_chars_1.emoji.hint} ${issue.suggestion}`);
|
|
2452
|
+
}
|
|
2453
|
+
if (!verbose && check.issues.length > 5) {
|
|
2454
|
+
console.log(` ... and ${check.issues.length - 5} more issues`);
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
console.log();
|
|
2458
|
+
}
|
|
2459
|
+
console.log((0, console_chars_1.createDivider)(80, "light"));
|
|
2460
|
+
console.log(`Total: ${result.summary.total} checks | ${console_chars_1.emoji.error} ${result.summary.errors} errors | ${console_chars_1.emoji.warning} ${result.summary.warnings} warnings`);
|
|
2461
|
+
console.log(`Duration: ${(result.totalDuration / 1000).toFixed(2)}s`);
|
|
2462
|
+
console.log((0, console_chars_1.createDivider)(80, "heavy"));
|
|
2463
|
+
if (result.summary.errors > 0) {
|
|
2464
|
+
console.log(`\n${console_chars_1.emoji.error} INTERACTIVE STATES CHECK FAILED\n`);
|
|
2465
|
+
process.exit(1);
|
|
2466
|
+
}
|
|
2467
|
+
else {
|
|
2468
|
+
console.log(`\n${console_chars_1.emoji.success} INTERACTIVE STATES CHECK PASSED\n`);
|
|
2469
|
+
process.exit(0);
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
if (require.main === module)
|
|
2473
|
+
main();
|
|
2474
|
+
//# sourceMappingURL=ui-interactive-states.js.map
|