diff --git a/openless-all/app/e2e/motion.spec.ts b/openless-all/app/e2e/motion.spec.ts index e8d81d9c8..6dc232fd8 100644 --- a/openless-all/app/e2e/motion.spec.ts +++ b/openless-all/app/e2e/motion.spec.ts @@ -331,6 +331,39 @@ test('multimodal settings fit one desktop page and keep inactive notices below l }); }); +test('pipeline selection stays evenly inset in both modes', async ({ page }) => { + await page.emulateMedia({ reducedMotion: 'reduce' }); + await page.getByRole('button', { name: '设置', exact: true }).click(); + const dialog = page.getByRole('dialog', { name: '设置', exact: true }); + await dialog.getByRole('button', { name: 'AI 与模型', exact: true }).click(); + const track = dialog.locator('.ol-service-pipeline-track'); + for (const name of ['传统模式', '多模态模式']) { + await dialog.getByRole('button', { name, exact: true }).click(); + await expect + .poll(() => + track.evaluate((element, selectedName) => { + const outer = element.getBoundingClientRect(); + const selected = element.querySelector('button[aria-pressed="true"]')!; + const chip = element.querySelector('.ol-segmented-indicator')!; + const item = selected.getBoundingClientRect(); + const inner = chip.getBoundingClientRect(); + return Math.max( + Math.abs(inner.top - item.top), + Math.abs(inner.left - item.left), + Math.abs(inner.width - item.width), + Math.abs(inner.height - item.height), + Math.abs(inner.top - outer.top - (outer.bottom - inner.bottom)), + Math.abs( + (selectedName === '传统模式' ? inner.left - outer.left : outer.right - inner.right) - + (inner.top - outer.top), + ), + ); + }, name), + ) + .toBeLessThan(0.75); + } +}); + test('selection highlights move and settle correctly after a rapid mode return', async ({ page, }) => { diff --git a/openless-all/app/e2e/refinements.spec.ts b/openless-all/app/e2e/refinements.spec.ts index 0aa3dc1cd..9eecabc8a 100644 --- a/openless-all/app/e2e/refinements.spec.ts +++ b/openless-all/app/e2e/refinements.spec.ts @@ -2,6 +2,20 @@ import { openMicrophoneSettings, settleNavigation } from './support/navigation'; import { test, expect, type Page } from '@playwright/test'; const fixture = 'http://127.0.0.1:5326/e2e/fixtures/refinements.html'; + +test('style and dictionary action buttons retain compact corners', async ({ page }) => { + await page.goto('/'); + await page.getByRole('button', { name: '风格', exact: true }).click(); + await page.getByRole('button', { name: '润色模式', exact: true }).click(); + await expect(page.getByRole('button', { name: '导入 ZIP' })).toHaveCSS('border-radius', '8px'); + await expect(page.getByRole('button', { name: '激活', exact: true }).first()).toHaveCSS( + 'border-radius', + '8px', + ); + await page.getByRole('button', { name: '词典', exact: true }).click(); + await expect(page.locator('.ol-action-button').first()).toHaveCSS('border-radius', '8px'); +}); + async function openShortcuts(page: Page) { await page.goto('/'); await page.getByRole('button', { name: '设置', exact: true }).click(); diff --git a/openless-all/app/src/lib/motion.ts b/openless-all/app/src/lib/motion.ts index 0a0aad0c6..076b9fd91 100644 --- a/openless-all/app/src/lib/motion.ts +++ b/openless-all/app/src/lib/motion.ts @@ -29,7 +29,7 @@ export function useReducedMotion() { ); } -type Frame = { opacity: string | number; transform: string }; +type Frame = { opacity: string | number; transform: string; width?: string }; const VISIBLE: Frame = { opacity: 1, transform: 'none' }; const FADED: Frame = { opacity: 0, transform: 'none' }; const POP_HIDDEN: Frame = { opacity: 0, transform: 'translate3d(0, 18px, 0) scale(0.84)' }; @@ -236,28 +236,51 @@ export function useSelectionMotion( } // Layout offsets stay correct while the parent dialog is scaled on entry. const y = underline ? group.clientHeight - 2 : button.offsetTop; - const target = `translate3d(${button.offsetLeft}px, ${y}px, 0) scaleX(${button.offsetWidth / 100})`; - if (target === lastTarget) return; - const from = interrupted.current ?? currentFrame(indicator); + const target = `translate3d(${button.offsetLeft}px, ${y}px, 0)`; + const width = `${button.offsetWidth}px`; + const targetKey = `${target}:${width}`; + if (targetKey === lastTarget) return; + const from = interrupted.current ?? { + ...currentFrame(indicator), + width: getComputedStyle(indicator).width, + }; interrupted.current = null; motion?.cancel(); indicator.style.transform = target; + indicator.style.width = width; indicator.style.height = `${underline ? 2 : button.offsetHeight}px`; indicator.style.visibility = 'visible'; - lastTarget = target; + lastTarget = targetKey; if (moving && !reduced && !keyboardInteraction() && typeof indicator.animate === 'function') { - motion = animate(indicator, from, { opacity: 1, transform: target }, ENTER_MS, false); + motion = animate( + indicator, + from, + { opacity: 1, transform: target, width }, + ENTER_MS, + false, + ); motion.animation.id = 'ol-selection-move'; void motion.animation.finished.then(motion.cancel).catch(() => {}); } }; position(previous.current !== null && previous.current !== selection); previous.current = selection; - const observer = new ResizeObserver(() => position(false)); + let resizeFrame = 0; + const observer = new ResizeObserver(() => { + if (resizeFrame) return; + resizeFrame = requestAnimationFrame(() => { + resizeFrame = 0; + position(false); + }); + }); observer.observe(group); return () => { - interrupted.current = currentFrame(indicator); + interrupted.current = { + ...currentFrame(indicator), + width: getComputedStyle(indicator).width, + }; observer.disconnect(); + cancelAnimationFrame(resizeFrame); motion?.cancel(); }; }, [groupRef, indicatorRef, selection, underline, reduced]); diff --git a/openless-all/app/src/pages/Vocab.tsx b/openless-all/app/src/pages/Vocab.tsx index 584ac13c8..db75ec2b3 100644 --- a/openless-all/app/src/pages/Vocab.tsx +++ b/openless-all/app/src/pages/Vocab.tsx @@ -610,7 +610,7 @@ export function Vocab() { icon={wordAdded ? 'check' : 'plus'} onClick={() => void addNewWord()} style={{ - borderRadius: 999, + borderRadius: 8, height: 38, minWidth: 92, justifyContent: 'center', diff --git a/openless-all/app/src/pages/_atoms.tsx b/openless-all/app/src/pages/_atoms.tsx index 1043dfd4c..665ddc828 100644 --- a/openless-all/app/src/pages/_atoms.tsx +++ b/openless-all/app/src/pages/_atoms.tsx @@ -240,7 +240,7 @@ export function Btn({ const muted = disabled && (variant === 'primary' || variant === 'blue'); return (