@ksmv/ui-checks 0.28.2 → 0.30.0

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,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.30.0 - 2026-10-30
4
+
5
+ - **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`.
6
+ - **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`.
7
+ - **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.
8
+ - **Contratos:** sem mudança, tema `3.12.0` e contraste `20.0.0`.
9
+
10
+ ## 0.29.0 - 2026-10-28
11
+
12
+ - **Contratos de tema 3.12.0 e contraste 20.0.0** empacotados.
13
+ - O tema acrescenta o derivado opcional `--ui-surface-chrome`, que vem de `--ui-surface-subtle` (454 derivados). É o fundo da moldura do app, a `Surface tone="chrome"`.
14
+ - O tema também troca o padrão de `--ui-action-secondary-border`: ele passa a vir de `--ui-action-secondary-text`, e não mais de `--ui-border-control`. **Um tema que pinta o secundário cheio, com texto claro, e não declara a borda** passa a reprovar `action-secondary-boundary`: declare a borda.
15
+ - O contraste acrescenta quatorze pares sobre a moldura, com o contexto terminado em `-chrome`: o texto primário e o secundário, o link, o texto do fantasma (sozinho, no hover e no ativo), o botão de texto e o hover dele, o ícone do fantasma e o hover dele, a borda do pressionado e a do hover dele, a borda do secundário e o contador do `SideNav` (635 pares). Nenhum par saiu e nenhum limiar baixou.
16
+ - **Quem usa `computedEvidence` precisa capturar a evidência de novo.**
17
+
3
18
  ## 0.28.2 - 2026-10-28
4
19
 
5
20
  - **Sai junto com o `@ksmv/ui-react` 0.28.2, sem mudança nas regras:** os contratos continuam tema 3.11.0 e contraste 19.0.0. A correção da posição dos painéis em RTL não muda nenhuma cor do contrato (ver o CHANGELOG do `@ksmv/ui-react`).
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, então nele 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`.
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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "contractVersion": "19.0.0",
2
+ "contractVersion": "20.0.0",
3
3
  "schemaVersion": 3,
4
4
  "pairs": [
5
5
  { "foreground": "--ui-action-primary-text", "background": "--ui-action-primary-bg", "minimum": 4.5, "context": "normal-text" },
@@ -622,6 +622,20 @@
622
622
  {"foreground": "--ui-reply-draft-author", "background": "--ui-surface", "minimum": 4.5, "context": "reply-draft-author"},
623
623
  {"foreground": "--ui-progress-ring-text", "background": "--ui-surface", "minimum": 4.5, "context": "progress-ring-text"},
624
624
  {"foreground": "--ui-progress-ring-text", "background": "--ui-surface-raised", "minimum": 4.5, "context": "progress-ring-text-raised"},
625
- {"foreground": "--ui-progress-ring-text", "background": "--ui-surface-subtle", "minimum": 4.5, "context": "progress-ring-text-subtle"}
625
+ {"foreground": "--ui-progress-ring-text", "background": "--ui-surface-subtle", "minimum": 4.5, "context": "progress-ring-text-subtle"},
626
+ { "foreground": "--ui-text-primary", "background": "--ui-surface-chrome", "minimum": 4.5, "context": "body-text-chrome" },
627
+ { "foreground": "--ui-text-secondary", "background": "--ui-surface-chrome", "minimum": 4.5, "context": "secondary-text-chrome" },
628
+ { "foreground": "--ui-link-text", "background": "--ui-surface-chrome", "minimum": 4.5, "context": "link-text-chrome" },
629
+ { "foreground": "--ui-action-ghost-text", "background": "--ui-surface-chrome", "minimum": 4.5, "context": "normal-text-chrome" },
630
+ { "foreground": "--ui-action-ghost-text-hover", "background": "--ui-action-ghost-bg-hover", "over": "--ui-surface-chrome", "minimum": 4.5, "context": "normal-text-hover-chrome" },
631
+ { "foreground": "--ui-action-ghost-text-hover", "background": "--ui-action-ghost-bg-active", "over": "--ui-surface-chrome", "minimum": 4.5, "context": "normal-text-active-chrome" },
632
+ { "foreground": "--ui-text-button-text", "background": "--ui-surface-chrome", "minimum": 4.5, "context": "text-button-text-chrome" },
633
+ { "foreground": "--ui-text-button-text-hover", "background": "--ui-surface-chrome", "minimum": 4.5, "context": "text-button-text-hover-chrome" },
634
+ { "foreground": "--ui-icon-button-ghost-text", "background": "--ui-surface-chrome", "minimum": 3, "context": "icon-button-ghost-icon-chrome" },
635
+ { "foreground": "--ui-icon-button-ghost-text-hover", "background": "--ui-action-ghost-bg-hover", "over": "--ui-surface-chrome", "minimum": 3, "context": "icon-button-ghost-icon-hover-chrome" },
636
+ { "foreground": "--ui-button-pressed-bg", "background": "--ui-surface-chrome", "minimum": 3, "context": "button-pressed-boundary-chrome" },
637
+ { "foreground": "--ui-button-pressed-bg-hover", "background": "--ui-surface-chrome", "minimum": 3, "context": "button-pressed-boundary-hover-chrome" },
638
+ { "foreground": "--ui-action-secondary-border", "background": "--ui-surface-chrome", "minimum": 3, "context": "action-secondary-boundary-chrome" },
639
+ { "foreground": "--ui-side-nav-count-text", "background": "--ui-side-nav-count-bg", "over": "--ui-surface-chrome", "minimum": 4.5, "context": "side-nav-count-text-chrome" }
626
640
  ]
627
641
  }
@@ -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 className.
86
- export const arrangingComponents = new Set(["Card"]);
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
  : [];
@@ -1,5 +1,5 @@
1
1
  {
2
- "contractVersion": "3.11.0",
2
+ "contractVersion": "3.12.0",
3
3
  "schemaVersion": 2,
4
4
  "prefix": "--ui-",
5
5
  "required": [
@@ -79,7 +79,7 @@
79
79
  "--ui-numeric-ring-info-bg": { "kind": "literal", "value": "transparent" },
80
80
  "--ui-numeric-ring-info-text": { "kind": "token", "token": "--ui-text-primary" },
81
81
  "--ui-numeric-ring-info-border": { "kind": "token", "token": "--ui-status-info" },
82
- "--ui-action-secondary-border": { "kind": "token", "token": "--ui-border-control" },
82
+ "--ui-action-secondary-border": { "kind": "token", "token": "--ui-action-secondary-text" },
83
83
  "--ui-status-success-border": { "kind": "token", "token": "--ui-status-success" },
84
84
  "--ui-status-warning-border": { "kind": "token", "token": "--ui-status-warning" },
85
85
  "--ui-status-danger-border": { "kind": "token", "token": "--ui-status-danger" },
@@ -405,6 +405,7 @@
405
405
  "--ui-board-drop-bg": { "kind": "token", "token": "--ui-action-soft-accent-bg" },
406
406
  "--ui-board-drop-border": { "kind": "token", "token": "--ui-action-primary-bg" },
407
407
  "--ui-surface-inverse-end": { "kind": "token", "token": "--ui-surface-inverse" },
408
+ "--ui-surface-chrome": { "kind": "token", "token": "--ui-surface-subtle" },
408
409
  "--ui-checkbox-circle-bg": { "kind": "literal", "value": "transparent" },
409
410
  "--ui-checkbox-circle-hover": { "kind": "token", "token": "--ui-control-checked-bg" },
410
411
  "--ui-checkbox-circle-border": { "kind": "token", "token": "--ui-checkbox-border" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ksmv/ui-checks",
3
- "version": "0.28.2",
3
+ "version": "0.30.0",
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",