diff --git a/src/renderer/src/components/right-sidebar/right-sidebar-edge-peek-tip-host.test.tsx b/src/renderer/src/components/right-sidebar/right-sidebar-edge-peek-tip-host.test.tsx index d614857d199..f0fb3937685 100644 --- a/src/renderer/src/components/right-sidebar/right-sidebar-edge-peek-tip-host.test.tsx +++ b/src/renderer/src/components/right-sidebar/right-sidebar-edge-peek-tip-host.test.tsx @@ -68,8 +68,10 @@ describe('RightSidebarEdgePeekTipHost', () => { rerender() 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', () => { diff --git a/src/renderer/src/components/right-sidebar/right-sidebar-edge-peek-tip-host.tsx b/src/renderer/src/components/right-sidebar/right-sidebar-edge-peek-tip-host.tsx index 266e3b45a55..694f77bfa50 100644 --- a/src/renderer/src/components/right-sidebar/right-sidebar-edge-peek-tip-host.tsx +++ b/src/renderer/src/components/right-sidebar/right-sidebar-edge-peek-tip-host.tsx @@ -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(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 (
-
- {translate( - 'auto.components.right.sidebar.edge.peek.tip.title', - 'Hover the right edge to peek' - )} -
-
- {translate( - 'auto.components.right.sidebar.edge.peek.tip.settings', - 'Turn this off in Settings' - )} +
{getRightSidebarEdgePeekTipTitle()}
+
+ {getRightSidebarEdgePeekTipSettingsPrefix()}{' '} +
{/* Tooltip-style caret pointing up at the toggle icon. */} { }) }) +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) }) }) diff --git a/src/renderer/src/components/right-sidebar/right-sidebar-edge-peek-tip.ts b/src/renderer/src/components/right-sidebar/right-sidebar-edge-peek-tip.ts index d3bcc42f734..070ad6df51d 100644 --- a/src/renderer/src/components/right-sidebar/right-sidebar-edge-peek-tip.ts +++ b/src/renderer/src/components/right-sidebar/right-sidebar-edge-peek-tip.ts @@ -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() +} diff --git a/src/renderer/src/components/settings/AppearanceWindowSidebarSection.tsx b/src/renderer/src/components/settings/AppearanceWindowSidebarSection.tsx index c893d930bf0..6402322e93b 100644 --- a/src/renderer/src/components/settings/AppearanceWindowSidebarSection.tsx +++ b/src/renderer/src/components/settings/AppearanceWindowSidebarSection.tsx @@ -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({ /> - - + - updateSettings({ rightSidebarEdgePeekEnabled: !edgePeekEnabled }) - } - ariaLabel={rightSidebarEdgePeekEntry.title} - /> - + keywords={rightSidebarEdgePeekEntry.keywords} + > + + updateSettings({ rightSidebarEdgePeekEnabled: !edgePeekEnabled }) + } + ariaLabel={rightSidebarEdgePeekEntry.title} + /> + +
({ title: translate( diff --git a/src/renderer/src/components/settings/appearance-usage-percentage-search.test.ts b/src/renderer/src/components/settings/appearance-usage-percentage-search.test.ts index fdb30dd8d80..4aee8f7e64e 100644 --- a/src/renderer/src/components/settings/appearance-usage-percentage-search.test.ts +++ b/src/renderer/src/components/settings/appearance-usage-percentage-search.test.ts @@ -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() diff --git a/src/renderer/src/components/settings/appearance-usage-percentage-search.ts b/src/renderer/src/components/settings/appearance-usage-percentage-search.ts index b7bbc49218b..5326eef3b6e 100644 --- a/src/renderer/src/components/settings/appearance-usage-percentage-search.ts +++ b/src/renderer/src/components/settings/appearance-usage-percentage-search.ts @@ -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