mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 13:02:50 +00:00
feat(theme): add semantic tokens, focus ring, and reduced-motion support
Adds the layer feature code has been missing: tokens that name a role — page surface, subtle border, danger — instead of a colour. Every one resolves to a Mantine variable rather than a literal, so they follow the colour scheme for free and cannot drift from the theme. A parallel palette of raw hexes would have recreated exactly the problem this exists to fix. Also closes three accessibility gaps that had no implementation anywhere in the codebase: no :focus-visible rule, no screen-reader-only utility, and no global prefers-reduced-motion handling. The focus work found a real bug, and nearly introduced a worse one. Mantine already rings its own controls, so the first attempt deferred to it with `outline: none` on .mantine-focus-auto. That suppressed Mantine's ring without replacing it, leaving portal buttons with no focus indicator at all — and it only showed up in one app, because which rule won depended on stylesheet order. The rules use identical values, so overlapping them is invisible and safe; opting out is not. The focus test now asserts computed outline width and style, not just pixels, since a screenshot alone would not have caught this. `status-tone.ts` establishes the six-tone vocabulary that the 48 scattered status→colour maps will eventually collapse onto. Nothing consumes it yet. No visual change: the 8 existing baselines pass unmodified. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,10 @@ import '@mantine/core/styles.css';
|
||||
import '@mantine/notifications/styles.css';
|
||||
import '@mantine/dates/styles.css';
|
||||
import '@mantine/spotlight/styles.css';
|
||||
// After Mantine's CSS (it defines the variables these tokens resolve to),
|
||||
// before the app's own, which may override them. Relative because the
|
||||
// @ema-platform aliases are tsconfig paths, which do not carry subpaths.
|
||||
import '../../../libs/shared/src/lib/theme/semantic.css';
|
||||
import './styles.css';
|
||||
import './app/i18n/config';
|
||||
import { App } from './app/app';
|
||||
|
||||
@@ -58,4 +58,43 @@ for (const app of APPS) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The focus ring, captured while actually focused.
|
||||
*
|
||||
* The full-page shots above can't show this: nothing is focused in them, so
|
||||
* a regression that removed the ring entirely would leave them all green.
|
||||
* Keyboard focus specifically, because `:focus-visible` deliberately does
|
||||
* not match a mouse click.
|
||||
*/
|
||||
test(`${app.name} — focus ring is visible`, async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
await gotoGallery(page, app.url, 'light');
|
||||
|
||||
const section = page.locator('section, div').filter({ hasText: 'Focus states' }).last();
|
||||
await section.scrollIntoViewIfNeeded();
|
||||
|
||||
const button = page.getByRole('button', { name: 'Button', exact: true });
|
||||
await button.focus();
|
||||
await expect(button).toBeFocused();
|
||||
|
||||
// Assert the ring in computed styles as well as pixels. A screenshot alone
|
||||
// would still pass if the outline came from somewhere unintended, and a
|
||||
// token that failed to resolve leaves an empty string rather than an error.
|
||||
const ring = await button.evaluate((el) => {
|
||||
const s = getComputedStyle(el);
|
||||
return {
|
||||
width: s.outlineWidth,
|
||||
style: s.outlineStyle,
|
||||
token: getComputedStyle(document.documentElement)
|
||||
.getPropertyValue('--ema-focus-ring')
|
||||
.trim(),
|
||||
};
|
||||
});
|
||||
expect(ring.style).toBe('solid');
|
||||
expect(ring.width).toBe('2px');
|
||||
expect(ring.token).not.toBe('');
|
||||
|
||||
await expect(button).toHaveScreenshot(`${app.name}-focus-ring.png`);
|
||||
});
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
@@ -3,6 +3,10 @@ import { createRoot } from 'react-dom/client';
|
||||
import '@mantine/core/styles.css';
|
||||
import '@mantine/dates/styles.css';
|
||||
import '@mantine/notifications/styles.css';
|
||||
// After Mantine's CSS (it defines the variables these tokens resolve to),
|
||||
// before the app's own, which may override them. Relative because the
|
||||
// @ema-platform aliases are tsconfig paths, which do not carry subpaths.
|
||||
import '../../../libs/shared/src/lib/theme/semantic.css';
|
||||
import './app/theme/portal.css';
|
||||
|
||||
import './app/i18n/config';
|
||||
|
||||
Reference in New Issue
Block a user