Prevent Settings zoom controls from drifting

Settings and keyboard zoom controls were able to diverge because the slider only committed on release and captured pointer events around the native range control. The slider now commits through the shared zoom store as it changes, while the range keeps its native drag behavior.

The full desktop gate also enforces Safari 15 compatibility for startup CSS, so the remaining memory theme color-mix tokens were replaced with static equivalents.

Constraint: macOS 12 Safari 15 WebView cannot rely on CSS color-mix in startup-critical desktop CSS
Rejected: Commit Settings zoom only on pointer release | leaves shortcut state and slider state temporarily out of sync
Confidence: high
Scope-risk: narrow
Directive: Keep Settings slider, reset, and keyboard shortcuts routed through the shared uiZoom/appZoom state
Tested: bun run check:desktop
Tested: agent-browser Settings zoom smoke against local Vite plus server
Not-tested: Packaged Tauri app manual drag on macOS hardware
This commit is contained in:
程序员阿江(Relakkes) 2026-05-15 19:36:14 +08:00
parent 4e969475f2
commit c279643cce
3 changed files with 42 additions and 24 deletions

View File

@ -268,7 +268,7 @@ describe('Settings > General tab', () => {
expect((uiZoomHeading.compareDocumentPosition(webFetchHeading) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0).toBe(true)
})
it('previews UI zoom while dragging and applies it only when the user releases the slider', async () => {
it('applies UI zoom while dragging so the slider and app zoom stay synchronized', async () => {
render(<Settings />)
fireEvent.click(screen.getByText('General'))
@ -287,7 +287,9 @@ describe('Settings > General tab', () => {
})
expect(screen.getAllByText('125%')).toHaveLength(2)
expect(useSettingsStore.getState().setUiZoom).not.toHaveBeenCalledWith(1.25)
expect(useSettingsStore.getState().setUiZoom).toHaveBeenCalledWith(1.25)
expect(useSettingsStore.getState().uiZoom).toBe(1.25)
expect(slider).toHaveValue('1.25')
expect(slider).toHaveClass('settings-zoom-range')
expect(slider.closest('.settings-zoom-control')).toHaveClass('is-dragging')
expect(slider.closest('.settings-zoom-control')).toHaveStyle({ '--settings-zoom-range-progress': '50%' })
@ -297,6 +299,7 @@ describe('Settings > General tab', () => {
})
expect(useSettingsStore.getState().setUiZoom).toHaveBeenCalledWith(1.25)
expect(slider.closest('.settings-zoom-control')).not.toHaveClass('is-dragging')
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: 'Reset UI zoom to 100%' }))
@ -305,6 +308,22 @@ describe('Settings > General tab', () => {
expect(useSettingsStore.getState().setUiZoom).toHaveBeenLastCalledWith(1)
})
it('updates the UI zoom slider when shortcut zoom changes the shared setting while Settings is open', async () => {
render(<Settings />)
fireEvent.click(screen.getByText('General'))
const slider = screen.getByLabelText('UI Zoom')
await act(async () => {
useSettingsStore.setState({ uiZoom: 1.1 })
})
expect(slider).toHaveValue('1.1')
expect(screen.getAllByText('110%')).toHaveLength(2)
expect(slider.closest('.settings-zoom-control')).toHaveStyle({ '--settings-zoom-range-progress': '40%' })
})
it('opens the Token usage tab from Settings navigation above Diagnostics', () => {
render(<Settings />)

View File

@ -1396,7 +1396,7 @@ function GeneralSettings() {
const [isUiZoomDragging, setIsUiZoomDragging] = useState(false)
const webSearchDirty = JSON.stringify(webSearchDraft) !== JSON.stringify(webSearch)
const uiZoomPercent = Math.round(uiZoomDraft * 100)
const uiZoomRangeProgress = `${((uiZoomDraft - UI_ZOOM_MIN) / (UI_ZOOM_MAX - UI_ZOOM_MIN)) * 100}%`
const uiZoomRangeProgress = `${Math.round(((uiZoomDraft - UI_ZOOM_MIN) / (UI_ZOOM_MAX - UI_ZOOM_MIN)) * 1000) / 10}%`
useEffect(() => {
setWebSearchDraft(webSearch)
@ -1523,9 +1523,11 @@ function GeneralSettings() {
}
}
const commitUiZoom = (value: number) => {
const commitUiZoom = (value: number, options: { keepDragging?: boolean } = {}) => {
const nextZoom = Number.isFinite(value) ? value : UI_ZOOM_DEFAULT
setIsUiZoomDragging(false)
if (!options.keepDragging) {
setIsUiZoomDragging(false)
}
setUiZoomDraft(nextZoom)
setUiZoom(nextZoom)
}
@ -1599,21 +1601,17 @@ function GeneralSettings() {
max={UI_ZOOM_MAX}
step={UI_ZOOM_STEP}
value={uiZoomDraft}
onPointerDown={(e) => {
onPointerDown={() => {
setIsUiZoomDragging(true)
e.currentTarget.setPointerCapture?.(e.pointerId)
}}
onPointerUp={(e) => commitUiZoom(e.currentTarget.valueAsNumber)}
onPointerCancel={(e) => commitUiZoom(e.currentTarget.valueAsNumber)}
onChange={(e) => setUiZoomDraft(e.currentTarget.valueAsNumber)}
onKeyUp={(e) => {
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End', 'PageUp', 'PageDown'].includes(e.key)) {
commitUiZoom(e.currentTarget.valueAsNumber)
}
}}
onPointerUp={() => setIsUiZoomDragging(false)}
onPointerCancel={() => setIsUiZoomDragging(false)}
onChange={(e) => commitUiZoom(e.currentTarget.valueAsNumber, { keepDragging: isUiZoomDragging })}
onBlur={(e) => {
if (uiZoomDraft !== uiZoom) {
commitUiZoom(e.currentTarget.valueAsNumber)
} else {
setIsUiZoomDragging(false)
}
}}
className="settings-zoom-range w-full"

View File

@ -199,6 +199,7 @@
cursor: pointer;
background: transparent;
accent-color: var(--color-brand);
touch-action: none;
}
.settings-zoom-range:focus {
@ -366,9 +367,9 @@
--color-surface-hover: var(--color-surface-container-high);
--color-surface-selected: var(--color-surface-container);
--color-memory-accent: #3B7068;
--color-memory-surface: color-mix(in srgb, var(--color-memory-accent) 7%, var(--color-surface-container-lowest));
--color-memory-border: color-mix(in srgb, var(--color-memory-accent) 26%, var(--color-border));
--color-memory-icon-bg: color-mix(in srgb, var(--color-memory-accent) 10%, var(--color-surface-container-lowest));
--color-memory-surface: #F1F5F4;
--color-memory-border: #B1ACA5;
--color-memory-icon-bg: #EBF1F0;
--color-model-option-selected-bg: var(--color-primary-fixed);
--color-model-option-selected-border: rgba(143, 72, 47, 0.2);
--color-activity-heat-0: var(--color-surface-container);
@ -567,9 +568,9 @@
--color-surface-hover: #F2F5F8;
--color-surface-selected: #E9EEF3;
--color-memory-accent: #0F766E;
--color-memory-surface: color-mix(in srgb, var(--color-memory-accent) 5%, #FFFFFF);
--color-memory-border: color-mix(in srgb, var(--color-memory-accent) 24%, var(--color-border));
--color-memory-icon-bg: color-mix(in srgb, var(--color-memory-accent) 8%, #FFFFFF);
--color-memory-surface: #F3F8F8;
--color-memory-border: #ACC9CC;
--color-memory-icon-bg: #ECF4F3;
--color-model-option-selected-bg: #FFF0EA;
--color-model-option-selected-border: rgba(143, 72, 47, 0.2);
--color-activity-heat-0: #EEF2F6;
@ -745,9 +746,9 @@
--color-surface-hover: var(--color-surface-container-highest);
--color-surface-selected: var(--color-surface-container);
--color-memory-accent: #7DD3C7;
--color-memory-surface: color-mix(in srgb, var(--color-memory-accent) 9%, var(--color-surface-container-lowest));
--color-memory-border: color-mix(in srgb, var(--color-memory-accent) 26%, var(--color-border));
--color-memory-icon-bg: color-mix(in srgb, var(--color-memory-accent) 12%, var(--color-surface-container-lowest));
--color-memory-surface: #18201F;
--color-memory-border: rgba(113, 162, 151, 0.39);
--color-memory-icon-bg: #1B2624;
--color-model-option-selected-bg: rgba(255, 181, 159, 0.13);
--color-model-option-selected-border: rgba(255, 181, 159, 0.34);
--color-activity-heat-0: #242322;