fix(design): claim audit part 1b — whole-row switches, the field-family edge, state-layer hovers, icon links, reason-disabled controls, 24/44 targets (E28)
- F12 (owner: A): SwitchRow, the whole settings row toggles; a Config boolean toggles from its title and hint (a label pointed at the switch). - F14 (owner: B): the form dropdown trigger wears the field frame (see-through, the >= 3:1 edge); labelled pills keep the faint edge and the accent-edge hover. DESIGN says so, with log rows. - F6: ~15 bg-card / bg-border / opacity hovers onto the B3 state layer; a lint rule (HOVER_SWAP) refuses the swap, contract-tested. - F10: bare 14px icon links onto IconLink / LinkButton; a local lint rule refuses a glyph-only <a>. - F13: reason-disabled controls are aria-disabled (Users, Channels backfill, Discovery, Scheduler runs, Config tests); FocusSafeButton now swallows the click of a caller's aria-disabled; the Config tests say why while dirty (testDirty, HU/EN). - F5/F11: every control measured live at 24 (mouse) and 44 (coarse) across every page; touch-target is a utility that wins over a caller's min-h-6; link variants carry it too; SegmentedToggle, Tabs, NavRail, menu rows, PriorityStepper, sliders, CheckRow, InnerInput, card links and the rest. - F9: DismissibleChip's label reaches the chip's 24; the Combobox chips are DismissibleChips. - F15: the "+N" / "More..." open state worded as it renders.
This commit is contained in:
@@ -102,7 +102,7 @@ const BUTTON_PRIMITIVES = new Set([
|
||||
"SortControl.tsx",
|
||||
"SelectTrigger.tsx", // the dropdown trigger (A5)
|
||||
"FilterSelect.tsx", // its option rows
|
||||
"Combobox.tsx", // the field's own chips and chevron (A4)
|
||||
"Combobox.tsx", // the field's chevron (A4)
|
||||
]);
|
||||
const buttonPrimitive = {
|
||||
meta: {
|
||||
@@ -300,8 +300,39 @@ const sortableA11y = {
|
||||
};
|
||||
},
|
||||
};
|
||||
// An <a> whose content is only a glyph is a link that looks like a button (DESIGN.md): `IconLink`, with
|
||||
// a name, a 24+ box and the icon button's layer. The E28 audit found five bare 14px ones. Text or an
|
||||
// expression anywhere in its CHILDREN (not its attributes: every link has an `href={…}`) is a text
|
||||
// link or a card, and passes; a self-closing <a> is a `Trans` slot that i18n fills with text.
|
||||
const glyphLink = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
messages: {
|
||||
glyph:
|
||||
"a glyph-only <a> is a link that looks like a button: use ui/LinkButton's IconLink (a name, a 24+ box, the state layer).",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const hasText = (children) =>
|
||||
children.some(
|
||||
(c) =>
|
||||
(c.type === "JSXText" && /\S/.test(c.value)) ||
|
||||
(c.type === "JSXExpressionContainer" && c.expression.type !== "JSXEmptyExpression") ||
|
||||
((c.type === "JSXElement" || c.type === "JSXFragment") && hasText(c.children)),
|
||||
);
|
||||
return {
|
||||
JSXElement(node) {
|
||||
const open = node.openingElement;
|
||||
if (open.name.type !== "JSXIdentifier" || open.name.name !== "a" || open.selfClosing)
|
||||
return;
|
||||
if (!hasText(node.children)) context.report({ node: open, messageId: "glyph" });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
const localRules = {
|
||||
rules: {
|
||||
"glyph-link": glyphLink,
|
||||
"sortable-a11y": sortableA11y,
|
||||
"jsx-comment-text": jsxCommentText,
|
||||
"focus-safe-disabled": focusSafeDisabled,
|
||||
@@ -324,6 +355,15 @@ const Z_RAW = "/(^|[\\s:])-?z-([1-9]\\d*|\\[[^\\]]+\\])(?![\\w-])/";
|
||||
const Z_RAW_MESSAGE =
|
||||
"z-index off the layer scale: use a named level (z-base, z-menu, z-chrome, z-panel, z-rail, z-overlay, z-popover, z-tooltip), not z-N or z-[…].";
|
||||
|
||||
// A hover that swaps a colour instead of laying the B3 state layer (DESIGN.md Tokens): `bg-card`,
|
||||
// `bg-border` and `bg-surface` vanish on a light card, an opacity dim reads as "disabled", and
|
||||
// `bg-(--hover)` restates the `state-layer` utility by hand. `group-hover:` (a reveal on a parent's
|
||||
// hover, C1) and an edged pill's accent hover (F14) are not matched.
|
||||
const HOVER_SWAP =
|
||||
"/(^|[\\s:])hover:(bg-(card|border|surface|muted)(?![\\w-])|bg-\\(--(hover|press)\\)|opacity-)/";
|
||||
const HOVER_SWAP_MESSAGE =
|
||||
"hover by a colour swap: use the B3 layer (`state-layer`, Pressable `layer`, `state-filled` on a fill), not hover:bg-card / bg-border / opacity-N.";
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ["dist", "node_modules", "playwright-report", "test-results", "e2e/.auth"] },
|
||||
{
|
||||
@@ -371,6 +411,7 @@ export default tseslint.config(
|
||||
"local/input-primitive": "error",
|
||||
"local/surface-role": "error",
|
||||
"local/literal-a11y-text": "error",
|
||||
"local/glyph-link": "error",
|
||||
"local/sortable-a11y": "error",
|
||||
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
||||
// tsc already enforces these under strict + noUnusedLocals/Parameters — let it own them so a
|
||||
@@ -438,6 +479,8 @@ export default tseslint.config(
|
||||
{ selector: `TemplateElement[value.raw=${BOX_PX}]`, message: BOX_PX_MESSAGE },
|
||||
{ selector: `Literal[value=${Z_RAW}]`, message: Z_RAW_MESSAGE },
|
||||
{ selector: `TemplateElement[value.raw=${Z_RAW}]`, message: Z_RAW_MESSAGE },
|
||||
{ selector: `Literal[value=${HOVER_SWAP}]`, message: HOVER_SWAP_MESSAGE },
|
||||
{ selector: `TemplateElement[value.raw=${HOVER_SWAP}]`, message: HOVER_SWAP_MESSAGE },
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -537,6 +580,7 @@ export default tseslint.config(
|
||||
"local/button-primitive": "off",
|
||||
"local/input-primitive": "off",
|
||||
"local/surface-role": "off",
|
||||
"local/glyph-link": "off",
|
||||
},
|
||||
},
|
||||
prettier,
|
||||
|
||||
Reference in New Issue
Block a user