mirror of
https://github.com/stablyai/orca.git
synced 2026-10-19 00:03:20 +00:00
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user