@ksmv/ui-checks 0.22.0 → 0.23.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.23.2 - 2026-10-19
4
+
5
+ - **Nem a 0.23.0 nem a 0.23.1 foram publicadas.** A release do `@ksmv/ui-react` parou nas duas, na suíte de navegadores, e as do checks, que saem junto com ela, ficaram retidas no npm sem aprovação. Nada mudou no pacote desde então: de 0.22.1, atualize direto para a 0.23.2. As tags `checks-v0.23.0` e `checks-v0.23.1` existem no repositório, mas não têm versão no registro.
6
+ - **Contratos de tema 3.5.0 e de contraste 13.0.0** empacotados (#397 a #401). O de tema ganha 70 derivados opcionais (338 no total); o de contraste ganha 115 pares e altera um (451 no total). Nenhum par sai. Atualize o `@ksmv/ui-react` junto (ver o CHANGELOG dele).
7
+ - **O contrato de contraste passa ao `schemaVersion` 3:** um par pode medir a opacidade de um grupo. Com `opacity`, o par compõe o texto e o fundo esmaecidos sobre a superfície de fora (`over`), e `group` dá o fundo próprio do grupo; uma opacidade sem `over` é recusada. A versão anterior do `@ksmv/ui-checks` recusa o schema 3.
8
+ - **Quem usa `computedEvidence` precisa capturar a evidência de novo:** os dois contratos mudaram, e um par com opacidade exige o `group` e o `over` na evidência.
9
+
10
+ ## 0.22.1 - 2026-10-17
11
+
12
+ - **Contratos de tema 3.4.0 e de contraste 12.0.0** empacotados (#394). O de tema ganha o derivado opcional `--ui-surface-inverse-end`, a ponta do degradê da superfície inversa; o de contraste ganha 20 pares: o texto apagado sobre a superfície inversa (`inverse-muted-text`) e cada par de um derivado `-on-inverse` sobre a superfície medido também sobre a ponta (contexto `-end`). Nenhum par sai. Um tema que não declara nenhum dos dois tokens passa como antes.
13
+ - **A regra `tokens/contrast` mede um par na ponta de um degradê ao longo dele:** um token `<início>-end`, quando o início também é um token, é a ponta de um degradê, e o par é medido em 17 pontos, em sRGB e em Oklab (os espaços em que o navegador interpola um degradê que não declara espaço), com o alfa pré-multiplicado, ficando com o pior. Um texto que passa nas duas pontas pode reprovar no meio.
14
+ - **Quem usa `computedEvidence` precisa capturar a evidência de novo:** os dois contratos mudaram.
15
+
3
16
  ## 0.22.0 - 2026-10-17
4
17
 
5
18
  - **Contratos de tema 3.3.0 e de contraste 11.0.0** empacotados. O de tema ganha 89 derivados opcionais e 5 tokens de forma; o de contraste ganha 104 pares. Nenhum par sai. Os pares do pressionado contra as superfícies e os da linha com tom são obrigações novas, e um tema com o primário claro demais, ou com um fundo de tom escuro, reprova. Atualize o `@ksmv/ui-react` junto (ver o CHANGELOG dele).
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @ksmv/ui-checks
2
2
 
3
- Verificações de qualidade para o contrato semântico `--ui-*` de `@ksmv/ui-react`: completude dos temas, contraste dos pares do contrato (ações, estados, campos, controles e, desde o contrato de contraste `3.0.0`, o texto base sobre as superfícies e, desde o `4.0.0`, a cor de link e, desde o `5.0.0`, a borda das ações e, desde o `6.0.0`, as variantes de ação da 0.17.0 e o botão de texto e, desde o `7.0.0`, os botões da casa da 0.18.1 e o hover por tom do botão de texto e, desde o `7.1.0`, o texto de hover dos botões suaves e do contorno de aviso e, desde o `9.0.0`, o texto, o limite de controle, o link, o botão de texto e o fantasma sobre a superfície inversa, a paleta categórica, a descrição do diálogo, o giro contra a superfície, o aviso e o rótulo de seção, o item de menu e o cartão de rádio, o cabeçalho da tabela e o grupo de alternância, e o selo cheio, a bolinha sobre a superfície inversa e a barra e, desde o `10.0.0`, o fundo do botão de ícone contornado, a cor da ajuda e a caixa de seleção e, desde o `11.0.0`, o botão pressionado contra as superfícies, a linha da lista com tom, o selo com marca à frente, o cabeçalho de página e a métrica, os passos e a navegação lateral, a conversa e a mídia, a escolha de horário e de dia, a grade de calendário e o quadro), prefixo dos tokens, cores literais fora dos arquivos de tema e evidência calculada vinculada às fontes.
3
+ Verificações de qualidade para o contrato semântico `--ui-*` de `@ksmv/ui-react`: completude dos temas, contraste dos pares do contrato (ações, estados, campos, controles e, desde o contrato de contraste `3.0.0`, o texto base sobre as superfícies e, desde o `4.0.0`, a cor de link e, desde o `5.0.0`, a borda das ações e, desde o `6.0.0`, as variantes de ação da 0.17.0 e o botão de texto e, desde o `7.0.0`, os botões da casa da 0.18.1 e o hover por tom do botão de texto e, desde o `7.1.0`, o texto de hover dos botões suaves e do contorno de aviso e, desde o `9.0.0`, o texto, o limite de controle, o link, o botão de texto e o fantasma sobre a superfície inversa, a paleta categórica, a descrição do diálogo, o giro contra a superfície, o aviso e o rótulo de seção, o item de menu e o cartão de rádio, o cabeçalho da tabela e o grupo de alternância, e o selo cheio, a bolinha sobre a superfície inversa e a barra e, desde o `10.0.0`, o fundo do botão de ícone contornado, a cor da ajuda e a caixa de seleção e, desde o `11.0.0`, o botão pressionado contra as superfícies, a linha da lista com tom, o selo com marca à frente, o cabeçalho de página e a métrica, os passos e a navegação lateral, a conversa e a mídia, a escolha de horário e de dia, a grade de calendário e o quadro e, desde o `12.0.0`, o texto apagado sobre a superfície inversa e cada par da superfície inversa medido também sobre a ponta do degradê e, desde o `13.0.0`, a caixa redonda, a linha de lista com o hover, a feita e o título como link, e os passos por fazer, com a opacidade de grupo, e a mídia na bolha, o documento por tom e o áudio), prefixo dos tokens, cores literais fora dos arquivos de tema e evidência calculada vinculada às fontes.
4
4
 
5
5
  A regra `css/no-literal-color` recusa cor literal fora dos temas configurados. Desde a 0.22.0, ela aceita uma exceção: o fallback de `var(--ui-x, <cor>)`, quando a cor é, texto por texto, o padrão literal que o contrato de tema dá à própria `--ui-x` (como `var(--ui-media-overlay-bg, rgb(0 0 0 / 70%))`). Quem escreve esse fallback implementa o contrato. Outra cor, ainda que equivalente, uma cor fora do `var()` ou o fallback de um token sem padrão literal continuam reprovados.
6
6
 
@@ -97,6 +97,8 @@ config Invalid computed theme evidence in artifacts/theme-evidence.json: the the
97
97
 
98
98
  Depois de atualizar o pacote React, a mensagem traz a versão do contrato da captura e a do contrato instalado; quando faltam tokens ou resta `var(...)`, traz os nomes dos tokens, em ordem, até dez, e a contagem dos demais. A mensagem não repete valores do produto: aparecem só o caminho da evidência e o nome do tema como estão na configuração, nomes de tokens e versões de contrato. Falha ao resolver os contratos também é relatada com a própria causa (`Unable to resolve the configured contract source.`, `Unsupported theme contract.` ou `Unsupported contrast manifest.`).
99
99
 
100
+ Desde o contrato de contraste `13.0.0` (`schemaVersion` 3), um par pode medir a opacidade de um grupo, como a linha feita a 0,75: `opacity` dá a opacidade, `over` a superfície opaca fora do grupo e, quando o texto está numa camada dentro dele, `group` o fundo do elemento que tem a opacidade. A regra pinta o grupo guardando o alfa e compõe o resultado, com a opacidade, sobre `over`; um degradê (um token `-end`) em qualquer camada é medido ao longo dele, e vale o pior ponto; um par sem `over`, com uma camada que não resolve ou com a superfície de fora translúcida fica sem cobertura, nunca aprovado. A evidência calculada precisa trazer todas as camadas de cada par (o texto, o fundo, `over` e `group`).
101
+
100
102
  Tokens opcionais, os derivados e os de marca, podem ficar vazios na captura: é assim que o navegador informa um token que o tema não declarou. Um derivado vazio assume o valor do token de origem, que é o padrão documentado no contrato. Quando a origem também é um derivado, como no texto do hover do contorno de aviso (`--ui-action-warning-outline-text-hover` deriva de `--ui-action-warning-outline-text`, que deriva do contorno de perigo), a cadeia é seguida até um token declarado. Um derivado que é ponta de um par de contraste, como `--ui-control-checked-bg`, também pode ficar vazio; a captura precisa então do token em que a cadeia dele termina. Sem evidência, `tokens/contrast` segue a mesma cadeia: um derivado que o tema não declara é avaliado pelo valor da origem, ou pelo padrão literal do contrato (`transparent`, uma cor). Um par com `over` mede um fundo translúcido composto sobre aquela superfície; sem `over`, um fundo translúcido continua cobertura pendente.
101
103
 
102
104
  Desde o contrato de tema `3.0.0`, a forma é da biblioteca: a regra `themes/no-shape` aponta, como violação, cada token da lista `shape` do contrato que um seletor de tema declarar. Quem migra de um tema que declarava forma pode carregar essas violações na baseline enquanto tira as linhas.
package/dist/color.d.ts CHANGED
@@ -18,5 +18,8 @@ export declare function containsLiteralColor(value: string, contractDefaults?: R
18
18
  export declare function referencedTokens(value: string): string[];
19
19
  export declare function unresolvedTokenReferences(value: string, tokens: ReadonlyMap<string, string>, stack?: ReadonlySet<string>): string[];
20
20
  export declare function resolveColor(value: string | undefined, tokens: ReadonlyMap<string, string>, stack?: ReadonlySet<string>): ColorResolution;
21
+ export declare function paintOver(top: RgbColor, bottom: RgbColor): RgbColor;
22
+ export declare function faded(color: RgbColor, opacity: number): RgbColor;
21
23
  export declare function composite(foreground: RgbColor, background: RgbColor): RgbColor;
24
+ export declare function mix(start: RgbColor, end: RgbColor, at: number): RgbColor[];
22
25
  export declare function contrastRatio(foreground: RgbColor, background: RgbColor): number | undefined;
package/dist/color.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { converter, parse } from "culori";
2
2
  import valueParser from "postcss-value-parser";
3
3
  const toRgb = converter("rgb");
4
+ const toOklab = converter("oklab");
4
5
  const unsupportedFunctions = new Set(["color-mix", "light-dark"]);
5
6
  const literalColorFunctions = new Set([
6
7
  "color",
@@ -186,6 +187,19 @@ export function resolveColor(value, tokens, stack = new Set()) {
186
187
  const color = parsedColor(value.trim());
187
188
  return color ? { color, status: "resolved" } : { status: "uncovered" };
188
189
  }
190
+ // One color painted over another, keeping the alpha of the result (Porter-Duff
191
+ // source-over), for a layer that is itself translucent: a group's paint
192
+ // before its opacity. composite() below assumes an opaque background.
193
+ export function paintOver(top, bottom) {
194
+ const [topAlpha, bottomAlpha] = [top.alpha ?? 1, bottom.alpha ?? 1];
195
+ const alpha = topAlpha + bottomAlpha * (1 - topAlpha);
196
+ const channel = (upper, lower) => alpha === 0 ? 0 : (upper * topAlpha + lower * bottomAlpha * (1 - topAlpha)) / alpha;
197
+ return { alpha, b: channel(top.b, bottom.b), g: channel(top.g, bottom.g), mode: "rgb", r: channel(top.r, bottom.r) };
198
+ }
199
+ // A color under a group's opacity: its alpha scaled by it.
200
+ export function faded(color, opacity) {
201
+ return { ...color, alpha: (color.alpha ?? 1) * opacity };
202
+ }
189
203
  export function composite(foreground, background) {
190
204
  const alpha = foreground.alpha ?? 1;
191
205
  return {
@@ -195,6 +209,21 @@ export function composite(foreground, background) {
195
209
  r: foreground.r * alpha + background.r * (1 - alpha),
196
210
  };
197
211
  }
212
+ // A point of a gradient between two colors. The browser interpolates legacy
213
+ // colors (hex, rgb(), hsl()) in sRGB and the others, as oklch(), in Oklab
214
+ // (CSS Color 4, §12.1); the space is chosen by the syntax, so the check takes
215
+ // both points. Both interpolate with premultiplied alpha (§12.3): a channel
216
+ // is weighted by its color's alpha, so a transparent end lends no hue.
217
+ export function mix(start, end, at) {
218
+ const [startAlpha, endAlpha] = [start.alpha ?? 1, end.alpha ?? 1];
219
+ const alpha = startAlpha + (endAlpha - startAlpha) * at;
220
+ const between = (from, to) => alpha === 0 ? 0 : (from * startAlpha + (to * endAlpha - from * startAlpha) * at) / alpha;
221
+ const srgb = { alpha, b: between(start.b, end.b), g: between(start.g, end.g), mode: "rgb", r: between(start.r, end.r) };
222
+ const [from, to] = [toOklab(start), toOklab(end)];
223
+ const clamp = (value) => Math.min(1, Math.max(0, value));
224
+ const oklab = toRgb({ a: between(from.a, to.a), alpha, b: between(from.b, to.b), l: between(from.l, to.l), mode: "oklab" });
225
+ return [srgb, { alpha, b: clamp(oklab.b), g: clamp(oklab.g), mode: "rgb", r: clamp(oklab.r) }];
226
+ }
198
227
  function luminance(color) {
199
228
  const channel = (value) => value <= 0.04045
200
229
  ? value / 12.92
@@ -29,13 +29,21 @@ export interface ContrastPair {
29
29
  context: string;
30
30
  foreground: string;
31
31
  minimum: number;
32
- /** The surface under a background that may be translucent (schema 2). */
32
+ /** The background of the element that has the opacity, when the text sits on
33
+ * a layer inside it, as a marker inside a card (schema 3). Without it, the
34
+ * background is the element's own and is painted once. */
35
+ group?: string;
36
+ /** The opacity of a group around the pair, as a done row's 0.75 (schema 3).
37
+ * It needs the opaque surface outside the group in over. */
38
+ opacity?: number;
39
+ /** The surface under a background that may be translucent (schema 2), or
40
+ * the surface outside a group with an opacity (schema 3). */
33
41
  over?: string;
34
42
  }
35
43
  export interface ContrastManifest {
36
44
  contractVersion: string;
37
45
  pairs: ContrastPair[];
38
- schemaVersion: 2;
46
+ schemaVersion: 2 | 3;
39
47
  }
40
48
  export interface ContractIdentity {
41
49
  contractVersion: string;
package/dist/contracts.js CHANGED
@@ -96,18 +96,20 @@ export function parseContrastManifest(content) {
96
96
  }
97
97
  if (!isRecord(value) ||
98
98
  !exactKeys(value, ["contractVersion", "pairs", "schemaVersion"]) ||
99
- value.schemaVersion !== 2 ||
99
+ (value.schemaVersion !== 2 && value.schemaVersion !== 3) ||
100
100
  typeof value.contractVersion !== "string" ||
101
101
  !semverPattern.test(value.contractVersion) ||
102
102
  !Array.isArray(value.pairs)) {
103
103
  throw new CheckSetupError("Unsupported contrast manifest.");
104
104
  }
105
+ const schema = value.schemaVersion;
105
106
  const identities = [];
106
107
  for (const pair of value.pairs) {
107
108
  if (!isRecord(pair) ||
108
109
  !(exactKeys(pair, ["background", "context", "foreground", "minimum"]) ||
109
110
  (exactKeys(pair, ["background", "context", "foreground", "minimum", "over"]) &&
110
- typeof pair.over === "string" && tokenNamePattern.test(pair.over))) ||
111
+ typeof pair.over === "string" && tokenNamePattern.test(pair.over)) ||
112
+ (schema === 3 && isGroupPair(pair))) ||
111
113
  typeof pair.background !== "string" ||
112
114
  !tokenNamePattern.test(pair.background) ||
113
115
  typeof pair.foreground !== "string" ||
@@ -119,13 +121,26 @@ export function parseContrastManifest(content) {
119
121
  pair.minimum <= 0) {
120
122
  throw new CheckSetupError("Unsupported contrast manifest.");
121
123
  }
122
- identities.push(`${pair.context}\u0000${pair.foreground}\u0000${pair.background}\u0000${pair.over ?? ""}`);
124
+ identities.push(`${pair.context}\u0000${pair.foreground}\u0000${pair.background}\u0000${pair.over ?? ""}\u0000${pair.group ?? ""}\u0000${pair.opacity ?? ""}`);
123
125
  }
124
126
  if (new Set(identities).size !== identities.length) {
125
127
  throw new CheckSetupError("Unsupported contrast manifest.");
126
128
  }
127
129
  return value;
128
130
  }
131
+ // A pair under a group's opacity (schema 3): the opacity in (0, 1], the
132
+ // opaque surface outside the group in over, and the group's own background
133
+ // when the text sits on a layer inside it.
134
+ function isGroupPair(pair) {
135
+ const keys = ["background", "context", "foreground", "minimum", "opacity", "over"];
136
+ return ((exactKeys(pair, keys) || (exactKeys(pair, [...keys, "group"]) && typeof pair.group === "string" && tokenNamePattern.test(pair.group))) &&
137
+ typeof pair.opacity === "number" &&
138
+ Number.isFinite(pair.opacity) &&
139
+ pair.opacity > 0 &&
140
+ pair.opacity <= 1 &&
141
+ typeof pair.over === "string" &&
142
+ tokenNamePattern.test(pair.over));
143
+ }
129
144
  function identity(content, contractVersion) {
130
145
  return {
131
146
  contractVersion,
@@ -1,6 +1,6 @@
1
1
  {
2
- "contractVersion": "11.0.0",
3
- "schemaVersion": 2,
2
+ "contractVersion": "13.0.0",
3
+ "schemaVersion": 3,
4
4
  "pairs": [
5
5
  { "foreground": "--ui-action-primary-text", "background": "--ui-action-primary-bg", "minimum": 4.5, "context": "normal-text" },
6
6
  { "foreground": "--ui-action-primary-text", "background": "--ui-action-primary-bg-hover", "minimum": 4.5, "context": "normal-text-hover" },
@@ -259,7 +259,7 @@
259
259
  { "foreground": "--ui-side-nav-count-text-on-inverse", "background": "--ui-side-nav-count-bg-on-inverse", "over": "--ui-surface-inverse", "minimum": 4.5, "context": "side-nav-count-text-on-inverse" },
260
260
  { "foreground": "--ui-steps-current", "background": "--ui-steps-current-bg", "over": "--ui-surface", "minimum": 3, "context": "steps-current-boundary" },
261
261
  { "foreground": "--ui-steps-current", "background": "--ui-surface", "minimum": 3, "context": "steps-current-border" },
262
- { "foreground": "--ui-steps-current", "background": "--ui-surface", "minimum": 3, "context": "steps-complete-mark" },
262
+ { "foreground": "--ui-steps-current", "background": "--ui-steps-complete-marker-bg", "minimum": 3, "context": "steps-complete-mark" },
263
263
  { "foreground": "--ui-steps-track-filled", "background": "--ui-surface", "minimum": 3, "context": "steps-track-filled" },
264
264
  { "foreground": "--ui-side-nav-count-dot", "background": "--ui-surface", "minimum": 3, "context": "side-nav-count-dot" },
265
265
  { "foreground": "--ui-message-incoming-text", "background": "--ui-message-incoming-bg", "over": "--ui-surface", "minimum": 4.5, "context": "message-incoming-text" },
@@ -317,6 +317,141 @@
317
317
  { "foreground": "--ui-board-card-border-hover", "background": "--ui-surface-subtle", "minimum": 3, "context": "board-card-border-hover" },
318
318
  { "foreground": "--ui-board-drop-border", "background": "--ui-surface-subtle", "minimum": 3, "context": "board-drop-border" },
319
319
  { "foreground": "--ui-board-drop-border", "background": "--ui-board-drop-bg", "over": "--ui-surface-subtle", "minimum": 3, "context": "board-drop-line" },
320
- { "foreground": "--ui-border-control", "background": "--ui-surface-subtle", "minimum": 3, "context": "board-placeholder-boundary" }
320
+ { "foreground": "--ui-border-control", "background": "--ui-surface-subtle", "minimum": 3, "context": "board-placeholder-boundary" },
321
+ { "foreground": "--ui-text-muted-on-inverse", "background": "--ui-surface-inverse", "minimum": 4.5, "context": "inverse-muted-text" },
322
+ { "foreground": "--ui-text-primary-on-inverse", "background": "--ui-surface-inverse-end", "minimum": 4.5, "context": "inverse-body-text-end" },
323
+ { "foreground": "--ui-text-secondary-on-inverse", "background": "--ui-surface-inverse-end", "minimum": 4.5, "context": "inverse-secondary-text-end" },
324
+ { "foreground": "--ui-border-control-on-inverse", "background": "--ui-surface-inverse-end", "minimum": 3, "context": "inverse-control-boundary-end" },
325
+ { "foreground": "--ui-link-text-on-inverse", "background": "--ui-surface-inverse-end", "minimum": 4.5, "context": "inverse-link-text-end" },
326
+ { "foreground": "--ui-text-button-text-on-inverse", "background": "--ui-surface-inverse-end", "minimum": 4.5, "context": "inverse-text-button-text-end" },
327
+ { "foreground": "--ui-action-ghost-text-on-inverse", "background": "--ui-surface-inverse-end", "minimum": 4.5, "context": "inverse-ghost-text-end" },
328
+ { "foreground": "--ui-action-ghost-text-hover-on-inverse", "background": "--ui-action-ghost-bg-hover-on-inverse", "over": "--ui-surface-inverse-end", "minimum": 4.5, "context": "inverse-ghost-text-hover-end" },
329
+ { "foreground": "--ui-action-ghost-text-hover-on-inverse", "background": "--ui-action-ghost-bg-active-on-inverse", "over": "--ui-surface-inverse-end", "minimum": 4.5, "context": "inverse-ghost-text-active-end" },
330
+ { "foreground": "--ui-icon-button-ghost-text-on-inverse", "background": "--ui-surface-inverse-end", "minimum": 3, "context": "inverse-icon-button-ghost-icon-end" },
331
+ { "foreground": "--ui-icon-button-ghost-text-on-inverse", "background": "--ui-action-ghost-bg-hover-on-inverse", "over": "--ui-surface-inverse-end", "minimum": 3, "context": "inverse-icon-button-ghost-icon-hover-end" },
332
+ { "foreground": "--ui-icon-button-ghost-text-on-inverse", "background": "--ui-action-ghost-bg-active-on-inverse", "over": "--ui-surface-inverse-end", "minimum": 3, "context": "inverse-icon-button-ghost-icon-active-end" },
333
+ { "foreground": "--ui-status-success-on-inverse", "background": "--ui-surface-inverse-end", "minimum": 3, "context": "status-indicator-success-on-inverse-end" },
334
+ { "foreground": "--ui-status-warning-on-inverse", "background": "--ui-surface-inverse-end", "minimum": 3, "context": "status-indicator-warning-on-inverse-end" },
335
+ { "foreground": "--ui-status-danger-on-inverse", "background": "--ui-surface-inverse-end", "minimum": 3, "context": "status-indicator-danger-on-inverse-end" },
336
+ { "foreground": "--ui-status-info-on-inverse", "background": "--ui-surface-inverse-end", "minimum": 3, "context": "status-indicator-info-on-inverse-end" },
337
+ { "foreground": "--ui-button-pressed-bg-on-inverse", "background": "--ui-surface-inverse-end", "minimum": 3, "context": "button-pressed-boundary-on-inverse-end" },
338
+ { "foreground": "--ui-button-pressed-bg-hover-on-inverse", "background": "--ui-surface-inverse-end", "minimum": 3, "context": "button-pressed-boundary-hover-on-inverse-end" },
339
+ { "foreground": "--ui-side-nav-count-text-on-inverse", "background": "--ui-side-nav-count-bg-on-inverse", "over": "--ui-surface-inverse-end", "minimum": 4.5, "context": "side-nav-count-text-on-inverse-end" },
340
+ { "foreground": "--ui-text-muted-on-inverse", "background": "--ui-surface-inverse-end", "minimum": 4.5, "context": "inverse-muted-text-end" },
341
+ { "foreground": "--ui-checkbox-border", "background": "--ui-checkbox-circle-bg", "over": "--ui-surface", "minimum": 3, "context": "checkbox-circle-ring" },
342
+ { "foreground": "--ui-checkbox-circle-hover", "background": "--ui-checkbox-circle-bg", "over": "--ui-surface", "minimum": 3, "context": "checkbox-circle-ring-hover" },
343
+ { "foreground": "--ui-checkbox-border", "background": "--ui-checkbox-circle-bg", "over": "--ui-surface-raised", "minimum": 3, "context": "checkbox-circle-ring-raised" },
344
+ { "foreground": "--ui-checkbox-circle-hover", "background": "--ui-checkbox-circle-bg", "over": "--ui-surface-raised", "minimum": 3, "context": "checkbox-circle-ring-hover-raised" },
345
+ { "foreground": "--ui-checkbox-border", "background": "--ui-checkbox-circle-bg", "over": "--ui-surface-subtle", "minimum": 3, "context": "checkbox-circle-ring-subtle" },
346
+ { "foreground": "--ui-checkbox-circle-hover", "background": "--ui-checkbox-circle-bg", "over": "--ui-surface-subtle", "minimum": 3, "context": "checkbox-circle-ring-hover-subtle" },
347
+ { "foreground": "--ui-text-primary", "background": "--ui-list-item-success-bg-hover", "over": "--ui-surface", "minimum": 4.5, "context": "list-item-title-success-hover" },
348
+ { "foreground": "--ui-text-secondary", "background": "--ui-list-item-success-bg-hover", "over": "--ui-surface", "minimum": 4.5, "context": "list-item-description-success-hover" },
349
+ { "foreground": "--ui-text-primary", "background": "--ui-list-item-warning-bg-hover", "over": "--ui-surface", "minimum": 4.5, "context": "list-item-title-warning-hover" },
350
+ { "foreground": "--ui-text-secondary", "background": "--ui-list-item-warning-bg-hover", "over": "--ui-surface", "minimum": 4.5, "context": "list-item-description-warning-hover" },
351
+ { "foreground": "--ui-text-primary", "background": "--ui-list-item-danger-bg-hover", "over": "--ui-surface", "minimum": 4.5, "context": "list-item-title-danger-hover" },
352
+ { "foreground": "--ui-text-secondary", "background": "--ui-list-item-danger-bg-hover", "over": "--ui-surface", "minimum": 4.5, "context": "list-item-description-danger-hover" },
353
+ { "foreground": "--ui-text-primary", "background": "--ui-list-item-info-bg-hover", "over": "--ui-surface", "minimum": 4.5, "context": "list-item-title-info-hover" },
354
+ { "foreground": "--ui-text-secondary", "background": "--ui-list-item-info-bg-hover", "over": "--ui-surface", "minimum": 4.5, "context": "list-item-description-info-hover" },
355
+ { "foreground": "--ui-text-primary", "background": "--ui-list-item-success-bg-hover", "over": "--ui-surface-raised", "minimum": 4.5, "context": "list-item-title-success-hover-raised" },
356
+ { "foreground": "--ui-text-secondary", "background": "--ui-list-item-success-bg-hover", "over": "--ui-surface-raised", "minimum": 4.5, "context": "list-item-description-success-hover-raised" },
357
+ { "foreground": "--ui-text-primary", "background": "--ui-list-item-warning-bg-hover", "over": "--ui-surface-raised", "minimum": 4.5, "context": "list-item-title-warning-hover-raised" },
358
+ { "foreground": "--ui-text-secondary", "background": "--ui-list-item-warning-bg-hover", "over": "--ui-surface-raised", "minimum": 4.5, "context": "list-item-description-warning-hover-raised" },
359
+ { "foreground": "--ui-text-primary", "background": "--ui-list-item-danger-bg-hover", "over": "--ui-surface-raised", "minimum": 4.5, "context": "list-item-title-danger-hover-raised" },
360
+ { "foreground": "--ui-text-secondary", "background": "--ui-list-item-danger-bg-hover", "over": "--ui-surface-raised", "minimum": 4.5, "context": "list-item-description-danger-hover-raised" },
361
+ { "foreground": "--ui-text-primary", "background": "--ui-list-item-info-bg-hover", "over": "--ui-surface-raised", "minimum": 4.5, "context": "list-item-title-info-hover-raised" },
362
+ { "foreground": "--ui-text-secondary", "background": "--ui-list-item-info-bg-hover", "over": "--ui-surface-raised", "minimum": 4.5, "context": "list-item-description-info-hover-raised" },
363
+ { "foreground": "--ui-list-item-title-hover", "background": "--ui-surface", "minimum": 4.5, "context": "list-item-link-hover" },
364
+ { "foreground": "--ui-list-item-title-hover", "background": "--ui-list-item-success-bg", "over": "--ui-surface", "minimum": 4.5, "context": "list-item-link-hover-success" },
365
+ { "foreground": "--ui-list-item-title-hover", "background": "--ui-list-item-success-bg-hover", "over": "--ui-surface", "minimum": 4.5, "context": "list-item-link-hover-success-hover" },
366
+ { "foreground": "--ui-list-item-title-hover", "background": "--ui-list-item-warning-bg", "over": "--ui-surface", "minimum": 4.5, "context": "list-item-link-hover-warning" },
367
+ { "foreground": "--ui-list-item-title-hover", "background": "--ui-list-item-warning-bg-hover", "over": "--ui-surface", "minimum": 4.5, "context": "list-item-link-hover-warning-hover" },
368
+ { "foreground": "--ui-list-item-title-hover", "background": "--ui-list-item-danger-bg", "over": "--ui-surface", "minimum": 4.5, "context": "list-item-link-hover-danger" },
369
+ { "foreground": "--ui-list-item-title-hover", "background": "--ui-list-item-danger-bg-hover", "over": "--ui-surface", "minimum": 4.5, "context": "list-item-link-hover-danger-hover" },
370
+ { "foreground": "--ui-list-item-title-hover", "background": "--ui-list-item-info-bg", "over": "--ui-surface", "minimum": 4.5, "context": "list-item-link-hover-info" },
371
+ { "foreground": "--ui-list-item-title-hover", "background": "--ui-list-item-info-bg-hover", "over": "--ui-surface", "minimum": 4.5, "context": "list-item-link-hover-info-hover" },
372
+ { "foreground": "--ui-list-item-title-hover", "background": "--ui-list-item-success-bg", "over": "--ui-surface-raised", "minimum": 4.5, "context": "list-item-link-hover-success-raised" },
373
+ { "foreground": "--ui-list-item-title-hover", "background": "--ui-list-item-success-bg-hover", "over": "--ui-surface-raised", "minimum": 4.5, "context": "list-item-link-hover-success-hover-raised" },
374
+ { "foreground": "--ui-list-item-title-hover", "background": "--ui-list-item-warning-bg", "over": "--ui-surface-raised", "minimum": 4.5, "context": "list-item-link-hover-warning-raised" },
375
+ { "foreground": "--ui-list-item-title-hover", "background": "--ui-list-item-warning-bg-hover", "over": "--ui-surface-raised", "minimum": 4.5, "context": "list-item-link-hover-warning-hover-raised" },
376
+ { "foreground": "--ui-list-item-title-hover", "background": "--ui-list-item-danger-bg", "over": "--ui-surface-raised", "minimum": 4.5, "context": "list-item-link-hover-danger-raised" },
377
+ { "foreground": "--ui-list-item-title-hover", "background": "--ui-list-item-danger-bg-hover", "over": "--ui-surface-raised", "minimum": 4.5, "context": "list-item-link-hover-danger-hover-raised" },
378
+ { "foreground": "--ui-list-item-title-hover", "background": "--ui-list-item-info-bg", "over": "--ui-surface-raised", "minimum": 4.5, "context": "list-item-link-hover-info-raised" },
379
+ { "foreground": "--ui-list-item-title-hover", "background": "--ui-list-item-info-bg-hover", "over": "--ui-surface-raised", "minimum": 4.5, "context": "list-item-link-hover-info-hover-raised" },
380
+ { "foreground": "--ui-list-item-done-title", "background": "--ui-surface-subtle", "over": "--ui-surface", "opacity": 0.75, "minimum": 4.5, "context": "list-item-done-title" },
381
+ { "foreground": "--ui-text-secondary", "background": "--ui-surface-subtle", "over": "--ui-surface", "opacity": 0.75, "minimum": 4.5, "context": "list-item-done-description" },
382
+ { "foreground": "--ui-control-checked-bg", "background": "--ui-surface-subtle", "over": "--ui-surface", "opacity": 0.75, "minimum": 3, "context": "list-item-done-circle-disc" },
383
+ { "foreground": "--ui-checkbox-mark", "background": "--ui-control-checked-bg", "group": "--ui-surface-subtle", "over": "--ui-surface", "opacity": 0.75, "minimum": 3, "context": "list-item-done-circle-mark" },
384
+ { "foreground": "--ui-list-item-done-title", "background": "--ui-surface-subtle", "over": "--ui-surface-raised", "opacity": 0.75, "minimum": 4.5, "context": "list-item-done-title-raised" },
385
+ { "foreground": "--ui-text-secondary", "background": "--ui-surface-subtle", "over": "--ui-surface-raised", "opacity": 0.75, "minimum": 4.5, "context": "list-item-done-description-raised" },
386
+ { "foreground": "--ui-control-checked-bg", "background": "--ui-surface-subtle", "over": "--ui-surface-raised", "opacity": 0.75, "minimum": 3, "context": "list-item-done-circle-disc-raised" },
387
+ { "foreground": "--ui-checkbox-mark", "background": "--ui-control-checked-bg", "group": "--ui-surface-subtle", "over": "--ui-surface-raised", "opacity": 0.75, "minimum": 3, "context": "list-item-done-circle-mark-raised" },
388
+ { "foreground": "--ui-list-item-done-title", "background": "--ui-surface-subtle", "over": "--ui-surface-subtle", "opacity": 0.75, "minimum": 4.5, "context": "list-item-done-title-subtle" },
389
+ { "foreground": "--ui-text-secondary", "background": "--ui-surface-subtle", "over": "--ui-surface-subtle", "opacity": 0.75, "minimum": 4.5, "context": "list-item-done-description-subtle" },
390
+ { "foreground": "--ui-control-checked-bg", "background": "--ui-surface-subtle", "over": "--ui-surface-subtle", "opacity": 0.75, "minimum": 3, "context": "list-item-done-circle-disc-subtle" },
391
+ { "foreground": "--ui-checkbox-mark", "background": "--ui-control-checked-bg", "group": "--ui-surface-subtle", "over": "--ui-surface-subtle", "opacity": 0.75, "minimum": 3, "context": "list-item-done-circle-mark-subtle" },
392
+ { "foreground": "--ui-list-item-done-ring", "background": "--ui-checkbox-circle-bg", "group": "--ui-surface-subtle", "over": "--ui-surface", "opacity": 0.75, "minimum": 3, "context": "list-item-done-circle-ring" },
393
+ { "foreground": "--ui-checkbox-circle-hover", "background": "--ui-checkbox-circle-bg", "group": "--ui-surface-subtle", "over": "--ui-surface", "opacity": 0.75, "minimum": 3, "context": "list-item-done-circle-ring-hover" },
394
+ { "foreground": "--ui-list-item-done-ring", "background": "--ui-checkbox-circle-bg", "group": "--ui-surface-subtle", "over": "--ui-surface-raised", "opacity": 0.75, "minimum": 3, "context": "list-item-done-circle-ring-raised" },
395
+ { "foreground": "--ui-checkbox-circle-hover", "background": "--ui-checkbox-circle-bg", "group": "--ui-surface-subtle", "over": "--ui-surface-raised", "opacity": 0.75, "minimum": 3, "context": "list-item-done-circle-ring-hover-raised" },
396
+ { "foreground": "--ui-list-item-done-ring", "background": "--ui-checkbox-circle-bg", "group": "--ui-surface-subtle", "over": "--ui-surface-subtle", "opacity": 0.75, "minimum": 3, "context": "list-item-done-circle-ring-subtle" },
397
+ { "foreground": "--ui-checkbox-circle-hover", "background": "--ui-checkbox-circle-bg", "group": "--ui-surface-subtle", "over": "--ui-surface-subtle", "opacity": 0.75, "minimum": 3, "context": "list-item-done-circle-ring-hover-subtle" },
398
+ { "foreground": "--ui-steps-upcoming-title", "background": "--ui-surface", "over": "--ui-surface", "opacity": 0.7, "minimum": 4.5, "context": "steps-upcoming-title" },
399
+ { "foreground": "--ui-steps-upcoming-description", "background": "--ui-surface", "over": "--ui-surface", "opacity": 0.7, "minimum": 4.5, "context": "steps-upcoming-description" },
400
+ { "foreground": "--ui-steps-upcoming-marker-text", "background": "--ui-steps-upcoming-marker-bg", "group": "--ui-surface", "over": "--ui-surface", "opacity": 0.7, "minimum": 4.5, "context": "steps-upcoming-marker" },
401
+ { "foreground": "--ui-steps-upcoming-title", "background": "--ui-surface", "over": "--ui-surface-raised", "opacity": 0.7, "minimum": 4.5, "context": "steps-upcoming-title-raised" },
402
+ { "foreground": "--ui-steps-upcoming-description", "background": "--ui-surface", "over": "--ui-surface-raised", "opacity": 0.7, "minimum": 4.5, "context": "steps-upcoming-description-raised" },
403
+ { "foreground": "--ui-steps-upcoming-marker-text", "background": "--ui-steps-upcoming-marker-bg", "group": "--ui-surface", "over": "--ui-surface-raised", "opacity": 0.7, "minimum": 4.5, "context": "steps-upcoming-marker-raised" },
404
+ { "foreground": "--ui-steps-upcoming-title", "background": "--ui-surface", "over": "--ui-surface-subtle", "opacity": 0.7, "minimum": 4.5, "context": "steps-upcoming-title-subtle" },
405
+ { "foreground": "--ui-steps-upcoming-description", "background": "--ui-surface", "over": "--ui-surface-subtle", "opacity": 0.7, "minimum": 4.5, "context": "steps-upcoming-description-subtle" },
406
+ { "foreground": "--ui-steps-upcoming-marker-text", "background": "--ui-steps-upcoming-marker-bg", "group": "--ui-surface", "over": "--ui-surface-subtle", "opacity": 0.7, "minimum": 4.5, "context": "steps-upcoming-marker-subtle" },
407
+ { "foreground": "--ui-message-incoming-document-text", "background": "--ui-message-incoming-document-bg", "over": "--ui-message-incoming-bg", "minimum": 4.5, "context": "message-incoming-document-text" },
408
+ { "foreground": "--ui-message-incoming-document-meta", "background": "--ui-message-incoming-document-bg", "over": "--ui-message-incoming-bg", "minimum": 4.5, "context": "message-incoming-document-meta" },
409
+ { "foreground": "--ui-message-incoming-document-download-icon", "background": "--ui-message-incoming-document-download-bg", "group": "--ui-message-incoming-document-bg", "over": "--ui-message-incoming-bg", "opacity": 1, "minimum": 3, "context": "message-incoming-document-download" },
410
+ { "foreground": "--ui-message-incoming-document-download-icon-hover", "background": "--ui-message-incoming-document-download-bg-hover", "group": "--ui-message-incoming-document-bg", "over": "--ui-message-incoming-bg", "opacity": 1, "minimum": 3, "context": "message-incoming-document-download-hover" },
411
+ { "foreground": "--ui-message-assistant-document-text", "background": "--ui-message-assistant-document-bg", "over": "--ui-message-assistant-bg", "minimum": 4.5, "context": "message-assistant-document-text" },
412
+ { "foreground": "--ui-message-assistant-document-meta", "background": "--ui-message-assistant-document-bg", "over": "--ui-message-assistant-bg", "minimum": 4.5, "context": "message-assistant-document-meta" },
413
+ { "foreground": "--ui-message-assistant-document-download-icon", "background": "--ui-message-assistant-document-download-bg", "group": "--ui-message-assistant-document-bg", "over": "--ui-message-assistant-bg", "opacity": 1, "minimum": 3, "context": "message-assistant-document-download" },
414
+ { "foreground": "--ui-message-assistant-document-download-icon-hover", "background": "--ui-message-assistant-document-download-bg-hover", "group": "--ui-message-assistant-document-bg", "over": "--ui-message-assistant-bg", "opacity": 1, "minimum": 3, "context": "message-assistant-document-download-hover" },
415
+ { "foreground": "--ui-message-outgoing-document-text", "background": "--ui-message-outgoing-document-bg", "over": "--ui-message-outgoing-bg", "minimum": 4.5, "context": "message-outgoing-document-text" },
416
+ { "foreground": "--ui-message-outgoing-document-meta", "background": "--ui-message-outgoing-document-bg", "over": "--ui-message-outgoing-bg", "minimum": 4.5, "context": "message-outgoing-document-meta" },
417
+ { "foreground": "--ui-message-outgoing-document-download-icon", "background": "--ui-message-outgoing-document-download-bg", "group": "--ui-message-outgoing-document-bg", "over": "--ui-message-outgoing-bg", "opacity": 1, "minimum": 3, "context": "message-outgoing-document-download" },
418
+ { "foreground": "--ui-message-outgoing-document-download-icon-hover", "background": "--ui-message-outgoing-document-download-bg-hover", "group": "--ui-message-outgoing-document-bg", "over": "--ui-message-outgoing-bg", "opacity": 1, "minimum": 3, "context": "message-outgoing-document-download-hover" },
419
+ { "foreground": "--ui-media-enlarge-icon", "background": "--ui-media-enlarge-bg", "group": "--ui-media-thumbnail-veil", "over": "--ui-surface", "opacity": 1, "minimum": 3, "context": "media-enlarge-icon" },
420
+ { "foreground": "--ui-message-incoming-audio-play-icon", "background": "--ui-message-incoming-audio-play-bg", "over": "--ui-message-incoming-bg", "minimum": 3, "context": "message-incoming-audio-play" },
421
+ { "foreground": "--ui-message-incoming-audio-wave-played", "background": "--ui-message-incoming-bg", "over": "--ui-surface", "minimum": 3, "context": "message-incoming-audio-wave-played" },
422
+ { "foreground": "--ui-message-incoming-audio-wave-unplayed", "background": "--ui-message-incoming-bg", "over": "--ui-surface", "minimum": 3, "context": "message-incoming-audio-wave-unplayed" },
423
+ { "foreground": "--ui-message-incoming-audio-playhead", "background": "--ui-message-incoming-bg", "over": "--ui-surface", "minimum": 3, "context": "message-incoming-audio-playhead" },
424
+ { "foreground": "--ui-message-incoming-audio-time", "background": "--ui-message-incoming-bg", "over": "--ui-surface", "minimum": 4.5, "context": "message-incoming-audio-time" },
425
+ { "foreground": "--ui-message-incoming-audio-speed-text", "background": "--ui-message-incoming-audio-speed-bg", "over": "--ui-message-incoming-bg", "minimum": 4.5, "context": "message-incoming-audio-speed" },
426
+ { "foreground": "--ui-message-assistant-audio-play-icon", "background": "--ui-message-assistant-audio-play-bg", "over": "--ui-message-assistant-bg", "minimum": 3, "context": "message-assistant-audio-play" },
427
+ { "foreground": "--ui-message-assistant-audio-wave-played", "background": "--ui-message-assistant-bg", "over": "--ui-surface", "minimum": 3, "context": "message-assistant-audio-wave-played" },
428
+ { "foreground": "--ui-message-assistant-audio-wave-unplayed", "background": "--ui-message-assistant-bg", "over": "--ui-surface", "minimum": 3, "context": "message-assistant-audio-wave-unplayed" },
429
+ { "foreground": "--ui-message-assistant-audio-playhead", "background": "--ui-message-assistant-bg", "over": "--ui-surface", "minimum": 3, "context": "message-assistant-audio-playhead" },
430
+ { "foreground": "--ui-message-assistant-audio-time", "background": "--ui-message-assistant-bg", "over": "--ui-surface", "minimum": 4.5, "context": "message-assistant-audio-time" },
431
+ { "foreground": "--ui-message-assistant-audio-speed-text", "background": "--ui-message-assistant-audio-speed-bg", "over": "--ui-message-assistant-bg", "minimum": 4.5, "context": "message-assistant-audio-speed" },
432
+ { "foreground": "--ui-message-outgoing-audio-play-icon", "background": "--ui-message-outgoing-audio-play-bg", "over": "--ui-message-outgoing-bg", "minimum": 3, "context": "message-outgoing-audio-play" },
433
+ { "foreground": "--ui-message-outgoing-audio-wave-played", "background": "--ui-message-outgoing-bg", "over": "--ui-surface", "minimum": 3, "context": "message-outgoing-audio-wave-played" },
434
+ { "foreground": "--ui-message-outgoing-audio-wave-unplayed", "background": "--ui-message-outgoing-bg", "over": "--ui-surface", "minimum": 3, "context": "message-outgoing-audio-wave-unplayed" },
435
+ { "foreground": "--ui-message-outgoing-audio-playhead", "background": "--ui-message-outgoing-bg", "over": "--ui-surface", "minimum": 3, "context": "message-outgoing-audio-playhead" },
436
+ { "foreground": "--ui-message-outgoing-audio-time", "background": "--ui-message-outgoing-bg", "over": "--ui-surface", "minimum": 4.5, "context": "message-outgoing-audio-time" },
437
+ { "foreground": "--ui-message-outgoing-audio-speed-text", "background": "--ui-message-outgoing-audio-speed-bg", "over": "--ui-message-outgoing-bg", "minimum": 4.5, "context": "message-outgoing-audio-speed" },
438
+ { "foreground": "--ui-message-incoming-audio-play-icon", "background": "--ui-message-incoming-audio-play-bg", "over": "--ui-surface", "minimum": 3, "context": "audio-play" },
439
+ { "foreground": "--ui-message-incoming-audio-wave-played", "background": "--ui-surface", "minimum": 3, "context": "audio-wave-played" },
440
+ { "foreground": "--ui-message-incoming-audio-wave-unplayed", "background": "--ui-surface", "minimum": 3, "context": "audio-wave-unplayed" },
441
+ { "foreground": "--ui-message-incoming-audio-playhead", "background": "--ui-surface", "minimum": 3, "context": "audio-playhead" },
442
+ { "foreground": "--ui-message-incoming-audio-time", "background": "--ui-surface", "minimum": 4.5, "context": "audio-time" },
443
+ { "foreground": "--ui-message-incoming-audio-speed-text", "background": "--ui-message-incoming-audio-speed-bg", "over": "--ui-surface", "minimum": 4.5, "context": "audio-speed" },
444
+ { "foreground": "--ui-message-incoming-audio-play-icon", "background": "--ui-message-incoming-audio-play-bg", "over": "--ui-surface-raised", "minimum": 3, "context": "audio-play-raised" },
445
+ { "foreground": "--ui-message-incoming-audio-wave-played", "background": "--ui-surface-raised", "minimum": 3, "context": "audio-wave-played-raised" },
446
+ { "foreground": "--ui-message-incoming-audio-wave-unplayed", "background": "--ui-surface-raised", "minimum": 3, "context": "audio-wave-unplayed-raised" },
447
+ { "foreground": "--ui-message-incoming-audio-playhead", "background": "--ui-surface-raised", "minimum": 3, "context": "audio-playhead-raised" },
448
+ { "foreground": "--ui-message-incoming-audio-time", "background": "--ui-surface-raised", "minimum": 4.5, "context": "audio-time-raised" },
449
+ { "foreground": "--ui-message-incoming-audio-speed-text", "background": "--ui-message-incoming-audio-speed-bg", "over": "--ui-surface-raised", "minimum": 4.5, "context": "audio-speed-raised" },
450
+ { "foreground": "--ui-message-incoming-audio-play-icon", "background": "--ui-message-incoming-audio-play-bg", "over": "--ui-surface-subtle", "minimum": 3, "context": "audio-play-subtle" },
451
+ { "foreground": "--ui-message-incoming-audio-wave-played", "background": "--ui-surface-subtle", "minimum": 3, "context": "audio-wave-played-subtle" },
452
+ { "foreground": "--ui-message-incoming-audio-wave-unplayed", "background": "--ui-surface-subtle", "minimum": 3, "context": "audio-wave-unplayed-subtle" },
453
+ { "foreground": "--ui-message-incoming-audio-playhead", "background": "--ui-surface-subtle", "minimum": 3, "context": "audio-playhead-subtle" },
454
+ { "foreground": "--ui-message-incoming-audio-time", "background": "--ui-surface-subtle", "minimum": 4.5, "context": "audio-time-subtle" },
455
+ { "foreground": "--ui-message-incoming-audio-speed-text", "background": "--ui-message-incoming-audio-speed-bg", "over": "--ui-surface-subtle", "minimum": 4.5, "context": "audio-speed-subtle" }
321
456
  ]
322
457
  }
@@ -1,7 +1,73 @@
1
1
  import { isComputedPropertyName, isIdentifier, isJsxAttribute, isJsxExpression, isObjectLiteralExpression, isPropertyAssignment, isJsxSpreadAttribute, isStringLiteralLikeNode, } from "typescript/unstable/ast";
2
- import { composite, contrastRatio, isColorBearingProperty, resolveColor, } from "../color.js";
2
+ import { composite, contrastRatio, faded, isColorBearingProperty, mix, paintOver, resolveColor, } from "../color.js";
3
3
  import { parseContrastManifest, } from "../contracts.js";
4
4
  import { createThemeCollector } from "./theme.js";
5
+ // The end of a gradient (#394): a token named <start>-end, when the start is
6
+ // a token too, paints a gradient from the start to itself. A pair on it is
7
+ // measured along the whole gradient, and not only at the end: between two
8
+ // colors that pass, the mix can fall below the minimum, as when the text's
9
+ // luminance lies between the two ends. Each point is taken in sRGB and in
10
+ // Oklab, the two spaces a browser may interpolate in.
11
+ const gradientSteps = 16;
12
+ function resolveAlong(token, tokens) {
13
+ const end = resolveColor(tokens.get(token), tokens);
14
+ const startName = token.endsWith("-end") ? token.slice(0, -"-end".length) : undefined;
15
+ if (!startName || !tokens.has(startName))
16
+ return [end];
17
+ const start = resolveColor(tokens.get(startName), tokens);
18
+ if (start.status !== "resolved" || end.status !== "resolved")
19
+ return [{ status: "uncovered" }];
20
+ return Array.from({ length: gradientSteps + 1 }, (_, step) => mix(start.color, end.color, step / gradientSteps))
21
+ .flat()
22
+ .map((color) => ({ color, status: "resolved" }));
23
+ }
24
+ function groupRatio(pair, tokens) {
25
+ const opacity = pair.opacity;
26
+ const foreground = resolveColor(tokens.get(pair.foreground), tokens);
27
+ const backgrounds = resolveAlong(pair.background, tokens);
28
+ const groups = pair.group === undefined ? [undefined] : resolveAlong(pair.group, tokens);
29
+ const surfaces = pair.over === undefined ? [] : resolveAlong(pair.over, tokens);
30
+ if (foreground.status !== "resolved" ||
31
+ [...backgrounds, ...groups, ...surfaces].some((layer) => layer !== undefined && layer.status !== "resolved") ||
32
+ surfaces.length === 0) {
33
+ return "uncovered";
34
+ }
35
+ let worst = Infinity;
36
+ for (const background of backgrounds) {
37
+ for (const group of groups) {
38
+ if (background.status !== "resolved" || group?.status === "uncovered")
39
+ return "uncovered";
40
+ const inner = group?.status === "resolved" ? paintOver(background.color, group.color) : background.color;
41
+ const text = paintOver(foreground.color, inner);
42
+ for (const surface of surfaces) {
43
+ if (surface.status !== "resolved" || (surface.color.alpha ?? 1) < 1)
44
+ return "translucent";
45
+ const seenBackground = composite(faded(inner, opacity), surface.color);
46
+ const seenText = composite(faded(text, opacity), surface.color);
47
+ const ratio = contrastRatio(seenText, seenBackground);
48
+ if (ratio === undefined)
49
+ return "translucent";
50
+ worst = Math.min(worst, ratio);
51
+ }
52
+ }
53
+ }
54
+ return worst;
55
+ }
56
+ function reportPair(result, location, themeName, pair, ratio) {
57
+ // A group pair (schema 3) may differ from another only by its group, the
58
+ // surface outside or the opacity: they are in its fingerprint. The pairs
59
+ // before schema 3 keep theirs, so a baseline stays valid.
60
+ const nodeKind = `contrast-pair:${pair.context}:${pair.foreground}:${pair.background}:${pair.group ?? ""}:${pair.over}:${pair.opacity}`;
61
+ if (ratio === "uncovered") {
62
+ result.uncovered.push({ ...location, message: `Contrast pair ${pair.context} could not be resolved.`, nodeKind, rule: "tokens/contrast", required: true });
63
+ }
64
+ else if (ratio === "translucent") {
65
+ result.uncovered.push({ ...location, message: `Contrast pair ${pair.context} requires a known opaque background.`, nodeKind, rule: "tokens/contrast", required: true });
66
+ }
67
+ else if (ratio < pair.minimum) {
68
+ result.violations.push({ ...location, anchor: themeName, message: `Contrast pair ${pair.context} is below ${pair.minimum}:1.`, nodeKind, rule: "tokens/contrast" });
69
+ }
70
+ }
5
71
  export async function loadContrastManifest(url) {
6
72
  const { readFile } = await import("node:fs/promises");
7
73
  const packaged = url ?? new URL("../contrast-contract.json", import.meta.url);
@@ -98,8 +164,9 @@ export function createTokenContrastRule({ computed = {}, defaults = {}, derived
98
164
  for (const theme of collector.themes) {
99
165
  const location = findingLocation(theme);
100
166
  const computedTheme = computed[theme.name] ?? {};
101
- const hasCompleteComputedEvidence = manifest.pairs.every(({ background, foreground }) => computedTheme[background] !== undefined &&
102
- computedTheme[foreground] !== undefined);
167
+ // Every layer a pair reads: the text, its background, the surface
168
+ // under a translucent one and a group's background.
169
+ const hasCompleteComputedEvidence = manifest.pairs.every((pair) => [pair.foreground, pair.background, pair.over, pair.group].every((token) => token === undefined || computedTheme[token] !== undefined));
103
170
  if (theme.overlapping && !hasCompleteComputedEvidence) {
104
171
  result.uncovered.push({
105
172
  ...location,
@@ -111,7 +178,7 @@ export function createTokenContrastRule({ computed = {}, defaults = {}, derived
111
178
  continue;
112
179
  }
113
180
  const missingComputedEndpoints = theme.unresolved
114
- ? manifest.pairs.flatMap(({ background, foreground }) => [background, foreground].filter((token) => computedTheme[token] === undefined))
181
+ ? manifest.pairs.flatMap(({ background, foreground, group, over }) => [background, foreground, over, group].filter((token) => token !== undefined && computedTheme[token] === undefined))
115
182
  : [];
116
183
  if (missingComputedEndpoints.length > 0) {
117
184
  result.uncovered.push({
@@ -144,17 +211,30 @@ export function createTokenContrastRule({ computed = {}, defaults = {}, derived
144
211
  tokens.set(token, value);
145
212
  }
146
213
  for (const pair of manifest.pairs) {
147
- let background = resolveColor(tokens.get(pair.background), tokens);
214
+ if (pair.opacity !== undefined) {
215
+ const ratio = groupRatio(pair, tokens);
216
+ reportPair(result, location, theme.name, pair, ratio);
217
+ continue;
218
+ }
148
219
  const foreground = resolveColor(tokens.get(pair.foreground), tokens);
149
- // A translucent background is measured as painted over the surface
150
- // the pair names (schema 2); without one it stays uncovered below.
151
- if (pair.over && background.status === "resolved" && (background.color.alpha ?? 1) < 1) {
152
- const over = resolveColor(tokens.get(pair.over), tokens);
153
- background = over.status === "resolved" && (over.color.alpha ?? 1) === 1
154
- ? { color: composite(background.color, over.color), status: "resolved" }
155
- : { status: "uncovered" };
220
+ // Each point of the background, a gradient's included, and of the
221
+ // surface under it; the pair is as good as its worst point.
222
+ const backgrounds = [];
223
+ for (const painted of resolveAlong(pair.background, tokens)) {
224
+ // A translucent background is measured as painted over the surface
225
+ // the pair names (schema 2); without one it stays uncovered below.
226
+ if (pair.over && painted.status === "resolved" && (painted.color.alpha ?? 1) < 1) {
227
+ for (const over of resolveAlong(pair.over, tokens)) {
228
+ backgrounds.push(over.status === "resolved" && (over.color.alpha ?? 1) === 1
229
+ ? { color: composite(painted.color, over.color), status: "resolved" }
230
+ : { status: "uncovered" });
231
+ }
232
+ }
233
+ else {
234
+ backgrounds.push(painted);
235
+ }
156
236
  }
157
- if (background.status === "uncovered" ||
237
+ if (backgrounds.some((background) => background.status === "uncovered") ||
158
238
  foreground.status === "uncovered") {
159
239
  result.uncovered.push({
160
240
  ...location,
@@ -165,7 +245,8 @@ export function createTokenContrastRule({ computed = {}, defaults = {}, derived
165
245
  });
166
246
  continue;
167
247
  }
168
- const ratio = contrastRatio(foreground.color, background.color);
248
+ const ratios = backgrounds.map((background) => background.status === "resolved" ? contrastRatio(foreground.color, background.color) : undefined);
249
+ const ratio = ratios.includes(undefined) ? undefined : Math.min(...ratios);
169
250
  if (ratio === undefined) {
170
251
  result.uncovered.push({
171
252
  ...location,
@@ -1,5 +1,5 @@
1
1
  {
2
- "contractVersion": "3.3.0",
2
+ "contractVersion": "3.5.0",
3
3
  "schemaVersion": 2,
4
4
  "prefix": "--ui-",
5
5
  "required": [
@@ -320,7 +320,78 @@
320
320
  "--ui-board-count-text": { "kind": "token", "token": "--ui-text-primary" },
321
321
  "--ui-board-card-border-hover": { "kind": "token", "token": "--ui-action-primary-bg" },
322
322
  "--ui-board-drop-bg": { "kind": "token", "token": "--ui-action-soft-accent-bg" },
323
- "--ui-board-drop-border": { "kind": "token", "token": "--ui-action-primary-bg" }
323
+ "--ui-board-drop-border": { "kind": "token", "token": "--ui-action-primary-bg" },
324
+ "--ui-surface-inverse-end": { "kind": "token", "token": "--ui-surface-inverse" },
325
+ "--ui-checkbox-circle-bg": { "kind": "literal", "value": "transparent" },
326
+ "--ui-checkbox-circle-hover": { "kind": "token", "token": "--ui-control-checked-bg" },
327
+ "--ui-list-item-border-hover": { "kind": "token", "token": "--ui-border-strong" },
328
+ "--ui-list-item-success-bg-hover": { "kind": "token", "token": "--ui-list-item-success-bg" },
329
+ "--ui-list-item-warning-bg-hover": { "kind": "token", "token": "--ui-list-item-warning-bg" },
330
+ "--ui-list-item-danger-bg-hover": { "kind": "token", "token": "--ui-list-item-danger-bg" },
331
+ "--ui-list-item-info-bg-hover": { "kind": "token", "token": "--ui-list-item-info-bg" },
332
+ "--ui-list-item-shadow-color": { "kind": "literal", "value": "transparent" },
333
+ "--ui-list-item-done-border": { "kind": "token", "token": "--ui-border" },
334
+ "--ui-list-item-done-title": { "kind": "token", "token": "--ui-text-secondary" },
335
+ "--ui-list-item-done-ring": { "kind": "token", "token": "--ui-text-secondary" },
336
+ "--ui-list-item-title-hover": { "kind": "token", "token": "--ui-link-text" },
337
+ "--ui-steps-complete-marker-bg": { "kind": "token", "token": "--ui-surface" },
338
+ "--ui-steps-upcoming-title": { "kind": "token", "token": "--ui-text-primary" },
339
+ "--ui-steps-upcoming-description": { "kind": "token", "token": "--ui-text-primary" },
340
+ "--ui-steps-upcoming-marker-bg": { "kind": "token", "token": "--ui-surface-subtle" },
341
+ "--ui-steps-upcoming-marker-text": { "kind": "token", "token": "--ui-text-primary" },
342
+ "--ui-media-thumbnail-veil": { "kind": "token", "token": "--ui-media-overlay-bg" },
343
+ "--ui-media-enlarge-bg": { "kind": "literal", "value": "transparent" },
344
+ "--ui-media-enlarge-icon": { "kind": "token", "token": "--ui-media-overlay-text" },
345
+ "--ui-media-enlarge-shadow-color": { "kind": "literal", "value": "transparent" },
346
+ "--ui-message-incoming-document-bg": { "kind": "token", "token": "--ui-surface" },
347
+ "--ui-message-incoming-document-border": { "kind": "token", "token": "--ui-border" },
348
+ "--ui-message-incoming-document-text": { "kind": "token", "token": "--ui-text-primary" },
349
+ "--ui-message-incoming-document-meta": { "kind": "token", "token": "--ui-text-secondary" },
350
+ "--ui-message-incoming-document-download-bg": { "kind": "literal", "value": "transparent" },
351
+ "--ui-message-incoming-document-download-bg-hover": { "kind": "token", "token": "--ui-action-ghost-bg-hover" },
352
+ "--ui-message-incoming-document-download-icon": { "kind": "token", "token": "--ui-action-ghost-text" },
353
+ "--ui-message-incoming-document-download-icon-hover": { "kind": "token", "token": "--ui-action-ghost-text-hover" },
354
+ "--ui-message-assistant-document-bg": { "kind": "token", "token": "--ui-surface" },
355
+ "--ui-message-assistant-document-border": { "kind": "token", "token": "--ui-border" },
356
+ "--ui-message-assistant-document-text": { "kind": "token", "token": "--ui-text-primary" },
357
+ "--ui-message-assistant-document-meta": { "kind": "token", "token": "--ui-text-secondary" },
358
+ "--ui-message-assistant-document-download-bg": { "kind": "literal", "value": "transparent" },
359
+ "--ui-message-assistant-document-download-bg-hover": { "kind": "token", "token": "--ui-action-ghost-bg-hover" },
360
+ "--ui-message-assistant-document-download-icon": { "kind": "token", "token": "--ui-action-ghost-text" },
361
+ "--ui-message-assistant-document-download-icon-hover": { "kind": "token", "token": "--ui-action-ghost-text-hover" },
362
+ "--ui-message-outgoing-document-bg": { "kind": "token", "token": "--ui-surface" },
363
+ "--ui-message-outgoing-document-border": { "kind": "token", "token": "--ui-border" },
364
+ "--ui-message-outgoing-document-text": { "kind": "token", "token": "--ui-text-primary" },
365
+ "--ui-message-outgoing-document-meta": { "kind": "token", "token": "--ui-text-secondary" },
366
+ "--ui-message-outgoing-document-download-bg": { "kind": "literal", "value": "transparent" },
367
+ "--ui-message-outgoing-document-download-bg-hover": { "kind": "token", "token": "--ui-action-ghost-bg-hover" },
368
+ "--ui-message-outgoing-document-download-icon": { "kind": "token", "token": "--ui-action-ghost-text" },
369
+ "--ui-message-outgoing-document-download-icon-hover": { "kind": "token", "token": "--ui-action-ghost-text-hover" },
370
+ "--ui-message-incoming-audio-play-bg": { "kind": "token", "token": "--ui-action-primary-bg" },
371
+ "--ui-message-incoming-audio-play-icon": { "kind": "token", "token": "--ui-action-primary-text" },
372
+ "--ui-message-incoming-audio-wave-played": { "kind": "token", "token": "--ui-action-primary-bg" },
373
+ "--ui-message-incoming-audio-wave-unplayed": { "kind": "token", "token": "--ui-border-strong" },
374
+ "--ui-message-incoming-audio-playhead": { "kind": "token", "token": "--ui-message-incoming-text" },
375
+ "--ui-message-incoming-audio-time": { "kind": "token", "token": "--ui-message-incoming-meta" },
376
+ "--ui-message-incoming-audio-speed-bg": { "kind": "token", "token": "--ui-surface-subtle" },
377
+ "--ui-message-incoming-audio-speed-text": { "kind": "token", "token": "--ui-text-primary" },
378
+ "--ui-message-assistant-audio-play-bg": { "kind": "token", "token": "--ui-action-primary-bg" },
379
+ "--ui-message-assistant-audio-play-icon": { "kind": "token", "token": "--ui-action-primary-text" },
380
+ "--ui-message-assistant-audio-wave-played": { "kind": "token", "token": "--ui-action-primary-bg" },
381
+ "--ui-message-assistant-audio-wave-unplayed": { "kind": "token", "token": "--ui-border-strong" },
382
+ "--ui-message-assistant-audio-playhead": { "kind": "token", "token": "--ui-message-assistant-text" },
383
+ "--ui-message-assistant-audio-time": { "kind": "token", "token": "--ui-message-assistant-meta" },
384
+ "--ui-message-assistant-audio-speed-bg": { "kind": "token", "token": "--ui-surface-subtle" },
385
+ "--ui-message-assistant-audio-speed-text": { "kind": "token", "token": "--ui-text-primary" },
386
+ "--ui-message-outgoing-audio-play-bg": { "kind": "token", "token": "--ui-action-primary-text" },
387
+ "--ui-message-outgoing-audio-play-icon": { "kind": "token", "token": "--ui-action-primary-bg" },
388
+ "--ui-message-outgoing-audio-wave-played": { "kind": "token", "token": "--ui-action-primary-text" },
389
+ "--ui-message-outgoing-audio-wave-unplayed": { "kind": "literal", "value": "rgb(255 255 255 / 60%)" },
390
+ "--ui-message-outgoing-audio-playhead": { "kind": "token", "token": "--ui-message-outgoing-text" },
391
+ "--ui-message-outgoing-audio-time": { "kind": "token", "token": "--ui-message-outgoing-meta" },
392
+ "--ui-message-outgoing-audio-speed-bg": { "kind": "literal", "value": "rgb(0 0 0 / 20%)" },
393
+ "--ui-message-outgoing-audio-speed-text": { "kind": "token", "token": "--ui-action-primary-text" },
394
+ "--ui-audio-play-shadow-color": { "kind": "literal", "value": "transparent" }
324
395
  },
325
396
  "shape": [
326
397
  "--ui-font-body",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ksmv/ui-checks",
3
- "version": "0.22.0",
3
+ "version": "0.23.2",
4
4
  "description": "Static and computed quality checks for semantic UI token contracts.",
5
5
  "keywords": ["accessibility", "design-tokens", "quality", "static-analysis"],
6
6
  "license": "Apache-2.0",