# 19.x → 20.0.0, entry by entry

**Both entries are `@ultimat3/ui` component behaviour, and neither changes a type** — so nothing
fails to compile and nothing throws. They change what a screen DOES, which is why they are a major:
a silent rendering change is worse than a build error, and you would have found these by looking.

| # | Surface | Costs you an edit if |
|---|---|---|
| 1 | `DataTable` while reloading | you relied on skeletons appearing on every load. A FIRST load (`rows: []`) still renders skeletons; a RELOAD now keeps the existing rows, dimmed, under `aria-busy`. Replacing rendered content with placeholders on every refresh is a second layout change for no news, and it is what makes a fast list feel slow — the reader has already read those rows, and taking them away to say "loading" tells them nothing they can act on. No edit if you pass fresh `rows` on each load, which is the ordinary case. If you genuinely want the placeholder back on a refresh, pass `rows: []` while the fetch is in flight |
| 2 | `Button` with `loading` | you style `button[disabled]`, or you assert on the `disabled` attribute in a test. `loading` no longer sets the native attribute — it sets `aria-disabled` + `aria-busy` and refuses the click in `onClick` with `preventDefault()`. The click is still refused; the mechanism moved. A `disabled` control loses focus the moment it becomes disabled — mid-flow, with no announcement — is exempt from the WCAG contrast minimum precisely because nobody is meant to read it, which is wrong for a control you are asking someone to WAIT on, and it does not actually prevent a double submit, because that race is server-side and always was. The edit is one selector: style `button[aria-disabled='true']` beside `button[disabled]`, and assert on `aria-disabled` |

**`defineTheme()` can now refuse your brand** — `X_UI_CONTRAST_INSUFFICIENT`. Listed here rather
than as a third entry because it throws at declaration with the measured ratio, the required one and
the role to move, so it cannot ship silently and needs no search. An override whose RESOLVED
channels put a pairing below WCAG 2.2 AA (4.5:1 text, 3:1 focus ring, 1.4:1 border) fails at boot.
Only pairings your brand can have CHANGED are measured — blaming an app for the framework's own
colours is how a rule gets switched off. The usual defect is half a pairing: a new `accent` against
the shipped white `accent-fg`. `@ultimat3/ui` exports `contrastRatio` and `roleContrast` so you can
measure a candidate before shipping it. AA and never APCA: APCA is not a standard, and AA is the
operative legal benchmark.

**Nothing else in this release costs an edit.** The five new app guards ship in `x new` only; an
existing app gains one with `x g guard <name>` (since 23.0.0; `x doctor --json` lists the shipped
ones it lacks in `data.guards.missing`), one file per rule, and deleting one drops that rule. `x shot --all-islands` is additive, and
`x g island` / `x g resource` now write a `.island.states.ts` beside what they generate.
