mirror of
https://github.com/stablyai/orca
synced 2026-04-21 14:17:16 +00:00
feat(settings): center jumped-to sidebar sections and flash their border (#795)
This commit is contained in:
parent
f2eb4f4866
commit
a4899dbbf1
2 changed files with 66 additions and 7 deletions
|
|
@ -672,6 +672,33 @@
|
|||
animation: settings-shell-enter 180ms ease-out;
|
||||
}
|
||||
|
||||
/* Why: after clicking a sidebar item we center the target section and briefly
|
||||
pulse its border so the user can see exactly which section their click
|
||||
landed on — the scroll destination alone is subtle when the section is
|
||||
already partially visible. */
|
||||
@keyframes settings-section-flash {
|
||||
0% {
|
||||
border-color: var(--ring);
|
||||
box-shadow:
|
||||
0 0 0 2px color-mix(in srgb, var(--ring) 45%, transparent),
|
||||
0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
}
|
||||
60% {
|
||||
border-color: var(--ring);
|
||||
box-shadow:
|
||||
0 0 0 2px color-mix(in srgb, var(--ring) 30%, transparent),
|
||||
0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
}
|
||||
100% {
|
||||
border-color: var(--border);
|
||||
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.settings-section-flash {
|
||||
animation: settings-section-flash 900ms ease-out;
|
||||
}
|
||||
|
||||
.sidebar.collapsed {
|
||||
width: 0;
|
||||
border-right: none;
|
||||
|
|
|
|||
|
|
@ -70,6 +70,41 @@ function getFallbackVisibleSection(sections: SettingsNavSection[]): SettingsNavS
|
|||
return sections.at(0)
|
||||
}
|
||||
|
||||
// Why: after a sidebar jump the target section is now in the viewport center
|
||||
// rather than the top, which can make it less obvious which section just
|
||||
// scrolled into view. Pulsing the border for a moment reassures the user that
|
||||
// their click landed on the right section.
|
||||
const SECTION_FLASH_CLASS = 'settings-section-flash'
|
||||
const SECTION_FLASH_DURATION_MS = 900
|
||||
|
||||
function scrollSectionIntoView(sectionId: string, container: HTMLElement | null): void {
|
||||
const target = document.getElementById(sectionId)
|
||||
if (!target) {
|
||||
return
|
||||
}
|
||||
// Why: centering a tall section pushes its heading above the viewport,
|
||||
// which defeats the purpose of jumping to it. Only center when the whole
|
||||
// section fits; otherwise align to the top so the title is always visible.
|
||||
const fitsInViewport = container
|
||||
? target.getBoundingClientRect().height <= container.clientHeight
|
||||
: true
|
||||
target.scrollIntoView({ block: fitsInViewport ? 'center' : 'start' })
|
||||
}
|
||||
|
||||
function flashSectionHighlight(sectionId: string): void {
|
||||
const target = document.getElementById(sectionId)
|
||||
if (!target) {
|
||||
return
|
||||
}
|
||||
target.classList.remove(SECTION_FLASH_CLASS)
|
||||
// Force a reflow so re-adding the class restarts the animation.
|
||||
void target.offsetWidth
|
||||
target.classList.add(SECTION_FLASH_CLASS)
|
||||
window.setTimeout(() => {
|
||||
target.classList.remove(SECTION_FLASH_CLASS)
|
||||
}, SECTION_FLASH_DURATION_MS)
|
||||
}
|
||||
|
||||
function Settings(): React.JSX.Element {
|
||||
const settings = useAppStore((s) => s.settings)
|
||||
const updateSettings = useAppStore((s) => s.updateSettings)
|
||||
|
|
@ -337,8 +372,8 @@ function Settings(): React.JSX.Element {
|
|||
const visibleIds = new Set(visibleNavSections.map((section) => section.id))
|
||||
|
||||
if (scrollTargetId && pendingNavSectionId && visibleIds.has(pendingNavSectionId)) {
|
||||
const target = document.getElementById(scrollTargetId)
|
||||
target?.scrollIntoView({ block: 'start' })
|
||||
scrollSectionIntoView(scrollTargetId, contentScrollRef.current)
|
||||
flashSectionHighlight(scrollTargetId)
|
||||
setActiveSectionId(pendingNavSectionId)
|
||||
pendingNavSectionRef.current = null
|
||||
pendingScrollTargetRef.current = null
|
||||
|
|
@ -430,11 +465,8 @@ function Settings(): React.JSX.Element {
|
|||
}, [visibleNavSections])
|
||||
|
||||
const scrollToSection = useCallback((sectionId: string) => {
|
||||
const target = document.getElementById(sectionId)
|
||||
if (!target) {
|
||||
return
|
||||
}
|
||||
target.scrollIntoView({ block: 'start' })
|
||||
scrollSectionIntoView(sectionId, contentScrollRef.current)
|
||||
flashSectionHighlight(sectionId)
|
||||
setActiveSectionId(sectionId)
|
||||
}, [])
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue