Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,49 @@ All notable changes to this project will be documented in this file.

### Fixed

- **@overeng/effect-schema-form-aria**: the pilot package's three independent
drift findings are resolved together, because all three were waiting on the
same prerequisite — a working visual gate for this package. The seventeen
per-line `oxlint-disable` suppressions are gone and all eight StyleX rules
gate the package again.
**Raw colours become semantic tokens**: `on-primary` for foregrounds drawn on
`primary`, and `shadow-raised` for popover elevation, defaulting to the
`shadows.lg` scale step. **`primary` moves `blue500` -> `blue600`**, which is
the accessibility fix rather than a palette preference: the gate failed ten of
thirty-nine stories because white text on the selected segment measured
3.76:1 against AA's 4.5:1, and `blue600` measures 5.25:1. Renaming the literal
alone would have preserved the violation behind a better name.
**Thirteen deprecated top-level pseudo-class sites move into condition
objects.** Every site was reviewed for a competing state rather than
translated mechanically, because nesting a pseudo-class changes which
condition wins. Eleven set a property nothing else touches; the two that
compete — the segmented control's and the list option's background under hover
versus selection — now resolve by `stylex.props` argument order, with the
selected style restating the hover value because a later unconditional
`backgroundColor` does not replace an earlier one under a `:hover` key.
**Focus moves to the accessible-component library's focus-visible state**
(native `:focus-visible` on the one plain `<input>`), so a pointer click no
longer paints a keyboard focus ring. The ring is still drawn with `boxShadow`
rather than converted to `outline`: the partitioning invariant already holds
and no story renders a focused element, so repainting it would be a change the
gate structurally cannot adjudicate.
**State resolution stops re-deriving what the component knows.** The checkbox
read its own `value` prop; selection lives on the CheckboxButton, an ancestor
of the styled box, so it now comes from React Aria's render prop. The
segmented control and list options applied one of two mutually exclusive style
objects, hover rule included, and now apply one additive override.
**The eleventh gate failure was structural, not colour**: React Aria's
`Header` renders `<header>`, which outside sectioning content is a `banner`
landmark, so nested field groups produced two banners. The label is a plain
element now and the group carries the accessible name it lacked.
Adjudicated against the gate: 20 stories changed, 18 of them the intended
`43,127,255 -> 21,93,252` recolour confined to selected segments, checkbox
boxes and the accent tick. The other two are the gate's own sub-pixel fringe,
proven by recapturing the *unchanged* baseline tree and reproducing both
diffs identically (689 and 693 pixels, max channel delta 2). Zero
accessibility failures remain, and the condition-nesting, ordered-argument and
landmark changes moved no pixels at all. Closes #1171.

- **nix/oxlint-with-plugins.nix**: three fixes to the oxlint wrapper.
`jsPlugins` entries other than ours are no longer discarded, so third-party JS
plugins can load beside `@overeng/oxc-config` (previously any such plugin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,7 @@ const styles = stylex.create({
check: {
width: '0.75rem',
height: '0.75rem',
// oxlint-disable-next-line overeng/stylex-no-raw-color, @stylexjs/valid-styles -- needs a semantic `onPrimary` token; see #1171
color: '#ffffff',
color: tokens['on-primary'],
opacity: 0,
transitionProperty: 'opacity',
transitionDuration: '150ms',
Expand Down Expand Up @@ -90,28 +89,37 @@ export const BooleanField = ({
isDisabled={isDisabled}
{...stylex.props(styles.root)}
>
{/*
Selection lives on the CheckboxButton, not on the box the style targets,
so the box cannot read it as one of its own conditions. React Aria
surfaces it as a render prop, which is the sanctioned mechanism for that
case, and the selected styles are ordered arguments rather than a
mutually exclusive branch — the winner is last and the precedence is the
argument order. Reading `isSelected` rather than the `value` prop also
stops the style re-deriving state the component already resolved.
*/}
<AriaCheckboxButton {...stylex.props(styles.button)}>
<div
className={stylex.props(styles.box, value === true && styles.boxSelected).className ?? ''}
>
<svg
viewBox="0 0 12 12"
className={
stylex.props(styles.check, value === true && styles.checkVisible).className ?? ''
}
aria-hidden="true"
>
<path
d="M3 6l2 2 4-5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
/>
</svg>
</div>
<span>{label}</span>
{({ isSelected }) => (
<>
<div {...stylex.props(styles.box, isSelected === true && styles.boxSelected)}>
<svg
viewBox="0 0 12 12"
{...stylex.props(styles.check, isSelected === true && styles.checkVisible)}
aria-hidden="true"
>
<path
d="M3 6l2 2 4-5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
/>
</svg>
</div>
<span>{label}</span>
</>
)}
</AriaCheckboxButton>
</AriaCheckboxField>
</FieldWrapper>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as stylex from '@stylexjs/stylex'
import type { ReactNode } from 'react'
import { Group, Header, Text } from 'react-aria-components'
import { Group, Text } from 'react-aria-components'

import { fontSizes, radii, spacing } from '@overeng/stylex-tokens/tokens.stylex'

Expand Down Expand Up @@ -90,9 +90,15 @@ export const FieldGroup = ({
.filter(Boolean)
.join(' ')

// The group label is a plain element, not React Aria's `Header`. `Header`
// renders `<header>`, and a `<header>` that is not inside sectioning content
// is a `banner` landmark — so two field groups on a page produced two banners
// and the accessibility gate failed the nested-group story on
// `landmark-no-duplicate-banner` and `landmark-unique`. Naming the group with
// `aria-label` gives it the accessible name it was missing anyway.
return (
<Group className={groupClassName}>
<Header {...stylex.props(styles.header)}>{label}</Header>
<Group aria-label={label} className={groupClassName}>
<div {...stylex.props(styles.header)}>{label}</div>
<div {...stylex.props(styles.fieldsGrid)}>{children}</div>
</Group>
)
Expand Down Expand Up @@ -120,9 +126,10 @@ export const FieldGroupEmpty = ({
.filter(Boolean)
.join(' ')

// Same landmark reasoning as `FieldGroup`.
return (
<Group className={rootClassName}>
<Header {...stylex.props(emptyGroupStyles.header)}>{label}</Header>
<Group aria-label={label} className={rootClassName}>
<div {...stylex.props(emptyGroupStyles.header)}>{label}</div>
<Text {...stylex.props(emptyGroupStyles.message)}>{message}</Text>
</Group>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,27 +68,21 @@ const styles = stylex.create({
fontSize: fontSizes.sm,
lineHeight: '1.25rem',
color: tokens.ink,
backgroundColor: tokens.surface,
borderRightWidth: 1,
backgroundColor: { default: tokens.surface, ':hover': tokens['surface-raised'] },
borderRightWidth: { default: 1, ':last-child': 0 },
borderRightStyle: 'solid',
borderRightColor: tokens.border,
transitionProperty: 'background-color, color',
transitionDuration: '150ms',
// oxlint-disable-next-line @stylexjs/no-legacy-contextual-styles, @stylexjs/valid-styles -- deprecated top-level pseudo-class; nesting it changes condition precedence, so it needs the visual gate. See #1171
':last-child': {
borderRightWidth: 0,
},
},
segmentUnselected: {
// oxlint-disable-next-line @stylexjs/no-legacy-contextual-styles, @stylexjs/valid-styles -- deprecated top-level pseudo-class; nesting it changes condition precedence, so it needs the visual gate. See #1171
':hover': {
backgroundColor: tokens['surface-raised'],
},
},
segmentSelected: {
backgroundColor: tokens.primary,
// oxlint-disable-next-line overeng/stylex-no-raw-color, @stylexjs/valid-styles -- needs a semantic `onPrimary` token; see #1171
color: '#ffffff',
// Applied as the argument after `segment`, so it is the last write to both
// properties and wins by application order rather than by which condition
// kind happens to outrank which. The hover value is restated rather than
// left to fall through: `segment` sets `backgroundColor` under a `:hover`
// key, and a later unconditional `backgroundColor` does not replace that key.
backgroundColor: { default: tokens.primary, ':hover': tokens.primary },
color: tokens['on-primary'],
},
root: {
display: 'grid',
Expand All @@ -110,15 +104,13 @@ const styles = stylex.create({
borderColor: tokens.border,
backgroundColor: tokens.input,
color: tokens.ink,
// `outline` is reserved for the focus ring design-system-wide, so the base
// only suppresses the user-agent one. React Aria's own focus-visible state
// is preferred over `:focus`: it normalises across input modalities, so a
// pointer click no longer paints a keyboard focus ring.
outline: 'none',
// oxlint-disable-next-line @stylexjs/no-legacy-contextual-styles, @stylexjs/valid-styles -- deprecated top-level pseudo-class; nesting it changes condition precedence, so it needs the visual gate. See #1171
':focus': {
boxShadow: `0 0 0 1px ${tokens.primary}`,
},
// oxlint-disable-next-line @stylexjs/no-legacy-contextual-styles, @stylexjs/valid-styles -- deprecated top-level pseudo-class; nesting it changes condition precedence, so it needs the visual gate. See #1171
':disabled': {
opacity: 0.5,
},
boxShadow: { default: null, '[data-focus-visible]': `0 0 0 1px ${tokens.primary}` },
opacity: { default: 1, ':disabled': 0.5 },
},
triggerValue: {
flexGrow: 1,
Expand All @@ -136,8 +128,7 @@ const styles = stylex.create({
borderStyle: 'solid',
borderColor: tokens.border,
backgroundColor: tokens.surface,
// oxlint-disable-next-line overeng/stylex-no-raw-color -- needs a scheme-varying elevation-shadow token; see #1171
boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)',
boxShadow: tokens['shadow-raised'],
},
listBox: {
outline: 'none',
Expand All @@ -152,12 +143,7 @@ const styles = stylex.create({
lineHeight: '1.25rem',
cursor: 'pointer',
borderRadius: radii.default,
},
optionUnselected: {
// oxlint-disable-next-line @stylexjs/no-legacy-contextual-styles, @stylexjs/valid-styles -- deprecated top-level pseudo-class; nesting it changes condition precedence, so it needs the visual gate. See #1171
':hover': {
backgroundColor: tokens['surface-raised'],
},
backgroundColor: { default: null, ':hover': tokens['surface-raised'] },
},
optionMuted: {
color: tokens['subtle-ink'],
Expand All @@ -166,9 +152,9 @@ const styles = stylex.create({
color: tokens.ink,
},
optionSelected: {
backgroundColor: tokens.primary,
// oxlint-disable-next-line overeng/stylex-no-raw-color, @stylexjs/valid-styles -- needs a semantic `onPrimary` token; see #1171
color: '#ffffff',
// Same ordering contract as `segmentSelected`.
backgroundColor: { default: tokens.primary, ':hover': tokens.primary },
color: tokens['on-primary'],
},
hint: {
fontSize: '12px',
Expand Down Expand Up @@ -219,10 +205,8 @@ export const LiteralField = ({
key={opt.value}
id={opt.value}
className={({ isSelected }) =>
stylex.props(
styles.segment,
isSelected === true ? styles.segmentSelected : styles.segmentUnselected,
).className ?? ''
stylex.props(styles.segment, isSelected === true && styles.segmentSelected)
.className ?? ''
}
>
{opt.label}
Expand Down Expand Up @@ -262,7 +246,7 @@ export const LiteralField = ({
stylex.props(
styles.option,
styles.optionMuted,
isSelected === true ? styles.optionSelected : styles.optionUnselected,
isSelected === true && styles.optionSelected,
).className ?? ''
}
>
Expand All @@ -278,7 +262,7 @@ export const LiteralField = ({
stylex.props(
styles.option,
styles.optionInk,
isSelected === true ? styles.optionSelected : styles.optionUnselected,
isSelected === true && styles.optionSelected,
).className ?? ''
}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,11 @@ const styles = stylex.create({
borderColor: tokens.border,
backgroundColor: tokens.input,
color: tokens.ink,
// A plain `<input>`, not a React Aria one, so the native focus-visible
// pseudo-class is the only focus state available here.
outline: 'none',
// oxlint-disable-next-line @stylexjs/no-legacy-contextual-styles, @stylexjs/valid-styles -- deprecated top-level pseudo-class; nesting it changes condition precedence, so it needs the visual gate. See #1171
':focus': {
boxShadow: `0 0 0 1px ${tokens.primary}`,
},
// oxlint-disable-next-line @stylexjs/no-legacy-contextual-styles, @stylexjs/valid-styles -- deprecated top-level pseudo-class; nesting it changes condition precedence, so it needs the visual gate. See #1171
':disabled': {
opacity: 0.5,
},
boxShadow: { default: null, ':focus-visible': `0 0 0 1px ${tokens.primary}` },
opacity: { default: 1, ':disabled': 0.5 },
},
toggle: {
width: '1rem',
Expand All @@ -70,14 +66,8 @@ const styles = stylex.create({
borderWidth: 1,
borderStyle: 'solid',
borderColor: tokens.border,
// oxlint-disable-next-line @stylexjs/no-legacy-contextual-styles, @stylexjs/valid-styles -- deprecated top-level pseudo-class; nesting it changes condition precedence, so it needs the visual gate. See #1171
':hover': {
backgroundColor: tokens['surface-raised'],
},
// oxlint-disable-next-line @stylexjs/no-legacy-contextual-styles, @stylexjs/valid-styles -- deprecated top-level pseudo-class; nesting it changes condition precedence, so it needs the visual gate. See #1171
':disabled': {
opacity: 0.5,
},
backgroundColor: { default: null, ':hover': tokens['surface-raised'] },
opacity: { default: 1, ':disabled': 0.5 },
},
root: {
display: 'grid',
Expand All @@ -96,15 +86,9 @@ const styles = stylex.create({
backgroundColor: tokens.input,
color: tokens.ink,
outline: 'none',
// oxlint-disable-next-line @stylexjs/no-legacy-contextual-styles, @stylexjs/valid-styles -- deprecated top-level pseudo-class; nesting it changes condition precedence, so it needs the visual gate. See #1171
':focus': {
boxShadow: `0 0 0 1px ${tokens.primary}`,
},
// oxlint-disable-next-line @stylexjs/no-legacy-contextual-styles, @stylexjs/valid-styles -- deprecated top-level pseudo-class; nesting it changes condition precedence, so it needs the visual gate. See #1171
':disabled': {
opacity: 0.5,
cursor: 'not-allowed',
},
boxShadow: { default: null, '[data-focus-visible]': `0 0 0 1px ${tokens.primary}` },
opacity: { default: 1, ':disabled': 0.5 },
cursor: { default: null, ':disabled': 'not-allowed' },
},
description: {
fontSize: '12px',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,12 @@ const styles = stylex.create({
backgroundColor: tokens.input,
color: tokens.ink,
outline: 'none',
// oxlint-disable-next-line @stylexjs/no-legacy-contextual-styles, @stylexjs/valid-styles -- deprecated top-level pseudo-class; nesting it changes condition precedence, so it needs the visual gate. See #1171
':focus': {
boxShadow: `0 0 0 1px ${tokens.primary}`,
},
boxShadow: { default: null, '[data-focus-visible]': `0 0 0 1px ${tokens.primary}` },
opacity: { default: 1, ':disabled': 0.5 },
cursor: { default: null, ':disabled': 'not-allowed' },
'::placeholder': {
color: tokens['subtle-ink'],
},
// oxlint-disable-next-line @stylexjs/no-legacy-contextual-styles, @stylexjs/valid-styles -- deprecated top-level pseudo-class; nesting it changes condition precedence, so it needs the visual gate. See #1171
':disabled': {
opacity: 0.5,
cursor: 'not-allowed',
},
},
description: {
fontSize: '12px',
Expand Down
Loading
Loading