Add settings deep-link to expand Appearance's Window accordion for one-s

- Replaces the searchQuery-based redirect (fragile, flashed filter UI) with a
  dedicated appearanceAccordionDeepLink store field that force-opens the
  correct accordion and scrolls to the target row
- Rebuilds the status-bar change notice as a plain elevated card instead of
  a Popover, since PopoverContent's glass/backdrop-filter defaults fought
  the opaque callout styling and needed heavy overrides
- Simplifies the light/dark card CSS tokens accordingly
This commit is contained in:
Jinjing
2026-07-11 14:30:55 -07:00
parent 82512983d4
commit 6b6844d8b2
9 changed files with 223 additions and 190 deletions
+45 -61
View File
@@ -1209,38 +1209,52 @@
background: color-mix(in srgb, var(--worktree-sidebar-foreground) 14%, var(--worktree-sidebar));
}
/* Why: status-bar usage-display change callout sits over same-luminance
terminal/titlebar chrome. Light and dark use separate opaque palettes (not
translucent glass) so the surface behind cannot bleed through. */
.status-bar-change-notice-popover {
/* Why: one-shot status-bar callout sits over same-luminance titlebar/terminal
chrome. Opaque elevated surfaces (not glass) so the surface cannot bleed
through; light and dark use separate token mixes. */
.status-bar-change-notice-card {
position: relative;
overflow: visible;
z-index: 60;
/* Why: kill default PopoverContent glass; blur + alpha re-darkens the fill. */
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
border: 1px solid color-mix(in srgb, var(--foreground) 16%, var(--border));
background: var(--card);
color: var(--card-foreground);
box-shadow:
0 0 0 1px color-mix(in srgb, var(--foreground) 6%, transparent),
0 14px 36px color-mix(in srgb, var(--foreground) 14%, transparent),
0 2px 8px color-mix(in srgb, var(--foreground) 8%, transparent);
}
/* ── Light mode ────────────────────────────────────────
Paper card over light chrome (#fff / #f5f5f5). White top + soft cool gray
bottom so it lifts off the titlebar without looking like a hole. */
:root:not(.dark) .status-bar-change-notice-popover,
:root:not(.dark) .status-bar-change-notice-popover[data-slot='popover-content'] {
background: linear-gradient(165deg, #ffffff 0%, #f7f7f8 52%, #eef0f3 100%) !important;
border-color: #c8cdd6 !important;
color: #0a0a0a !important;
/* Light: pure paper above light chrome — lift with a cool edge, not gray fill. */
:root:not(.dark) .status-bar-change-notice-card {
background: #ffffff;
border-color: #c4c9d2;
color: #0a0a0a;
box-shadow:
0 0 0 1px rgb(15 23 42 / 0.06),
0 18px 40px rgb(15 23 42 / 0.14),
0 4px 12px rgb(15 23 42 / 0.08),
inset 0 1px 0 #ffffff !important;
0 16px 40px rgb(15 23 42 / 0.16),
0 3px 10px rgb(15 23 42 / 0.08);
}
:root:not(.dark) .status-bar-change-notice-popover .text-muted-foreground {
color: #52525b !important;
:root:not(.dark) .status-bar-change-notice-card .text-muted-foreground {
color: #52525b;
}
:root:not(.dark) .status-bar-change-notice-popover::after {
/* Dark: lifted charcoal above #0a0a0a / #171717 — not translucent glass. */
.dark .status-bar-change-notice-card {
background: #2a2a2e;
border-color: color-mix(in srgb, #ffffff 26%, transparent);
color: #fafafa;
box-shadow:
0 0 0 1px color-mix(in srgb, #ffffff 12%, transparent),
0 18px 44px rgb(0 0 0 / 0.62),
0 3px 12px rgb(0 0 0 / 0.4);
}
.dark .status-bar-change-notice-card .text-muted-foreground {
color: color-mix(in srgb, #fafafa 68%, transparent);
}
/* Why: caret points down at the usage meters this notice explains. */
.status-bar-change-notice-card::after {
content: '';
position: absolute;
left: 1.25rem;
@@ -1248,47 +1262,17 @@
width: 12px;
height: 12px;
transform: rotate(45deg);
border-right: 1px solid #c8cdd6;
border-bottom: 1px solid #c8cdd6;
background: #eef0f3;
border-right: 1px solid #c4c9d2;
border-bottom: 1px solid #c4c9d2;
background: #ffffff;
box-shadow: 2px 2px 5px rgb(15 23 42 / 0.1);
}
/* ── Dark mode ─────────────────────────────────────────
Lifted charcoal over near-black terminal/titlebar (#0a0a0a / #171717). */
.dark .status-bar-change-notice-popover,
.dark .status-bar-change-notice-popover[data-slot='popover-content'] {
background: linear-gradient(
165deg,
color-mix(in srgb, #ffffff 18%, #2a2a2e) 0%,
#252528 48%,
#1a1a1d 100%
) !important;
border-color: color-mix(in srgb, #ffffff 28%, transparent) !important;
color: #fafafa !important;
box-shadow:
0 0 0 1px color-mix(in srgb, #ffffff 14%, transparent),
0 20px 48px rgb(0 0 0 / 0.65),
0 4px 14px rgb(0 0 0 / 0.45),
inset 0 1px 0 color-mix(in srgb, #ffffff 16%, transparent) !important;
}
.dark .status-bar-change-notice-popover .text-muted-foreground {
color: color-mix(in srgb, #fafafa 68%, transparent) !important;
}
.dark .status-bar-change-notice-popover::after {
content: '';
position: absolute;
left: 1.25rem;
bottom: -6px;
width: 12px;
height: 12px;
transform: rotate(45deg);
border-right: 1px solid color-mix(in srgb, #ffffff 28%, transparent);
border-bottom: 1px solid color-mix(in srgb, #ffffff 28%, transparent);
background: #1a1a1d;
box-shadow: 2px 2px 8px rgb(0 0 0 / 0.5);
.dark .status-bar-change-notice-card::after {
border-right-color: color-mix(in srgb, #ffffff 26%, transparent);
border-bottom-color: color-mix(in srgb, #ffffff 26%, transparent);
background: #2a2a2e;
box-shadow: 2px 2px 8px rgb(0 0 0 / 0.45);
}
/* Why: the first-run Mobile Emulator intro sits directly under that menu row;
@@ -23,7 +23,9 @@ const mocks = vi.hoisted(() => ({
usagePercentageDisplay: 'used' as 'used' | 'remaining',
setUsagePercentageDisplay: vi.fn(),
recordFeatureInteraction: vi.fn(),
setWorktreeCardMode: vi.fn()
setWorktreeCardMode: vi.fn(),
appearanceAccordionDeepLink: null as 'interface' | 'terminal' | 'window' | null,
clearAppearanceAccordionDeepLink: vi.fn()
}
}))
@@ -1,5 +1,5 @@
import type React from 'react'
import { useState } from 'react'
import { useLayoutEffect, useState } from 'react'
import { AppWindow, PanelLeft, TerminalSquare } from 'lucide-react'
import type { GlobalSettings } from '../../../../shared/types'
@@ -10,6 +10,7 @@ import { AppearanceWindowSidebarSection } from './AppearanceWindowSidebarSection
import { SearchableSetting } from './SearchableSetting'
import { matchesSettingsSearch, normalizeSettingsSearchQuery } from './settings-search'
import { useAppStore } from '../../store'
import { USAGE_PERCENTAGE_DISPLAY_SETTING_ID } from './appearance-usage-percentage-search'
import {
getAppIconEntries,
getAppearancePaneSearchEntries,
@@ -75,6 +76,10 @@ export function AppearancePane({
warpThemes
}: AppearancePaneProps): React.JSX.Element {
const searchQuery = useAppStore((state) => state.settingsSearchQuery)
const appearanceAccordionDeepLink = useAppStore((state) => state.appearanceAccordionDeepLink)
const clearAppearanceAccordionDeepLink = useAppStore(
(state) => state.clearAppearanceAccordionDeepLink
)
const isSearching = normalizeSettingsSearchQuery(searchQuery).length > 0
const isWebClient = isWebClientLocation()
// Why: the system tray behavior is desktop-Electron Windows-only; a Windows
@@ -84,6 +89,26 @@ export function AppearancePane({
const [manuallyOpenSection, setManuallyOpenSection] = useState<AppearanceSectionKey | null>(
'interface'
)
// Why: nested deep links (e.g. Usage percentages) land under Window & Sidebar;
// expand that accordion before Settings scrolls so the row is actually visible.
useLayoutEffect(() => {
if (!appearanceAccordionDeepLink) {
return
}
setManuallyOpenSection(appearanceAccordionDeepLink)
clearAppearanceAccordionDeepLink()
// Why: accordion expand is layout-synchronous; scroll on the next frame so
// the target has non-zero height when Settings (or this fallback) scrolls.
const frameId = requestAnimationFrame(() => {
document
.getElementById(USAGE_PERCENTAGE_DISPLAY_SETTING_ID)
?.scrollIntoView({ block: 'nearest' })
})
return () => {
cancelAnimationFrame(frameId)
}
}, [appearanceAccordionDeepLink, clearAppearanceAccordionDeepLink])
const interfaceTitle = translate(
'auto.components.settings.AppearancePane.interfaceTitle',
'Interface'
@@ -65,6 +65,7 @@ import { SettingsSidebar } from './SettingsSidebar'
import { SettingsSetupGuidePane } from './SettingsSetupGuidePane'
import { ActiveSettingsSectionProvider, SettingsSection } from './SettingsSection'
import { getSettingsSectionSearchEntries, rankSettingsSearchItems } from './settings-search'
import { resolveAppearanceAccordionDeepLink } from './appearance-usage-percentage-search'
import { cn } from '@/lib/utils'
import { isIntentionalAppRestartInProgress } from '@/lib/updater-beforeunload'
import { registerWindowCloseGuard } from '../window-close-request-coordinator'
@@ -618,6 +619,14 @@ function Settings(): React.JSX.Element {
)
pendingNavSectionRef.current = paneSectionId
pendingScrollTargetRef.current = settingsNavigationTarget.sectionId ?? paneSectionId
// Why: Appearance nests status-bar controls under a collapsed accordion;
// force that accordion open before scrolling so the row is actually visible.
if (settingsNavigationTarget.pane === 'appearance') {
const accordion = resolveAppearanceAccordionDeepLink(settingsNavigationTarget.sectionId)
if (accordion) {
useAppStore.getState().setAppearanceAccordionDeepLink(accordion)
}
}
if (settingsNavigationTarget.intent === 'add-quick-command') {
setQuickCommandAddIntentSignal((signal) => signal + 1)
}
@@ -0,0 +1,16 @@
import { describe, expect, it } from 'vitest'
import {
resolveAppearanceAccordionDeepLink,
USAGE_PERCENTAGE_DISPLAY_SETTING_ID
} from './appearance-usage-percentage-search'
describe('resolveAppearanceAccordionDeepLink', () => {
it('maps the usage percentage row to the Window accordion', () => {
expect(resolveAppearanceAccordionDeepLink(USAGE_PERCENTAGE_DISPLAY_SETTING_ID)).toBe('window')
})
it('returns null for unknown or missing section ids', () => {
expect(resolveAppearanceAccordionDeepLink(undefined)).toBeNull()
expect(resolveAppearanceAccordionDeepLink('something-else')).toBeNull()
})
})
@@ -5,6 +5,22 @@ import { translateSearchKeyword } from './settings-search-keywords'
/** Stable Settings deep-link / scroll target for the Used/Remaining control. */
export const USAGE_PERCENTAGE_DISPLAY_SETTING_ID = 'usage-percentage-display'
/** Appearance accordion keys that can be force-opened for nested deep links. */
export type AppearanceAccordionSection = 'interface' | 'terminal' | 'window'
/**
* Map a Settings subsection id to the Appearance accordion that must be open
* before the row is visible (collapsed accordion content is not user-visible).
*/
export function resolveAppearanceAccordionDeepLink(
sectionId: string | undefined
): AppearanceAccordionSection | null {
if (sectionId === USAGE_PERCENTAGE_DISPLAY_SETTING_ID) {
return 'window'
}
return null
}
export const getUsagePercentageDisplayEntry = createLocalizedCatalog(() => ({
title: translate(
'auto.components.settings.appearance.search.usagePercentageDisplayTitle',
@@ -1,9 +1,10 @@
// @vitest-environment happy-dom
import { act, createContext, useContext, type ReactNode } from 'react'
import { act } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { UsagePercentageDisplayChangeNotice } from './UsagePercentageDisplayChangeNotice'
import { USAGE_PERCENTAGE_DISPLAY_SETTING_ID } from '../settings/appearance-usage-percentage-search'
const storeState = {
persistedUIReady: true,
@@ -11,7 +12,6 @@ const storeState = {
dismissUsagePercentageDisplayChangeNotice: vi.fn(),
statusBarVisible: true,
activeModal: 'none' as string,
setSettingsSearchQuery: vi.fn(),
openSettingsTarget: vi.fn(),
openSettingsPage: vi.fn()
}
@@ -25,29 +25,6 @@ vi.mock('@/store', () => ({
)
}))
vi.mock('@/components/ui/popover', () => {
const OpenContext = createContext(false)
return {
Popover: ({ open, children }: { open?: boolean; children: ReactNode }) => (
<OpenContext.Provider value={open === true}>
<div data-testid="popover" data-open={open ? 'true' : 'false'}>
{children}
</div>
</OpenContext.Provider>
),
PopoverAnchor: ({ children }: { children: ReactNode }) => (
<div data-testid="popover-anchor">{children}</div>
),
PopoverContent: ({ children }: { children: ReactNode }) => {
const open = useContext(OpenContext)
if (!open) {
return null
}
return <div data-testid="popover-content">{children}</div>
}
}
})
describe('UsagePercentageDisplayChangeNotice', () => {
let container: HTMLDivElement
let root: Root
@@ -59,6 +36,8 @@ describe('UsagePercentageDisplayChangeNotice', () => {
storeState.statusBarVisible = true
storeState.activeModal = 'none'
storeState.dismissUsagePercentageDisplayChangeNotice = vi.fn()
storeState.openSettingsPage = vi.fn()
storeState.openSettingsTarget = vi.fn()
container = document.createElement('div')
document.body.appendChild(container)
root = createRoot(container)
@@ -72,7 +51,7 @@ describe('UsagePercentageDisplayChangeNotice', () => {
vi.useRealTimers()
})
it('opens the callout next to usage meters after a short delay when eligible', () => {
it('shows the callout above usage meters after a short delay when eligible', () => {
act(() => {
root.render(
<UsagePercentageDisplayChangeNotice hasVisibleUsageMeters>
@@ -81,11 +60,11 @@ describe('UsagePercentageDisplayChangeNotice', () => {
)
})
expect(container.querySelector('[data-open="true"]')).toBeNull()
expect(container.querySelector('.status-bar-change-notice-card')).toBeNull()
act(() => {
vi.advanceTimersByTime(1_800)
})
expect(container.querySelector('[data-open="true"]')).not.toBeNull()
expect(container.querySelector('.status-bar-change-notice-card')).not.toBeNull()
expect(container.textContent).toContain('usage-meters')
expect(container.textContent).toContain('Usage now shows % used')
expect(container.textContent).toContain('Prefer remaining? Change it in Settings.')
@@ -102,8 +81,7 @@ describe('UsagePercentageDisplayChangeNotice', () => {
act(() => {
vi.advanceTimersByTime(2_000)
})
expect(container.querySelector('[data-open="true"]')).toBeNull()
expect(container.textContent).not.toContain('Usage now shows % used')
expect(container.querySelector('.status-bar-change-notice-card')).toBeNull()
})
it('does not open when the notice was already dismissed', () => {
@@ -118,8 +96,7 @@ describe('UsagePercentageDisplayChangeNotice', () => {
act(() => {
vi.advanceTimersByTime(2_000)
})
expect(container.querySelector('[data-open="true"]')).toBeNull()
expect(container.textContent).not.toContain('Usage now shows % used')
expect(container.querySelector('.status-bar-change-notice-card')).toBeNull()
})
it('does not open while another modal is open', () => {
@@ -134,10 +111,10 @@ describe('UsagePercentageDisplayChangeNotice', () => {
act(() => {
vi.advanceTimersByTime(2_000)
})
expect(container.querySelector('[data-open="true"]')).toBeNull()
expect(container.querySelector('.status-bar-change-notice-card')).toBeNull()
})
it('opens Appearance with a Usage percentages filter after wiping prior search', () => {
it('deep-links to the Usage percentages setting without a search filter', () => {
const callOrder: string[] = []
storeState.openSettingsPage = vi.fn(() => {
callOrder.push('openSettingsPage')
@@ -145,9 +122,6 @@ describe('UsagePercentageDisplayChangeNotice', () => {
storeState.openSettingsTarget = vi.fn(() => {
callOrder.push('openSettingsTarget')
})
storeState.setSettingsSearchQuery = vi.fn(() => {
callOrder.push('setSettingsSearchQuery')
})
storeState.dismissUsagePercentageDisplayChangeNotice = vi.fn()
act(() => {
@@ -169,13 +143,12 @@ describe('UsagePercentageDisplayChangeNotice', () => {
openSettingsButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
})
// Why: openSettingsPage clears search first; the filter must be applied after.
expect(callOrder).toEqual(['openSettingsPage', 'openSettingsTarget', 'setSettingsSearchQuery'])
expect(callOrder).toEqual(['openSettingsPage', 'openSettingsTarget'])
expect(storeState.openSettingsTarget).toHaveBeenCalledWith({
pane: 'appearance',
repoId: null
repoId: null,
sectionId: USAGE_PERCENTAGE_DISPLAY_SETTING_ID
})
expect(storeState.setSettingsSearchQuery).toHaveBeenCalledWith('Usage percentages')
expect(storeState.dismissUsagePercentageDisplayChangeNotice).toHaveBeenCalled()
})
})
@@ -1,32 +1,29 @@
import { useEffect, useState, type ReactNode } from 'react'
import { BarChart3, X } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Popover, PopoverAnchor, PopoverContent } from '@/components/ui/popover'
import { translate } from '@/i18n/i18n'
import { useAppStore } from '@/store'
import { shouldShowUsagePercentageDisplayChangeNotice } from '../../../../shared/usage-percentage-display-change-notice'
import { USAGE_PERCENTAGE_DISPLAY_SETTING_ID } from '../settings/appearance-usage-percentage-search'
// Why: let startup modals settle before the status-bar callout competes for focus.
const SHOW_DELAY_MS = 1_800
function openUsagePercentageSettings(): void {
const store = useAppStore.getState()
// Why: openSettingsPage always wipes the search filter first — set the
// Appearance target and filter after that wipe. The search string force-opens
// Appearance → Window & Sidebar and surfaces the Usage percentages control.
// Why: openSettingsPage wipes any leftover search; do not re-apply a search
// filter — deep-link to the stable row id and let Appearance expand Window.
store.openSettingsPage()
store.openSettingsTarget({ pane: 'appearance', repoId: null })
store.setSettingsSearchQuery(
translate(
'auto.components.settings.appearance.search.usagePercentageDisplayTitle',
'Usage percentages'
)
)
store.openSettingsTarget({
pane: 'appearance',
repoId: null,
sectionId: USAGE_PERCENTAGE_DISPLAY_SETTING_ID
})
}
/**
* Anchors a one-time high-contrast callout above the status-bar usage meters
* after the default flipped from remaining → used. Permanent dismiss only.
* One-time elevated callout anchored above the status-bar usage meters after
* the default flipped from remaining → used. Permanent dismiss only.
*/
export function UsagePercentageDisplayChangeNotice({
children,
@@ -67,89 +64,87 @@ export function UsagePercentageDisplayChangeNotice({
const open = eligible && delayElapsed
useEffect(() => {
if (!open) {
return
}
const onKeyDown = (event: KeyboardEvent): void => {
if (event.key === 'Escape') {
event.preventDefault()
dismiss()
}
}
window.addEventListener('keydown', onKeyDown)
return () => {
window.removeEventListener('keydown', onKeyDown)
}
}, [dismiss, open])
const openSettings = (): void => {
dismiss()
openUsagePercentageSettings()
}
return (
<Popover open={open}>
<PopoverAnchor asChild>
<div className="flex items-center gap-3">{children}</div>
</PopoverAnchor>
<PopoverContent
side="top"
align="start"
sideOffset={12}
// Why: permanent dismiss is intentional only via Got it / X / Settings —
// outside click must not silently clear a one-shot education surface.
onInteractOutside={(event) => {
event.preventDefault()
}}
onOpenAutoFocus={(event) => {
// Why: keep focus in the terminal/workspace; this is a soft callout,
// not a modal that should steal keyboard context.
event.preventDefault()
}}
onEscapeKeyDown={(event) => {
event.preventDefault()
dismiss()
}}
// Why: reuse elevated glass popover chrome (border + blur + shadow) and
// layer status-bar-change-notice so the caret + contrast boost still apply.
className="status-bar-change-notice-popover w-[340px] max-w-[calc(100vw-24px)] overflow-visible p-3.5"
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 space-y-1.5">
<div className="flex items-center gap-2">
<span
className="flex size-6 shrink-0 items-center justify-center rounded-full border border-border bg-secondary text-foreground"
aria-hidden="true"
>
<BarChart3 className="size-3.5" />
</span>
<div className="text-sm font-semibold leading-snug">
{translate(
'auto.components.status.bar.UsagePercentageDisplayChangeNotice.title',
'Usage now shows % used'
)}
<div className="relative flex items-center gap-3">
{children}
{open ? (
<div
role="status"
className="status-bar-change-notice-card absolute bottom-full left-0 z-50 mb-2.5 w-[320px] max-w-[calc(100vw-24px)] rounded-lg p-3.5"
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 space-y-1.5">
<div className="flex items-center gap-2">
<span
className="flex size-6 shrink-0 items-center justify-center rounded-full border border-border bg-secondary text-foreground"
aria-hidden="true"
>
<BarChart3 className="size-3.5" />
</span>
<div className="text-sm font-semibold leading-snug">
{translate(
'auto.components.status.bar.UsagePercentageDisplayChangeNotice.title',
'Usage now shows % used'
)}
</div>
</div>
<p className="text-sm leading-5 text-muted-foreground">
{translate(
'auto.components.status.bar.UsagePercentageDisplayChangeNotice.body',
'Prefer remaining? Change it in Settings.'
)}
</p>
</div>
<p className="text-sm leading-5 text-muted-foreground">
{translate(
'auto.components.status.bar.UsagePercentageDisplayChangeNotice.body',
'Prefer remaining? Change it in Settings.'
<Button
variant="ghost"
size="icon"
className="size-7 shrink-0"
onClick={dismiss}
aria-label={translate(
'auto.components.status.bar.UsagePercentageDisplayChangeNotice.dismiss',
'Dismiss'
)}
</p>
>
<X className="size-3.5" />
</Button>
</div>
<div className="mt-3 flex gap-2">
<Button variant="default" size="sm" className="min-w-0 flex-1" onClick={openSettings}>
{translate(
'auto.components.status.bar.UsagePercentageDisplayChangeNotice.openSettings',
'Open Settings'
)}
</Button>
<Button variant="secondary" size="sm" className="w-[84px]" onClick={dismiss}>
{translate(
'auto.components.status.bar.UsagePercentageDisplayChangeNotice.gotIt',
'Got it'
)}
</Button>
</div>
<Button
variant="ghost"
size="icon"
className="size-7 shrink-0"
onClick={dismiss}
aria-label={translate(
'auto.components.status.bar.UsagePercentageDisplayChangeNotice.dismiss',
'Dismiss'
)}
>
<X className="size-3.5" />
</Button>
</div>
<div className="mt-3 flex gap-2">
<Button variant="default" size="sm" className="min-w-0 flex-1" onClick={openSettings}>
{translate(
'auto.components.status.bar.UsagePercentageDisplayChangeNotice.openSettings',
'Open Settings'
)}
</Button>
<Button variant="secondary" size="sm" className="w-[84px]" onClick={dismiss}>
{translate(
'auto.components.status.bar.UsagePercentageDisplayChangeNotice.gotIt',
'Got it'
)}
</Button>
</div>
</PopoverContent>
</Popover>
) : null}
</div>
)
}
+13
View File
@@ -747,6 +747,16 @@ export type UISlice = {
} | null
openSettingsTarget: (target: NonNullable<UISlice['settingsNavigationTarget']>) => void
clearSettingsTarget: () => void
/**
* One-shot Appearance accordion to expand for nested Settings deep links
* (e.g. Usage percentages lives under Window & Sidebar). Cleared when
* Appearance consumes it.
*/
appearanceAccordionDeepLink: 'interface' | 'terminal' | 'window' | null
setAppearanceAccordionDeepLink: (
section: NonNullable<UISlice['appearanceAccordionDeepLink']>
) => void
clearAppearanceAccordionDeepLink: () => void
activeModal:
| 'none'
| 'create-worktree'
@@ -1499,6 +1509,9 @@ export const createUISlice: StateCreator<AppState, [], [], UISlice> = (set, get)
settingsNavigationTarget: null,
openSettingsTarget: (target) => set({ settingsNavigationTarget: target }),
clearSettingsTarget: () => set({ settingsNavigationTarget: null }),
appearanceAccordionDeepLink: null,
setAppearanceAccordionDeepLink: (section) => set({ appearanceAccordionDeepLink: section }),
clearAppearanceAccordionDeepLink: () => set({ appearanceAccordionDeepLink: null }),
activeModal: 'none',
modalData: {},