admin: ui polish pass, thirty findings from the emil audit
selected states stop changing font weight, buttons gain a pressed scale and scoped 120ms transitions with a reduced-motion override, every loading and empty state reserves its height, charts measure before first paint, hit targets rise to the 44px enhanced target where layout permits, long domains clamp to two lines on an unpadded inner span, chips and name cells truncate, tabular figures on counts and time columns, a z-index layer scale replaces magic numbers and fixes the dialog-over-confirm tie, history's empty state gains a clear-filters action, page headings balance, font smoothing and color-scheme land on the html reset.
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
import type { ReactNode } from "react";
|
||||
import * as stylex from "@stylexjs/stylex";
|
||||
import { Dialog as AriaDialog, Heading, Modal, ModalOverlay } from "react-aria-components";
|
||||
import { colors } from "./tokens.stylex";
|
||||
import { colors, layers } from "./tokens.stylex";
|
||||
import { styles as shared } from "./styles";
|
||||
|
||||
interface Props {
|
||||
@@ -36,7 +36,7 @@ const styles = stylex.create({
|
||||
overlay: {
|
||||
position: "fixed",
|
||||
inset: 0,
|
||||
zIndex: 50,
|
||||
zIndex: layers.confirm,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import type { ReactNode } from "react";
|
||||
import * as stylex from "@stylexjs/stylex";
|
||||
import { Dialog as AriaDialog, Heading, Modal, ModalOverlay } from "react-aria-components";
|
||||
import { colors } from "./tokens.stylex";
|
||||
import { colors, layers, metrics } from "./tokens.stylex";
|
||||
import { styles as shared } from "./styles";
|
||||
|
||||
interface Props {
|
||||
@@ -30,7 +30,7 @@ const styles = stylex.create({
|
||||
overlay: {
|
||||
position: "fixed",
|
||||
inset: 0,
|
||||
zIndex: 50,
|
||||
zIndex: layers.overlay,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
@@ -88,10 +88,10 @@ const styles = stylex.create({
|
||||
lineHeight: "1.75rem",
|
||||
fontWeight: 600,
|
||||
},
|
||||
/** 44px on both axes: the pointer-target floor, which the word alone misses. */
|
||||
/** The pointer-target floor on both axes, which the word alone misses. */
|
||||
close: {
|
||||
minWidth: 44,
|
||||
minHeight: 44,
|
||||
minWidth: metrics.hitTarget,
|
||||
minHeight: metrics.hitTarget,
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import type { ReactNode } from "react";
|
||||
import * as stylex from "@stylexjs/stylex";
|
||||
import { Tab, TabList, TabPanel, Tabs as AriaTabs } from "react-aria-components";
|
||||
import { colors } from "./tokens.stylex";
|
||||
import { colors, metrics } from "./tokens.stylex";
|
||||
|
||||
export interface TabSpec {
|
||||
id: string;
|
||||
@@ -44,6 +44,10 @@ const styles = stylex.create({
|
||||
borderBottomColor: colors.border,
|
||||
},
|
||||
tab: {
|
||||
/** The padding alone leaves the label short of the pointer-target floor. */
|
||||
minHeight: metrics.hitTarget,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
marginBottom: -1,
|
||||
borderBottomWidth: 2,
|
||||
borderBottomStyle: "solid",
|
||||
|
||||
+26
-1
@@ -8,10 +8,20 @@
|
||||
*/
|
||||
|
||||
import * as stylex from "@stylexjs/stylex";
|
||||
import { colors } from "./tokens.stylex";
|
||||
import { colors, metrics } from "./tokens.stylex";
|
||||
|
||||
const FOCUS = ":focus-visible";
|
||||
const DISABLED = ":disabled";
|
||||
const ACTIVE = ":active";
|
||||
const REDUCED_MOTION = "@media (prefers-reduced-motion: reduce)";
|
||||
|
||||
// Press depression and hover settling, shared by every button variant; inert
|
||||
// when the reader asked the platform for reduced motion.
|
||||
const press = {
|
||||
transform: { default: "none", [ACTIVE]: { default: "scale(0.97)", [REDUCED_MOTION]: "none" } },
|
||||
transitionProperty: metrics.transitionProperty,
|
||||
transitionDuration: { default: metrics.transitionDuration, [REDUCED_MOTION]: "0s" },
|
||||
};
|
||||
|
||||
/** The half-fade loop a placeholder runs while its data is in flight. */
|
||||
const pulseFrames = stylex.keyframes({
|
||||
@@ -60,6 +70,8 @@ export const styles = stylex.create({
|
||||
},
|
||||
|
||||
button: {
|
||||
minHeight: 40,
|
||||
...press,
|
||||
cursor: { default: "pointer", [DISABLED]: "not-allowed" },
|
||||
borderRadius: "0.25rem",
|
||||
borderWidth: 1,
|
||||
@@ -70,7 +82,13 @@ export const styles = stylex.create({
|
||||
fontSize: "0.875rem",
|
||||
lineHeight: "1.25rem",
|
||||
},
|
||||
/**
|
||||
* Deliberately below the 44px hit floor: it sits inline in table rows and
|
||||
* filter bars where a padded-out target would break the row rhythm. Still
|
||||
* above WCAG 2.5.8's 24px minimum.
|
||||
*/
|
||||
smallButton: {
|
||||
...press,
|
||||
cursor: { default: "pointer", [DISABLED]: "not-allowed" },
|
||||
borderRadius: "0.25rem",
|
||||
borderWidth: 1,
|
||||
@@ -82,6 +100,7 @@ export const styles = stylex.create({
|
||||
lineHeight: "1.25rem",
|
||||
},
|
||||
largeButton: {
|
||||
...press,
|
||||
cursor: { default: "pointer", [DISABLED]: "not-allowed" },
|
||||
borderRadius: "0.25rem",
|
||||
borderWidth: 1,
|
||||
@@ -93,6 +112,7 @@ export const styles = stylex.create({
|
||||
},
|
||||
|
||||
primaryButton: {
|
||||
...press,
|
||||
cursor: { default: "pointer", [DISABLED]: "not-allowed" },
|
||||
borderRadius: "0.25rem",
|
||||
borderStyle: "none",
|
||||
@@ -106,6 +126,7 @@ export const styles = stylex.create({
|
||||
opacity: { default: 1, [DISABLED]: 0.5 },
|
||||
},
|
||||
largePrimaryButton: {
|
||||
...press,
|
||||
cursor: { default: "pointer", [DISABLED]: "not-allowed" },
|
||||
borderRadius: "0.25rem",
|
||||
borderStyle: "none",
|
||||
@@ -118,6 +139,7 @@ export const styles = stylex.create({
|
||||
},
|
||||
|
||||
rowButton: {
|
||||
...press,
|
||||
cursor: { default: "pointer", [DISABLED]: "not-allowed" },
|
||||
borderRadius: "0.25rem",
|
||||
borderStyle: "none",
|
||||
@@ -129,6 +151,7 @@ export const styles = stylex.create({
|
||||
color: colors.primaryOnSurface,
|
||||
},
|
||||
linkButton: {
|
||||
...press,
|
||||
cursor: { default: "pointer", [DISABLED]: "not-allowed" },
|
||||
borderStyle: "none",
|
||||
backgroundColor: "transparent",
|
||||
@@ -139,6 +162,7 @@ export const styles = stylex.create({
|
||||
color: colors.primaryOnSurface,
|
||||
},
|
||||
dangerLinkButton: {
|
||||
...press,
|
||||
cursor: { default: "pointer", [DISABLED]: "not-allowed" },
|
||||
borderStyle: "none",
|
||||
backgroundColor: "transparent",
|
||||
@@ -150,6 +174,7 @@ export const styles = stylex.create({
|
||||
opacity: { default: 1, [DISABLED]: 0.5 },
|
||||
},
|
||||
retryButton: {
|
||||
...press,
|
||||
cursor: { default: "pointer", [DISABLED]: "not-allowed" },
|
||||
marginTop: "0.75rem",
|
||||
borderRadius: "0.25rem",
|
||||
|
||||
@@ -62,3 +62,35 @@ export const colors = stylex.defineVars({
|
||||
/** The focus ring colour. The ring itself is a floor, not a variant. */
|
||||
focus: { default: "oklch(54.6% 0.245 262.881)", [DARK]: "oklch(54.6% 0.245 262.881)" },
|
||||
});
|
||||
|
||||
/**
|
||||
* The stacking order. Three layers is the whole app: a chart tooltip floats
|
||||
* over its own panel, a dialog overlay covers the page, and a confirmation sits
|
||||
* over the dialog that opened it. Values are strings because a StyleX var holds
|
||||
* a CSS token, not a number.
|
||||
*/
|
||||
export const layers = stylex.defineVars({
|
||||
tooltip: "10",
|
||||
overlay: "50",
|
||||
confirm: "60",
|
||||
});
|
||||
|
||||
/**
|
||||
* Values that are shared but are not theme: they never vary by colour scheme,
|
||||
* and they are the same number wherever they appear.
|
||||
*
|
||||
* `defineConsts` rather than a plain exported constant, and in this module
|
||||
* rather than in `ui/styles`, because of how the compiler reads a
|
||||
* `stylex.create` body. A constant imported from an ordinary module is rejected
|
||||
* outright, and a plain export from a `.stylex.ts` module is read as a variable
|
||||
* object rather than a literal — so neither can be shared. `defineConsts` is
|
||||
* the one mechanism that inlines a literal across module boundaries, which is
|
||||
* why these are strings with their units baked in.
|
||||
*/
|
||||
export const metrics = stylex.defineConsts({
|
||||
/** WCAG 2.5.5's enhanced 44px target, applied where layout permits; some inline controls stop at 40px or above 2.5.8's 24px minimum. */
|
||||
hitTarget: "44px",
|
||||
/** The press and hover settle shared by every control that styles its own states. */
|
||||
transitionProperty: "background-color, color, border-color, transform",
|
||||
transitionDuration: "120ms",
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user