Deep-link edge-peek tip to its Appearance setting

Update tip copy for the edge gesture and add a clickable Settings ›
Appearance › Right Sidebar Edge Peek link that opens the control.
This commit is contained in:
Brennan Benson
2026-07-16 17:43:04 -07:00
parent 7f93469b61
commit 3cde721fce
8 changed files with 157 additions and 38 deletions
@@ -68,8 +68,10 @@ describe('RightSidebarEdgePeekTipHost', () => {
rerender(<RightSidebarEdgePeekTipHost />)
const tip = screen.getByTestId('right-sidebar-edge-peek-tip')
expect(tip.textContent).toContain('Hover the right edge to peek')
expect(tip.textContent).toContain('Turn this off in Settings')
expect(tip.textContent).toContain('Hover over the right edge of the screen')
expect(tip.textContent).toContain('Turn this off in')
expect(tip.textContent).toMatch(/Settings/)
expect(screen.getByTestId('right-sidebar-edge-peek-tip-settings-link')).toBeTruthy()
})
it('auto-hides after the visible duration and marks the tip dismissed', () => {
@@ -1,9 +1,12 @@
import { useEffect, useLayoutEffect, useRef, useState, type CSSProperties, type JSX } from 'react'
import { useAppStore } from '@/store'
import { translate } from '@/i18n/i18n'
import {
RIGHT_SIDEBAR_EDGE_PEEK_TIP_VISIBLE_MS,
getRightSidebarEdgePeekSettingsLinkLabel,
getRightSidebarEdgePeekTipSettingsPrefix,
getRightSidebarEdgePeekTipTitle,
markRightSidebarEdgePeekTipDismissed,
openRightSidebarEdgePeekSetting,
shouldShowRightSidebarEdgePeekTip
} from './right-sidebar-edge-peek-tip'
import { isRightSidebarEdgePeekEnabled } from './right-sidebar-edge-peek-preference'
@@ -26,14 +29,17 @@ function measureToggleAnchor(): AnchorRect | null {
/**
* One-shot tip anchored under the right-sidebar titlebar toggle after the first
* close while peek is enabled. Auto-hides after ~1s. Successful peeks also mark
* the tip seen.
* close while peek is enabled. Auto-hides after a short delay. Successful peeks
* also mark the tip seen.
*/
export function RightSidebarEdgePeekTipHost(): JSX.Element | null {
const rightSidebarOpen = useAppStore((s) => s.rightSidebarOpen)
const rightSidebarPeek = useAppStore((s) => s.rightSidebarPeek)
const settings = useAppStore((s) => s.settings)
const updateSettings = useAppStore((s) => s.updateSettings)
const openSettingsPage = useAppStore((s) => s.openSettingsPage)
const openSettingsTarget = useAppStore((s) => s.openSettingsTarget)
const setSettingsSearchQuery = useAppStore((s) => s.setSettingsSearchQuery)
const [visible, setVisible] = useState(false)
const [anchor, setAnchor] = useState<AnchorRect | null>(null)
// Why: only fire on a true open→closed transition, not on first mount when
@@ -50,6 +56,12 @@ export function RightSidebarEdgePeekTipHost(): JSX.Element | null {
}
}
const dismissVisibleTip = (): void => {
clearHideTimer()
setVisible(false)
markRightSidebarEdgePeekTipDismissed({ updateSettings: updateSettingsRef.current })
}
useEffect(() => {
return () => clearHideTimer()
}, [])
@@ -65,8 +77,7 @@ export function RightSidebarEdgePeekTipHost(): JSX.Element | null {
}
setVisible(true)
clearHideTimer()
// Why: brief, non-blocking discoverability — a full toast is too heavy for
// a one-shot edge gesture hint.
// Why: brief, non-blocking discoverability — not a sticky toast.
hideTimerRef.current = window.setTimeout(() => {
hideTimerRef.current = null
setVisible(false)
@@ -132,26 +143,37 @@ export function RightSidebarEdgePeekTipHost(): JSX.Element | null {
transform: 'translateX(-100%)'
}
const openSettings = (): void => {
dismissVisibleTip()
openRightSidebarEdgePeekSetting({
openSettingsPage,
openSettingsTarget,
setSettingsSearchQuery
})
}
return (
<div
role="status"
aria-live="polite"
data-testid="right-sidebar-edge-peek-tip"
style={style}
className="pointer-events-none z-[90] w-max max-w-[240px] animate-in fade-in-0 zoom-in-95 slide-in-from-top-1 duration-150 motion-reduce:animate-none"
// Why: tip is mostly non-interactive; only the Settings link receives
// pointer events so it doesn't block titlebar clicks around the chip.
className="pointer-events-none z-[90] w-max max-w-[280px] animate-in fade-in-0 zoom-in-95 slide-in-from-top-1 duration-150 motion-reduce:animate-none"
>
<div className="relative rounded-md bg-foreground px-3 py-1.5 text-xs leading-snug text-background shadow-xs">
<div className="whitespace-nowrap">
{translate(
'auto.components.right.sidebar.edge.peek.tip.title',
'Hover the right edge to peek'
)}
</div>
<div className="whitespace-nowrap opacity-80">
{translate(
'auto.components.right.sidebar.edge.peek.tip.settings',
'Turn this off in Settings'
)}
<div>{getRightSidebarEdgePeekTipTitle()}</div>
<div className="opacity-80">
{getRightSidebarEdgePeekTipSettingsPrefix()}{' '}
<button
type="button"
data-testid="right-sidebar-edge-peek-tip-settings-link"
onClick={openSettings}
className="pointer-events-auto cursor-pointer font-medium underline underline-offset-2 hover:opacity-100"
>
{getRightSidebarEdgePeekSettingsLinkLabel()}
</button>
</div>
{/* Tooltip-style caret pointing up at the toggle icon. */}
<span
@@ -1,7 +1,11 @@
import { describe, expect, it, vi } from 'vitest'
import {
RIGHT_SIDEBAR_EDGE_PEEK_SETTING_ID,
RIGHT_SIDEBAR_EDGE_PEEK_TIP_VISIBLE_MS,
getRightSidebarEdgePeekSettingsLinkLabel,
getRightSidebarEdgePeekTipTitle,
markRightSidebarEdgePeekTipDismissed,
openRightSidebarEdgePeekSetting,
shouldShowRightSidebarEdgePeekTip
} from './right-sidebar-edge-peek-tip'
@@ -35,8 +39,38 @@ describe('markRightSidebarEdgePeekTipDismissed', () => {
})
})
describe('tip copy', () => {
it('describes the edge gesture and links into Settings', () => {
expect(getRightSidebarEdgePeekTipTitle()).toContain('right edge')
expect(getRightSidebarEdgePeekSettingsLinkLabel()).toMatch(/Settings/)
expect(getRightSidebarEdgePeekSettingsLinkLabel()).toMatch(/Appearance/)
expect(getRightSidebarEdgePeekSettingsLinkLabel()).toMatch(/Edge Peek/)
})
})
describe('openRightSidebarEdgePeekSetting', () => {
it('opens Appearance with the edge-peek section and seeds search', () => {
const openSettingsPage = vi.fn()
const openSettingsTarget = vi.fn()
const setSettingsSearchQuery = vi.fn()
openRightSidebarEdgePeekSetting({
openSettingsPage,
openSettingsTarget,
setSettingsSearchQuery
})
expect(setSettingsSearchQuery).toHaveBeenCalled()
expect(openSettingsTarget).toHaveBeenCalledWith({
pane: 'appearance',
repoId: null,
sectionId: RIGHT_SIDEBAR_EDGE_PEEK_SETTING_ID
})
expect(openSettingsPage).toHaveBeenCalled()
})
})
describe('RIGHT_SIDEBAR_EDGE_PEEK_TIP_VISIBLE_MS', () => {
it('stays brief (tooltip-length, not toast-length)', () => {
expect(RIGHT_SIDEBAR_EDGE_PEEK_TIP_VISIBLE_MS).toBe(1000)
expect(RIGHT_SIDEBAR_EDGE_PEEK_TIP_VISIBLE_MS).toBeLessThanOrEqual(3000)
expect(RIGHT_SIDEBAR_EDGE_PEEK_TIP_VISIBLE_MS).toBeGreaterThanOrEqual(1000)
})
})
@@ -1,11 +1,18 @@
import type { GlobalSettings } from '../../../../shared/types'
import { translate } from '@/i18n/i18n'
import {
getRightSidebarEdgePeekEntry,
RIGHT_SIDEBAR_EDGE_PEEK_SETTING_ID
} from '@/components/settings/appearance-sidebar-search'
import {
isRightSidebarEdgePeekEnabled,
isRightSidebarEdgePeekTipDismissed
} from './right-sidebar-edge-peek-preference'
/** How long the one-shot edge-peek hint stays visible. */
export const RIGHT_SIDEBAR_EDGE_PEEK_TIP_VISIBLE_MS = 1000
/** How long the one-shot edge-peek hint stays visible (long enough to read). */
export const RIGHT_SIDEBAR_EDGE_PEEK_TIP_VISIBLE_MS = 2500
export { RIGHT_SIDEBAR_EDGE_PEEK_SETTING_ID }
type TipSettings = Pick<
GlobalSettings,
@@ -23,3 +30,42 @@ export function markRightSidebarEdgePeekTipDismissed(args: {
() => {}
)
}
export function getRightSidebarEdgePeekTipTitle(): string {
// Why: new key so older locale catalog rows for the short "to peek" title
// don't override this longer, more specific copy.
return translate(
'auto.components.right.sidebar.edge.peek.tip.titleLong',
'Hover over the right edge of the screen to quickly show this sidebar.'
)
}
export function getRightSidebarEdgePeekTipSettingsPrefix(): string {
return translate('auto.components.right.sidebar.edge.peek.tip.settingsPrefix', 'Turn this off in')
}
/** Clickable Settings breadcrumb for the edge-peek control. */
export function getRightSidebarEdgePeekSettingsLinkLabel(): string {
return translate(
'auto.components.right.sidebar.edge.peek.tip.settingsLink',
'Settings › Appearance › {{value0}}',
{ value0: getRightSidebarEdgePeekEntry().title }
)
}
/** Opens Appearance and scrolls/highlights the edge-peek switch. */
export function openRightSidebarEdgePeekSetting(args: {
openSettingsPage: () => void
openSettingsTarget: (target: { pane: 'appearance'; repoId: null; sectionId: string }) => void
setSettingsSearchQuery: (query: string) => void
}): void {
// Why: seed search so Appearance's Advanced disclosure force-opens (the switch
// lives under Advanced and is unmounted when collapsed).
args.setSettingsSearchQuery(getRightSidebarEdgePeekEntry().title)
args.openSettingsTarget({
pane: 'appearance',
repoId: null,
sectionId: RIGHT_SIDEBAR_EDGE_PEEK_SETTING_ID
})
args.openSettingsPage()
}
@@ -23,7 +23,8 @@ import { LeftSidebarAppearanceSetting } from './LeftSidebarAppearanceSetting'
import {
getLeftSidebarAppearanceEntry,
getRightSidebarEdgePeekEntry,
getWorkspaceCardLayoutEntry
getWorkspaceCardLayoutEntry,
RIGHT_SIDEBAR_EDGE_PEEK_SETTING_ID
} from './appearance-sidebar-search'
import { isRightSidebarEdgePeekEnabled } from '../right-sidebar/right-sidebar-edge-peek-preference'
import { translate } from '@/i18n/i18n'
@@ -248,21 +249,23 @@ export function AppearanceWindowSidebarSection({
/>
</SearchableSetting>
<SearchableSetting
title={rightSidebarEdgePeekEntry.title}
description={rightSidebarEdgePeekEntry.description}
keywords={rightSidebarEdgePeekEntry.keywords}
>
<SettingsSwitchRow
label={rightSidebarEdgePeekEntry.title}
<div data-settings-section={RIGHT_SIDEBAR_EDGE_PEEK_SETTING_ID}>
<SearchableSetting
title={rightSidebarEdgePeekEntry.title}
description={rightSidebarEdgePeekEntry.description}
checked={edgePeekEnabled}
onChange={() =>
updateSettings({ rightSidebarEdgePeekEnabled: !edgePeekEnabled })
}
ariaLabel={rightSidebarEdgePeekEntry.title}
/>
</SearchableSetting>
keywords={rightSidebarEdgePeekEntry.keywords}
>
<SettingsSwitchRow
label={rightSidebarEdgePeekEntry.title}
description={rightSidebarEdgePeekEntry.description}
checked={edgePeekEnabled}
onChange={() =>
updateSettings({ rightSidebarEdgePeekEnabled: !edgePeekEnabled })
}
ariaLabel={rightSidebarEdgePeekEntry.title}
/>
</SearchableSetting>
</div>
<SearchableSetting
title={translate(
@@ -79,6 +79,9 @@ export const getWorkspaceCardLayoutEntry = createLocalizedCatalog(
})
)
/** Stable Settings deep-link / scroll target for the edge-peek switch. */
export const RIGHT_SIDEBAR_EDGE_PEEK_SETTING_ID = 'right-sidebar-edge-peek'
export const getRightSidebarEdgePeekEntry = createLocalizedCatalog(
(): SettingsSearchEntry => ({
title: translate(
@@ -1,4 +1,5 @@
import { describe, expect, it } from 'vitest'
import { RIGHT_SIDEBAR_EDGE_PEEK_SETTING_ID } from './appearance-sidebar-search'
import {
resolveAppearanceAccordionDeepLink,
USAGE_PERCENTAGE_DISPLAY_SETTING_ID
@@ -9,6 +10,10 @@ describe('resolveAppearanceAccordionDeepLink', () => {
expect(resolveAppearanceAccordionDeepLink(USAGE_PERCENTAGE_DISPLAY_SETTING_ID)).toBe('window')
})
it('maps the right-sidebar edge peek row to the Window accordion', () => {
expect(resolveAppearanceAccordionDeepLink(RIGHT_SIDEBAR_EDGE_PEEK_SETTING_ID)).toBe('window')
})
it('returns null for unknown or missing section ids', () => {
expect(resolveAppearanceAccordionDeepLink(undefined)).toBeNull()
expect(resolveAppearanceAccordionDeepLink('something-else')).toBeNull()
@@ -1,6 +1,7 @@
import { createLocalizedCatalog } from '@/i18n/localized-catalog'
import { translate } from '@/i18n/i18n'
import { translateSearchKeyword } from './settings-search-keywords'
import { RIGHT_SIDEBAR_EDGE_PEEK_SETTING_ID } from './appearance-sidebar-search'
/** Stable Settings deep-link / scroll target for the Used/Remaining control. */
export const USAGE_PERCENTAGE_DISPLAY_SETTING_ID = 'usage-percentage-display'
@@ -15,7 +16,10 @@ export type AppearanceAccordionSection = 'interface' | 'terminal' | 'window'
export function resolveAppearanceAccordionDeepLink(
sectionId: string | undefined
): AppearanceAccordionSection | null {
if (sectionId === USAGE_PERCENTAGE_DISPLAY_SETTING_ID) {
if (
sectionId === USAGE_PERCENTAGE_DISPLAY_SETTING_ID ||
sectionId === RIGHT_SIDEBAR_EDGE_PEEK_SETTING_ID
) {
return 'window'
}
return null