@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,2212 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* UI Mobile UX Preflight (NON-BLOCKING)
|
|
5
|
+
*
|
|
6
|
+
* Comprehensive mobile user experience validation.
|
|
7
|
+
* Ensures the app provides a great experience on mobile devices.
|
|
8
|
+
*
|
|
9
|
+
* Checks:
|
|
10
|
+
* 1. Safe Area Insets - env(safe-area-inset-*) for notched devices
|
|
11
|
+
* 2. Input Types - Proper HTML input types for mobile keyboards
|
|
12
|
+
* 3. Font Size Minimum - 16px minimum to prevent iOS auto-zoom
|
|
13
|
+
* 4. Horizontal Overflow - Elements wider than viewport
|
|
14
|
+
* 5. Viewport Units - 100vh issues, prefer dvh/svh
|
|
15
|
+
* 6. Hover Media Query - @media (hover: hover) for hover-only styles
|
|
16
|
+
* 7. Bottom Navigation - Mobile nav patterns
|
|
17
|
+
* 8. Sticky Position - iOS Safari quirks
|
|
18
|
+
* 9. Orientation Support - Landscape handling
|
|
19
|
+
* 10. Form Labels - Visible labels, not just placeholders
|
|
20
|
+
* 11. Mobile Menu Focus - Focus management in mobile menus
|
|
21
|
+
* 12. Tap Delay - touch-action: manipulation
|
|
22
|
+
*
|
|
23
|
+
* Usage:
|
|
24
|
+
* pnpm preflight:ui-mobile-ux # All checks
|
|
25
|
+
* pnpm preflight:ui-mobile-ux safearea # Safe area only
|
|
26
|
+
* pnpm preflight:ui-mobile-ux inputs # Input types only
|
|
27
|
+
* pnpm preflight:ui-mobile-ux fonts # Font size only
|
|
28
|
+
* pnpm preflight:ui-mobile-ux overflow # Horizontal overflow only
|
|
29
|
+
* pnpm preflight:ui-mobile-ux viewport # Viewport units only
|
|
30
|
+
* pnpm preflight:ui-mobile-ux hover # Hover media query only
|
|
31
|
+
*/
|
|
32
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
33
|
+
if (k2 === undefined) k2 = k;
|
|
34
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
35
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
36
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
37
|
+
}
|
|
38
|
+
Object.defineProperty(o, k2, desc);
|
|
39
|
+
}) : (function(o, m, k, k2) {
|
|
40
|
+
if (k2 === undefined) k2 = k;
|
|
41
|
+
o[k2] = m[k];
|
|
42
|
+
}));
|
|
43
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
44
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
45
|
+
}) : function(o, v) {
|
|
46
|
+
o["default"] = v;
|
|
47
|
+
});
|
|
48
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
49
|
+
var ownKeys = function(o) {
|
|
50
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
51
|
+
var ar = [];
|
|
52
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
53
|
+
return ar;
|
|
54
|
+
};
|
|
55
|
+
return ownKeys(o);
|
|
56
|
+
};
|
|
57
|
+
return function (mod) {
|
|
58
|
+
if (mod && mod.__esModule) return mod;
|
|
59
|
+
var result = {};
|
|
60
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
61
|
+
__setModuleDefault(result, mod);
|
|
62
|
+
return result;
|
|
63
|
+
};
|
|
64
|
+
})();
|
|
65
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
66
|
+
exports.UIMobileUXModule = void 0;
|
|
67
|
+
const fs = __importStar(require("fs"));
|
|
68
|
+
const path = __importStar(require("path"));
|
|
69
|
+
const console_chars_1 = require("../../utils/console-chars");
|
|
70
|
+
const file_cache_1 = require("../../shared/file-cache");
|
|
71
|
+
const glob_patterns_1 = require("../../shared/glob-patterns");
|
|
72
|
+
const EXCLUDED = [...glob_patterns_1.STANDARD_EXCLUDES, "**/*.test.tsx", "**/*.spec.tsx", "**/*.stories.tsx"];
|
|
73
|
+
// Input type mappings for mobile keyboards
|
|
74
|
+
const INPUT_TYPE_PATTERNS = {
|
|
75
|
+
email: { pattern: /email|e-mail/i, type: "email", keyboard: "email keyboard with @" },
|
|
76
|
+
phone: { pattern: /phone|tel|mobile|cell/i, type: "tel", keyboard: "numeric keypad" },
|
|
77
|
+
url: { pattern: /url|website|link|href/i, type: "url", keyboard: "URL keyboard with .com" },
|
|
78
|
+
number: { pattern: /quantity|amount|count|age|year|zip|postal/i, type: "number", keyboard: "numeric keyboard" },
|
|
79
|
+
search: { pattern: /search|query|find/i, type: "search", keyboard: "search keyboard" },
|
|
80
|
+
};
|
|
81
|
+
class UIMobileUXModule {
|
|
82
|
+
verbose;
|
|
83
|
+
constructor(options = {}) {
|
|
84
|
+
this.verbose = options.verbose || false;
|
|
85
|
+
}
|
|
86
|
+
async getTsxFiles() {
|
|
87
|
+
return file_cache_1.fileCache.getAppAndComponentsTSX();
|
|
88
|
+
}
|
|
89
|
+
async getCssFiles() {
|
|
90
|
+
return file_cache_1.fileCache.getCSSFiles();
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Check if line should be skipped due to preflight-ignore comment
|
|
94
|
+
*/
|
|
95
|
+
shouldSkipLine(lines, lineIndex) {
|
|
96
|
+
// Check current line and up to 5 previous lines for preflight-ignore
|
|
97
|
+
const startIdx = Math.max(0, lineIndex - 5);
|
|
98
|
+
const contextLines = lines.slice(startIdx, lineIndex + 1).join("\n");
|
|
99
|
+
return contextLines.includes("preflight-ignore");
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* 1. Safe Area Insets
|
|
103
|
+
* Check for proper safe-area-inset usage for notched devices
|
|
104
|
+
*/
|
|
105
|
+
async checkSafeAreaInsets() {
|
|
106
|
+
const startTime = Date.now();
|
|
107
|
+
const issues = [];
|
|
108
|
+
const tsxFiles = await this.getTsxFiles();
|
|
109
|
+
const cssFiles = await this.getCssFiles();
|
|
110
|
+
let hasSafeAreaSupport = false;
|
|
111
|
+
let hasFixedBottomElements = false;
|
|
112
|
+
let hasFixedTopElements = false;
|
|
113
|
+
// Check CSS files for safe-area support
|
|
114
|
+
for (const file of cssFiles) {
|
|
115
|
+
const content = fs.readFileSync(file, "utf8");
|
|
116
|
+
if (/env\(safe-area-inset-/.test(content)) {
|
|
117
|
+
hasSafeAreaSupport = true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Check TSX files
|
|
121
|
+
for (const file of tsxFiles) {
|
|
122
|
+
const content = fs.readFileSync(file, "utf8");
|
|
123
|
+
const lines = content.split("\n");
|
|
124
|
+
if (/env\(safe-area-inset-|safe-area-inset/.test(content)) {
|
|
125
|
+
hasSafeAreaSupport = true;
|
|
126
|
+
}
|
|
127
|
+
for (let i = 0; i < lines.length; i++) {
|
|
128
|
+
const line = lines[i];
|
|
129
|
+
// Check for fixed/sticky bottom elements
|
|
130
|
+
if (/fixed.*bottom-0|sticky.*bottom-0|position:\s*fixed.*bottom:\s*0/.test(line)) {
|
|
131
|
+
hasFixedBottomElements = true;
|
|
132
|
+
const contextBlock = lines.slice(i, Math.min(i + 5, lines.length)).join("\n");
|
|
133
|
+
if (!/safe-area|pb-safe|padding-bottom.*env/.test(contextBlock)) {
|
|
134
|
+
issues.push({
|
|
135
|
+
file,
|
|
136
|
+
line: i + 1,
|
|
137
|
+
type: "fixed-bottom-no-safe-area",
|
|
138
|
+
severity: "warning",
|
|
139
|
+
message: "Fixed bottom element without safe area padding",
|
|
140
|
+
suggestion: "Add pb-[env(safe-area-inset-bottom)] for notched devices",
|
|
141
|
+
snippet: line.trim().substring(0, 80),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Check for fixed/sticky top elements (status bar area)
|
|
146
|
+
if (/fixed.*top-0|sticky.*top-0|position:\s*fixed.*top:\s*0/.test(line)) {
|
|
147
|
+
hasFixedTopElements = true;
|
|
148
|
+
const contextBlock = lines.slice(i, Math.min(i + 5, lines.length)).join("\n");
|
|
149
|
+
if (!/safe-area|pt-safe|padding-top.*env/.test(contextBlock)) {
|
|
150
|
+
issues.push({
|
|
151
|
+
file,
|
|
152
|
+
line: i + 1,
|
|
153
|
+
type: "fixed-top-no-safe-area",
|
|
154
|
+
severity: "info",
|
|
155
|
+
message: "Fixed top element - consider safe area for status bar",
|
|
156
|
+
suggestion: "Add pt-[env(safe-area-inset-top)] if content shouldn't go under status bar",
|
|
157
|
+
snippet: line.trim().substring(0, 80),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Global check
|
|
164
|
+
if ((hasFixedBottomElements || hasFixedTopElements) && !hasSafeAreaSupport) {
|
|
165
|
+
issues.push({
|
|
166
|
+
file: "codebase-wide",
|
|
167
|
+
line: 0,
|
|
168
|
+
type: "no-safe-area-support",
|
|
169
|
+
severity: "warning",
|
|
170
|
+
message: "Fixed positioned elements found but no safe-area-inset support detected",
|
|
171
|
+
suggestion: "Add CSS: padding-bottom: env(safe-area-inset-bottom, 0px) for notched devices",
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
name: "Safe Area Insets",
|
|
176
|
+
passed: issues.filter((i) => i.severity === "error").length === 0,
|
|
177
|
+
blocking: false,
|
|
178
|
+
issues,
|
|
179
|
+
duration: Date.now() - startTime,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* 2. Input Types for Mobile Keyboards
|
|
184
|
+
* Check that inputs use appropriate types for mobile keyboards
|
|
185
|
+
*/
|
|
186
|
+
async checkInputTypes() {
|
|
187
|
+
const startTime = Date.now();
|
|
188
|
+
const issues = [];
|
|
189
|
+
const files = await this.getTsxFiles();
|
|
190
|
+
for (const file of files) {
|
|
191
|
+
const content = fs.readFileSync(file, "utf8");
|
|
192
|
+
const lines = content.split("\n");
|
|
193
|
+
for (let i = 0; i < lines.length; i++) {
|
|
194
|
+
const line = lines[i];
|
|
195
|
+
// Check TextField/Input components
|
|
196
|
+
if (/<(?:TextField|Input|input)[^>]*/.test(line)) {
|
|
197
|
+
const contextBlock = lines.slice(i, Math.min(i + 3, lines.length)).join("\n");
|
|
198
|
+
// Extract label/name/placeholder
|
|
199
|
+
const labelMatch = contextBlock.match(/(?:label|name|placeholder)=["']([^"']+)["']/i);
|
|
200
|
+
if (labelMatch) {
|
|
201
|
+
const label = labelMatch[1].toLowerCase();
|
|
202
|
+
for (const [key, config] of Object.entries(INPUT_TYPE_PATTERNS)) {
|
|
203
|
+
if (config.pattern.test(label)) {
|
|
204
|
+
// Check if correct type is used
|
|
205
|
+
const typeMatch = contextBlock.match(/type=["'](\w+)["']/);
|
|
206
|
+
const currentType = typeMatch ? typeMatch[1] : "text";
|
|
207
|
+
if (currentType === "text" && config.type !== "text") {
|
|
208
|
+
issues.push({
|
|
209
|
+
file,
|
|
210
|
+
line: i + 1,
|
|
211
|
+
type: "input-type-mismatch",
|
|
212
|
+
severity: "info",
|
|
213
|
+
message: `Input "${labelMatch[1]}" uses type="text" but could use type="${config.type}"`,
|
|
214
|
+
suggestion: `Use type="${config.type}" for ${config.keyboard}`,
|
|
215
|
+
snippet: line.trim().substring(0, 80),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// Check for inputMode attribute (even better for mobile)
|
|
223
|
+
if (/type=["']number["']/.test(contextBlock) && !/inputMode/.test(contextBlock)) {
|
|
224
|
+
issues.push({
|
|
225
|
+
file,
|
|
226
|
+
line: i + 1,
|
|
227
|
+
type: "number-input-no-inputmode",
|
|
228
|
+
severity: "info",
|
|
229
|
+
message: 'type="number" without inputMode - consider inputMode="numeric" or "decimal"',
|
|
230
|
+
suggestion: 'Add inputMode="numeric" for integer-only or inputMode="decimal" for decimals',
|
|
231
|
+
snippet: line.trim().substring(0, 80),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
name: "Input Types",
|
|
239
|
+
passed: true, // Info only
|
|
240
|
+
blocking: false,
|
|
241
|
+
issues,
|
|
242
|
+
duration: Date.now() - startTime,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* 3. Font Size Minimum (iOS Auto-Zoom Prevention)
|
|
247
|
+
* Check that input font sizes are at least 16px to prevent iOS zoom
|
|
248
|
+
*/
|
|
249
|
+
async checkFontSizeMinimum() {
|
|
250
|
+
const startTime = Date.now();
|
|
251
|
+
const issues = [];
|
|
252
|
+
const files = await this.getTsxFiles();
|
|
253
|
+
const cssFiles = await this.getCssFiles();
|
|
254
|
+
// Check CSS for small input font sizes
|
|
255
|
+
for (const file of cssFiles) {
|
|
256
|
+
const content = fs.readFileSync(file, "utf8");
|
|
257
|
+
const lines = content.split("\n");
|
|
258
|
+
for (let i = 0; i < lines.length; i++) {
|
|
259
|
+
const line = lines[i];
|
|
260
|
+
// Check for input/textarea font-size < 16px
|
|
261
|
+
if (/input|textarea|select/i.test(content.substring(Math.max(0, content.indexOf(line) - 100), content.indexOf(line)))) {
|
|
262
|
+
const fontSizeMatch = line.match(/font-size:\s*(\d+(?:\.\d+)?)(px|rem)/);
|
|
263
|
+
if (fontSizeMatch) {
|
|
264
|
+
const size = parseFloat(fontSizeMatch[1]);
|
|
265
|
+
const unit = fontSizeMatch[2];
|
|
266
|
+
const pxSize = unit === "rem" ? size * 16 : size;
|
|
267
|
+
if (pxSize < 16) {
|
|
268
|
+
issues.push({
|
|
269
|
+
file,
|
|
270
|
+
line: i + 1,
|
|
271
|
+
type: "input-font-too-small",
|
|
272
|
+
severity: "warning",
|
|
273
|
+
message: `Input font-size ${fontSizeMatch[1]}${unit} (${pxSize}px) causes iOS auto-zoom`,
|
|
274
|
+
suggestion: "Use at least 16px (1rem) for input font-size to prevent iOS zoom on focus",
|
|
275
|
+
snippet: line.trim().substring(0, 80),
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// Check TSX for small text on inputs
|
|
283
|
+
for (const file of files) {
|
|
284
|
+
const content = fs.readFileSync(file, "utf8");
|
|
285
|
+
const lines = content.split("\n");
|
|
286
|
+
for (let i = 0; i < lines.length; i++) {
|
|
287
|
+
const line = lines[i];
|
|
288
|
+
if (/<(?:TextField|Input|input|textarea|select)/i.test(line)) {
|
|
289
|
+
// Check for text-xs or text-sm classes
|
|
290
|
+
if (/className=["'][^"']*\btext-(?:xs|sm)\b/.test(line)) {
|
|
291
|
+
issues.push({
|
|
292
|
+
file,
|
|
293
|
+
line: i + 1,
|
|
294
|
+
type: "input-text-class-small",
|
|
295
|
+
severity: "warning",
|
|
296
|
+
message: "Input with text-xs/text-sm class may cause iOS auto-zoom",
|
|
297
|
+
suggestion: "Use text-base (16px) or larger for inputs to prevent zoom",
|
|
298
|
+
snippet: line.trim().substring(0, 80),
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
name: "Font Size Minimum",
|
|
306
|
+
passed: issues.filter((i) => i.severity === "error").length === 0,
|
|
307
|
+
blocking: false,
|
|
308
|
+
issues,
|
|
309
|
+
duration: Date.now() - startTime,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* 4. Horizontal Overflow Prevention
|
|
314
|
+
* Check for elements that might cause horizontal scroll on mobile
|
|
315
|
+
*/
|
|
316
|
+
async checkHorizontalOverflow() {
|
|
317
|
+
const startTime = Date.now();
|
|
318
|
+
const issues = [];
|
|
319
|
+
const files = await this.getTsxFiles();
|
|
320
|
+
for (const file of files) {
|
|
321
|
+
const content = fs.readFileSync(file, "utf8");
|
|
322
|
+
const lines = content.split("\n");
|
|
323
|
+
for (let i = 0; i < lines.length; i++) {
|
|
324
|
+
const line = lines[i];
|
|
325
|
+
// Check for fixed widths larger than typical mobile screens
|
|
326
|
+
const widthMatch = line.match(/(?:width|min-width):\s*["']?(\d+)px/);
|
|
327
|
+
if (widthMatch) {
|
|
328
|
+
const width = parseInt(widthMatch[1]);
|
|
329
|
+
if (width > 500) {
|
|
330
|
+
const contextBlock = lines.slice(Math.max(0, i - 2), Math.min(i + 3, lines.length)).join("\n");
|
|
331
|
+
// Check if it has responsive handling
|
|
332
|
+
if (!/max-width|overflow|sm:|md:|lg:|@media/.test(contextBlock)) {
|
|
333
|
+
issues.push({
|
|
334
|
+
file,
|
|
335
|
+
line: i + 1,
|
|
336
|
+
type: "fixed-width-overflow-risk",
|
|
337
|
+
severity: "info",
|
|
338
|
+
message: `Fixed width ${width}px may cause horizontal scroll on mobile`,
|
|
339
|
+
suggestion: "Add max-width: 100% or use responsive width classes",
|
|
340
|
+
snippet: line.trim().substring(0, 80),
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// Check for w-screen without overflow handling
|
|
346
|
+
if (/\bw-screen\b/.test(line)) {
|
|
347
|
+
const contextBlock = lines.slice(i, Math.min(i + 3, lines.length)).join("\n");
|
|
348
|
+
if (!/overflow-x-hidden|overflow-hidden/.test(contextBlock)) {
|
|
349
|
+
issues.push({
|
|
350
|
+
file,
|
|
351
|
+
line: i + 1,
|
|
352
|
+
type: "w-screen-no-overflow",
|
|
353
|
+
severity: "info",
|
|
354
|
+
message: "w-screen can cause horizontal scroll if content overflows",
|
|
355
|
+
suggestion: "Add overflow-x-hidden to parent or use w-full instead",
|
|
356
|
+
snippet: line.trim().substring(0, 80),
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// Check for negative margins that might cause overflow
|
|
361
|
+
if (/-mx-\d+|-ml-\d+|-mr-\d+/.test(line)) {
|
|
362
|
+
const contextBlock = lines.slice(Math.max(0, i - 3), Math.min(i + 3, lines.length)).join("\n");
|
|
363
|
+
if (!/overflow-hidden|overflow-x-hidden/.test(contextBlock)) {
|
|
364
|
+
issues.push({
|
|
365
|
+
file,
|
|
366
|
+
line: i + 1,
|
|
367
|
+
type: "negative-margin-overflow-risk",
|
|
368
|
+
severity: "info",
|
|
369
|
+
message: "Negative horizontal margin can cause mobile overflow",
|
|
370
|
+
suggestion: "Ensure parent has overflow-hidden or equivalent",
|
|
371
|
+
snippet: line.trim().substring(0, 80),
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
name: "Horizontal Overflow",
|
|
379
|
+
passed: true, // Info only
|
|
380
|
+
blocking: false,
|
|
381
|
+
issues,
|
|
382
|
+
duration: Date.now() - startTime,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* 5. Viewport Units (100vh issues)
|
|
387
|
+
* Check for 100vh usage which is problematic on mobile browsers
|
|
388
|
+
*/
|
|
389
|
+
async checkViewportUnits() {
|
|
390
|
+
const startTime = Date.now();
|
|
391
|
+
const issues = [];
|
|
392
|
+
const files = await this.getTsxFiles();
|
|
393
|
+
const cssFiles = await this.getCssFiles();
|
|
394
|
+
const allFiles = [...files, ...cssFiles];
|
|
395
|
+
for (const file of allFiles) {
|
|
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
|
+
// Check for 100vh usage
|
|
401
|
+
if (/\b100vh\b|h-screen/.test(line)) {
|
|
402
|
+
const contextBlock = lines.slice(Math.max(0, i - 2), Math.min(i + 3, lines.length)).join("\n");
|
|
403
|
+
// Check if modern viewport units are used as fallback
|
|
404
|
+
const hasModernUnits = /\b100dvh\b|\b100svh\b|\b100lvh\b|dvh|svh/.test(contextBlock);
|
|
405
|
+
const hasMinHeight = /min-h-screen|min-height:\s*100vh/.test(contextBlock);
|
|
406
|
+
if (!hasModernUnits) {
|
|
407
|
+
issues.push({
|
|
408
|
+
file,
|
|
409
|
+
line: i + 1,
|
|
410
|
+
type: "100vh-mobile-issue",
|
|
411
|
+
severity: "info",
|
|
412
|
+
message: "100vh/h-screen is unreliable on mobile (address bar changes height)",
|
|
413
|
+
suggestion: "Use min-h-[100dvh] or height: 100dvh for dynamic viewport on mobile",
|
|
414
|
+
snippet: line.trim().substring(0, 80),
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
// Check for vh units in animations (can cause reflows on mobile)
|
|
419
|
+
if (/transform.*translateY.*vh|animation.*vh/.test(line)) {
|
|
420
|
+
issues.push({
|
|
421
|
+
file,
|
|
422
|
+
line: i + 1,
|
|
423
|
+
type: "vh-in-animation",
|
|
424
|
+
severity: "info",
|
|
425
|
+
message: "vh units in animations can cause jank on mobile (address bar resize)",
|
|
426
|
+
suggestion: "Use fixed px values or % for smoother animations",
|
|
427
|
+
snippet: line.trim().substring(0, 80),
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
name: "Viewport Units",
|
|
434
|
+
passed: true, // Info only
|
|
435
|
+
blocking: false,
|
|
436
|
+
issues,
|
|
437
|
+
duration: Date.now() - startTime,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* 6. Hover Media Query
|
|
442
|
+
* Check that hover styles use @media (hover: hover) for touch devices
|
|
443
|
+
*/
|
|
444
|
+
async checkHoverMediaQuery() {
|
|
445
|
+
const startTime = Date.now();
|
|
446
|
+
const issues = [];
|
|
447
|
+
const cssFiles = await this.getCssFiles();
|
|
448
|
+
let hasHoverMediaQuery = false;
|
|
449
|
+
let hoverStyleCount = 0;
|
|
450
|
+
for (const file of cssFiles) {
|
|
451
|
+
const content = fs.readFileSync(file, "utf8");
|
|
452
|
+
if (/@media\s*\(hover:\s*hover\)/.test(content)) {
|
|
453
|
+
hasHoverMediaQuery = true;
|
|
454
|
+
}
|
|
455
|
+
// Count hover styles
|
|
456
|
+
const hoverMatches = content.match(/:hover\s*\{/g);
|
|
457
|
+
if (hoverMatches) {
|
|
458
|
+
hoverStyleCount += hoverMatches.length;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
// Check TSX for complex hover states that might need media query
|
|
462
|
+
const tsxFiles = await this.getTsxFiles();
|
|
463
|
+
for (const file of tsxFiles) {
|
|
464
|
+
const content = fs.readFileSync(file, "utf8");
|
|
465
|
+
const lines = content.split("\n");
|
|
466
|
+
for (let i = 0; i < lines.length; i++) {
|
|
467
|
+
const line = lines[i];
|
|
468
|
+
// Skip lines with preflight-ignore
|
|
469
|
+
if (this.shouldSkipLine(lines, i)) {
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
// Check for hover state that changes layout/visibility
|
|
473
|
+
if (/hover:(?:block|flex|grid|visible|opacity-100)/.test(line)) {
|
|
474
|
+
const contextBlock = lines.slice(Math.max(0, i - 2), Math.min(i + 3, lines.length)).join("\n");
|
|
475
|
+
// Check if there's a touch/click alternative or mobile-first responsive classes
|
|
476
|
+
if (!/onClick|onTouchStart|group-focus|focus:|md:opacity-0|sm:opacity-0/.test(contextBlock)) {
|
|
477
|
+
issues.push({
|
|
478
|
+
file,
|
|
479
|
+
line: i + 1,
|
|
480
|
+
type: "hover-reveals-content",
|
|
481
|
+
severity: "warning",
|
|
482
|
+
message: "Hover-revealed content is inaccessible on touch devices",
|
|
483
|
+
suggestion: "Add onClick handler or focus state for touch device access",
|
|
484
|
+
snippet: line.trim().substring(0, 80),
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
// Global suggestion if many hover styles but no media query
|
|
491
|
+
if (hoverStyleCount > 10 && !hasHoverMediaQuery) {
|
|
492
|
+
issues.push({
|
|
493
|
+
file: "codebase-wide",
|
|
494
|
+
line: 0,
|
|
495
|
+
type: "no-hover-media-query",
|
|
496
|
+
severity: "info",
|
|
497
|
+
message: `${hoverStyleCount} hover styles found - consider @media (hover: hover) for touch devices`,
|
|
498
|
+
suggestion: "Wrap complex hover effects in @media (hover: hover) { } to avoid sticky hover on touch",
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
return {
|
|
502
|
+
name: "Hover Media Query",
|
|
503
|
+
passed: issues.filter((i) => i.severity === "error").length === 0,
|
|
504
|
+
blocking: false,
|
|
505
|
+
issues,
|
|
506
|
+
duration: Date.now() - startTime,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* 7. Form Labels (Not Just Placeholders)
|
|
511
|
+
* Check that form inputs have visible labels, not just placeholders
|
|
512
|
+
*/
|
|
513
|
+
async checkFormLabels() {
|
|
514
|
+
const startTime = Date.now();
|
|
515
|
+
const issues = [];
|
|
516
|
+
const files = await this.getTsxFiles();
|
|
517
|
+
for (const file of files) {
|
|
518
|
+
const content = fs.readFileSync(file, "utf8");
|
|
519
|
+
const lines = content.split("\n");
|
|
520
|
+
for (let i = 0; i < lines.length; i++) {
|
|
521
|
+
const line = lines[i];
|
|
522
|
+
// Check for input with placeholder but no label
|
|
523
|
+
if (/<(?:input|Input|TextField)[^>]*placeholder=/.test(line)) {
|
|
524
|
+
const contextBlock = lines.slice(Math.max(0, i - 5), Math.min(i + 5, lines.length)).join("\n");
|
|
525
|
+
const hasLabel = /label=|<label|<Label|aria-label|aria-labelledby|id=.*for=|htmlFor/.test(contextBlock);
|
|
526
|
+
if (!hasLabel) {
|
|
527
|
+
issues.push({
|
|
528
|
+
file,
|
|
529
|
+
line: i + 1,
|
|
530
|
+
type: "placeholder-only-label",
|
|
531
|
+
severity: "info",
|
|
532
|
+
message: "Input has placeholder but no visible label",
|
|
533
|
+
suggestion: "Add a visible label - placeholders disappear on focus and aren't accessible",
|
|
534
|
+
snippet: line.trim().substring(0, 80),
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return {
|
|
541
|
+
name: "Form Labels",
|
|
542
|
+
passed: true, // Info only
|
|
543
|
+
blocking: false,
|
|
544
|
+
issues,
|
|
545
|
+
duration: Date.now() - startTime,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* 8. Sticky Position Mobile Issues
|
|
550
|
+
* Check for sticky positioning that may have iOS Safari issues
|
|
551
|
+
*/
|
|
552
|
+
async checkStickyPosition() {
|
|
553
|
+
const startTime = Date.now();
|
|
554
|
+
const issues = [];
|
|
555
|
+
const files = await this.getTsxFiles();
|
|
556
|
+
for (const file of files) {
|
|
557
|
+
const content = fs.readFileSync(file, "utf8");
|
|
558
|
+
const lines = content.split("\n");
|
|
559
|
+
for (let i = 0; i < lines.length; i++) {
|
|
560
|
+
const line = lines[i];
|
|
561
|
+
const trimmedLine = line.trim();
|
|
562
|
+
// Skip comment lines, JSDoc, and type definitions
|
|
563
|
+
if (trimmedLine.startsWith("//") ||
|
|
564
|
+
trimmedLine.startsWith("*") ||
|
|
565
|
+
trimmedLine.startsWith("/**") ||
|
|
566
|
+
trimmedLine.startsWith("/*") ||
|
|
567
|
+
/^\s*\*/.test(line) ||
|
|
568
|
+
/interface\s|type\s|:\s*boolean/.test(line)) {
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
// Check for sticky in className or style context (not just any word "sticky")
|
|
572
|
+
// Avoid matching "sticky" in variable names like "z-sticky" or "--z-sticky"
|
|
573
|
+
const hasStickyClass = /className=["'][^"']*\bsticky\b(?!-)/.test(line);
|
|
574
|
+
const hasStickyStyle = /position:\s*["']?sticky/.test(line);
|
|
575
|
+
// Skip lines where "sticky" is just in a variable name (e.g., z-[var(--z-sticky)])
|
|
576
|
+
const isStickyInVarName = /var\(--[^)]*sticky|z-sticky/.test(line);
|
|
577
|
+
if ((hasStickyClass || hasStickyStyle) && !isStickyInVarName) {
|
|
578
|
+
// Check 5 lines of context for top/bottom positioning
|
|
579
|
+
const contextBlock = lines.slice(i, Math.min(i + 5, lines.length)).join("\n");
|
|
580
|
+
// Sticky can use either top or bottom for positioning
|
|
581
|
+
// Match: top-0, top: 0, top: sticky ? 0, bottom-0, bottom: 0, top: CONSTANT, etc.
|
|
582
|
+
const hasPositioning = /top-\d|top:\s*(?:\w+\s*\?\s*)?[\d\w]|bottom-\d|bottom:\s*(?:\w+\s*\?\s*)?[\d\w]/.test(contextBlock);
|
|
583
|
+
if (!hasPositioning) {
|
|
584
|
+
issues.push({
|
|
585
|
+
file,
|
|
586
|
+
line: i + 1,
|
|
587
|
+
type: "sticky-no-top",
|
|
588
|
+
severity: "warning",
|
|
589
|
+
message: "Sticky element without top/bottom value - won't stick properly",
|
|
590
|
+
suggestion: "Add top-0 (for headers) or bottom-0 (for footers) for sticky positioning",
|
|
591
|
+
snippet: line.trim().substring(0, 80),
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
// Check for sticky inside overflow container (common issue)
|
|
595
|
+
// This is harder to detect statically, so just info
|
|
596
|
+
}
|
|
597
|
+
// Check for -webkit-sticky fallback
|
|
598
|
+
if (/position:\s*sticky/.test(line) && !/position:\s*-webkit-sticky/.test(content)) {
|
|
599
|
+
// Modern browsers don't need this anymore, but good to be aware
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return {
|
|
604
|
+
name: "Sticky Position",
|
|
605
|
+
passed: issues.filter((i) => i.severity === "error").length === 0,
|
|
606
|
+
blocking: false,
|
|
607
|
+
issues,
|
|
608
|
+
duration: Date.now() - startTime,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* 9. Orientation Support
|
|
613
|
+
* Check for landscape orientation handling
|
|
614
|
+
*/
|
|
615
|
+
async checkOrientationSupport() {
|
|
616
|
+
const startTime = Date.now();
|
|
617
|
+
const issues = [];
|
|
618
|
+
const cssFiles = await this.getCssFiles();
|
|
619
|
+
const tsxFiles = await this.getTsxFiles();
|
|
620
|
+
let hasOrientationHandling = false;
|
|
621
|
+
for (const file of cssFiles) {
|
|
622
|
+
const content = fs.readFileSync(file, "utf8");
|
|
623
|
+
if (/orientation:\s*(?:landscape|portrait)/.test(content)) {
|
|
624
|
+
hasOrientationHandling = true;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
// Check for full-screen modals that might need orientation handling
|
|
628
|
+
for (const file of tsxFiles) {
|
|
629
|
+
const content = fs.readFileSync(file, "utf8");
|
|
630
|
+
const lines = content.split("\n");
|
|
631
|
+
for (let i = 0; i < lines.length; i++) {
|
|
632
|
+
const line = lines[i];
|
|
633
|
+
// Full-screen overlays should handle both orientations
|
|
634
|
+
if (/\bh-screen\b.*\bw-screen\b|\bfixed\b.*\binset-0\b/.test(line)) {
|
|
635
|
+
const contextBlock = lines.slice(i, Math.min(i + 10, lines.length)).join("\n");
|
|
636
|
+
if (!/overflow|scroll|landscape|orientation/.test(contextBlock)) {
|
|
637
|
+
issues.push({
|
|
638
|
+
file,
|
|
639
|
+
line: i + 1,
|
|
640
|
+
type: "fullscreen-no-scroll",
|
|
641
|
+
severity: "info",
|
|
642
|
+
message: "Full-screen element - ensure content is scrollable in landscape mode",
|
|
643
|
+
suggestion: "Add overflow-y-auto for landscape orientation support",
|
|
644
|
+
snippet: line.trim().substring(0, 80),
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return {
|
|
651
|
+
name: "Orientation Support",
|
|
652
|
+
passed: true, // Info only
|
|
653
|
+
blocking: false,
|
|
654
|
+
issues,
|
|
655
|
+
duration: Date.now() - startTime,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* 10. Touch Action Optimization
|
|
660
|
+
* Check for touch-action: manipulation for better touch performance
|
|
661
|
+
*/
|
|
662
|
+
async checkTouchAction() {
|
|
663
|
+
const startTime = Date.now();
|
|
664
|
+
const issues = [];
|
|
665
|
+
const cssFiles = await this.getCssFiles();
|
|
666
|
+
const tsxFiles = await this.getTsxFiles();
|
|
667
|
+
let hasTouchActionManipulation = false;
|
|
668
|
+
let hasDoubleClickElements = false;
|
|
669
|
+
// Check global CSS for touch-action
|
|
670
|
+
for (const file of cssFiles) {
|
|
671
|
+
const content = fs.readFileSync(file, "utf8");
|
|
672
|
+
if (/touch-action:\s*manipulation/.test(content)) {
|
|
673
|
+
hasTouchActionManipulation = true;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
// Check TSX files
|
|
677
|
+
for (const file of tsxFiles) {
|
|
678
|
+
const content = fs.readFileSync(file, "utf8");
|
|
679
|
+
if (/touch-manipulation|touch-action.*manipulation/.test(content)) {
|
|
680
|
+
hasTouchActionManipulation = true;
|
|
681
|
+
}
|
|
682
|
+
if (/onDoubleClick|dblclick/.test(content)) {
|
|
683
|
+
hasDoubleClickElements = true;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
// Global suggestion if no touch-action: manipulation
|
|
687
|
+
if (!hasTouchActionManipulation) {
|
|
688
|
+
issues.push({
|
|
689
|
+
file: "codebase-wide",
|
|
690
|
+
line: 0,
|
|
691
|
+
type: "no-touch-action-manipulation",
|
|
692
|
+
severity: "info",
|
|
693
|
+
message: "No global touch-action: manipulation detected",
|
|
694
|
+
suggestion: "Add touch-action: manipulation to <html> or clickable elements to remove 300ms tap delay",
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
return {
|
|
698
|
+
name: "Touch Action Optimization",
|
|
699
|
+
passed: true, // Info only
|
|
700
|
+
blocking: false,
|
|
701
|
+
issues,
|
|
702
|
+
duration: Date.now() - startTime,
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* 11. Mobile Menu Focus Management
|
|
707
|
+
* Check that mobile menus trap focus properly
|
|
708
|
+
*/
|
|
709
|
+
async checkMobileMenuFocus() {
|
|
710
|
+
const startTime = Date.now();
|
|
711
|
+
const issues = [];
|
|
712
|
+
const files = await this.getTsxFiles();
|
|
713
|
+
for (const file of files) {
|
|
714
|
+
const content = fs.readFileSync(file, "utf8");
|
|
715
|
+
const lines = content.split("\n");
|
|
716
|
+
for (let i = 0; i < lines.length; i++) {
|
|
717
|
+
const line = lines[i];
|
|
718
|
+
// Check for mobile menu/drawer components
|
|
719
|
+
if (/MobileMenu|MobileNav|Drawer|Sheet|Sidebar.*mobile/i.test(line)) {
|
|
720
|
+
const contextBlock = lines.slice(i, Math.min(i + 30, lines.length)).join("\n");
|
|
721
|
+
// Check for focus trap
|
|
722
|
+
const hasFocusTrap = /FocusTrap|focus-trap|trapFocus|inert|aria-modal/.test(contextBlock);
|
|
723
|
+
if (!hasFocusTrap) {
|
|
724
|
+
issues.push({
|
|
725
|
+
file,
|
|
726
|
+
line: i + 1,
|
|
727
|
+
type: "mobile-menu-no-focus-trap",
|
|
728
|
+
severity: "info",
|
|
729
|
+
message: "Mobile menu/drawer may lack focus trap",
|
|
730
|
+
suggestion: "Add FocusTrap component or aria-modal for accessibility",
|
|
731
|
+
snippet: line.trim().substring(0, 80),
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
// Check for close on escape
|
|
735
|
+
const hasEscapeClose = /Escape|onKeyDown|handleKeyDown/.test(contextBlock);
|
|
736
|
+
if (!hasEscapeClose) {
|
|
737
|
+
issues.push({
|
|
738
|
+
file,
|
|
739
|
+
line: i + 1,
|
|
740
|
+
type: "mobile-menu-no-escape",
|
|
741
|
+
severity: "info",
|
|
742
|
+
message: "Mobile menu may not close on Escape key",
|
|
743
|
+
suggestion: "Add keyboard handler to close on Escape press",
|
|
744
|
+
snippet: line.trim().substring(0, 80),
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return {
|
|
751
|
+
name: "Mobile Menu Focus",
|
|
752
|
+
passed: true, // Info only
|
|
753
|
+
blocking: false,
|
|
754
|
+
issues,
|
|
755
|
+
duration: Date.now() - startTime,
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* 12. Bottom Navigation Pattern
|
|
760
|
+
* Check for proper mobile bottom navigation patterns
|
|
761
|
+
*/
|
|
762
|
+
async checkBottomNavigation() {
|
|
763
|
+
const startTime = Date.now();
|
|
764
|
+
const issues = [];
|
|
765
|
+
const files = await this.getTsxFiles();
|
|
766
|
+
let hasBottomNav = false;
|
|
767
|
+
for (const file of files) {
|
|
768
|
+
const content = fs.readFileSync(file, "utf8");
|
|
769
|
+
const lines = content.split("\n");
|
|
770
|
+
for (let i = 0; i < lines.length; i++) {
|
|
771
|
+
const line = lines[i];
|
|
772
|
+
// Skip lines with preflight-ignore or comment lines
|
|
773
|
+
if (this.shouldSkipLine(lines, i)) {
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
const trimmedLine = line.trim();
|
|
777
|
+
if (trimmedLine.startsWith("//") || trimmedLine.startsWith("*") || trimmedLine.startsWith("{/*")) {
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
// Check for bottom navigation in actual code (not comments)
|
|
781
|
+
// Only match fixed bottom-positioned elements (viewport level), not absolute (container level)
|
|
782
|
+
const matchesBottomNav = /BottomNav|MobileTabBar/i.test(line) ||
|
|
783
|
+
/fixed\s+bottom-0|bottom-0\s+.*fixed|fixed.*\s+bottom-\d/.test(line);
|
|
784
|
+
if (matchesBottomNav) {
|
|
785
|
+
hasBottomNav = true;
|
|
786
|
+
const contextBlock = lines.slice(i, Math.min(i + 10, lines.length)).join("\n");
|
|
787
|
+
// Check for safe area padding
|
|
788
|
+
if (!/safe-area|pb-safe|padding-bottom.*env/.test(contextBlock)) {
|
|
789
|
+
issues.push({
|
|
790
|
+
file,
|
|
791
|
+
line: i + 1,
|
|
792
|
+
type: "bottom-nav-no-safe-area",
|
|
793
|
+
severity: "warning",
|
|
794
|
+
message: "Bottom navigation without safe area padding",
|
|
795
|
+
suggestion: "Add pb-[env(safe-area-inset-bottom)] for notched devices",
|
|
796
|
+
snippet: line.trim().substring(0, 80),
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
// Check for adequate touch targets
|
|
800
|
+
if (/h-\[(\d+)px\]|height:\s*(\d+)px/.test(contextBlock)) {
|
|
801
|
+
const heightMatch = contextBlock.match(/h-\[(\d+)px\]|height:\s*(\d+)px/);
|
|
802
|
+
if (heightMatch) {
|
|
803
|
+
const height = parseInt(heightMatch[1] || heightMatch[2]);
|
|
804
|
+
if (height < 48) {
|
|
805
|
+
issues.push({
|
|
806
|
+
file,
|
|
807
|
+
line: i + 1,
|
|
808
|
+
type: "bottom-nav-too-short",
|
|
809
|
+
severity: "warning",
|
|
810
|
+
message: `Bottom nav height ${height}px is below 48px minimum`,
|
|
811
|
+
suggestion: "Increase height to at least 48px for comfortable touch targets",
|
|
812
|
+
snippet: line.trim().substring(0, 80),
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return {
|
|
821
|
+
name: "Bottom Navigation",
|
|
822
|
+
passed: issues.filter((i) => i.severity === "error").length === 0,
|
|
823
|
+
blocking: false,
|
|
824
|
+
issues,
|
|
825
|
+
duration: Date.now() - startTime,
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* 13. Pull-to-Refresh Detection
|
|
830
|
+
* Check if scrollable lists implement pull-to-refresh patterns
|
|
831
|
+
*/
|
|
832
|
+
async checkPullToRefresh() {
|
|
833
|
+
const startTime = Date.now();
|
|
834
|
+
const issues = [];
|
|
835
|
+
const files = await this.getTsxFiles();
|
|
836
|
+
for (const file of files) {
|
|
837
|
+
const content = fs.readFileSync(file, "utf8");
|
|
838
|
+
const lines = content.split("\n");
|
|
839
|
+
for (let i = 0; i < lines.length; i++) {
|
|
840
|
+
const line = lines[i];
|
|
841
|
+
// Check for scrollable list components that might need pull-to-refresh
|
|
842
|
+
if (/useInfiniteQuery|InfiniteScroll|VirtualList|virtualized/i.test(line)) {
|
|
843
|
+
const contextBlock = lines.slice(Math.max(0, i - 10), Math.min(i + 20, lines.length)).join("\n");
|
|
844
|
+
const hasPullToRefresh = /pullToRefresh|onRefresh|RefreshControl|pull.*refresh|usePullToRefresh/i.test(contextBlock);
|
|
845
|
+
const hasRefetchButton = /refetch|refresh.*button|reload/i.test(contextBlock);
|
|
846
|
+
if (!hasPullToRefresh && !hasRefetchButton) {
|
|
847
|
+
issues.push({
|
|
848
|
+
file,
|
|
849
|
+
line: i + 1,
|
|
850
|
+
type: "infinite-list-no-refresh",
|
|
851
|
+
severity: "info",
|
|
852
|
+
message: "Infinite/virtual list without pull-to-refresh or refresh button",
|
|
853
|
+
suggestion: "Add pull-to-refresh for mobile or a visible refresh button",
|
|
854
|
+
snippet: line.trim().substring(0, 80),
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
// Check for overscroll-behavior on scrollable containers
|
|
859
|
+
if (/overflow-y-auto|overflow-y-scroll|overflow-auto/.test(line)) {
|
|
860
|
+
const contextBlock = lines.slice(i, Math.min(i + 5, lines.length)).join("\n");
|
|
861
|
+
// overscroll-contain prevents pull-to-refresh from triggering page refresh
|
|
862
|
+
if (!/overscroll-contain|overscroll-behavior/.test(contextBlock)) {
|
|
863
|
+
// Only flag if it looks like a main scrollable area
|
|
864
|
+
if (/h-full|h-screen|flex-1/.test(contextBlock)) {
|
|
865
|
+
issues.push({
|
|
866
|
+
file,
|
|
867
|
+
line: i + 1,
|
|
868
|
+
type: "scrollable-no-overscroll",
|
|
869
|
+
severity: "info",
|
|
870
|
+
message: "Main scrollable container without overscroll-behavior",
|
|
871
|
+
suggestion: "Add overscroll-contain to prevent scroll chaining on mobile",
|
|
872
|
+
snippet: line.trim().substring(0, 80),
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return {
|
|
880
|
+
name: "Pull-to-Refresh",
|
|
881
|
+
passed: true, // Info only
|
|
882
|
+
blocking: false,
|
|
883
|
+
issues,
|
|
884
|
+
duration: Date.now() - startTime,
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* 14. Mobile Table Patterns
|
|
889
|
+
* Check that tables are responsive on mobile (scrollable or card-based)
|
|
890
|
+
*/
|
|
891
|
+
async checkMobileTablePatterns() {
|
|
892
|
+
const startTime = Date.now();
|
|
893
|
+
const issues = [];
|
|
894
|
+
const files = await this.getTsxFiles();
|
|
895
|
+
for (const file of files) {
|
|
896
|
+
const content = fs.readFileSync(file, "utf8");
|
|
897
|
+
const lines = content.split("\n");
|
|
898
|
+
for (let i = 0; i < lines.length; i++) {
|
|
899
|
+
const line = lines[i];
|
|
900
|
+
// Skip lines with preflight-ignore
|
|
901
|
+
if (this.shouldSkipLine(lines, i)) {
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
// Check for table elements
|
|
905
|
+
if (/<(?:Table|table)\b/.test(line)) {
|
|
906
|
+
const contextBlock = lines.slice(Math.max(0, i - 5), Math.min(i + 15, lines.length)).join("\n");
|
|
907
|
+
// Check for responsive handling (Tailwind classes or inline styles)
|
|
908
|
+
const hasOverflowScroll = /overflow-x-auto|overflow-x-scroll|overflow-auto|overflow:\s*["']?auto/.test(contextBlock);
|
|
909
|
+
const hasMobileCard = /md:|lg:|sm:|hidden.*table|table.*hidden|MobileCard|CardView/i.test(contextBlock);
|
|
910
|
+
const hasResponsiveWrapper = /TableContainer|responsive|mobile/i.test(contextBlock);
|
|
911
|
+
if (!hasOverflowScroll && !hasMobileCard && !hasResponsiveWrapper) {
|
|
912
|
+
issues.push({
|
|
913
|
+
file,
|
|
914
|
+
line: i + 1,
|
|
915
|
+
type: "table-not-mobile-responsive",
|
|
916
|
+
severity: "warning",
|
|
917
|
+
message: "Table without mobile-responsive handling",
|
|
918
|
+
suggestion: "Wrap in overflow-x-auto container or use responsive card pattern on mobile",
|
|
919
|
+
snippet: line.trim().substring(0, 80),
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
// Check for minimum column widths that might cause issues
|
|
923
|
+
if (/min-w-\[(\d+)px\]/.test(contextBlock)) {
|
|
924
|
+
const minWidthMatch = contextBlock.match(/min-w-\[(\d+)px\]/g);
|
|
925
|
+
if (minWidthMatch && minWidthMatch.length > 3) {
|
|
926
|
+
issues.push({
|
|
927
|
+
file,
|
|
928
|
+
line: i + 1,
|
|
929
|
+
type: "table-many-min-widths",
|
|
930
|
+
severity: "info",
|
|
931
|
+
message: "Table with multiple min-width columns may be wide on mobile",
|
|
932
|
+
suggestion: "Consider hiding less important columns on mobile with hidden md:table-cell",
|
|
933
|
+
snippet: line.trim().substring(0, 80),
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
return {
|
|
941
|
+
name: "Mobile Table Patterns",
|
|
942
|
+
passed: issues.filter((i) => i.severity === "error").length === 0,
|
|
943
|
+
blocking: false,
|
|
944
|
+
issues,
|
|
945
|
+
duration: Date.now() - startTime,
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* 15. Mobile Modal Sizing
|
|
950
|
+
* Check that modals are appropriately sized on mobile
|
|
951
|
+
*/
|
|
952
|
+
async checkMobileModalSizing() {
|
|
953
|
+
const startTime = Date.now();
|
|
954
|
+
const issues = [];
|
|
955
|
+
const files = await this.getTsxFiles();
|
|
956
|
+
for (const file of files) {
|
|
957
|
+
const content = fs.readFileSync(file, "utf8");
|
|
958
|
+
const lines = content.split("\n");
|
|
959
|
+
for (let i = 0; i < lines.length; i++) {
|
|
960
|
+
const line = lines[i];
|
|
961
|
+
// Check for modal/dialog components
|
|
962
|
+
if (/<(?:Dialog|Modal|Sheet|Drawer)\b/i.test(line)) {
|
|
963
|
+
const contextBlock = lines.slice(i, Math.min(i + 20, lines.length)).join("\n");
|
|
964
|
+
// Check for responsive width
|
|
965
|
+
const hasResponsiveWidth = /w-full|max-w-.*sm:|sm:max-w|md:max-w|inset-x-0/.test(contextBlock);
|
|
966
|
+
const hasFixedSmallWidth = /max-w-(?:xs|sm)\b/.test(contextBlock);
|
|
967
|
+
const isBottomSheet = /Sheet|bottom|slide-up/i.test(line);
|
|
968
|
+
if (!hasResponsiveWidth && !isBottomSheet) {
|
|
969
|
+
// Check for fixed pixel widths
|
|
970
|
+
const fixedWidthMatch = contextBlock.match(/w-\[(\d+)px\]|width:\s*(\d+)px/);
|
|
971
|
+
if (fixedWidthMatch) {
|
|
972
|
+
const width = parseInt(fixedWidthMatch[1] || fixedWidthMatch[2]);
|
|
973
|
+
if (width > 400 && width < 600) {
|
|
974
|
+
issues.push({
|
|
975
|
+
file,
|
|
976
|
+
line: i + 1,
|
|
977
|
+
type: "modal-fixed-width",
|
|
978
|
+
severity: "info",
|
|
979
|
+
message: `Modal with fixed ${width}px width may not fit mobile screens`,
|
|
980
|
+
suggestion: "Use w-full sm:max-w-md for responsive modal sizing",
|
|
981
|
+
snippet: line.trim().substring(0, 80),
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
// Check for modal padding on mobile
|
|
987
|
+
if (!/p-4|p-6|px-4|px-6|sm:p-/.test(contextBlock)) {
|
|
988
|
+
issues.push({
|
|
989
|
+
file,
|
|
990
|
+
line: i + 1,
|
|
991
|
+
type: "modal-no-padding",
|
|
992
|
+
severity: "info",
|
|
993
|
+
message: "Modal may lack adequate padding for mobile",
|
|
994
|
+
suggestion: "Add p-4 or p-6 for comfortable touch spacing",
|
|
995
|
+
snippet: line.trim().substring(0, 80),
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
return {
|
|
1002
|
+
name: "Mobile Modal Sizing",
|
|
1003
|
+
passed: true, // Info only
|
|
1004
|
+
blocking: false,
|
|
1005
|
+
issues,
|
|
1006
|
+
duration: Date.now() - startTime,
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* 16. iOS Rubber Banding (overscroll-behavior)
|
|
1011
|
+
* Check for proper overscroll handling on iOS
|
|
1012
|
+
*/
|
|
1013
|
+
async checkOverscrollBehavior() {
|
|
1014
|
+
const startTime = Date.now();
|
|
1015
|
+
const issues = [];
|
|
1016
|
+
const files = await this.getTsxFiles();
|
|
1017
|
+
const cssFiles = await this.getCssFiles();
|
|
1018
|
+
let hasGlobalOverscroll = false;
|
|
1019
|
+
// Check CSS for global overscroll
|
|
1020
|
+
for (const file of cssFiles) {
|
|
1021
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1022
|
+
if (/overscroll-behavior:\s*(?:none|contain)/.test(content)) {
|
|
1023
|
+
hasGlobalOverscroll = true;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
for (const file of files) {
|
|
1027
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1028
|
+
const lines = content.split("\n");
|
|
1029
|
+
// Check for overscroll classes
|
|
1030
|
+
if (/overscroll-(?:none|contain|auto)/.test(content)) {
|
|
1031
|
+
hasGlobalOverscroll = true;
|
|
1032
|
+
}
|
|
1033
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1034
|
+
const line = lines[i];
|
|
1035
|
+
// Check for nested scrollable containers (can cause scroll chaining)
|
|
1036
|
+
if (/overflow-(?:y-auto|y-scroll|auto)/.test(line)) {
|
|
1037
|
+
const contextBlock = lines.slice(Math.max(0, i - 10), Math.min(i + 10, lines.length)).join("\n");
|
|
1038
|
+
// Check if this is nested inside another scrollable
|
|
1039
|
+
const isNested = (contextBlock.match(/overflow-(?:y-auto|y-scroll|auto)/g) || []).length > 1;
|
|
1040
|
+
if (isNested && !/overscroll-contain/.test(contextBlock)) {
|
|
1041
|
+
issues.push({
|
|
1042
|
+
file,
|
|
1043
|
+
line: i + 1,
|
|
1044
|
+
type: "nested-scroll-no-overscroll",
|
|
1045
|
+
severity: "info",
|
|
1046
|
+
message: "Nested scrollable container without overscroll-contain",
|
|
1047
|
+
suggestion: "Add overscroll-contain to prevent scroll chaining issues",
|
|
1048
|
+
snippet: line.trim().substring(0, 80),
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
// Check for modals/drawers that should trap scroll
|
|
1053
|
+
if (/<(?:Dialog|Modal|Drawer|Sheet)\b/i.test(line)) {
|
|
1054
|
+
const contextBlock = lines.slice(i, Math.min(i + 15, lines.length)).join("\n");
|
|
1055
|
+
if (!/overscroll-contain|overflow-hidden/.test(contextBlock)) {
|
|
1056
|
+
issues.push({
|
|
1057
|
+
file,
|
|
1058
|
+
line: i + 1,
|
|
1059
|
+
type: "modal-no-scroll-trap",
|
|
1060
|
+
severity: "info",
|
|
1061
|
+
message: "Modal/drawer may allow background scroll on iOS",
|
|
1062
|
+
suggestion: "Add overscroll-contain or use body scroll lock",
|
|
1063
|
+
snippet: line.trim().substring(0, 80),
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
return {
|
|
1070
|
+
name: "Overscroll Behavior",
|
|
1071
|
+
passed: true, // Info only
|
|
1072
|
+
blocking: false,
|
|
1073
|
+
issues,
|
|
1074
|
+
duration: Date.now() - startTime,
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* 17. PWA Meta Tags
|
|
1079
|
+
* Check for Progressive Web App meta tag support
|
|
1080
|
+
*/
|
|
1081
|
+
async checkPWAMetaTags() {
|
|
1082
|
+
const startTime = Date.now();
|
|
1083
|
+
const issues = [];
|
|
1084
|
+
const files = await this.getTsxFiles();
|
|
1085
|
+
let hasViewportFitCover = false;
|
|
1086
|
+
let hasThemeColor = false;
|
|
1087
|
+
let hasAppleTouchIcon = false;
|
|
1088
|
+
let hasManifest = false;
|
|
1089
|
+
let hasStatusBarStyle = false;
|
|
1090
|
+
// Check layout/root files
|
|
1091
|
+
for (const file of files) {
|
|
1092
|
+
if (!/layout|_app|_document|head/i.test(file))
|
|
1093
|
+
continue;
|
|
1094
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1095
|
+
if (/viewport-fit\s*=\s*cover/.test(content)) {
|
|
1096
|
+
hasViewportFitCover = true;
|
|
1097
|
+
}
|
|
1098
|
+
if (/theme-color/.test(content)) {
|
|
1099
|
+
hasThemeColor = true;
|
|
1100
|
+
}
|
|
1101
|
+
if (/apple-touch-icon/.test(content)) {
|
|
1102
|
+
hasAppleTouchIcon = true;
|
|
1103
|
+
}
|
|
1104
|
+
if (/manifest\.json|manifest\.webmanifest/.test(content)) {
|
|
1105
|
+
hasManifest = true;
|
|
1106
|
+
}
|
|
1107
|
+
if (/apple-mobile-web-app-status-bar-style/.test(content)) {
|
|
1108
|
+
hasStatusBarStyle = true;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
// Report missing PWA features
|
|
1112
|
+
if (!hasViewportFitCover) {
|
|
1113
|
+
issues.push({
|
|
1114
|
+
file: "layout.tsx or _app.tsx",
|
|
1115
|
+
line: 0,
|
|
1116
|
+
type: "missing-viewport-fit",
|
|
1117
|
+
severity: "info",
|
|
1118
|
+
message: "Missing viewport-fit=cover for notched device support",
|
|
1119
|
+
suggestion: "Add <meta name=\"viewport\" content=\"..., viewport-fit=cover\">",
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
if (!hasThemeColor) {
|
|
1123
|
+
issues.push({
|
|
1124
|
+
file: "layout.tsx or _app.tsx",
|
|
1125
|
+
line: 0,
|
|
1126
|
+
type: "missing-theme-color",
|
|
1127
|
+
severity: "info",
|
|
1128
|
+
message: "Missing theme-color meta tag for browser chrome styling",
|
|
1129
|
+
suggestion: "Add <meta name=\"theme-color\" content=\"#yourColor\">",
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
if (!hasAppleTouchIcon) {
|
|
1133
|
+
issues.push({
|
|
1134
|
+
file: "layout.tsx or _app.tsx",
|
|
1135
|
+
line: 0,
|
|
1136
|
+
type: "missing-apple-touch-icon",
|
|
1137
|
+
severity: "info",
|
|
1138
|
+
message: "Missing apple-touch-icon for iOS home screen",
|
|
1139
|
+
suggestion: "Add <link rel=\"apple-touch-icon\" href=\"/icon-180.png\">",
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
if (!hasManifest) {
|
|
1143
|
+
issues.push({
|
|
1144
|
+
file: "layout.tsx or _app.tsx",
|
|
1145
|
+
line: 0,
|
|
1146
|
+
type: "missing-manifest",
|
|
1147
|
+
severity: "info",
|
|
1148
|
+
message: "Missing web app manifest for PWA support",
|
|
1149
|
+
suggestion: "Add <link rel=\"manifest\" href=\"/manifest.json\">",
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
return {
|
|
1153
|
+
name: "PWA Meta Tags",
|
|
1154
|
+
passed: true, // Info only
|
|
1155
|
+
blocking: false,
|
|
1156
|
+
issues,
|
|
1157
|
+
duration: Date.now() - startTime,
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* 18. Mobile Autofill Styling
|
|
1162
|
+
* Check for handling of browser autofill background colors
|
|
1163
|
+
*/
|
|
1164
|
+
async checkMobileAutofillStyling() {
|
|
1165
|
+
const startTime = Date.now();
|
|
1166
|
+
const issues = [];
|
|
1167
|
+
const files = await this.getTsxFiles();
|
|
1168
|
+
const cssFiles = await this.getCssFiles();
|
|
1169
|
+
let hasAutofillStyling = false;
|
|
1170
|
+
// Check CSS for autofill handling
|
|
1171
|
+
for (const file of cssFiles) {
|
|
1172
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1173
|
+
if (/:autofill|:-webkit-autofill/.test(content)) {
|
|
1174
|
+
hasAutofillStyling = true;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
// Check TSX for autofill handling
|
|
1178
|
+
for (const file of files) {
|
|
1179
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1180
|
+
if (/autofill|:-webkit-autofill/.test(content)) {
|
|
1181
|
+
hasAutofillStyling = true;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
// Count input fields
|
|
1185
|
+
let inputCount = 0;
|
|
1186
|
+
for (const file of files) {
|
|
1187
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1188
|
+
const matches = content.match(/<(?:input|Input|TextField)/gi);
|
|
1189
|
+
if (matches)
|
|
1190
|
+
inputCount += matches.length;
|
|
1191
|
+
}
|
|
1192
|
+
if (inputCount > 10 && !hasAutofillStyling) {
|
|
1193
|
+
issues.push({
|
|
1194
|
+
file: "codebase-wide",
|
|
1195
|
+
line: 0,
|
|
1196
|
+
type: "no-autofill-styling",
|
|
1197
|
+
severity: "info",
|
|
1198
|
+
message: `${inputCount} inputs found but no autofill styling detected`,
|
|
1199
|
+
suggestion: "Add CSS: input:-webkit-autofill { -webkit-box-shadow: 0 0 0 30px var(--bg-primary) inset; }",
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
return {
|
|
1203
|
+
name: "Mobile Autofill Styling",
|
|
1204
|
+
passed: true, // Info only
|
|
1205
|
+
blocking: false,
|
|
1206
|
+
issues,
|
|
1207
|
+
duration: Date.now() - startTime,
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* 19. Thumb Zone Ergonomics
|
|
1212
|
+
* Check that important actions are in thumb-reachable zones
|
|
1213
|
+
*/
|
|
1214
|
+
async checkThumbZoneErgonomics() {
|
|
1215
|
+
const startTime = Date.now();
|
|
1216
|
+
const issues = [];
|
|
1217
|
+
const files = await this.getTsxFiles();
|
|
1218
|
+
for (const file of files) {
|
|
1219
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1220
|
+
const lines = content.split("\n");
|
|
1221
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1222
|
+
const line = lines[i];
|
|
1223
|
+
// Check for primary actions that might be at the top
|
|
1224
|
+
if (/(?:primary|submit|save|confirm|add|create)\s*(?:button|action)/i.test(line) ||
|
|
1225
|
+
/<Button[^>]*variant=["'](?:default|primary)["']/.test(line)) {
|
|
1226
|
+
const contextBlock = lines.slice(Math.max(0, i - 10), Math.min(i + 5, lines.length)).join("\n");
|
|
1227
|
+
// Check if it's at the top of the page
|
|
1228
|
+
const isAtTop = /fixed.*top|sticky.*top|header|PageHeader|AppBar/i.test(contextBlock);
|
|
1229
|
+
const hasBottomAlternative = /fixed.*bottom|sticky.*bottom|BottomNav|MobileActions/i.test(content);
|
|
1230
|
+
if (isAtTop && !hasBottomAlternative) {
|
|
1231
|
+
issues.push({
|
|
1232
|
+
file,
|
|
1233
|
+
line: i + 1,
|
|
1234
|
+
type: "primary-action-not-thumb-zone",
|
|
1235
|
+
severity: "info",
|
|
1236
|
+
message: "Primary action button at top - may be hard to reach on mobile",
|
|
1237
|
+
suggestion: "Consider duplicating important actions in bottom bar for mobile thumb zone",
|
|
1238
|
+
snippet: line.trim().substring(0, 80),
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
// Check for FAB (Floating Action Button) positioning
|
|
1243
|
+
if (/FloatingActionButton|fab|FAB/.test(line)) {
|
|
1244
|
+
const contextBlock = lines.slice(i, Math.min(i + 5, lines.length)).join("\n");
|
|
1245
|
+
// FAB should be bottom-right for right-handed users
|
|
1246
|
+
const isBottomRight = /bottom.*right|right.*bottom/.test(contextBlock);
|
|
1247
|
+
if (!isBottomRight) {
|
|
1248
|
+
issues.push({
|
|
1249
|
+
file,
|
|
1250
|
+
line: i + 1,
|
|
1251
|
+
type: "fab-not-optimal-position",
|
|
1252
|
+
severity: "info",
|
|
1253
|
+
message: "FAB not positioned at bottom-right (optimal thumb zone)",
|
|
1254
|
+
suggestion: "Position FAB at bottom-right for right-handed thumb reachability",
|
|
1255
|
+
snippet: line.trim().substring(0, 80),
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
return {
|
|
1262
|
+
name: "Thumb Zone Ergonomics",
|
|
1263
|
+
passed: true, // Info only
|
|
1264
|
+
blocking: false,
|
|
1265
|
+
issues,
|
|
1266
|
+
duration: Date.now() - startTime,
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
/**
|
|
1270
|
+
* 20. Mobile Image Srcset
|
|
1271
|
+
* Check for responsive images with srcset for bandwidth optimization
|
|
1272
|
+
*/
|
|
1273
|
+
async checkMobileImageSrcset() {
|
|
1274
|
+
const startTime = Date.now();
|
|
1275
|
+
const issues = [];
|
|
1276
|
+
const files = await this.getTsxFiles();
|
|
1277
|
+
let imagesWithoutSrcset = 0;
|
|
1278
|
+
let imagesWithSrcset = 0;
|
|
1279
|
+
for (const file of files) {
|
|
1280
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1281
|
+
const lines = content.split("\n");
|
|
1282
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1283
|
+
const line = lines[i];
|
|
1284
|
+
// Check for img elements (not Next.js Image which handles this)
|
|
1285
|
+
if (/<img\b/.test(line) && !/<Image\b/.test(line)) {
|
|
1286
|
+
const contextBlock = lines.slice(i, Math.min(i + 5, lines.length)).join("\n");
|
|
1287
|
+
const hasSrcset = /srcSet|srcset/.test(contextBlock);
|
|
1288
|
+
const hasSizes = /sizes=/.test(contextBlock);
|
|
1289
|
+
if (!hasSrcset) {
|
|
1290
|
+
imagesWithoutSrcset++;
|
|
1291
|
+
// Only flag larger images (not icons)
|
|
1292
|
+
const isLikelyLargeImage = /hero|banner|product|card|thumbnail/i.test(contextBlock) ||
|
|
1293
|
+
/w-\[(?:[2-9]\d{2}|[1-9]\d{3})px\]|w-full/.test(contextBlock);
|
|
1294
|
+
if (isLikelyLargeImage) {
|
|
1295
|
+
issues.push({
|
|
1296
|
+
file,
|
|
1297
|
+
line: i + 1,
|
|
1298
|
+
type: "img-no-srcset",
|
|
1299
|
+
severity: "info",
|
|
1300
|
+
message: "Large <img> without srcset for responsive images",
|
|
1301
|
+
suggestion: "Add srcset for different screen sizes or use Next.js <Image> component",
|
|
1302
|
+
snippet: line.trim().substring(0, 80),
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
else {
|
|
1307
|
+
imagesWithSrcset++;
|
|
1308
|
+
if (!hasSizes) {
|
|
1309
|
+
issues.push({
|
|
1310
|
+
file,
|
|
1311
|
+
line: i + 1,
|
|
1312
|
+
type: "srcset-no-sizes",
|
|
1313
|
+
severity: "info",
|
|
1314
|
+
message: "<img> has srcset but missing sizes attribute",
|
|
1315
|
+
suggestion: "Add sizes attribute to help browser select correct image size",
|
|
1316
|
+
snippet: line.trim().substring(0, 80),
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
// Check Next.js Image for sizes prop on responsive images
|
|
1322
|
+
if (/<Image\b/.test(line)) {
|
|
1323
|
+
const contextBlock = lines.slice(i, Math.min(i + 5, lines.length)).join("\n");
|
|
1324
|
+
const hasFill = /fill/.test(contextBlock);
|
|
1325
|
+
const hasResponsiveWidth = /w-full|width.*%|responsive/i.test(contextBlock);
|
|
1326
|
+
const hasSizes = /sizes=/.test(contextBlock);
|
|
1327
|
+
if ((hasFill || hasResponsiveWidth) && !hasSizes) {
|
|
1328
|
+
issues.push({
|
|
1329
|
+
file,
|
|
1330
|
+
line: i + 1,
|
|
1331
|
+
type: "next-image-no-sizes",
|
|
1332
|
+
severity: "info",
|
|
1333
|
+
message: "Next.js Image with fill/responsive width but no sizes prop",
|
|
1334
|
+
suggestion: "Add sizes prop like sizes=\"(max-width: 768px) 100vw, 50vw\"",
|
|
1335
|
+
snippet: line.trim().substring(0, 80),
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
return {
|
|
1342
|
+
name: "Mobile Image Srcset",
|
|
1343
|
+
passed: true, // Info only
|
|
1344
|
+
blocking: false,
|
|
1345
|
+
issues,
|
|
1346
|
+
duration: Date.now() - startTime,
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* 21. Tap Target Size
|
|
1351
|
+
* Verify interactive elements meet minimum 44px tap target
|
|
1352
|
+
* Note: This check focuses on explicit small dimensions, not icon sizes
|
|
1353
|
+
*/
|
|
1354
|
+
async checkTapTargetSize() {
|
|
1355
|
+
const startTime = Date.now();
|
|
1356
|
+
const issues = [];
|
|
1357
|
+
const files = await this.getTsxFiles();
|
|
1358
|
+
const MIN_TAP_SIZE = 44;
|
|
1359
|
+
for (const file of files) {
|
|
1360
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1361
|
+
const lines = content.split("\n");
|
|
1362
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1363
|
+
const line = lines[i];
|
|
1364
|
+
if (this.shouldSkipLine(lines, i))
|
|
1365
|
+
continue;
|
|
1366
|
+
// Only check IconButton specifically - regular Buttons typically have sufficient padding
|
|
1367
|
+
// We need to check the className prop on the IconButton, not children icons
|
|
1368
|
+
if (/<IconButton\b/i.test(line)) {
|
|
1369
|
+
// Get the IconButton props (up to closing > or multi-line until />)
|
|
1370
|
+
let propsText = line;
|
|
1371
|
+
let endIdx = i;
|
|
1372
|
+
// Find the end of the IconButton opening tag
|
|
1373
|
+
while (!/>/.test(lines[endIdx]) && endIdx < Math.min(i + 5, lines.length - 1)) {
|
|
1374
|
+
endIdx++;
|
|
1375
|
+
propsText += "\n" + lines[endIdx];
|
|
1376
|
+
}
|
|
1377
|
+
// Extract className from IconButton props only, not from child icons
|
|
1378
|
+
// Match className="..." on the IconButton, stopping before icon= or children
|
|
1379
|
+
const classNameMatch = propsText.match(/className=["']([^"']+)["']/);
|
|
1380
|
+
const buttonClasses = classNameMatch ? classNameMatch[1] : "";
|
|
1381
|
+
// Skip if button has padding classes or adequate sizes in className
|
|
1382
|
+
if (/\bp-\d|min-h-|min-w-|h-1[0-2]|h-[89]|w-1[0-2]|w-[89]/.test(buttonClasses)) {
|
|
1383
|
+
continue;
|
|
1384
|
+
}
|
|
1385
|
+
// Check for explicit small height in the button's className only
|
|
1386
|
+
const heightMatch = buttonClasses.match(/\bh-(\d+)\b/);
|
|
1387
|
+
if (heightMatch) {
|
|
1388
|
+
const height = parseInt(heightMatch[1]);
|
|
1389
|
+
// Tailwind h-N: h-5=20px, h-6=24px, h-7=28px, h-8=32px, h-9=36px, h-10=40px
|
|
1390
|
+
const pxHeight = height <= 12 ? height * 4 : height;
|
|
1391
|
+
if (pxHeight > 0 && pxHeight < 32) { // Only flag very small (< 32px)
|
|
1392
|
+
issues.push({
|
|
1393
|
+
file, line: i + 1, type: "tap-target-too-small", severity: "warning",
|
|
1394
|
+
message: `IconButton height ${pxHeight}px may be too small for touch`,
|
|
1395
|
+
suggestion: "Use h-10 (40px) or larger, or add padding for 44px tap target",
|
|
1396
|
+
snippet: line.trim().substring(0, 80),
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
return { name: "Tap Target Size", passed: issues.filter((i) => i.severity === "error").length === 0, blocking: false, issues, duration: Date.now() - startTime };
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* 22. Keyboard Dismiss Patterns
|
|
1407
|
+
*/
|
|
1408
|
+
async checkKeyboardDismiss() {
|
|
1409
|
+
const startTime = Date.now();
|
|
1410
|
+
const issues = [];
|
|
1411
|
+
const files = await this.getTsxFiles();
|
|
1412
|
+
for (const file of files) {
|
|
1413
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1414
|
+
const lines = content.split("\n");
|
|
1415
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1416
|
+
const line = lines[i];
|
|
1417
|
+
if (/<form\b|<Form\b/i.test(line)) {
|
|
1418
|
+
const contextBlock = lines.slice(i, Math.min(i + 30, lines.length)).join("\n");
|
|
1419
|
+
const hasSubmit = /type=["']submit|onSubmit|handleSubmit/.test(contextBlock);
|
|
1420
|
+
const hasKeyDown = /onKeyDown|enterKeyHint/.test(contextBlock);
|
|
1421
|
+
if (!hasSubmit && !hasKeyDown) {
|
|
1422
|
+
issues.push({
|
|
1423
|
+
file, line: i + 1, type: "form-no-submit", severity: "info",
|
|
1424
|
+
message: "Form without submit action for keyboard dismiss",
|
|
1425
|
+
suggestion: "Add submit button or enterKeyHint for mobile keyboard",
|
|
1426
|
+
snippet: line.trim().substring(0, 80),
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
return { name: "Keyboard Dismiss", passed: true, blocking: false, issues, duration: Date.now() - startTime };
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* 23. Navigation Depth
|
|
1436
|
+
*/
|
|
1437
|
+
async checkNavigationDepth() {
|
|
1438
|
+
const startTime = Date.now();
|
|
1439
|
+
const issues = [];
|
|
1440
|
+
const files = await this.getTsxFiles();
|
|
1441
|
+
for (const file of files) {
|
|
1442
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1443
|
+
const lines = content.split("\n");
|
|
1444
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1445
|
+
const line = lines[i];
|
|
1446
|
+
if (/SubMenu|NestedMenu|submenu/i.test(line)) {
|
|
1447
|
+
const contextBlock = lines.slice(i, Math.min(i + 20, lines.length)).join("\n");
|
|
1448
|
+
const nestedCount = (contextBlock.match(/SubMenu|NestedMenu|submenu/gi) || []).length;
|
|
1449
|
+
if (nestedCount > 2) {
|
|
1450
|
+
issues.push({
|
|
1451
|
+
file, line: i + 1, type: "deep-nested-menu", severity: "warning",
|
|
1452
|
+
message: `Deeply nested menu (${nestedCount} levels) - hard on mobile`,
|
|
1453
|
+
suggestion: "Flatten menu or use breadcrumb navigation",
|
|
1454
|
+
snippet: line.trim().substring(0, 80),
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
return { name: "Navigation Depth", passed: issues.filter((i) => i.severity === "error").length === 0, blocking: false, issues, duration: Date.now() - startTime };
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* 24. Touch Scroll Momentum
|
|
1464
|
+
*/
|
|
1465
|
+
async checkTouchScrollMomentum() {
|
|
1466
|
+
const startTime = Date.now();
|
|
1467
|
+
const issues = [];
|
|
1468
|
+
const files = await this.getTsxFiles();
|
|
1469
|
+
for (const file of files) {
|
|
1470
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1471
|
+
const lines = content.split("\n");
|
|
1472
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1473
|
+
const line = lines[i];
|
|
1474
|
+
if (/scroll-snap/.test(line)) {
|
|
1475
|
+
const contextBlock = lines.slice(i, Math.min(i + 5, lines.length)).join("\n");
|
|
1476
|
+
if (!/scroll-smooth|scroll-behavior/.test(contextBlock)) {
|
|
1477
|
+
issues.push({
|
|
1478
|
+
file, line: i + 1, type: "scroll-snap-no-smooth", severity: "info",
|
|
1479
|
+
message: "Scroll snap without smooth behavior",
|
|
1480
|
+
suggestion: "Add scroll-smooth for smoother snap scrolling",
|
|
1481
|
+
snippet: line.trim().substring(0, 80),
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
return { name: "Touch Scroll Momentum", passed: true, blocking: false, issues, duration: Date.now() - startTime };
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* 25. Mobile Form Chunking
|
|
1491
|
+
*/
|
|
1492
|
+
async checkMobileFormChunking() {
|
|
1493
|
+
const startTime = Date.now();
|
|
1494
|
+
const issues = [];
|
|
1495
|
+
const files = await this.getTsxFiles();
|
|
1496
|
+
for (const file of files) {
|
|
1497
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1498
|
+
const inputCount = (content.match(/<(?:input|Input|TextField|Select)\b/gi) || []).length;
|
|
1499
|
+
if (inputCount > 8 && !/step|wizard|FormSection/i.test(content)) {
|
|
1500
|
+
issues.push({
|
|
1501
|
+
file, line: 1, type: "long-form-no-chunking", severity: "info",
|
|
1502
|
+
message: `Form with ${inputCount} inputs - consider chunking into sections`,
|
|
1503
|
+
suggestion: "Break long forms into sections or multi-step wizard",
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
return { name: "Mobile Form Chunking", passed: true, blocking: false, issues, duration: Date.now() - startTime };
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* 26. Pinch-to-Zoom Accessibility
|
|
1511
|
+
*/
|
|
1512
|
+
async checkPinchToZoom() {
|
|
1513
|
+
const startTime = Date.now();
|
|
1514
|
+
const issues = [];
|
|
1515
|
+
const files = await this.getTsxFiles();
|
|
1516
|
+
for (const file of files) {
|
|
1517
|
+
if (!/layout|_app|_document|head/i.test(file))
|
|
1518
|
+
continue;
|
|
1519
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1520
|
+
const lines = content.split("\n");
|
|
1521
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1522
|
+
const line = lines[i];
|
|
1523
|
+
if (/user-scalable\s*=\s*["']?no|maximum-scale\s*=\s*["']?1(?:\.0)?["']?/.test(line)) {
|
|
1524
|
+
issues.push({
|
|
1525
|
+
file, line: i + 1, type: "zoom-disabled", severity: "error",
|
|
1526
|
+
message: "Pinch-to-zoom disabled - WCAG violation",
|
|
1527
|
+
suggestion: "Remove user-scalable=no to allow zoom",
|
|
1528
|
+
snippet: line.trim().substring(0, 80),
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
return { name: "Pinch-to-Zoom", passed: issues.length === 0, blocking: true, issues, duration: Date.now() - startTime };
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* 27. Text Selection
|
|
1537
|
+
*/
|
|
1538
|
+
async checkTextSelection() {
|
|
1539
|
+
const startTime = Date.now();
|
|
1540
|
+
const issues = [];
|
|
1541
|
+
const files = await this.getTsxFiles();
|
|
1542
|
+
for (const file of files) {
|
|
1543
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1544
|
+
const lines = content.split("\n");
|
|
1545
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1546
|
+
const line = lines[i];
|
|
1547
|
+
if (/select-none/.test(line) && /<(?:p|span|div|article)[^>]*select-none/.test(line)) {
|
|
1548
|
+
issues.push({
|
|
1549
|
+
file, line: i + 1, type: "content-select-none", severity: "info",
|
|
1550
|
+
message: "select-none on content - users can't copy text",
|
|
1551
|
+
suggestion: "Only use select-none on buttons, not content",
|
|
1552
|
+
snippet: line.trim().substring(0, 80),
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
return { name: "Text Selection", passed: true, blocking: false, issues, duration: Date.now() - startTime };
|
|
1558
|
+
}
|
|
1559
|
+
/**
|
|
1560
|
+
* 28. Mobile Date Pickers
|
|
1561
|
+
*/
|
|
1562
|
+
async checkMobileDatePickers() {
|
|
1563
|
+
const startTime = Date.now();
|
|
1564
|
+
const issues = [];
|
|
1565
|
+
const files = await this.getTsxFiles();
|
|
1566
|
+
for (const file of files) {
|
|
1567
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1568
|
+
const lines = content.split("\n");
|
|
1569
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1570
|
+
const line = lines[i];
|
|
1571
|
+
if (/<(?:DatePicker|DateTimePicker|TimePicker)\b/i.test(line)) {
|
|
1572
|
+
const contextBlock = lines.slice(i, Math.min(i + 10, lines.length)).join("\n");
|
|
1573
|
+
if (!/mobile|native|useNative/i.test(contextBlock)) {
|
|
1574
|
+
issues.push({
|
|
1575
|
+
file, line: i + 1, type: "custom-datepicker-no-mobile", severity: "info",
|
|
1576
|
+
message: "Custom date picker may not be optimal on mobile",
|
|
1577
|
+
suggestion: "Consider native date input on mobile",
|
|
1578
|
+
snippet: line.trim().substring(0, 80),
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
return { name: "Mobile Date Pickers", passed: true, blocking: false, issues, duration: Date.now() - startTime };
|
|
1585
|
+
}
|
|
1586
|
+
/**
|
|
1587
|
+
* 29. Network Status Indicator
|
|
1588
|
+
* Check for offline/online status handling
|
|
1589
|
+
*/
|
|
1590
|
+
async checkNetworkStatusIndicator() {
|
|
1591
|
+
const startTime = Date.now();
|
|
1592
|
+
const issues = [];
|
|
1593
|
+
const files = await this.getTsxFiles();
|
|
1594
|
+
let hasOnlineCheck = false;
|
|
1595
|
+
let hasOfflineUI = false;
|
|
1596
|
+
let hasServiceWorker = false;
|
|
1597
|
+
for (const file of files) {
|
|
1598
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1599
|
+
// Check for online/offline detection
|
|
1600
|
+
if (/navigator\.onLine|useOnline|useNetworkState|isOnline|isOffline/i.test(content)) {
|
|
1601
|
+
hasOnlineCheck = true;
|
|
1602
|
+
}
|
|
1603
|
+
// Check for offline UI
|
|
1604
|
+
if (/offline.*banner|offline.*indicator|no.*connection|network.*error/i.test(content)) {
|
|
1605
|
+
hasOfflineUI = true;
|
|
1606
|
+
}
|
|
1607
|
+
// Check for service worker
|
|
1608
|
+
if (/serviceWorker|workbox|sw\.js/i.test(content)) {
|
|
1609
|
+
hasServiceWorker = true;
|
|
1610
|
+
}
|
|
1611
|
+
const lines = content.split("\n");
|
|
1612
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1613
|
+
const line = lines[i];
|
|
1614
|
+
// Check for fetch without offline handling
|
|
1615
|
+
if (/fetch\(|useMutation|useQuery/.test(line)) {
|
|
1616
|
+
const contextBlock = lines.slice(i, Math.min(i + 20, lines.length)).join("\n");
|
|
1617
|
+
const hasNetworkError = /network.*error|offline|onLine|retry/i.test(contextBlock);
|
|
1618
|
+
// Only flag if it's a user-facing data fetch
|
|
1619
|
+
if (/useQuery|useMutation/.test(line) && !hasNetworkError && !hasOnlineCheck) {
|
|
1620
|
+
// This is tracked globally, not per-instance
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
// Global check
|
|
1626
|
+
if (!hasOnlineCheck && !hasOfflineUI) {
|
|
1627
|
+
issues.push({
|
|
1628
|
+
file: "codebase-wide",
|
|
1629
|
+
line: 0,
|
|
1630
|
+
type: "no-offline-handling",
|
|
1631
|
+
severity: "info",
|
|
1632
|
+
message: "No offline/online status detection found",
|
|
1633
|
+
suggestion: "Add navigator.onLine check or useOnline hook to show offline indicator",
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
if (hasOnlineCheck && !hasOfflineUI) {
|
|
1637
|
+
issues.push({
|
|
1638
|
+
file: "codebase-wide",
|
|
1639
|
+
line: 0,
|
|
1640
|
+
type: "online-check-no-ui",
|
|
1641
|
+
severity: "info",
|
|
1642
|
+
message: "Online status checked but no offline UI indicator found",
|
|
1643
|
+
suggestion: "Add visible banner or toast when user goes offline",
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
return {
|
|
1647
|
+
name: "Network Status Indicator",
|
|
1648
|
+
passed: true,
|
|
1649
|
+
blocking: false,
|
|
1650
|
+
issues,
|
|
1651
|
+
duration: Date.now() - startTime,
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
/**
|
|
1655
|
+
* 30. iOS Smart App Banner
|
|
1656
|
+
* Check for smart app banner meta tags
|
|
1657
|
+
*/
|
|
1658
|
+
async checkIOSSmartAppBanner() {
|
|
1659
|
+
const startTime = Date.now();
|
|
1660
|
+
const issues = [];
|
|
1661
|
+
const files = await this.getTsxFiles();
|
|
1662
|
+
let hasSmartBanner = false;
|
|
1663
|
+
let hasAppId = false;
|
|
1664
|
+
for (const file of files) {
|
|
1665
|
+
if (!/layout|_app|_document|head/i.test(file))
|
|
1666
|
+
continue;
|
|
1667
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1668
|
+
if (/apple-itunes-app/.test(content)) {
|
|
1669
|
+
hasSmartBanner = true;
|
|
1670
|
+
if (/app-id=/.test(content)) {
|
|
1671
|
+
hasAppId = true;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
// Check for custom app install prompts
|
|
1675
|
+
if (/installPrompt|beforeinstallprompt|AddToHomeScreen/i.test(content)) {
|
|
1676
|
+
hasSmartBanner = true;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
// This is optional - only suggest if it looks like they have a native app
|
|
1680
|
+
// We can't really detect this, so this is a very light check
|
|
1681
|
+
return {
|
|
1682
|
+
name: "iOS Smart App Banner",
|
|
1683
|
+
passed: true,
|
|
1684
|
+
blocking: false,
|
|
1685
|
+
issues,
|
|
1686
|
+
duration: Date.now() - startTime,
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
/**
|
|
1690
|
+
* 31. Color Scheme Detection
|
|
1691
|
+
* Check for prefers-color-scheme handling
|
|
1692
|
+
*/
|
|
1693
|
+
async checkColorSchemeDetection() {
|
|
1694
|
+
const startTime = Date.now();
|
|
1695
|
+
const issues = [];
|
|
1696
|
+
const files = await this.getTsxFiles();
|
|
1697
|
+
const cssFiles = await this.getCssFiles();
|
|
1698
|
+
let hasColorScheme = false;
|
|
1699
|
+
let hasDarkMode = false;
|
|
1700
|
+
let hasSystemPreference = false;
|
|
1701
|
+
// Check CSS
|
|
1702
|
+
for (const file of cssFiles) {
|
|
1703
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1704
|
+
if (/prefers-color-scheme/.test(content)) {
|
|
1705
|
+
hasColorScheme = true;
|
|
1706
|
+
hasSystemPreference = true;
|
|
1707
|
+
}
|
|
1708
|
+
if (/dark:|\.dark\s|dark-mode|color-scheme:\s*dark/i.test(content)) {
|
|
1709
|
+
hasDarkMode = true;
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
// Check TSX
|
|
1713
|
+
for (const file of files) {
|
|
1714
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1715
|
+
if (/prefers-color-scheme|useColorScheme|useTheme|darkMode/i.test(content)) {
|
|
1716
|
+
hasColorScheme = true;
|
|
1717
|
+
}
|
|
1718
|
+
if (/dark:|theme.*dark|isDark|darkMode/i.test(content)) {
|
|
1719
|
+
hasDarkMode = true;
|
|
1720
|
+
}
|
|
1721
|
+
if (/system|matchMedia.*prefers-color-scheme/i.test(content)) {
|
|
1722
|
+
hasSystemPreference = true;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
if (hasDarkMode && !hasSystemPreference) {
|
|
1726
|
+
issues.push({
|
|
1727
|
+
file: "codebase-wide",
|
|
1728
|
+
line: 0,
|
|
1729
|
+
type: "dark-mode-no-system-pref",
|
|
1730
|
+
severity: "info",
|
|
1731
|
+
message: "Dark mode exists but may not respect system preference",
|
|
1732
|
+
suggestion: "Add @media (prefers-color-scheme: dark) or detect system preference",
|
|
1733
|
+
});
|
|
1734
|
+
}
|
|
1735
|
+
if (!hasDarkMode) {
|
|
1736
|
+
issues.push({
|
|
1737
|
+
file: "codebase-wide",
|
|
1738
|
+
line: 0,
|
|
1739
|
+
type: "no-dark-mode",
|
|
1740
|
+
severity: "info",
|
|
1741
|
+
message: "No dark mode support detected - important for OLED mobile screens",
|
|
1742
|
+
suggestion: "Add dark mode support using Tailwind dark: classes or CSS variables",
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
return {
|
|
1746
|
+
name: "Color Scheme Detection",
|
|
1747
|
+
passed: true,
|
|
1748
|
+
blocking: false,
|
|
1749
|
+
issues,
|
|
1750
|
+
duration: Date.now() - startTime,
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
/**
|
|
1754
|
+
* 32. Haptic Feedback
|
|
1755
|
+
* Check for vibration API on important actions
|
|
1756
|
+
*/
|
|
1757
|
+
async checkHapticFeedback() {
|
|
1758
|
+
const startTime = Date.now();
|
|
1759
|
+
const issues = [];
|
|
1760
|
+
const files = await this.getTsxFiles();
|
|
1761
|
+
let hasVibration = false;
|
|
1762
|
+
let hasHapticFeedback = false;
|
|
1763
|
+
for (const file of files) {
|
|
1764
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1765
|
+
if (/navigator\.vibrate|Vibration|haptic/i.test(content)) {
|
|
1766
|
+
hasVibration = true;
|
|
1767
|
+
}
|
|
1768
|
+
if (/useHaptic|hapticFeedback|taptic/i.test(content)) {
|
|
1769
|
+
hasHapticFeedback = true;
|
|
1770
|
+
}
|
|
1771
|
+
const lines = content.split("\n");
|
|
1772
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1773
|
+
const line = lines[i];
|
|
1774
|
+
// Check for critical actions that might benefit from haptic
|
|
1775
|
+
if (/handleDelete|handleSubmit|handlePurchase|handlePayment|onError/i.test(line)) {
|
|
1776
|
+
const contextBlock = lines.slice(i, Math.min(i + 10, lines.length)).join("\n");
|
|
1777
|
+
if (!hasVibration && !hasHapticFeedback) {
|
|
1778
|
+
// Don't flag individual instances, just track globally
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
// Optional feature - just informational
|
|
1784
|
+
if (!hasVibration && !hasHapticFeedback) {
|
|
1785
|
+
issues.push({
|
|
1786
|
+
file: "codebase-wide",
|
|
1787
|
+
line: 0,
|
|
1788
|
+
type: "no-haptic-feedback",
|
|
1789
|
+
severity: "info",
|
|
1790
|
+
message: "No haptic feedback detected - consider for critical actions",
|
|
1791
|
+
suggestion: "Add navigator.vibrate([10]) for subtle feedback on important actions",
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
return {
|
|
1795
|
+
name: "Haptic Feedback",
|
|
1796
|
+
passed: true,
|
|
1797
|
+
blocking: false,
|
|
1798
|
+
issues,
|
|
1799
|
+
duration: Date.now() - startTime,
|
|
1800
|
+
};
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1803
|
+
* 33. Font Loading Strategy
|
|
1804
|
+
* Check for font-display and system font fallbacks
|
|
1805
|
+
*/
|
|
1806
|
+
async checkFontLoadingStrategy() {
|
|
1807
|
+
const startTime = Date.now();
|
|
1808
|
+
const issues = [];
|
|
1809
|
+
const cssFiles = await this.getCssFiles();
|
|
1810
|
+
const files = await this.getTsxFiles();
|
|
1811
|
+
let hasFontDisplay = false;
|
|
1812
|
+
let hasSystemFallback = false;
|
|
1813
|
+
let hasFontPreload = false;
|
|
1814
|
+
// Check CSS
|
|
1815
|
+
for (const file of cssFiles) {
|
|
1816
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1817
|
+
const lines = content.split("\n");
|
|
1818
|
+
if (/font-display:\s*(?:swap|optional|fallback)/.test(content)) {
|
|
1819
|
+
hasFontDisplay = true;
|
|
1820
|
+
}
|
|
1821
|
+
// Check for system font fallback
|
|
1822
|
+
if (/system-ui|-apple-system|BlinkMacSystemFont|Segoe UI/.test(content)) {
|
|
1823
|
+
hasSystemFallback = true;
|
|
1824
|
+
}
|
|
1825
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1826
|
+
const line = lines[i];
|
|
1827
|
+
// Check @font-face without font-display
|
|
1828
|
+
if (/@font-face/.test(line)) {
|
|
1829
|
+
const fontBlock = content.substring(content.indexOf(line), content.indexOf("}", content.indexOf(line)) + 1);
|
|
1830
|
+
if (!/font-display/.test(fontBlock)) {
|
|
1831
|
+
issues.push({
|
|
1832
|
+
file,
|
|
1833
|
+
line: i + 1,
|
|
1834
|
+
type: "font-face-no-display",
|
|
1835
|
+
severity: "warning",
|
|
1836
|
+
message: "@font-face without font-display causes FOIT on mobile",
|
|
1837
|
+
suggestion: "Add font-display: swap to show fallback font while loading",
|
|
1838
|
+
snippet: line.trim().substring(0, 80),
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
// Check for font preload in layout files
|
|
1845
|
+
for (const file of files) {
|
|
1846
|
+
if (!/layout|_app|_document|head/i.test(file))
|
|
1847
|
+
continue;
|
|
1848
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1849
|
+
if (/rel=["']preload["'][^>]*as=["']font/.test(content) ||
|
|
1850
|
+
/next\/font|@next\/font/.test(content)) {
|
|
1851
|
+
hasFontPreload = true;
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
if (!hasFontDisplay && !hasFontPreload) {
|
|
1855
|
+
issues.push({
|
|
1856
|
+
file: "codebase-wide",
|
|
1857
|
+
line: 0,
|
|
1858
|
+
type: "no-font-loading-strategy",
|
|
1859
|
+
severity: "info",
|
|
1860
|
+
message: "No font loading optimization detected",
|
|
1861
|
+
suggestion: "Use next/font or add font-display: swap and preload critical fonts",
|
|
1862
|
+
});
|
|
1863
|
+
}
|
|
1864
|
+
if (!hasSystemFallback) {
|
|
1865
|
+
issues.push({
|
|
1866
|
+
file: "codebase-wide",
|
|
1867
|
+
line: 0,
|
|
1868
|
+
type: "no-system-font-fallback",
|
|
1869
|
+
severity: "info",
|
|
1870
|
+
message: "No system font fallback in font stack",
|
|
1871
|
+
suggestion: "Add system-ui, -apple-system as fallback for faster initial render",
|
|
1872
|
+
});
|
|
1873
|
+
}
|
|
1874
|
+
return {
|
|
1875
|
+
name: "Font Loading Strategy",
|
|
1876
|
+
passed: issues.filter((i) => i.severity === "error").length === 0,
|
|
1877
|
+
blocking: false,
|
|
1878
|
+
issues,
|
|
1879
|
+
duration: Date.now() - startTime,
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
/**
|
|
1883
|
+
* 34. Passive Event Listeners
|
|
1884
|
+
* Check for passive scroll/touch listeners for better performance
|
|
1885
|
+
*/
|
|
1886
|
+
async checkPassiveEventListeners() {
|
|
1887
|
+
const startTime = Date.now();
|
|
1888
|
+
const issues = [];
|
|
1889
|
+
const files = await this.getTsxFiles();
|
|
1890
|
+
for (const file of files) {
|
|
1891
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1892
|
+
const lines = content.split("\n");
|
|
1893
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1894
|
+
const line = lines[i];
|
|
1895
|
+
// Check for addEventListener with scroll/touch/wheel
|
|
1896
|
+
if (/addEventListener\s*\(\s*["'](?:scroll|touchstart|touchmove|wheel)["']/.test(line)) {
|
|
1897
|
+
const contextBlock = lines.slice(i, Math.min(i + 3, lines.length)).join("\n");
|
|
1898
|
+
if (!/passive:\s*true|\{\s*passive/.test(contextBlock)) {
|
|
1899
|
+
issues.push({
|
|
1900
|
+
file,
|
|
1901
|
+
line: i + 1,
|
|
1902
|
+
type: "scroll-listener-not-passive",
|
|
1903
|
+
severity: "warning",
|
|
1904
|
+
message: "Scroll/touch listener without passive option hurts mobile scroll performance",
|
|
1905
|
+
suggestion: "Add { passive: true } or use React's built-in passive handling",
|
|
1906
|
+
snippet: line.trim().substring(0, 80),
|
|
1907
|
+
});
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
// Check for onScroll handlers that might block
|
|
1911
|
+
if (/onScroll=\{[^}]*preventDefault/.test(line)) {
|
|
1912
|
+
issues.push({
|
|
1913
|
+
file,
|
|
1914
|
+
line: i + 1,
|
|
1915
|
+
type: "scroll-prevent-default",
|
|
1916
|
+
severity: "warning",
|
|
1917
|
+
message: "preventDefault in scroll handler blocks smooth scrolling",
|
|
1918
|
+
suggestion: "Avoid preventDefault in scroll handlers if possible",
|
|
1919
|
+
snippet: line.trim().substring(0, 80),
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
// Check for wheel handler without passive
|
|
1923
|
+
if (/onWheel=/.test(line)) {
|
|
1924
|
+
const contextBlock = lines.slice(i, Math.min(i + 5, lines.length)).join("\n");
|
|
1925
|
+
if (/preventDefault/.test(contextBlock)) {
|
|
1926
|
+
issues.push({
|
|
1927
|
+
file,
|
|
1928
|
+
line: i + 1,
|
|
1929
|
+
type: "wheel-prevent-default",
|
|
1930
|
+
severity: "info",
|
|
1931
|
+
message: "Wheel handler with preventDefault may affect trackpad scrolling",
|
|
1932
|
+
suggestion: "Consider using CSS touch-action instead of JS prevention",
|
|
1933
|
+
snippet: line.trim().substring(0, 80),
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
return {
|
|
1940
|
+
name: "Passive Event Listeners",
|
|
1941
|
+
passed: issues.filter((i) => i.severity === "error").length === 0,
|
|
1942
|
+
blocking: false,
|
|
1943
|
+
issues,
|
|
1944
|
+
duration: Date.now() - startTime,
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
/**
|
|
1948
|
+
* 35. Viewport Resize Handling
|
|
1949
|
+
* Check for keyboard resize handling on Android
|
|
1950
|
+
*/
|
|
1951
|
+
async checkViewportResizeHandling() {
|
|
1952
|
+
const startTime = Date.now();
|
|
1953
|
+
const issues = [];
|
|
1954
|
+
const files = await this.getTsxFiles();
|
|
1955
|
+
let hasVisualViewport = false;
|
|
1956
|
+
let hasResizeHandler = false;
|
|
1957
|
+
for (const file of files) {
|
|
1958
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1959
|
+
if (/visualViewport/.test(content)) {
|
|
1960
|
+
hasVisualViewport = true;
|
|
1961
|
+
}
|
|
1962
|
+
if (/resize.*keyboard|keyboard.*resize|innerHeight/i.test(content)) {
|
|
1963
|
+
hasResizeHandler = true;
|
|
1964
|
+
}
|
|
1965
|
+
const lines = content.split("\n");
|
|
1966
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1967
|
+
const line = lines[i];
|
|
1968
|
+
// Check for fixed bottom elements that might be hidden by keyboard
|
|
1969
|
+
if (/fixed.*bottom-0|sticky.*bottom-0/.test(line)) {
|
|
1970
|
+
const contextBlock = lines.slice(i, Math.min(i + 10, lines.length)).join("\n");
|
|
1971
|
+
// Check if it's a form or input-related
|
|
1972
|
+
const hasInputNearby = /input|Input|TextField|form|Form/i.test(contextBlock);
|
|
1973
|
+
if (hasInputNearby && !hasVisualViewport && !hasResizeHandler) {
|
|
1974
|
+
issues.push({
|
|
1975
|
+
file,
|
|
1976
|
+
line: i + 1,
|
|
1977
|
+
type: "fixed-bottom-keyboard-issue",
|
|
1978
|
+
severity: "info",
|
|
1979
|
+
message: "Fixed bottom element near inputs - may be hidden by mobile keyboard",
|
|
1980
|
+
suggestion: "Use visualViewport API to adjust position when keyboard opens",
|
|
1981
|
+
snippet: line.trim().substring(0, 80),
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
// Check for apps with forms but no keyboard handling
|
|
1988
|
+
let hasFormsWithFixedElements = false;
|
|
1989
|
+
for (const file of files) {
|
|
1990
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1991
|
+
if (/<form|<Form/i.test(content) && /fixed.*bottom|sticky.*bottom/.test(content)) {
|
|
1992
|
+
hasFormsWithFixedElements = true;
|
|
1993
|
+
break;
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
if (hasFormsWithFixedElements && !hasVisualViewport) {
|
|
1997
|
+
issues.push({
|
|
1998
|
+
file: "codebase-wide",
|
|
1999
|
+
line: 0,
|
|
2000
|
+
type: "no-visual-viewport-api",
|
|
2001
|
+
severity: "info",
|
|
2002
|
+
message: "Forms with fixed elements but no visualViewport handling",
|
|
2003
|
+
suggestion: "Use window.visualViewport for accurate viewport size with mobile keyboard",
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
return {
|
|
2007
|
+
name: "Viewport Resize Handling",
|
|
2008
|
+
passed: true,
|
|
2009
|
+
blocking: false,
|
|
2010
|
+
issues,
|
|
2011
|
+
duration: Date.now() - startTime,
|
|
2012
|
+
};
|
|
2013
|
+
}
|
|
2014
|
+
/**
|
|
2015
|
+
* 36. Network-Aware Loading
|
|
2016
|
+
* Check for connection-aware image/data loading
|
|
2017
|
+
*/
|
|
2018
|
+
async checkNetworkAwareLoading() {
|
|
2019
|
+
const startTime = Date.now();
|
|
2020
|
+
const issues = [];
|
|
2021
|
+
const files = await this.getTsxFiles();
|
|
2022
|
+
let hasNetworkInfo = false;
|
|
2023
|
+
let hasLazyLoading = false;
|
|
2024
|
+
let hasProgressiveImages = false;
|
|
2025
|
+
for (const file of files) {
|
|
2026
|
+
const content = fs.readFileSync(file, "utf8");
|
|
2027
|
+
// Check for Network Information API
|
|
2028
|
+
if (/navigator\.connection|effectiveType|saveData|NetworkInformation/i.test(content)) {
|
|
2029
|
+
hasNetworkInfo = true;
|
|
2030
|
+
}
|
|
2031
|
+
// Check for lazy loading
|
|
2032
|
+
if (/loading=["']lazy|LazyLoad|lazy.*load|IntersectionObserver/i.test(content)) {
|
|
2033
|
+
hasLazyLoading = true;
|
|
2034
|
+
}
|
|
2035
|
+
// Check for progressive images
|
|
2036
|
+
if (/blur.*placeholder|placeholder.*blur|lqip|progressive/i.test(content)) {
|
|
2037
|
+
hasProgressiveImages = true;
|
|
2038
|
+
}
|
|
2039
|
+
const lines = content.split("\n");
|
|
2040
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2041
|
+
const line = lines[i];
|
|
2042
|
+
// Check for large data fetches without pagination
|
|
2043
|
+
if (/useQuery|useSWR|fetch/.test(line)) {
|
|
2044
|
+
const contextBlock = lines.slice(i, Math.min(i + 15, lines.length)).join("\n");
|
|
2045
|
+
// Check for limit/pagination
|
|
2046
|
+
const hasPagination = /limit|page|offset|cursor|take|first|last/i.test(contextBlock);
|
|
2047
|
+
const hasInfinite = /infinite|loadMore|fetchMore/i.test(contextBlock);
|
|
2048
|
+
// Only flag if fetching "all" without pagination
|
|
2049
|
+
if (/getAll|fetchAll|loadAll/i.test(line) && !hasPagination && !hasInfinite) {
|
|
2050
|
+
issues.push({
|
|
2051
|
+
file,
|
|
2052
|
+
line: i + 1,
|
|
2053
|
+
type: "fetch-all-no-pagination",
|
|
2054
|
+
severity: "info",
|
|
2055
|
+
message: "Fetching all data without pagination - heavy on mobile networks",
|
|
2056
|
+
suggestion: "Add pagination or infinite scroll for large data sets",
|
|
2057
|
+
snippet: line.trim().substring(0, 80),
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
if (!hasLazyLoading) {
|
|
2064
|
+
issues.push({
|
|
2065
|
+
file: "codebase-wide",
|
|
2066
|
+
line: 0,
|
|
2067
|
+
type: "no-lazy-loading",
|
|
2068
|
+
severity: "info",
|
|
2069
|
+
message: "No image lazy loading detected",
|
|
2070
|
+
suggestion: "Add loading=\"lazy\" to below-fold images for faster initial load",
|
|
2071
|
+
});
|
|
2072
|
+
}
|
|
2073
|
+
if (!hasProgressiveImages) {
|
|
2074
|
+
issues.push({
|
|
2075
|
+
file: "codebase-wide",
|
|
2076
|
+
line: 0,
|
|
2077
|
+
type: "no-progressive-images",
|
|
2078
|
+
severity: "info",
|
|
2079
|
+
message: "No progressive/blur-up image loading detected",
|
|
2080
|
+
suggestion: "Use Next.js Image placeholder=\"blur\" for better perceived performance",
|
|
2081
|
+
});
|
|
2082
|
+
}
|
|
2083
|
+
return {
|
|
2084
|
+
name: "Network-Aware Loading",
|
|
2085
|
+
passed: true,
|
|
2086
|
+
blocking: false,
|
|
2087
|
+
issues,
|
|
2088
|
+
duration: Date.now() - startTime,
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
/**
|
|
2092
|
+
* Run all checks
|
|
2093
|
+
*/
|
|
2094
|
+
async runAll(filter) {
|
|
2095
|
+
const startTime = Date.now();
|
|
2096
|
+
const checks = [];
|
|
2097
|
+
const checkMap = {
|
|
2098
|
+
safearea: () => this.checkSafeAreaInsets(),
|
|
2099
|
+
inputs: () => this.checkInputTypes(),
|
|
2100
|
+
fonts: () => this.checkFontSizeMinimum(),
|
|
2101
|
+
overflow: () => this.checkHorizontalOverflow(),
|
|
2102
|
+
viewport: () => this.checkViewportUnits(),
|
|
2103
|
+
hover: () => this.checkHoverMediaQuery(),
|
|
2104
|
+
labels: () => this.checkFormLabels(),
|
|
2105
|
+
sticky: () => this.checkStickyPosition(),
|
|
2106
|
+
orientation: () => this.checkOrientationSupport(),
|
|
2107
|
+
touch: () => this.checkTouchAction(),
|
|
2108
|
+
focus: () => this.checkMobileMenuFocus(),
|
|
2109
|
+
bottomnav: () => this.checkBottomNavigation(),
|
|
2110
|
+
pulltorefresh: () => this.checkPullToRefresh(),
|
|
2111
|
+
tables: () => this.checkMobileTablePatterns(),
|
|
2112
|
+
modals: () => this.checkMobileModalSizing(),
|
|
2113
|
+
overscroll: () => this.checkOverscrollBehavior(),
|
|
2114
|
+
pwa: () => this.checkPWAMetaTags(),
|
|
2115
|
+
autofill: () => this.checkMobileAutofillStyling(),
|
|
2116
|
+
thumbzone: () => this.checkThumbZoneErgonomics(),
|
|
2117
|
+
srcset: () => this.checkMobileImageSrcset(),
|
|
2118
|
+
taptarget: () => this.checkTapTargetSize(),
|
|
2119
|
+
keyboard: () => this.checkKeyboardDismiss(),
|
|
2120
|
+
navdepth: () => this.checkNavigationDepth(),
|
|
2121
|
+
momentum: () => this.checkTouchScrollMomentum(),
|
|
2122
|
+
formchunk: () => this.checkMobileFormChunking(),
|
|
2123
|
+
zoom: () => this.checkPinchToZoom(),
|
|
2124
|
+
textselect: () => this.checkTextSelection(),
|
|
2125
|
+
datepicker: () => this.checkMobileDatePickers(),
|
|
2126
|
+
network: () => this.checkNetworkStatusIndicator(),
|
|
2127
|
+
smartbanner: () => this.checkIOSSmartAppBanner(),
|
|
2128
|
+
colorscheme: () => this.checkColorSchemeDetection(),
|
|
2129
|
+
haptic: () => this.checkHapticFeedback(),
|
|
2130
|
+
fontload: () => this.checkFontLoadingStrategy(),
|
|
2131
|
+
passive: () => this.checkPassiveEventListeners(),
|
|
2132
|
+
vpresize: () => this.checkViewportResizeHandling(),
|
|
2133
|
+
networkaware: () => this.checkNetworkAwareLoading(),
|
|
2134
|
+
};
|
|
2135
|
+
if (filter && checkMap[filter]) {
|
|
2136
|
+
checks.push(await checkMap[filter]());
|
|
2137
|
+
}
|
|
2138
|
+
else {
|
|
2139
|
+
for (const check of Object.values(checkMap)) {
|
|
2140
|
+
checks.push(await check());
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
const errors = checks.reduce((sum, c) => sum + c.issues.filter((i) => i.severity === "error").length, 0);
|
|
2144
|
+
const warnings = checks.reduce((sum, c) => sum + c.issues.filter((i) => i.severity === "warning").length, 0);
|
|
2145
|
+
return {
|
|
2146
|
+
module: "UI Mobile UX",
|
|
2147
|
+
passed: errors === 0,
|
|
2148
|
+
totalDuration: Date.now() - startTime,
|
|
2149
|
+
checks,
|
|
2150
|
+
summary: {
|
|
2151
|
+
total: checks.length,
|
|
2152
|
+
passed: checks.filter((c) => c.passed).length,
|
|
2153
|
+
failed: checks.filter((c) => !c.passed).length,
|
|
2154
|
+
errors,
|
|
2155
|
+
warnings,
|
|
2156
|
+
},
|
|
2157
|
+
};
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
exports.UIMobileUXModule = UIMobileUXModule;
|
|
2161
|
+
// CLI ENTRY POINT
|
|
2162
|
+
async function main() {
|
|
2163
|
+
const args = process.argv.slice(2);
|
|
2164
|
+
const filter = args.find((a) => !a.startsWith("-"));
|
|
2165
|
+
const verbose = args.includes("--verbose") || args.includes("-v");
|
|
2166
|
+
const module = new UIMobileUXModule({ verbose });
|
|
2167
|
+
const result = await module.runAll(filter);
|
|
2168
|
+
console.log((0, console_chars_1.createDivider)(80, "heavy"));
|
|
2169
|
+
console.log(`${console_chars_1.emoji.mobile} UI MOBILE UX CHECK`);
|
|
2170
|
+
console.log((0, console_chars_1.createDivider)(80, "heavy"));
|
|
2171
|
+
console.log();
|
|
2172
|
+
for (const check of result.checks) {
|
|
2173
|
+
const icon = check.passed
|
|
2174
|
+
? console_chars_1.emoji.success
|
|
2175
|
+
: check.issues.some((i) => i.severity === "error")
|
|
2176
|
+
? console_chars_1.emoji.error
|
|
2177
|
+
: console_chars_1.emoji.warning;
|
|
2178
|
+
console.log(`${icon} ${check.name} (${check.duration}ms)`);
|
|
2179
|
+
if (check.issues.length > 0 && (verbose || check.issues.some((i) => i.severity !== "info"))) {
|
|
2180
|
+
for (const issue of check.issues.slice(0, verbose ? 100 : 5)) {
|
|
2181
|
+
const sevIcon = issue.severity === "error"
|
|
2182
|
+
? console_chars_1.emoji.error
|
|
2183
|
+
: issue.severity === "warning"
|
|
2184
|
+
? console_chars_1.emoji.warning
|
|
2185
|
+
: console_chars_1.emoji.info;
|
|
2186
|
+
const relPath = path.relative(process.cwd(), issue.file).replace(/\\/g, "/");
|
|
2187
|
+
console.log(` ${sevIcon} ${relPath}:${issue.line}`);
|
|
2188
|
+
console.log(` ${issue.message}`);
|
|
2189
|
+
console.log(` ${console_chars_1.emoji.hint} ${issue.suggestion}`);
|
|
2190
|
+
}
|
|
2191
|
+
if (!verbose && check.issues.length > 5) {
|
|
2192
|
+
console.log(` ... and ${check.issues.length - 5} more issues`);
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
console.log();
|
|
2196
|
+
}
|
|
2197
|
+
console.log((0, console_chars_1.createDivider)(80, "light"));
|
|
2198
|
+
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`);
|
|
2199
|
+
console.log(`Duration: ${(result.totalDuration / 1000).toFixed(2)}s`);
|
|
2200
|
+
console.log((0, console_chars_1.createDivider)(80, "heavy"));
|
|
2201
|
+
if (result.summary.errors > 0) {
|
|
2202
|
+
console.log(`\n${console_chars_1.emoji.error} MOBILE UX CHECK FAILED\n`);
|
|
2203
|
+
process.exit(1);
|
|
2204
|
+
}
|
|
2205
|
+
else {
|
|
2206
|
+
console.log(`\n${console_chars_1.emoji.success} MOBILE UX CHECK PASSED\n`);
|
|
2207
|
+
process.exit(0);
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
if (require.main === module)
|
|
2211
|
+
main();
|
|
2212
|
+
//# sourceMappingURL=ui-mobile-ux.js.map
|