@ksmv/ui-checks 0.22.0 → 0.22.1
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 +6 -0
- package/README.md +1 -1
- package/dist/color.d.ts +1 -0
- package/dist/color.js +16 -0
- package/dist/contrast-contract.json +22 -2
- package/dist/rules/tokenContrast.js +39 -11
- package/dist/theme-contract.json +3 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.22.1 - 2026-10-17
|
|
4
|
+
|
|
5
|
+
- **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.
|
|
6
|
+
- **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.
|
|
7
|
+
- **Quem usa `computedEvidence` precisa capturar a evidência de novo:** os dois contratos mudaram.
|
|
8
|
+
|
|
3
9
|
## 0.22.0 - 2026-10-17
|
|
4
10
|
|
|
5
11
|
- **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ê), 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
|
|
package/dist/color.d.ts
CHANGED
|
@@ -19,4 +19,5 @@ 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
21
|
export declare function composite(foreground: RgbColor, background: RgbColor): RgbColor;
|
|
22
|
+
export declare function mix(start: RgbColor, end: RgbColor, at: number): RgbColor[];
|
|
22
23
|
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",
|
|
@@ -195,6 +196,21 @@ export function composite(foreground, background) {
|
|
|
195
196
|
r: foreground.r * alpha + background.r * (1 - alpha),
|
|
196
197
|
};
|
|
197
198
|
}
|
|
199
|
+
// A point of a gradient between two colors. The browser interpolates legacy
|
|
200
|
+
// colors (hex, rgb(), hsl()) in sRGB and the others, as oklch(), in Oklab
|
|
201
|
+
// (CSS Color 4, §12.1); the space is chosen by the syntax, so the check takes
|
|
202
|
+
// both points. Both interpolate with premultiplied alpha (§12.3): a channel
|
|
203
|
+
// is weighted by its color's alpha, so a transparent end lends no hue.
|
|
204
|
+
export function mix(start, end, at) {
|
|
205
|
+
const [startAlpha, endAlpha] = [start.alpha ?? 1, end.alpha ?? 1];
|
|
206
|
+
const alpha = startAlpha + (endAlpha - startAlpha) * at;
|
|
207
|
+
const between = (from, to) => alpha === 0 ? 0 : (from * startAlpha + (to * endAlpha - from * startAlpha) * at) / alpha;
|
|
208
|
+
const srgb = { alpha, b: between(start.b, end.b), g: between(start.g, end.g), mode: "rgb", r: between(start.r, end.r) };
|
|
209
|
+
const [from, to] = [toOklab(start), toOklab(end)];
|
|
210
|
+
const clamp = (value) => Math.min(1, Math.max(0, value));
|
|
211
|
+
const oklab = toRgb({ a: between(from.a, to.a), alpha, b: between(from.b, to.b), l: between(from.l, to.l), mode: "oklab" });
|
|
212
|
+
return [srgb, { alpha, b: clamp(oklab.b), g: clamp(oklab.g), mode: "rgb", r: clamp(oklab.r) }];
|
|
213
|
+
}
|
|
198
214
|
function luminance(color) {
|
|
199
215
|
const channel = (value) => value <= 0.04045
|
|
200
216
|
? value / 12.92
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"contractVersion": "
|
|
2
|
+
"contractVersion": "12.0.0",
|
|
3
3
|
"schemaVersion": 2,
|
|
4
4
|
"pairs": [
|
|
5
5
|
{ "foreground": "--ui-action-primary-text", "background": "--ui-action-primary-bg", "minimum": 4.5, "context": "normal-text" },
|
|
@@ -317,6 +317,26 @@
|
|
|
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" }
|
|
321
341
|
]
|
|
322
342
|
}
|
|
@@ -1,7 +1,26 @@
|
|
|
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, isColorBearingProperty, mix, 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
|
+
}
|
|
5
24
|
export async function loadContrastManifest(url) {
|
|
6
25
|
const { readFile } = await import("node:fs/promises");
|
|
7
26
|
const packaged = url ?? new URL("../contrast-contract.json", import.meta.url);
|
|
@@ -144,17 +163,25 @@ export function createTokenContrastRule({ computed = {}, defaults = {}, derived
|
|
|
144
163
|
tokens.set(token, value);
|
|
145
164
|
}
|
|
146
165
|
for (const pair of manifest.pairs) {
|
|
147
|
-
let background = resolveColor(tokens.get(pair.background), tokens);
|
|
148
166
|
const foreground = resolveColor(tokens.get(pair.foreground), tokens);
|
|
149
|
-
//
|
|
150
|
-
// the pair
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
167
|
+
// Each point of the background, a gradient's included, and of the
|
|
168
|
+
// surface under it; the pair is as good as its worst point.
|
|
169
|
+
const backgrounds = [];
|
|
170
|
+
for (const painted of resolveAlong(pair.background, tokens)) {
|
|
171
|
+
// A translucent background is measured as painted over the surface
|
|
172
|
+
// the pair names (schema 2); without one it stays uncovered below.
|
|
173
|
+
if (pair.over && painted.status === "resolved" && (painted.color.alpha ?? 1) < 1) {
|
|
174
|
+
for (const over of resolveAlong(pair.over, tokens)) {
|
|
175
|
+
backgrounds.push(over.status === "resolved" && (over.color.alpha ?? 1) === 1
|
|
176
|
+
? { color: composite(painted.color, over.color), status: "resolved" }
|
|
177
|
+
: { status: "uncovered" });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
backgrounds.push(painted);
|
|
182
|
+
}
|
|
156
183
|
}
|
|
157
|
-
if (background.status === "uncovered" ||
|
|
184
|
+
if (backgrounds.some((background) => background.status === "uncovered") ||
|
|
158
185
|
foreground.status === "uncovered") {
|
|
159
186
|
result.uncovered.push({
|
|
160
187
|
...location,
|
|
@@ -165,7 +192,8 @@ export function createTokenContrastRule({ computed = {}, defaults = {}, derived
|
|
|
165
192
|
});
|
|
166
193
|
continue;
|
|
167
194
|
}
|
|
168
|
-
const
|
|
195
|
+
const ratios = backgrounds.map((background) => background.status === "resolved" ? contrastRatio(foreground.color, background.color) : undefined);
|
|
196
|
+
const ratio = ratios.includes(undefined) ? undefined : Math.min(...ratios);
|
|
169
197
|
if (ratio === undefined) {
|
|
170
198
|
result.uncovered.push({
|
|
171
199
|
...location,
|
package/dist/theme-contract.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"contractVersion": "3.
|
|
2
|
+
"contractVersion": "3.4.0",
|
|
3
3
|
"schemaVersion": 2,
|
|
4
4
|
"prefix": "--ui-",
|
|
5
5
|
"required": [
|
|
@@ -320,7 +320,8 @@
|
|
|
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" }
|
|
324
325
|
},
|
|
325
326
|
"shape": [
|
|
326
327
|
"--ui-font-body",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ksmv/ui-checks",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.1",
|
|
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",
|