mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
Stabilize desktop zoom settings interaction
The settings zoom slider is a secondary desktop preference, so it now previews drag movement locally before committing the app zoom on release and sits below system notifications. The shortcut hint also spells out reset behavior, and the dark theme slider thumb uses stronger contrast tokens so it stays visible while dragging. Constraint: Desktop zoom changes affect the full application surface and should not update continuously during pointer drag. Rejected: Keep zoom directly under theme selection | the control is low-frequency and was visually dominating General settings. Confidence: high Scope-risk: narrow Tested: bunx vitest run src/__tests__/generalSettings.test.tsx src/theme/globals.test.ts Tested: browser smoke verified dark-theme order and slider visibility at localhost:5174 Not-tested: Full bun run verify
This commit is contained in:
parent
e070c4a0d0
commit
1f0c888a13
@ -255,18 +255,48 @@ describe('Settings > General tab', () => {
|
||||
expect(screen.getByRole('button', { name: 'Warm Classic' })).toHaveAttribute('aria-pressed', 'false')
|
||||
})
|
||||
|
||||
it('updates the shared UI zoom setting from the General display control', async () => {
|
||||
it('keeps UI zoom below system notifications because it is a secondary setting', () => {
|
||||
render(<Settings />)
|
||||
|
||||
fireEvent.click(screen.getByText('General'))
|
||||
|
||||
const notificationsHeading = screen.getByRole('heading', { name: 'System Notifications' })
|
||||
const uiZoomHeading = screen.getByRole('heading', { name: 'UI Zoom' })
|
||||
const webFetchHeading = screen.getByRole('heading', { name: 'WebFetch Preflight' })
|
||||
|
||||
expect((notificationsHeading.compareDocumentPosition(uiZoomHeading) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0).toBe(true)
|
||||
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 () => {
|
||||
render(<Settings />)
|
||||
|
||||
fireEvent.click(screen.getByText('General'))
|
||||
expect(screen.getByText('Shortcuts are faster:')).toBeInTheDocument()
|
||||
expect(screen.getByText('macOS')).toBeInTheDocument()
|
||||
expect(screen.getByText('Windows / Linux')).toBeInTheDocument()
|
||||
expect(screen.getByText('0 resets zoom to 100%.')).toBeInTheDocument()
|
||||
|
||||
const slider = screen.getByLabelText('UI Zoom')
|
||||
|
||||
fireEvent.pointerDown(slider, { pointerId: 1 })
|
||||
await act(async () => {
|
||||
fireEvent.change(screen.getByLabelText('UI Zoom'), {
|
||||
fireEvent.change(slider, {
|
||||
target: { value: '1.25', valueAsNumber: 1.25 },
|
||||
})
|
||||
})
|
||||
|
||||
expect(screen.getAllByText('125%')).toHaveLength(2)
|
||||
expect(useSettingsStore.getState().setUiZoom).not.toHaveBeenCalledWith(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%' })
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.pointerUp(slider, { pointerId: 1 })
|
||||
})
|
||||
|
||||
expect(useSettingsStore.getState().setUiZoom).toHaveBeenCalledWith(1.25)
|
||||
expect(screen.getByText('125%')).toBeInTheDocument()
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Reset UI zoom to 100%' }))
|
||||
|
||||
@ -803,6 +803,10 @@ export const en = {
|
||||
'settings.general.webSearchSave': 'Save',
|
||||
'settings.general.uiZoom': 'UI Zoom',
|
||||
'settings.general.uiZoomDescription': 'Adjust the size of the entire interface.',
|
||||
'settings.general.uiZoomShortcutHint': 'Shortcuts are faster:',
|
||||
'settings.general.uiZoomShortcutMac': 'macOS',
|
||||
'settings.general.uiZoomShortcutWindows': 'Windows / Linux',
|
||||
'settings.general.uiZoomShortcutResetHint': '0 resets zoom to 100%.',
|
||||
'settings.general.uiZoomReset': 'Reset UI zoom to 100%',
|
||||
|
||||
// ─── Empty Session ──────────────────────────────────────
|
||||
|
||||
@ -805,6 +805,10 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'settings.general.webSearchSave': '保存',
|
||||
'settings.general.uiZoom': '界面缩放',
|
||||
'settings.general.uiZoomDescription': '调整整个界面的显示大小。',
|
||||
'settings.general.uiZoomShortcutHint': '快捷键更直接:',
|
||||
'settings.general.uiZoomShortcutMac': 'macOS',
|
||||
'settings.general.uiZoomShortcutWindows': 'Windows / Linux',
|
||||
'settings.general.uiZoomShortcutResetHint': '0 表示恢复到 100%。',
|
||||
'settings.general.uiZoomReset': '重置界面缩放到 100%',
|
||||
|
||||
// ─── Empty Session ──────────────────────────────────────
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useMemo, useRef, type ReactNode } from 'react'
|
||||
import { useState, useEffect, useMemo, useRef, type CSSProperties, type ReactNode } from 'react'
|
||||
import QRCode from 'qrcode'
|
||||
import { Copy, Eye, EyeOff, PowerOff, QrCode, RotateCw } from 'lucide-react'
|
||||
import { useSettingsStore, UI_ZOOM_DEFAULT, UI_ZOOM_MIN, UI_ZOOM_MAX, UI_ZOOM_STEP } from '../stores/settingsStore'
|
||||
@ -1392,12 +1392,22 @@ function GeneralSettings() {
|
||||
const [webSearchDraft, setWebSearchDraft] = useState(webSearch)
|
||||
const [notificationPermission, setNotificationPermission] = useState<DesktopNotificationPermission>('default')
|
||||
const [notificationActionRunning, setNotificationActionRunning] = useState(false)
|
||||
const [uiZoomDraft, setUiZoomDraft] = useState(uiZoom)
|
||||
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}%`
|
||||
|
||||
useEffect(() => {
|
||||
setWebSearchDraft(webSearch)
|
||||
}, [webSearch])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isUiZoomDragging) {
|
||||
setUiZoomDraft(uiZoom)
|
||||
}
|
||||
}, [isUiZoomDragging, uiZoom])
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
getDesktopNotificationPermission().then((permission) => {
|
||||
@ -1513,6 +1523,107 @@ function GeneralSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
const commitUiZoom = (value: number) => {
|
||||
const nextZoom = Number.isFinite(value) ? value : UI_ZOOM_DEFAULT
|
||||
setIsUiZoomDragging(false)
|
||||
setUiZoomDraft(nextZoom)
|
||||
setUiZoom(nextZoom)
|
||||
}
|
||||
|
||||
const uiZoomSection = (
|
||||
<div className="mt-8">
|
||||
<div className="mb-3 flex items-end justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<h2 className="text-base font-semibold text-[var(--color-text-primary)] mb-1">{t('settings.general.uiZoom')}</h2>
|
||||
<p className="text-sm text-[var(--color-text-tertiary)]">{t('settings.general.uiZoomDescription')}</p>
|
||||
<div className="mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-[var(--color-text-tertiary)]">
|
||||
<span>{t('settings.general.uiZoomShortcutHint')}</span>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<span className="font-medium text-[var(--color-text-secondary)]">{t('settings.general.uiZoomShortcutMac')}</span>
|
||||
<kbd className="settings-zoom-kbd">⌘</kbd>
|
||||
<kbd className="settings-zoom-kbd">+</kbd>
|
||||
<span>/</span>
|
||||
<kbd className="settings-zoom-kbd">⌘</kbd>
|
||||
<kbd className="settings-zoom-kbd">-</kbd>
|
||||
<span>/</span>
|
||||
<kbd className="settings-zoom-kbd">⌘</kbd>
|
||||
<kbd className="settings-zoom-kbd">0</kbd>
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<span className="font-medium text-[var(--color-text-secondary)]">{t('settings.general.uiZoomShortcutWindows')}</span>
|
||||
<kbd className="settings-zoom-kbd">Ctrl</kbd>
|
||||
<kbd className="settings-zoom-kbd">+</kbd>
|
||||
<span>/</span>
|
||||
<kbd className="settings-zoom-kbd">Ctrl</kbd>
|
||||
<kbd className="settings-zoom-kbd">-</kbd>
|
||||
<span>/</span>
|
||||
<kbd className="settings-zoom-kbd">Ctrl</kbd>
|
||||
<kbd className="settings-zoom-kbd">0</kbd>
|
||||
</span>
|
||||
<span>{t('settings.general.uiZoomShortcutResetHint')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-shrink-0 items-center gap-2">
|
||||
<span className="min-w-[48px] rounded-md bg-[var(--color-surface-container-low)] px-2 py-1 text-center text-sm font-medium text-[var(--color-text-secondary)]">
|
||||
{uiZoomPercent}%
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('settings.general.uiZoomReset')}
|
||||
title={t('settings.general.uiZoomReset')}
|
||||
onClick={() => {
|
||||
setIsUiZoomDragging(false)
|
||||
setUiZoomDraft(UI_ZOOM_DEFAULT)
|
||||
setUiZoom(UI_ZOOM_DEFAULT)
|
||||
}}
|
||||
className="inline-flex h-8 items-center gap-1.5 rounded-md border border-[var(--color-border)] px-2 text-xs font-medium text-[var(--color-text-secondary)] transition-colors hover:border-[var(--color-border-focus)] hover:bg-[var(--color-surface-hover)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-focus)]"
|
||||
>
|
||||
<RotateCw className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
100%
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`settings-zoom-control flex items-center gap-3 ${isUiZoomDragging ? 'is-dragging' : ''}`}
|
||||
style={{ '--settings-zoom-range-progress': uiZoomRangeProgress } as CSSProperties}
|
||||
>
|
||||
<span className="w-9 text-right text-xs text-[var(--color-text-tertiary)]">{Math.round(UI_ZOOM_MIN * 100)}%</span>
|
||||
<div className="settings-zoom-range-wrap flex-1">
|
||||
<div className="settings-zoom-preview" aria-hidden="true">
|
||||
{uiZoomPercent}%
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
aria-label={t('settings.general.uiZoom')}
|
||||
min={UI_ZOOM_MIN}
|
||||
max={UI_ZOOM_MAX}
|
||||
step={UI_ZOOM_STEP}
|
||||
value={uiZoomDraft}
|
||||
onPointerDown={(e) => {
|
||||
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)
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
if (uiZoomDraft !== uiZoom) {
|
||||
commitUiZoom(e.currentTarget.valueAsNumber)
|
||||
}
|
||||
}}
|
||||
className="settings-zoom-range w-full"
|
||||
/>
|
||||
</div>
|
||||
<span className="w-9 text-xs text-[var(--color-text-tertiary)]">{Math.round(UI_ZOOM_MAX * 100)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
{/* Appearance selector */}
|
||||
@ -1535,45 +1646,6 @@ function GeneralSettings() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* UI Zoom */}
|
||||
<div className="mb-8">
|
||||
<div className="mb-3 flex items-end justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<h2 className="text-base font-semibold text-[var(--color-text-primary)] mb-1">{t('settings.general.uiZoom')}</h2>
|
||||
<p className="text-sm text-[var(--color-text-tertiary)]">{t('settings.general.uiZoomDescription')}</p>
|
||||
</div>
|
||||
<div className="flex flex-shrink-0 items-center gap-2">
|
||||
<span className="min-w-[48px] rounded-md bg-[var(--color-surface-container-low)] px-2 py-1 text-center text-sm font-medium text-[var(--color-text-secondary)]">
|
||||
{Math.round(uiZoom * 100)}%
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('settings.general.uiZoomReset')}
|
||||
title={t('settings.general.uiZoomReset')}
|
||||
onClick={() => setUiZoom(UI_ZOOM_DEFAULT)}
|
||||
className="inline-flex h-8 items-center gap-1.5 rounded-md border border-[var(--color-border)] px-2 text-xs font-medium text-[var(--color-text-secondary)] transition-colors hover:border-[var(--color-border-focus)] hover:bg-[var(--color-surface-hover)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-focus)]"
|
||||
>
|
||||
<RotateCw className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
100%
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="w-9 text-right text-xs text-[var(--color-text-tertiary)]">{Math.round(UI_ZOOM_MIN * 100)}%</span>
|
||||
<input
|
||||
type="range"
|
||||
aria-label={t('settings.general.uiZoom')}
|
||||
min={UI_ZOOM_MIN}
|
||||
max={UI_ZOOM_MAX}
|
||||
step={UI_ZOOM_STEP}
|
||||
value={uiZoom}
|
||||
onChange={(e) => setUiZoom(e.currentTarget.valueAsNumber)}
|
||||
className="flex-1"
|
||||
/>
|
||||
<span className="w-9 text-xs text-[var(--color-text-tertiary)]">{Math.round(UI_ZOOM_MAX * 100)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Language selector */}
|
||||
<h2 className="text-base font-semibold text-[var(--color-text-primary)] mb-1">{t('settings.general.languageTitle')}</h2>
|
||||
<p className="text-sm text-[var(--color-text-tertiary)] mb-3">{t('settings.general.languageDescription')}</p>
|
||||
@ -1704,6 +1776,8 @@ function GeneralSettings() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{uiZoomSection}
|
||||
|
||||
<div className="mt-8">
|
||||
<h2 className="text-base font-semibold text-[var(--color-text-primary)] mb-1">{t('settings.general.webFetchPreflightTitle')}</h2>
|
||||
<p className="text-sm text-[var(--color-text-tertiary)] mb-3">{t('settings.general.webFetchPreflightDescription')}</p>
|
||||
|
||||
@ -108,6 +108,194 @@
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.settings-zoom-kbd {
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 5px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 5px;
|
||||
background: var(--color-surface-container-lowest);
|
||||
color: var(--color-text-secondary);
|
||||
font-family: var(--font-label);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
box-shadow: inset 0 -1px 0 rgba(27, 28, 26, 0.08);
|
||||
}
|
||||
|
||||
.settings-zoom-control {
|
||||
--settings-zoom-range-progress: 33.333%;
|
||||
--settings-zoom-preview-scale: 0.88;
|
||||
--settings-zoom-preview-opacity: 0;
|
||||
--settings-zoom-track-empty: var(--color-surface-container-high);
|
||||
--settings-zoom-thumb-bg: var(--color-surface-container-lowest);
|
||||
--settings-zoom-thumb-border: var(--color-border);
|
||||
--settings-zoom-thumb-shadow: 0 2px 8px rgba(27, 28, 26, 0.16);
|
||||
padding-top: 22px;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .settings-zoom-control {
|
||||
--settings-zoom-track-empty: var(--color-surface-container-highest);
|
||||
--settings-zoom-thumb-bg: var(--color-surface-bright);
|
||||
--settings-zoom-thumb-border: rgba(255, 181, 159, 0.78);
|
||||
--settings-zoom-thumb-shadow: 0 0 0 3px rgba(255, 181, 159, 0.16), 0 10px 24px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.settings-zoom-control.is-dragging {
|
||||
--settings-zoom-preview-scale: 1;
|
||||
--settings-zoom-preview-opacity: 1;
|
||||
}
|
||||
|
||||
.settings-zoom-range-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.settings-zoom-preview {
|
||||
position: absolute;
|
||||
left: clamp(24px, var(--settings-zoom-range-progress), calc(100% - 24px));
|
||||
top: -30px;
|
||||
z-index: 1;
|
||||
min-width: 52px;
|
||||
transform: translateX(-50%) scale(var(--settings-zoom-preview-scale));
|
||||
transform-origin: 50% 100%;
|
||||
border: 1px solid var(--color-surface-glass-border);
|
||||
border-radius: 999px;
|
||||
background: var(--color-surface-glass);
|
||||
padding: 4px 9px;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
opacity: var(--settings-zoom-preview-opacity);
|
||||
box-shadow: var(--shadow-dropdown);
|
||||
backdrop-filter: blur(14px) saturate(1.15);
|
||||
-webkit-backdrop-filter: blur(14px) saturate(1.15);
|
||||
pointer-events: none;
|
||||
transition:
|
||||
left 260ms cubic-bezier(0.22, 1, 0.36, 1),
|
||||
opacity 160ms ease,
|
||||
transform 220ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.settings-zoom-preview::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: -4px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
transform: translateX(-50%) rotate(45deg);
|
||||
border-right: 1px solid var(--color-surface-glass-border);
|
||||
border-bottom: 1px solid var(--color-surface-glass-border);
|
||||
background: var(--color-surface-glass);
|
||||
}
|
||||
|
||||
.settings-zoom-range {
|
||||
appearance: none;
|
||||
display: block;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
accent-color: var(--color-brand);
|
||||
}
|
||||
|
||||
.settings-zoom-range:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.settings-zoom-range:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.settings-zoom-range::-webkit-slider-runnable-track {
|
||||
height: 6px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--color-border);
|
||||
background:
|
||||
linear-gradient(
|
||||
90deg,
|
||||
var(--color-brand) 0%,
|
||||
var(--color-brand) var(--settings-zoom-range-progress),
|
||||
var(--settings-zoom-track-empty) var(--settings-zoom-range-progress),
|
||||
var(--settings-zoom-track-empty) 100%
|
||||
);
|
||||
transition: background 220ms ease, border-color 180ms ease;
|
||||
}
|
||||
|
||||
.settings-zoom-range::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-top: -7px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--settings-zoom-thumb-border);
|
||||
background: var(--settings-zoom-thumb-bg);
|
||||
box-shadow: var(--settings-zoom-thumb-shadow);
|
||||
transition: border-color 180ms ease, box-shadow 220ms ease, transform 220ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.settings-zoom-range:hover::-webkit-slider-thumb {
|
||||
border-color: var(--color-border-focus);
|
||||
}
|
||||
|
||||
.settings-zoom-control.is-dragging .settings-zoom-range::-webkit-slider-thumb {
|
||||
transform: scale(1.18);
|
||||
border-color: var(--color-brand);
|
||||
box-shadow: var(--shadow-dropdown);
|
||||
}
|
||||
|
||||
.settings-zoom-range:focus-visible::-webkit-slider-thumb {
|
||||
border-color: var(--color-brand);
|
||||
box-shadow: var(--shadow-focus-ring), 0 2px 8px rgba(27, 28, 26, 0.16);
|
||||
}
|
||||
|
||||
.settings-zoom-range::-moz-range-track {
|
||||
height: 6px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--settings-zoom-track-empty);
|
||||
transition: background 220ms ease, border-color 180ms ease;
|
||||
}
|
||||
|
||||
.settings-zoom-range::-moz-range-progress {
|
||||
height: 6px;
|
||||
border-radius: 999px;
|
||||
background: var(--color-brand);
|
||||
}
|
||||
|
||||
.settings-zoom-range::-moz-range-thumb {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--settings-zoom-thumb-border);
|
||||
background: var(--settings-zoom-thumb-bg);
|
||||
box-shadow: var(--settings-zoom-thumb-shadow);
|
||||
transition: border-color 180ms ease, box-shadow 220ms ease, transform 220ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.settings-zoom-control.is-dragging .settings-zoom-range::-moz-range-thumb {
|
||||
transform: scale(1.18);
|
||||
border-color: var(--color-brand);
|
||||
box-shadow: var(--shadow-dropdown);
|
||||
}
|
||||
|
||||
.settings-zoom-range:focus-visible::-moz-range-thumb {
|
||||
border-color: var(--color-brand);
|
||||
box-shadow: var(--shadow-focus-ring), 0 2px 8px rgba(27, 28, 26, 0.16);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.settings-zoom-preview,
|
||||
.settings-zoom-range::-webkit-slider-runnable-track,
|
||||
.settings-zoom-range::-webkit-slider-thumb,
|
||||
.settings-zoom-range::-moz-range-track,
|
||||
.settings-zoom-range::-moz-range-thumb {
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Tailwind Theme Override (matches prototype design system) ─── */
|
||||
@theme {
|
||||
--color-primary: #8F482F;
|
||||
|
||||
@ -66,4 +66,11 @@ describe('desktop theme tokens', () => {
|
||||
it('avoids color-mix in startup-critical shell chrome for Safari 15 WebView support', () => {
|
||||
expect(css).not.toContain('color-mix(')
|
||||
})
|
||||
|
||||
it('keeps the UI zoom slider thumb visible in dark mode', () => {
|
||||
expect(css).toContain('[data-theme="dark"] .settings-zoom-control')
|
||||
expect(css).toContain('--settings-zoom-thumb-bg: var(--color-surface-bright);')
|
||||
expect(css).toContain('--settings-zoom-thumb-border: rgba(255, 181, 159, 0.78);')
|
||||
expect(css).toContain('box-shadow: var(--settings-zoom-thumb-shadow);')
|
||||
})
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user