From d3b6d97140cf3810dc26b578c7fcbde97d3ea2dd Mon Sep 17 00:00:00 2001 From: b-l-i-n-d Date: Fri, 31 Jul 2026 16:09:15 +0600 Subject: [PATCH 1/4] feat(motion): Add custom hook to determine current motion settings --- .../shared/hooks/usePrefersReducedMotion.ts | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 assets/src/js/v3/shared/hooks/usePrefersReducedMotion.ts diff --git a/assets/src/js/v3/shared/hooks/usePrefersReducedMotion.ts b/assets/src/js/v3/shared/hooks/usePrefersReducedMotion.ts new file mode 100644 index 0000000000..0aa9d6266f --- /dev/null +++ b/assets/src/js/v3/shared/hooks/usePrefersReducedMotion.ts @@ -0,0 +1,64 @@ +import { useEffect, useState } from 'react'; + +type MotionAttribute = 'auto' | 'reduce' | null; + +const MOTION_ATTRIBUTE = 'data-tutor-motion'; + +const readMotionAttribute = (): MotionAttribute => { + if (typeof document === 'undefined') { + return null; + } + return document.documentElement.getAttribute(MOTION_ATTRIBUTE) as MotionAttribute; +}; + +const systemPrefersReducedMotion = () => { + if (typeof window === 'undefined' || !window.matchMedia) { + return false; + } + return window.matchMedia('(prefers-reduced-motion: reduce)').matches; +}; + +const resolvePreference = () => { + const attribute = readMotionAttribute(); + + // Explicit override always wins. + if (attribute === 'reduce') return true; + + // 'auto' (or unset) defers to the OS/browser setting. + return systemPrefersReducedMotion(); +}; + +/** + * Resolves whether motion should be reduced, honoring: + * - data-tutor-motion="reduce" on -> always reduce + * - data-tutor-motion="auto" (or absent) -> defer to prefers-reduced-motion + */ +export const usePrefersReducedMotion = () => { + const [prefersReduced, setPrefersReduced] = useState(resolvePreference); + + useEffect(() => { + if (typeof window === 'undefined' || !window.matchMedia) { + return; + } + + const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)'); + const update = () => setPrefersReduced(resolvePreference()); + + update(); + mediaQuery.addEventListener('change', update); + + // In case data-tutor-motion is toggled at runtime (e.g. a settings panel) + const observer = new MutationObserver(update); + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: [MOTION_ATTRIBUTE], + }); + + return () => { + mediaQuery.removeEventListener('change', update); + observer.disconnect(); + }; + }, []); + + return prefersReduced; +}; From db7fd562a7f68e1e6a779bd13c1aec2125d38756 Mon Sep 17 00:00:00 2001 From: b-l-i-n-d Date: Fri, 31 Jul 2026 16:10:57 +0600 Subject: [PATCH 2/4] feat: Use custom hook to determine when to animate --- assets/src/js/v3/shared/components/fields/FormDateInput.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/assets/src/js/v3/shared/components/fields/FormDateInput.tsx b/assets/src/js/v3/shared/components/fields/FormDateInput.tsx index f9700a3318..99f746698f 100644 --- a/assets/src/js/v3/shared/components/fields/FormDateInput.tsx +++ b/assets/src/js/v3/shared/components/fields/FormDateInput.tsx @@ -11,6 +11,7 @@ import { DateFormats, isRTL } from '@TutorShared/config/constants'; import { borderRadius, colorTokens, fontSize, fontWeight, shadow, spacing } from '@TutorShared/config/styles'; import { typography } from '@TutorShared/config/typography'; import { POPOVER_PLACEMENTS, Portal, usePortalPopover } from '@TutorShared/hooks/usePortalPopover'; +import { usePrefersReducedMotion } from '@TutorShared/hooks/usePrefersReducedMotion'; import type { FormControllerProps } from '@TutorShared/utils/form'; import { styleUtils } from '@TutorShared/utils/style-utils'; @@ -69,6 +70,7 @@ const FormDateInput = ({ onChange, dateFormat = DateFormats.monthDayYear, }: FormDateInputProps) => { + const prefersReducedMotion = usePrefersReducedMotion(); const inputRef = useRef(null); const [isOpen, setIsOpen] = useState(false); const parsedDate = parseDate(field.value); @@ -159,7 +161,7 @@ const FormDateInput = ({ > Date: Fri, 31 Jul 2026 16:17:31 +0600 Subject: [PATCH 3/4] fix(curriculum): Resolve title layout shift --- assets/src/js/v3/entries/course-builder/pages/Curriculum.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/src/js/v3/entries/course-builder/pages/Curriculum.tsx b/assets/src/js/v3/entries/course-builder/pages/Curriculum.tsx index cc6084d5ce..cfe47be1cc 100644 --- a/assets/src/js/v3/entries/course-builder/pages/Curriculum.tsx +++ b/assets/src/js/v3/entries/course-builder/pages/Curriculum.tsx @@ -562,7 +562,7 @@ export default Curriculum; const styles = { container: css` - margin-top: ${spacing[32]}; + margin-top: calc(${spacing[32]} - 1px); width: 100%; ${Breakpoint.smallTablet} { From b87eda2b1723420891a77f3b9630b5b3ec2449ab Mon Sep 17 00:00:00 2001 From: b-l-i-n-d Date: Fri, 31 Jul 2026 16:33:33 +0600 Subject: [PATCH 4/4] style(header): Fix top for course builder header --- .../entries/course-builder/components/layouts/header/Header.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/src/js/v3/entries/course-builder/components/layouts/header/Header.tsx b/assets/src/js/v3/entries/course-builder/components/layouts/header/Header.tsx index 4a412b3995..1fc2419244 100644 --- a/assets/src/js/v3/entries/course-builder/components/layouts/header/Header.tsx +++ b/assets/src/js/v3/entries/course-builder/components/layouts/header/Header.tsx @@ -188,6 +188,7 @@ const styles = { 'logo closeButton' 'container container'; row-gap: ${spacing[8]}; + top: 0; } `, container: css`