diff --git a/apps/desktop/layer/renderer/src/modules/settings/modal/SettingModalContent.tsx b/apps/desktop/layer/renderer/src/modules/settings/modal/SettingModalContent.tsx index c6e4e4133..3bf62ed84 100644 --- a/apps/desktop/layer/renderer/src/modules/settings/modal/SettingModalContent.tsx +++ b/apps/desktop/layer/renderer/src/modules/settings/modal/SettingModalContent.tsx @@ -8,7 +8,6 @@ import { useDeferredValue, useEffect, useLayoutEffect, - useMemo, useRef, useState, } from "react" @@ -19,7 +18,7 @@ import { ModalClose } from "~/components/ui/modal/stacked/components" import { SettingsTitle } from "~/modules/settings/title" import { useAvailableSettings } from "../hooks/use-setting-ctx" -import { SettingSectionHighlightContext } from "../section" +import { SettingSectionHighlightIdContext } from "../section" import { getSettingPages } from "../settings-glob" import type { SettingPageConfig } from "../utils" import { SettingTabProvider, useSettingTab } from "./context" @@ -46,83 +45,54 @@ export const SettingModalContent: FC<{ } const Content: FC<{ - initialSection?: string + initialSection?: string | null }> = ({ initialSection }) => { const key = useDeferredValue(useSettingTab() || "general") const pages = getSettingPages() const { Component, loader } = pages[key] const [scroller, setScroller] = useState(null) - const sectionRefs = useRef(new Map()) + const pendingSectionRef = useRef(initialSection ?? null) const hasAppliedInitialSectionRef = useRef(false) - const [highlightedSectionId, setHighlightedSectionId] = useState() useEffect(() => { pendingSectionRef.current = initialSection ?? null hasAppliedInitialSectionRef.current = false - setHighlightedSectionId(undefined) }, [initialSection]) useLayoutEffect(() => { if (scroller) { scroller.scrollTop = 0 } - }, [key]) + }, [key, scroller]) - const scrollToSection = useCallback((sectionId: string) => { - if (!sectionId) return false - const element = sectionRefs.current.get(sectionId) - if (!element) return false + const scrollToSection = useCallback( + (sectionId: string) => { + if (!sectionId) return false + if (!scroller) return false + const element = scroller.querySelector(`[data-setting-section="${sectionId}"]`) as HTMLElement + if (!element) return false - element.scrollIntoView({ - behavior: "smooth", - block: "start", - }) + const elementTop = element.offsetTop + const scrollerTop = scroller.scrollTop + const delta = elementTop - scrollerTop - setHighlightedSectionId(sectionId) + scroller.scrollTo({ + top: delta, + behavior: "smooth", + }) - return true - }, []) - - const registerSection = useCallback( - (sectionId: string, element: HTMLElement | null) => { - if (!sectionId) return - if (element) { - sectionRefs.current.set(sectionId, element) - if (pendingSectionRef.current === sectionId) { - const handled = scrollToSection(sectionId) - if (handled) { - pendingSectionRef.current = null - hasAppliedInitialSectionRef.current = true - } - } - } else { - sectionRefs.current.delete(sectionId) - } + return true }, - [scrollToSection], + [scroller], ) useEffect(() => { - if (!initialSection || hasAppliedInitialSectionRef.current) return - - const handled = scrollToSection(initialSection) - if (handled) { - hasAppliedInitialSectionRef.current = true - pendingSectionRef.current = null - } else { - pendingSectionRef.current = initialSection + if (initialSection) { + scrollToSection(initialSection) } - }, [initialSection, key, scrollToSection]) - - const highlightContextValue = useMemo( - () => ({ - highlightedSectionId: highlightedSectionId ?? undefined, - registerSection, - }), - [highlightedSectionId, registerSection], - ) + }, [initialSection, scrollToSection]) const config = (useLoaderData() || loader || {}) as SettingPageConfig if (!Component) return null @@ -140,9 +110,9 @@ const Content: FC<{ config.viewportClassName, )} > - + - +

diff --git a/apps/desktop/layer/renderer/src/modules/settings/section.tsx b/apps/desktop/layer/renderer/src/modules/settings/section.tsx index e48714312..6d6eb6303 100644 --- a/apps/desktop/layer/renderer/src/modules/settings/section.tsx +++ b/apps/desktop/layer/renderer/src/modules/settings/section.tsx @@ -9,13 +9,7 @@ import { titleCase } from "title-case" import { SettingActionItem, SettingDescription, SettingSwitch } from "./control" -type SettingSectionHighlightContextValue = { - highlightedSectionId?: string - registerSection: (sectionId: string, element: HTMLElement | null) => void -} - -export const SettingSectionHighlightContext = - createContext(null) +export const SettingSectionHighlightIdContext = createContext(null) export const SettingSectionTitle: FC<{ title: string | ReactNode @@ -23,32 +17,59 @@ export const SettingSectionTitle: FC<{ margin?: "compact" | "normal" sectionId?: string }> = ({ title, margin, className, sectionId }) => { - const highlightCtx = use(SettingSectionHighlightContext) + const highlightedSectionId = use(SettingSectionHighlightIdContext) const elementRef = useRef(null) + const isHighlighted = !!sectionId && highlightedSectionId === sectionId && !!elementRef.current + useEffect(() => { - if (!sectionId || !highlightCtx) return - highlightCtx.registerSection(sectionId, elementRef.current) - return () => { - highlightCtx.registerSection(sectionId, null) + if (!isHighlighted) { + return } - }, [highlightCtx, sectionId]) - const isHighlighted = - !!sectionId && highlightCtx?.highlightedSectionId === sectionId && !!elementRef.current + let rollingAnimation: Animation | null = null + const timer = setTimeout(() => { + const highlightedElement = elementRef.current?.querySelector( + "[data-highlighted-element]", + ) as HTMLElement + if (!highlightedElement) { + clearTimeout(timer) + return + } + const keyframeEffect = new KeyframeEffect( + highlightedElement, + [ + { + backgroundColor: "color-mix(in srgb, hsl(var(--fo-a)) 33%, hsl(var(--background)) 67%)", + }, + { backgroundColor: "transparent" }, + ], + { + duration: 1000, + easing: "ease-in-out", + }, + ) + rollingAnimation = new Animation(keyframeEffect, document.timeline) + rollingAnimation.play() + }, 500) + return () => { + rollingAnimation?.cancel() + clearTimeout(timer) + } + }, [isHighlighted]) return (

+ {isHighlighted &&
} {typeof title === "string" ? titleCase(title) : title}
) diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/tasks/TaskSchedulingSection.tsx b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/tasks/TaskSchedulingSection.tsx index 444487e1d..ef5d654dd 100644 --- a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/tasks/TaskSchedulingSection.tsx +++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/tasks/TaskSchedulingSection.tsx @@ -34,6 +34,7 @@ export const TaskSchedulingSection = () => { size={"sm"} variant={"outline"} onClick={handleCreateTask} + buttonClassName="!-translate-y-7" > {t("tasks.actions.new_task")}