diff --git a/docs/design/TASK-OVERLAY-ACCEPTANCE.md b/docs/design/TASK-OVERLAY-ACCEPTANCE.md index 562c6180..445c271f 100644 --- a/docs/design/TASK-OVERLAY-ACCEPTANCE.md +++ b/docs/design/TASK-OVERLAY-ACCEPTANCE.md @@ -124,4 +124,6 @@ The initial component regression reproduced immediate dismissal after dispatch. ## Remaining acceptance +Shared loading buttons retain a static centering transform when reduced-motion transitions omit their styles. Normal-motion transitions continue to own their animated transform. The HTML artifact Refresh regression checks a held request at normal and enlarged text sizes, requiring the loading indicator to remain centered and fully inside its button. The original packaged-candidate clipping remains historical evidence; fresh affected native verification is required after integration and rebuilding. + Complete every family in both themes, normal and enlarged text, minimum native window size, keyboard entry/dismissal, reduced motion, pending-operation states, and viewport/footer reachability. Rebuild the packaged application with the complete family and inspect native captures. Reconcile the consumer inventory only against that evidence. Final installed-app verification and the maintained documentation screenshots/GIF refresh remain separate, unfinished work. diff --git a/e2e/task-artifact-format-popouts.spec.ts b/e2e/task-artifact-format-popouts.spec.ts index 3ed8920e..7da81b6f 100644 --- a/e2e/task-artifact-format-popouts.spec.ts +++ b/e2e/task-artifact-format-popouts.spec.ts @@ -231,6 +231,42 @@ for (const kind of ['image', 'pdf', 'html'] as const) { await expect(dialog).toBeVisible(); await expect(dialog.getByRole('button', { name: 'Close dialog' })).toBeDisabled(); await expect(dialog.getByRole('button', { name: 'Causal event' })).toBeDisabled(); + // Reduced-motion transitions must not remove the loader's layout transform. + for (const size of [ + { width: 1700, height: 900, fontSize: '16px' }, + { width: 1180, height: 760, fontSize: '20px' }, + { width: 900, height: 480, fontSize: '20px' }, + ]) { + await page.setViewportSize(size); + await page.evaluate((fontSize) => { + document.documentElement.style.fontSize = fontSize; + }, size.fontSize); + const refresh = dialog.getByRole('button', { name: 'Refresh', exact: true }); + await expect(refresh).toBeDisabled(); + await expect + .poll( + () => + refresh.evaluate((button) => { + const loader = button.querySelector('.mantine-Button-loader'); + if (!loader) return false; + const outer = button.getBoundingClientRect(); + const inner = loader.getBoundingClientRect(); + return ( + inner.width > 0 && + inner.height > 0 && + inner.left >= outer.left && + inner.right <= outer.right && + inner.top >= outer.top && + inner.bottom <= outer.bottom && + Math.abs(inner.left + inner.width / 2 - outer.left - outer.width / 2) <= + 1 && + Math.abs(inner.top + inner.height / 2 - outer.top - outer.height / 2) <= 2 + ); + }), + { message: 'Loading indicator must be centered and fully inside its button' } + ) + .toBe(true); + } expect( audits.filter((entry) => (entry as { action: string }).action === 'refresh') ).toHaveLength(1); diff --git a/web/src/theme/mantine-theme.ts b/web/src/theme/mantine-theme.ts index af63d83b..267a62fb 100644 --- a/web/src/theme/mantine-theme.ts +++ b/web/src/theme/mantine-theme.ts @@ -100,6 +100,11 @@ export const veritasMantineTheme = createTheme({ radius: 'sm', }, styles: { + // Reduced-motion Transition omits styles, including loader positioning. + // Normal-motion transition styles override this static layout fallback. + loader: { + transform: 'translate(-50%, -50%)', + }, root: { minHeight: `${VERITAS_UI_METRICS.actionMinHeight}px`, minWidth: `${VERITAS_UI_METRICS.actionMinHeight}px`,