| author | |
| committer | |
| log | da6a85b1365993d0ff2a79698cd82498de247d8a |
| tree | 60926c78fc591a79abe48b2b0288995d9a96ecac |
| parent | aff9318bf46beaf05cc7155b428d3f0b8711efd2 |
| signature | Signed by PGP key B5690EEEBB952194 |
5 files changed, 93 insertions(+), 36 deletions(-)
apps/web/src/components/ChatView.tsx+26-16| ... | @@ -406,7 +406,6 @@ import { | ... | @@ -406,7 +406,6 @@ import { |
| 406 | import { deriveLatestContextWindowSnapshot, formatContextWindowTokens } from "../lib/contextWindow"; | 406 | import { deriveLatestContextWindowSnapshot, formatContextWindowTokens } from "../lib/contextWindow"; |
| 407 | import { | 407 | import { |
| 408 | DRAFT_HERO_TRANSITION_ANIMATION_ID, | 408 | DRAFT_HERO_TRANSITION_ANIMATION_ID, |
| 409 | DRAFT_HERO_TRANSITION_DURATION_MS, | ||
| 410 | DRAFT_HERO_TRANSITION_EASING, | 409 | DRAFT_HERO_TRANSITION_EASING, |
| 411 | MOBILE_COMPOSER_VIEW_TRANSITION_NAME, | 410 | MOBILE_COMPOSER_VIEW_TRANSITION_NAME, |
| 412 | MOBILE_DRAFT_HEADLINE_VIEW_TRANSITION_NAME, | 411 | MOBILE_DRAFT_HEADLINE_VIEW_TRANSITION_NAME, |
| ... | @@ -539,7 +538,11 @@ const EMPTY_PROVIDERS: ServerProvider[] = []; | ... | @@ -539,7 +538,11 @@ const EMPTY_PROVIDERS: ServerProvider[] = []; |
| 539 | const EMPTY_USAGE_LIMIT_SOURCES: UsageLimitSourceSnapshots = []; | 538 | const EMPTY_USAGE_LIMIT_SOURCES: UsageLimitSourceSnapshots = []; |
| 540 | const EMPTY_PROVIDER_SKILLS: ServerProvider["skills"] = []; | 539 | const EMPTY_PROVIDER_SKILLS: ServerProvider["skills"] = []; |
| 541 | const EMPTY_PENDING_USER_INPUT_ANSWERS: Record<string, PendingUserInputDraftAnswer> = {}; | 540 | const EMPTY_PENDING_USER_INPUT_ANSWERS: Record<string, PendingUserInputDraftAnswer> = {}; |
| 542 | function useDraftHeroLayoutTransition(isDraftHeroState: boolean) { | 541 | function useDraftHeroLayoutTransition( |
| 542 | isDraftHeroState: boolean, | ||
| 543 | animationsActive: boolean, | ||
| 544 | animationDurationMs: number, | ||
| 545 | ) { | ||
| 543 | const transitionGroupRef = useRef<HTMLDivElement | null>(null); | 546 | const transitionGroupRef = useRef<HTMLDivElement | null>(null); |
| 544 | const composerAnchorRef = useRef<HTMLDivElement | null>(null); | 547 | const composerAnchorRef = useRef<HTMLDivElement | null>(null); |
| 545 | const previousStateRef = useRef(isDraftHeroState); | 548 | const previousStateRef = useRef(isDraftHeroState); |
| ... | @@ -559,9 +562,6 @@ function useDraftHeroLayoutTransition(isDraftHeroState: boolean) { | ... | @@ -559,9 +562,6 @@ function useDraftHeroLayoutTransition(isDraftHeroState: boolean) { |
| 559 | const transitionGroup = transitionGroupRef.current; | 562 | const transitionGroup = transitionGroupRef.current; |
| 560 | const nextComposerRect = composerAnchorRef.current?.getBoundingClientRect() ?? null; | 563 | const nextComposerRect = composerAnchorRef.current?.getBoundingClientRect() ?? null; |
| 561 | const stateChanged = previousStateRef.current !== isDraftHeroState; | 564 | const stateChanged = previousStateRef.current !== isDraftHeroState; |
| 562 | const prefersReducedMotion = | ||
| 563 | typeof window !== "undefined" && | ||
| 564 | window.matchMedia?.("(prefers-reduced-motion: reduce)").matches; | ||
| 565 | const mobileComposerTransitionActive = | 565 | const mobileComposerTransitionActive = |
| 566 | typeof document !== "undefined" && | 566 | typeof document !== "undefined" && |
| 567 | document.documentElement.dataset.mobileComposerRouteTransition === "true"; | 567 | document.documentElement.dataset.mobileComposerRouteTransition === "true"; |
| ... | @@ -572,7 +572,7 @@ function useDraftHeroLayoutTransition(isDraftHeroState: boolean) { | ... | @@ -572,7 +572,7 @@ function useDraftHeroLayoutTransition(isDraftHeroState: boolean) { |
| 572 | const previousComposerRect = previousComposerRectRef.current; | 572 | const previousComposerRect = previousComposerRectRef.current; |
| 573 | if ( | 573 | if ( |
| 574 | stateChanged && | 574 | stateChanged && |
| 575 | !prefersReducedMotion && | 575 | animationsActive && |
| 576 | !mobileComposerTransitionActive && | 576 | !mobileComposerTransitionActive && |
| 577 | transitionGroup && | 577 | transitionGroup && |
| 578 | previousComposerRect && | 578 | previousComposerRect && |
| ... | @@ -588,7 +588,7 @@ function useDraftHeroLayoutTransition(isDraftHeroState: boolean) { | ... | @@ -588,7 +588,7 @@ function useDraftHeroLayoutTransition(isDraftHeroState: boolean) { |
| 588 | { transform: "translate3d(0, 0, 0)" }, | 588 | { transform: "translate3d(0, 0, 0)" }, |
| 589 | ], | 589 | ], |
| 590 | { | 590 | { |
| 591 | duration: DRAFT_HERO_TRANSITION_DURATION_MS, | 591 | duration: animationDurationMs, |
| 592 | easing: DRAFT_HERO_TRANSITION_EASING, | 592 | easing: DRAFT_HERO_TRANSITION_EASING, |
| 593 | }, | 593 | }, |
| 594 | ); | 594 | ); |
| ... | @@ -607,7 +607,7 @@ function useDraftHeroLayoutTransition(isDraftHeroState: boolean) { | ... | @@ -607,7 +607,7 @@ function useDraftHeroLayoutTransition(isDraftHeroState: boolean) { |
| 607 | 607 | ||
| 608 | previousStateRef.current = isDraftHeroState; | 608 | previousStateRef.current = isDraftHeroState; |
| 609 | previousComposerRectRef.current = nextComposerRect; | 609 | previousComposerRectRef.current = nextComposerRect; |
| 610 | }, [isDraftHeroState]); | 610 | }, [animationDurationMs, animationsActive, isDraftHeroState]); |
| 611 | 611 | ||
| 612 | return [attachTransitionGroupRef, attachComposerAnchorRef, captureComposerRect] as const; | 612 | return [attachTransitionGroupRef, attachComposerAnchorRef, captureComposerRect] as const; |
| 613 | } | 613 | } |
| ... | @@ -3634,7 +3634,11 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -3634,7 +3634,11 @@ export default function ChatView(props: ChatViewProps) { |
| 3634 | attachDraftHeroTransitionGroupRef, | 3634 | attachDraftHeroTransitionGroupRef, |
| 3635 | attachDraftHeroComposerAnchorRef, | 3635 | attachDraftHeroComposerAnchorRef, |
| 3636 | captureDraftHeroComposerRect, | 3636 | captureDraftHeroComposerRect, |
| 3637 | ] = useDraftHeroLayoutTransition(isDraftHeroState); | 3637 | ] = useDraftHeroLayoutTransition( |
| 3638 | isDraftHeroState, | ||
| 3639 | panelAnimationsActive, | ||
| 3640 | panelAnimationDurationMs, | ||
| 3641 | ); | ||
| 3638 | 3642 | ||
| 3639 | const gitCwd = activeProject | 3643 | const gitCwd = activeProject |
| 3640 | ? projectScriptCwd({ | 3644 | ? projectScriptCwd({ |
| ... | @@ -7877,13 +7881,19 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -7877,13 +7881,19 @@ export default function ChatView(props: ChatViewProps) { |
| 7877 | const dockStarted = new Promise<void>((resolve) => { | 7881 | const dockStarted = new Promise<void>((resolve) => { |
| 7878 | resolveDockStarted = resolve; | 7882 | resolveDockStarted = resolve; |
| 7879 | }); | 7883 | }); |
| 7880 | const dockTransition = runMobileComposerTransition(() => { | 7884 | const dockTransition = runMobileComposerTransition( |
| 7881 | flushSync(() => { | 7885 | () => { |
| 7882 | captureDraftHeroComposerRect(); | 7886 | flushSync(() => { |
| 7883 | setDockedDraftHeroThreadKey(activeThreadKey); | 7887 | captureDraftHeroComposerRect(); |
| 7884 | }); | 7888 | setDockedDraftHeroThreadKey(activeThreadKey); |
| 7885 | resolveDockStarted?.(); | 7889 | }); |
| 7886 | }); | 7890 | resolveDockStarted?.(); |
| 7891 | }, | ||
| 7892 | { | ||
| 7893 | active: panelAnimationsActive, | ||
| 7894 | durationMs: panelAnimationDurationMs, | ||
| 7895 | }, | ||
| 7896 | ); | ||
| 7887 | void dockTransition.catch(() => resolveDockStarted?.()); | 7897 | void dockTransition.catch(() => resolveDockStarted?.()); |
| 7888 | await dockStarted; | 7898 | await dockStarted; |
| 7889 | } | 7899 | } |
apps/web/src/components/chat/ChatComposer.tsx+14-8| ... | @@ -395,7 +395,6 @@ const EMPTY_PULL_REQUEST_LIST_TARGETS: ReadonlyArray<EnvironmentQueryTarget<Pull | ... | @@ -395,7 +395,6 @@ const EMPTY_PULL_REQUEST_LIST_TARGETS: ReadonlyArray<EnvironmentQueryTarget<Pull |
| 395 | 395 | ||
| 396 | const COMPOSER_SCROLL_COLLAPSE_THRESHOLD_PX = 24; | 396 | const COMPOSER_SCROLL_COLLAPSE_THRESHOLD_PX = 24; |
| 397 | const COMPOSER_SCROLL_GESTURE_RESET_MS = 120; | 397 | const COMPOSER_SCROLL_GESTURE_RESET_MS = 120; |
| 398 | const COMPOSER_RESTING_TRANSITION_DURATION_MS = 280; | ||
| 399 | const COMPOSER_RESTING_TRANSITION_CLEANUP_BUFFER_MS = 50; | 398 | const COMPOSER_RESTING_TRANSITION_CLEANUP_BUFFER_MS = 50; |
| 400 | const COMPOSER_RESTING_TRANSITION_EASING = "cubic-bezier(0.32, 0.72, 0, 1)"; | 399 | const COMPOSER_RESTING_TRANSITION_EASING = "cubic-bezier(0.32, 0.72, 0, 1)"; |
| 401 | const COMPOSER_RESTING_CONTROLS_ARRIVAL_DRIFT_PX = 4; | 400 | const COMPOSER_RESTING_CONTROLS_ARRIVAL_DRIFT_PX = 4; |
| ... | @@ -405,6 +404,8 @@ function useComposerRestingTransition( | ... | @@ -405,6 +404,8 @@ function useComposerRestingTransition( |
| 405 | isResting: boolean, | 404 | isResting: boolean, |
| 406 | restingControlsRef: React.RefObject<HTMLDivElement | null>, | 405 | restingControlsRef: React.RefObject<HTMLDivElement | null>, |
| 407 | onOverlayHeightChange: (height: number) => void, | 406 | onOverlayHeightChange: (height: number) => void, |
| 407 | animationsActive: boolean, | ||
| 408 | animationDurationMs: number, | ||
| 408 | ) { | 409 | ) { |
| 409 | const elementRef = useRef<HTMLDivElement>(null); | 410 | const elementRef = useRef<HTMLDivElement>(null); |
| 410 | const isCollapsedRef = useRef(isCollapsed); | 411 | const isCollapsedRef = useRef(isCollapsed); |
| ... | @@ -523,7 +524,6 @@ function useComposerRestingTransition( | ... | @@ -523,7 +524,6 @@ function useComposerRestingTransition( |
| 523 | const previousHeight = interruptedHeight ?? previousHeightRef.current; | 524 | const previousHeight = interruptedHeight ?? previousHeightRef.current; |
| 524 | const targetChanged = | 525 | const targetChanged = |
| 525 | interruptedTargetHeight === null || Math.abs(interruptedTargetHeight - nextHeight) >= 0.5; | 526 | interruptedTargetHeight === null || Math.abs(interruptedTargetHeight - nextHeight) >= 0.5; |
| 526 | const prefersReducedMotion = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches; | ||
| 527 | const shouldAnimate = shouldAnimateComposerRestingTransition({ | 527 | const shouldAnimate = shouldAnimateComposerRestingTransition({ |
| 528 | hasCompletedInitialLayout: hasCompletedInitialLayoutRef.current, | 528 | hasCompletedInitialLayout: hasCompletedInitialLayoutRef.current, |
| 529 | stateChanged, | 529 | stateChanged, |
| ... | @@ -532,18 +532,16 @@ function useComposerRestingTransition( | ... | @@ -532,18 +532,16 @@ function useComposerRestingTransition( |
| 532 | 532 | ||
| 533 | if ( | 533 | if ( |
| 534 | shouldAnimate && | 534 | shouldAnimate && |
| 535 | !prefersReducedMotion && | 535 | animationsActive && |
| 536 | previousHeight !== null && | 536 | previousHeight !== null && |
| 537 | Math.abs(previousHeight - nextHeight) >= 0.5 | 537 | Math.abs(previousHeight - nextHeight) >= 0.5 |
| 538 | ) { | 538 | ) { |
| 539 | const remainingDuration = | 539 | const remainingDuration = |
| 540 | typeof interruptedDuration === "number" && interruptedCurrentTime !== null | 540 | typeof interruptedDuration === "number" && interruptedCurrentTime !== null |
| 541 | ? Math.max(1, interruptedDuration - interruptedCurrentTime) | 541 | ? Math.max(1, interruptedDuration - interruptedCurrentTime) |
| 542 | : COMPOSER_RESTING_TRANSITION_DURATION_MS; | 542 | : animationDurationMs; |
| 543 | const duration = | 543 | const duration = |
| 544 | interruptedHeight !== null && !targetChanged | 544 | interruptedHeight !== null && !targetChanged ? remainingDuration : animationDurationMs; |
| 545 | ? remainingDuration | ||
| 546 | : COMPOSER_RESTING_TRANSITION_DURATION_MS; | ||
| 547 | element.style.overflow = "clip"; | 545 | element.style.overflow = "clip"; |
| 548 | surface.style.height = "100%"; | 546 | surface.style.height = "100%"; |
| 549 | 547 | ||
| ... | @@ -743,7 +741,13 @@ function useComposerRestingTransition( | ... | @@ -743,7 +741,13 @@ function useComposerRestingTransition( |
| 743 | actionFromBottom: nextActionTop === null ? null : nextRect.bottom - nextActionTop, | 741 | actionFromBottom: nextActionTop === null ? null : nextRect.bottom - nextActionTop, |
| 744 | }; | 742 | }; |
| 745 | }, | 743 | }, |
| 746 | [clearTransitionStyles, onOverlayHeightChange, restingControlsRef], | 744 | [ |
| 745 | animationDurationMs, | ||
| 746 | animationsActive, | ||
| 747 | clearTransitionStyles, | ||
| 748 | onOverlayHeightChange, | ||
| 749 | restingControlsRef, | ||
| 750 | ], | ||
| 747 | ); | 751 | ); |
| 748 | 752 | ||
| 749 | useLayoutEffect(() => { | 753 | useLayoutEffect(() => { |
| ... | @@ -4826,6 +4830,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | ... | @@ -4826,6 +4830,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 4826 | isComposerResting, | 4830 | isComposerResting, |
| 4827 | restingComposerControlsRef, | 4831 | restingComposerControlsRef, |
| 4828 | onComposerOverlayHeightChange, | 4832 | onComposerOverlayHeightChange, |
| 4833 | panelAnimationsActive, | ||
| 4834 | panelAnimationDurationMs, | ||
| 4829 | ); | 4835 | ); |
| 4830 | const canTrackComposerScrollGesture = | 4836 | const canTrackComposerScrollGesture = |
| 4831 | routeKind === "server" && activeThreadId !== null && !isMobileViewport; | 4837 | routeKind === "server" && activeThreadId !== null && !isMobileViewport; |
apps/web/src/components/chat/draftHeroTransition.test.ts+32-5| ... | @@ -56,8 +56,9 @@ describe("runMobileComposerTransition", () => { | ... | @@ -56,8 +56,9 @@ describe("runMobileComposerTransition", () => { |
| 56 | finishTransition = resolve; | 56 | finishTransition = resolve; |
| 57 | }); | 57 | }); |
| 58 | const dataset: Record<string, string> = {}; | 58 | const dataset: Record<string, string> = {}; |
| 59 | const style = { setProperty: vi.fn(), removeProperty: vi.fn() }; | ||
| 59 | vi.stubGlobal("document", { | 60 | vi.stubGlobal("document", { |
| 60 | documentElement: { dataset }, | 61 | documentElement: { dataset, style }, |
| 61 | getAnimations: () => [], | 62 | getAnimations: () => [], |
| 62 | startViewTransition: (update: () => void | Promise<void>) => { | 63 | startViewTransition: (update: () => void | Promise<void>) => { |
| 63 | void update(); | 64 | void update(); |
| ... | @@ -68,7 +69,10 @@ describe("runMobileComposerTransition", () => { | ... | @@ -68,7 +69,10 @@ describe("runMobileComposerTransition", () => { |
| 68 | matchMedia: (query: string) => ({ matches: query === "(max-width: 639px)" }), | 69 | matchMedia: (query: string) => ({ matches: query === "(max-width: 639px)" }), |
| 69 | }); | 70 | }); |
| 70 | 71 | ||
| 71 | const transition = runMobileComposerTransition(() => undefined); | 72 | const transition = runMobileComposerTransition(() => undefined, { |
| 73 | active: true, | ||
| 74 | durationMs: 240, | ||
| 75 | }); | ||
| 72 | await Promise.resolve(); | 76 | await Promise.resolve(); |
| 73 | 77 | ||
| 74 | let handoffComplete = false; | 78 | let handoffComplete = false; |
| ... | @@ -85,11 +89,12 @@ describe("runMobileComposerTransition", () => { | ... | @@ -85,11 +89,12 @@ describe("runMobileComposerTransition", () => { |
| 85 | 89 | ||
| 86 | it("uses a scoped view transition on mobile", async () => { | 90 | it("uses a scoped view transition on mobile", async () => { |
| 87 | const dataset: Record<string, string> = {}; | 91 | const dataset: Record<string, string> = {}; |
| 92 | const style = { setProperty: vi.fn(), removeProperty: vi.fn() }; | ||
| 88 | const startViewTransition = vi.fn((update: () => void | Promise<void>) => ({ | 93 | const startViewTransition = vi.fn((update: () => void | Promise<void>) => ({ |
| 89 | finished: Promise.resolve(update()).then(() => undefined), | 94 | finished: Promise.resolve(update()).then(() => undefined), |
| 90 | })); | 95 | })); |
| 91 | vi.stubGlobal("document", { | 96 | vi.stubGlobal("document", { |
| 92 | documentElement: { dataset }, | 97 | documentElement: { dataset, style }, |
| 93 | startViewTransition, | 98 | startViewTransition, |
| 94 | }); | 99 | }); |
| 95 | vi.stubGlobal("window", { | 100 | vi.stubGlobal("window", { |
| ... | @@ -97,13 +102,35 @@ describe("runMobileComposerTransition", () => { | ... | @@ -97,13 +102,35 @@ describe("runMobileComposerTransition", () => { |
| 97 | }); | 102 | }); |
| 98 | const update = vi.fn(); | 103 | const update = vi.fn(); |
| 99 | 104 | ||
| 100 | await runMobileComposerTransition(update); | 105 | await runMobileComposerTransition(update, { active: true, durationMs: 360 }); |
| 101 | 106 | ||
| 102 | expect(startViewTransition).toHaveBeenCalledOnce(); | 107 | expect(startViewTransition).toHaveBeenCalledOnce(); |
| 103 | expect(update).toHaveBeenCalledOnce(); | 108 | expect(update).toHaveBeenCalledOnce(); |
| 109 | expect(style.setProperty).toHaveBeenCalledWith( | ||
| 110 | "--mobile-composer-transition-duration", | ||
| 111 | "360ms", | ||
| 112 | ); | ||
| 113 | expect(style.removeProperty).toHaveBeenCalledWith("--mobile-composer-transition-duration"); | ||
| 104 | expect(dataset).not.toHaveProperty("mobileComposerRouteTransition"); | 114 | expect(dataset).not.toHaveProperty("mobileComposerRouteTransition"); |
| 105 | }); | 115 | }); |
| 106 | 116 | ||
| 117 | it("updates without a view transition when panel animations are inactive", async () => { | ||
| 118 | const startViewTransition = vi.fn(); | ||
| 119 | vi.stubGlobal("document", { | ||
| 120 | documentElement: { dataset: {} }, | ||
| 121 | startViewTransition, | ||
| 122 | }); | ||
| 123 | vi.stubGlobal("window", { | ||
| 124 | matchMedia: () => ({ matches: true }), | ||
| 125 | }); | ||
| 126 | const update = vi.fn(); | ||
| 127 | |||
| 128 | await runMobileComposerTransition(update, { active: false, durationMs: 360 }); | ||
| 129 | |||
| 130 | expect(startViewTransition).not.toHaveBeenCalled(); | ||
| 131 | expect(update).toHaveBeenCalledOnce(); | ||
| 132 | }); | ||
| 133 | |||
| 107 | it("updates without a view transition when reduced motion is preferred", async () => { | 134 | it("updates without a view transition when reduced motion is preferred", async () => { |
| 108 | const startViewTransition = vi.fn(); | 135 | const startViewTransition = vi.fn(); |
| 109 | vi.stubGlobal("document", { | 136 | vi.stubGlobal("document", { |
| ... | @@ -115,7 +142,7 @@ describe("runMobileComposerTransition", () => { | ... | @@ -115,7 +142,7 @@ describe("runMobileComposerTransition", () => { |
| 115 | }); | 142 | }); |
| 116 | const update = vi.fn(); | 143 | const update = vi.fn(); |
| 117 | 144 | ||
| 118 | await runMobileComposerTransition(update); | 145 | await runMobileComposerTransition(update, { active: true, durationMs: 360 }); |
| 119 | 146 | ||
| 120 | expect(startViewTransition).not.toHaveBeenCalled(); | 147 | expect(startViewTransition).not.toHaveBeenCalled(); |
| 121 | expect(update).toHaveBeenCalledOnce(); | 148 | expect(update).toHaveBeenCalledOnce(); |
apps/web/src/components/chat/draftHeroTransition.ts+15-2| ... | @@ -1,8 +1,8 @@ | ... | @@ -1,8 +1,8 @@ |
| 1 | export const DRAFT_HERO_TRANSITION_ANIMATION_ID = "t3-draft-hero-transition"; | 1 | export const DRAFT_HERO_TRANSITION_ANIMATION_ID = "t3-draft-hero-transition"; |
| 2 | export const DRAFT_HERO_TRANSITION_DURATION_MS = 180; | ||
| 3 | export const DRAFT_HERO_TRANSITION_EASING = "cubic-bezier(0.4, 0, 0.2, 1)"; | 2 | export const DRAFT_HERO_TRANSITION_EASING = "cubic-bezier(0.4, 0, 0.2, 1)"; |
| 4 | export const MOBILE_COMPOSER_VIEW_TRANSITION_NAME = "t3-mobile-composer"; | 3 | export const MOBILE_COMPOSER_VIEW_TRANSITION_NAME = "t3-mobile-composer"; |
| 5 | export const MOBILE_DRAFT_HEADLINE_VIEW_TRANSITION_NAME = "t3-mobile-draft-headline"; | 4 | export const MOBILE_DRAFT_HEADLINE_VIEW_TRANSITION_NAME = "t3-mobile-draft-headline"; |
| 5 | const MOBILE_COMPOSER_TRANSITION_DURATION_PROPERTY = "--mobile-composer-transition-duration"; | ||
| 6 | 6 | ||
| 7 | type ComposerViewTransition = { | 7 | type ComposerViewTransition = { |
| 8 | readonly finished: Promise<void>; | 8 | readonly finished: Promise<void>; |
| ... | @@ -39,6 +39,7 @@ export async function waitForDraftHeroTransition(): Promise<void> { | ... | @@ -39,6 +39,7 @@ export async function waitForDraftHeroTransition(): Promise<void> { |
| 39 | 39 | ||
| 40 | export async function runMobileComposerTransition( | 40 | export async function runMobileComposerTransition( |
| 41 | update: () => void | Promise<void>, | 41 | update: () => void | Promise<void>, |
| 42 | options: { active: boolean; durationMs: number }, | ||
| 42 | ): Promise<void> { | 43 | ): Promise<void> { |
| 43 | if (typeof document === "undefined" || typeof window === "undefined") { | 44 | if (typeof document === "undefined" || typeof window === "undefined") { |
| 44 | await update(); | 45 | await update(); |
| ... | @@ -49,7 +50,12 @@ export async function runMobileComposerTransition( | ... | @@ -49,7 +50,12 @@ export async function runMobileComposerTransition( |
| 49 | const mobileViewport = window.matchMedia?.("(max-width: 639px)").matches ?? false; | 50 | const mobileViewport = window.matchMedia?.("(max-width: 639px)").matches ?? false; |
| 50 | const prefersReducedMotion = | 51 | const prefersReducedMotion = |
| 51 | window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false; | 52 | window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false; |
| 52 | if (!mobileViewport || prefersReducedMotion || !transitionDocument.startViewTransition) { | 53 | if ( |
| 54 | !options.active || | ||
| 55 | !mobileViewport || | ||
| 56 | prefersReducedMotion || | ||
| 57 | !transitionDocument.startViewTransition | ||
| 58 | ) { | ||
| 53 | await update(); | 59 | await update(); |
| 54 | return; | 60 | return; |
| 55 | } | 61 | } |
| ... | @@ -61,6 +67,10 @@ export async function runMobileComposerTransition( | ... | @@ -61,6 +67,10 @@ export async function runMobileComposerTransition( |
| 61 | await update(); | 67 | await update(); |
| 62 | }; | 68 | }; |
| 63 | let transitionFinished: Promise<void> | null = null; | 69 | let transitionFinished: Promise<void> | null = null; |
| 70 | transitionDocument.documentElement.style.setProperty( | ||
| 71 | MOBILE_COMPOSER_TRANSITION_DURATION_PROPERTY, | ||
| 72 | `${String(options.durationMs)}ms`, | ||
| 73 | ); | ||
| 64 | transitionDocument.documentElement.dataset.mobileComposerRouteTransition = "true"; | 74 | transitionDocument.documentElement.dataset.mobileComposerRouteTransition = "true"; |
| 65 | try { | 75 | try { |
| 66 | const transition = transitionDocument.startViewTransition(runUpdate); | 76 | const transition = transitionDocument.startViewTransition(runUpdate); |
| ... | @@ -78,5 +88,8 @@ export async function runMobileComposerTransition( | ... | @@ -78,5 +88,8 @@ export async function runMobileComposerTransition( |
| 78 | activeMobileComposerTransition = null; | 88 | activeMobileComposerTransition = null; |
| 79 | } | 89 | } |
| 80 | delete transitionDocument.documentElement.dataset.mobileComposerRouteTransition; | 90 | delete transitionDocument.documentElement.dataset.mobileComposerRouteTransition; |
| 91 | transitionDocument.documentElement.style.removeProperty( | ||
| 92 | MOBILE_COMPOSER_TRANSITION_DURATION_PROPERTY, | ||
| 93 | ); | ||
| 81 | } | 94 | } |
| 82 | } | 95 | } |
apps/web/src/index.css+6-5| ... | @@ -13,7 +13,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-new(root) { | ... | @@ -13,7 +13,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-new(root) { |
| 13 | } | 13 | } |
| 14 | 14 | ||
| 15 | html[data-mobile-composer-route-transition="true"]::view-transition-group(t3-mobile-composer) { | 15 | html[data-mobile-composer-route-transition="true"]::view-transition-group(t3-mobile-composer) { |
| 16 | animation-duration: 180ms; | 16 | animation-duration: var(--mobile-composer-transition-duration); |
| 17 | animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); | 17 | animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); |
| 18 | } | 18 | } |
| 19 | 19 | ||
| ... | @@ -22,12 +22,12 @@ html[data-mobile-composer-route-transition="true"]::view-transition-image-pair(t | ... | @@ -22,12 +22,12 @@ html[data-mobile-composer-route-transition="true"]::view-transition-image-pair(t |
| 22 | } | 22 | } |
| 23 | 23 | ||
| 24 | html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobile-composer) { | 24 | html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobile-composer) { |
| 25 | animation: t3-mobile-composer-old 180ms linear both; | 25 | animation: t3-mobile-composer-old var(--mobile-composer-transition-duration) linear both; |
| 26 | mix-blend-mode: normal; | 26 | mix-blend-mode: normal; |
| 27 | } | 27 | } |
| 28 | 28 | ||
| 29 | html[data-mobile-composer-route-transition="true"]::view-transition-new(t3-mobile-composer) { | 29 | html[data-mobile-composer-route-transition="true"]::view-transition-new(t3-mobile-composer) { |
| 30 | animation: t3-mobile-composer-new 180ms linear both; | 30 | animation: t3-mobile-composer-new var(--mobile-composer-transition-duration) linear both; |
| 31 | mix-blend-mode: normal; | 31 | mix-blend-mode: normal; |
| 32 | } | 32 | } |
| 33 | 33 | ||
| ... | @@ -57,11 +57,12 @@ html[data-mobile-composer-route-transition="true"]::view-transition-new(t3-mobil | ... | @@ -57,11 +57,12 @@ html[data-mobile-composer-route-transition="true"]::view-transition-new(t3-mobil |
| 57 | } | 57 | } |
| 58 | 58 | ||
| 59 | html[data-mobile-composer-route-transition="true"]::view-transition-group(t3-mobile-draft-headline) { | 59 | html[data-mobile-composer-route-transition="true"]::view-transition-group(t3-mobile-draft-headline) { |
| 60 | animation-duration: 130ms; | 60 | animation-duration: var(--mobile-composer-transition-duration); |
| 61 | } | 61 | } |
| 62 | 62 | ||
| 63 | html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobile-draft-headline) { | 63 | html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobile-draft-headline) { |
| 64 | animation: t3-mobile-draft-headline-exit 130ms cubic-bezier(0.4, 0, 1, 1) both; | 64 | animation: t3-mobile-draft-headline-exit var(--mobile-composer-transition-duration) |
| 65 | cubic-bezier(0.4, 0, 1, 1) both; | ||
| 65 | mix-blend-mode: normal; | 66 | mix-blend-mode: normal; |
| 66 | } | 67 | } |
| 67 | 68 |