@ksmv/ui-checks 0.29.0 → 0.30.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 +11 -0
- package/README.md +4 -2
- package/dist/rules/overrideCatalog.js +5 -2
- package/dist/rules/tokenContrast.js +24 -2
- package/dist/scan.js +21 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.30.1 - 2026-10-30
|
|
4
|
+
|
|
5
|
+
- **Sai junto com o `@ksmv/ui-react` 0.30.1, sem mudança nas regras:** os contratos continuam tema 3.12.0 e contraste 20.0.0. A correção do `Combobox` não muda nenhuma cor do contrato (ver o CHANGELOG do `@ksmv/ui-react`).
|
|
6
|
+
|
|
7
|
+
## 0.30.0 - 2026-10-30
|
|
8
|
+
|
|
9
|
+
- **A `Surface` arranja os filhos, como o `Card`** (#606). Em todos os tons, o arranjo dos filhos pela `className` (`flex`, `grid`, `gap-*`, `space-*`, `items-*`, `justify-*` e as propriedades de `style` equivalentes) passa a ser livre em `components/override`. O fundo, o padding e a borda continuam exigindo `data-ui-override`.
|
|
10
|
+
- **Um arquivo de tema fora do `include` é erro de configuração** (#608). Quando um arquivo de `themes[].files` existe mas o `include` o deixa de fora, a saída é 2, com a mensagem que diz qual arquivo acrescentar. Antes, as declarações dele não eram lidas, e o tema aparecia como incompleto e os pares como sem cobertura, sem dizer por quê. Vale também para um tema com `computedEvidence`, porque a regra de forma e as declarações leem o arquivo. **Quem aponta o tema em `node_modules`** acrescenta o arquivo ao `include`.
|
|
11
|
+
- **Spread de props escritas** (#608). Em `tokens/contrast`, um spread de objeto literal em JSX, ou da escolha entre dois (`{...(pronto ? { onBuscar } : {})}`), em que nenhuma prop é `className`, `class`, `style`, `dangerouslySetInnerHTML` ou de cor, passa a ficar coberto. Os outros spreads continuam sem cobertura.
|
|
12
|
+
- **Contratos:** sem mudança, tema `3.12.0` e contraste `20.0.0`.
|
|
13
|
+
|
|
3
14
|
## 0.29.0 - 2026-10-28
|
|
4
15
|
|
|
5
16
|
- **Contratos de tema 3.12.0 e contraste 20.0.0** empacotados.
|
package/README.md
CHANGED
|
@@ -45,7 +45,9 @@ export default defineConfig({
|
|
|
45
45
|
|
|
46
46
|
O valor de `selector` deve ser **textualmente idêntico** ao seletor escrito no CSS, inclusive nas aspas. O verificador não tenta provar que grafias diferentes são equivalentes.
|
|
47
47
|
|
|
48
|
-
`include` precisa encontrar arquivos: nenhum arquivo analisado é falha, não sucesso. `exclude` e `suppressions` são opcionais.
|
|
48
|
+
`include` precisa encontrar arquivos: nenhum arquivo analisado é falha, não sucesso. `exclude` e `suppressions` são opcionais. Os arquivos de `themes[].files` são lidos como os outros do `include`. Por isso, um tema de fora da pasta do app, como `node_modules/@ksmv/ui-react/dist/themes/neutral.css`, também entra no `include`. Desde a 0.30.0, um arquivo de tema que existe mas fica fora do `include` é erro de configuração (saída 2), com a mensagem que diz qual arquivo acrescentar. Antes, o tema aparecia como incompleto e os pares como sem cobertura, sem dizer por quê. Vale também para um tema com `computedEvidence`: a regra de forma e as declarações continuam lendo o arquivo.
|
|
49
|
+
|
|
50
|
+
Um spread em JSX (`{...props}`) fica sem cobertura em `tokens/contrast`, porque pode levar `className`, `style` ou uma cor que a regra não lê. Desde a 0.30.0, o spread de props escritas no código, seja um objeto literal ou a escolha entre dois (`{...(pronto ? { onBuscar } : {})}`), em que nenhuma é `className`, `class`, `style`, `dangerouslySetInnerHTML` ou uma propriedade de cor, fica coberto.
|
|
49
51
|
|
|
50
52
|
## Baseline
|
|
51
53
|
|
|
@@ -176,7 +178,7 @@ components: {
|
|
|
176
178
|
```
|
|
177
179
|
|
|
178
180
|
- **Livre:** o que encaixa o componente no pai — margem, largura, item de flex/grid (`flex-1`, `self-*`, `col-span-*`…), posição, `z-*`, visibilidade (`hidden`, `sr-only`) e alinhamento do texto. No `style`, as propriedades equivalentes.
|
|
179
|
-
- **Exige `data-ui-override="motivo"` no mesmo elemento:** aparência (cor, fundo, borda, raio, sombra, fonte, altura, padding, `gap-*`, `overflow-*`), arranjo interno (`flex`, `grid-cols-*`, `justify-*`, `items-*`, `block`) e qualquer classe fora do catálogo, inclusive classes próprias do produto. Exceção, desde a 0.24.0: o `Card` é um bloco cujos filhos o consumidor arranja pela `className`, como a casa,
|
|
181
|
+
- **Exige `data-ui-override="motivo"` no mesmo elemento:** aparência (cor, fundo, borda, raio, sombra, fonte, altura, padding, `gap-*`, `overflow-*`), arranjo interno (`flex`, `grid-cols-*`, `justify-*`, `items-*`, `block`) e qualquer classe fora do catálogo, inclusive classes próprias do produto. Exceção, desde a 0.24.0: o `Card` é um bloco cujos filhos o consumidor arranja pela `className`, como a casa, e desde a 0.30.0 a `Surface` também, em todos os tons. Então nos dois o arranjo dos filhos é livre (`block`, `flex`, `grid`, `space-x-*`, `space-y-*`, `gap-*`, `flex-col`, `grid-cols-*`, `items-*`, `justify-*` e afins, e as propriedades de `style` equivalentes, com o `display` só nos valores de arranjo escritos no código: `block`, `flex`, `inline-flex`, `grid`, `inline-grid` e `flow-root`; `contents`, `none` ou um valor em variável exigem a marca); uma divisória entre os filhos (`divide-*`), um fundo ou um padding continuam exigindo a marca. Também exigem a marca `className` ou `style` que a regra não consegue ler e props repassadas depois de `className` ou `style`.
|
|
180
182
|
- **Relatório:** sem marca, é uma violação (saída 1), que pode entrar em baseline durante a migração. Com marca, o elemento aparece em `overrides`, com o motivo.
|
|
181
183
|
- **Arquivos:** os do `components.include` recebem só esta regra. Um arquivo que também está no `include` principal recebe as duas coisas, e aí a cobertura dinâmica volta a valer nele.
|
|
182
184
|
- **O que a regra não vê:** um componente do produto que repassa props para um componente da biblioteca, e CSS do produto que mira as classes internas `.ui-*`; props repassadas sozinhas ou antes de `className`/`style` (`<Button {...props} />`); componente usado como valor (`const X = Button; <X />`); arquivos que não compilam — aparecem como erro operacional de parse (saída 2) e não são analisados pela regra.
|
|
@@ -82,8 +82,11 @@ const arrangementUtilities = [
|
|
|
82
82
|
/^(?:items|justify-items|place-items)-(?:start|end|center|baseline|stretch|normal)$/u,
|
|
83
83
|
/^(?:justify|content|place-content)-(?:start|end|center|between|around|evenly|stretch|normal|baseline)$/u,
|
|
84
84
|
];
|
|
85
|
-
// The library's components that arrange their children by the consumer's
|
|
86
|
-
|
|
85
|
+
// The library's components that arrange their children by the consumer's
|
|
86
|
+
// className: the Card, and since 0.30.0 the Surface in every tone, whose
|
|
87
|
+
// panels and frames the apps lay out as the house does (a request of the
|
|
88
|
+
// conselhos).
|
|
89
|
+
export const arrangingComponents = new Set(["Card", "Surface"]);
|
|
87
90
|
export function classifyClass(token, component) {
|
|
88
91
|
const utility = utilityOf(token);
|
|
89
92
|
if (freeUtilities.some((pattern) => pattern.test(utility)))
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isComputedPropertyName, isIdentifier, isJsxAttribute, isJsxExpression, isObjectLiteralExpression, isPropertyAssignment, isJsxSpreadAttribute, isStringLiteralLikeNode, } from "typescript/unstable/ast";
|
|
1
|
+
import { isComputedPropertyName, isConditionalExpression, isIdentifier, isJsxAttribute, isJsxExpression, isObjectLiteralExpression, isPropertyAssignment, isJsxSpreadAttribute, isParenthesizedExpression, isShorthandPropertyAssignment, isStringLiteralLikeNode, } from "typescript/unstable/ast";
|
|
2
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";
|
|
@@ -89,6 +89,28 @@ function propertyName(node) {
|
|
|
89
89
|
? node.expression.text
|
|
90
90
|
: undefined;
|
|
91
91
|
}
|
|
92
|
+
// A spread whose props are written out in the source, as an object literal or
|
|
93
|
+
// a choice between two (`{...(ready ? { onSearch } : {})}`), and none of them
|
|
94
|
+
// carries a style: nothing in it can paint (a request of the login, 0.30.0).
|
|
95
|
+
// Any other spread, or one that may pass className, style or a color, still
|
|
96
|
+
// needs computed coverage; class too, which a custom element, or a component
|
|
97
|
+
// that passes it on, renders, and dangerouslySetInnerHTML, whose markup may
|
|
98
|
+
// carry its own classes and colors.
|
|
99
|
+
const styleProps = new Set(["className", "class", "style", "dangerouslySetInnerHTML"]);
|
|
100
|
+
function spreadsNoStyle(node) {
|
|
101
|
+
if (isParenthesizedExpression(node))
|
|
102
|
+
return spreadsNoStyle(node.expression);
|
|
103
|
+
if (isConditionalExpression(node))
|
|
104
|
+
return spreadsNoStyle(node.whenTrue) && spreadsNoStyle(node.whenFalse);
|
|
105
|
+
if (!isObjectLiteralExpression(node))
|
|
106
|
+
return false;
|
|
107
|
+
return node.properties.every((property) => {
|
|
108
|
+
if (!isPropertyAssignment(property) && !isShorthandPropertyAssignment(property))
|
|
109
|
+
return false;
|
|
110
|
+
const name = propertyName(property.name);
|
|
111
|
+
return name !== undefined && !styleProps.has(name) && !isColorBearingProperty(name);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
92
114
|
function dynamicStyleResult(file) {
|
|
93
115
|
const result = { uncovered: [], violations: [] };
|
|
94
116
|
if (!file.sourceFile) {
|
|
@@ -96,7 +118,7 @@ function dynamicStyleResult(file) {
|
|
|
96
118
|
}
|
|
97
119
|
const sourceFile = file.sourceFile;
|
|
98
120
|
const visit = (node) => {
|
|
99
|
-
if (isJsxSpreadAttribute(node)) {
|
|
121
|
+
if (isJsxSpreadAttribute(node) && !spreadsNoStyle(node.expression)) {
|
|
100
122
|
const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
101
123
|
result.uncovered.push({
|
|
102
124
|
column: position.character + 1,
|
package/dist/scan.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { glob, readFile as readFileFromDisk } from "node:fs/promises";
|
|
1
|
+
import { glob, readFile as readFileFromDisk, stat } from "node:fs/promises";
|
|
2
2
|
import { extname, relative, resolve } from "node:path";
|
|
3
3
|
import postcss from "postcss";
|
|
4
4
|
import { computeLineStarts, createScanner, LanguageVariant, SyntaxKind, } from "typescript/unstable/ast";
|
|
@@ -290,6 +290,26 @@ export async function runChecks(config, options = {}) {
|
|
|
290
290
|
}))
|
|
291
291
|
.sort((left, right) => left.path.localeCompare(right.path));
|
|
292
292
|
report.filesDiscovered = includedBeforeExcludes.length;
|
|
293
|
+
// A theme's files are read as any other file of include: one that exists
|
|
294
|
+
// but include leaves out would report the theme as incomplete and every pair
|
|
295
|
+
// unresolved, with nothing saying why (measured by the login, 0.30.0). With
|
|
296
|
+
// computed evidence too: the shape rule and the declarations read the file.
|
|
297
|
+
for (const theme of config.themes ?? []) {
|
|
298
|
+
for (const file of theme.files) {
|
|
299
|
+
const absolute = resolve(root, file);
|
|
300
|
+
if (selected.has(absolute))
|
|
301
|
+
continue;
|
|
302
|
+
const exists = await stat(absolute).then((found) => found.isFile(), () => false);
|
|
303
|
+
if (!exists)
|
|
304
|
+
continue;
|
|
305
|
+
report.operationalErrors.push({
|
|
306
|
+
code: "config",
|
|
307
|
+
message: includedBeforeExcludes.includes(absolute)
|
|
308
|
+
? `Theme ${theme.name} file ${toPosixPath(relative(root, absolute))} is removed by exclude: its declarations are not read. Take it out of exclude.`
|
|
309
|
+
: `Theme ${theme.name} file ${toPosixPath(relative(root, absolute))} is not matched by include: its declarations are not read. Add it to include.`,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
293
313
|
const componentPaths = config.components
|
|
294
314
|
? (await discoverFiles(root, config.components.include, config.components.exclude)).filter((path) => extname(path).toLowerCase() !== ".css")
|
|
295
315
|
: [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ksmv/ui-checks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.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",
|