@ksmv/ui-checks 0.30.0 → 0.31.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 +11 -0
- package/README.md +3 -1
- package/dist/config.d.ts +2 -0
- package/dist/rules/componentOverride.d.ts +4 -0
- package/dist/rules/componentOverride.js +2 -2
- package/dist/rules/default.js +2 -0
- package/dist/rules/nativeDate.d.ts +2 -0
- package/dist/rules/nativeDate.js +59 -0
- package/dist/rules/overrideCatalog.js +7 -0
- package/dist/scan.js +5 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.31.0 - 2026-10-31
|
|
4
|
+
|
|
5
|
+
- **Regra opcional `components/native-date`** (#634). `components.nativeDates: true` recusa `type="date"` e `type="datetime-local"` literais em JSX. Fica desligada por padrão, até cada app ligar.
|
|
6
|
+
- **A altura é livre na `Surface`** (#629). `h-*` e `height` não exigem mais `data-ui-override` nela, e só nela.
|
|
7
|
+
- **Tema com barra invertida em POSIX** (#632). Um arquivo de tema escrito com `\` agora passa pelo diagnóstico do `include`.
|
|
8
|
+
- **Contratos:** sem mudança, tema `3.12.0` e contraste `20.0.0`.
|
|
9
|
+
|
|
10
|
+
## 0.30.1 - 2026-10-30
|
|
11
|
+
|
|
12
|
+
- **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`).
|
|
13
|
+
|
|
3
14
|
## 0.30.0 - 2026-10-30
|
|
4
15
|
|
|
5
16
|
- **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`.
|
package/README.md
CHANGED
|
@@ -178,9 +178,11 @@ components: {
|
|
|
178
178
|
```
|
|
179
179
|
|
|
180
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.
|
|
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`.
|
|
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. Na `Surface`, desde a 0.31.0, a altura é livre (`h-*` e `height`), para um painel ou uma moldura que ocupa o espaço. 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`.
|
|
182
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.
|
|
183
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.
|
|
184
|
+
|
|
185
|
+
Desde a 0.31.0, `components.nativeDates: true` liga a regra opcional `components/native-date`, desligada por padrão: ela recusa um `type="date"` ou `type="datetime-local"` escrito literalmente em JSX, em maiúsculas ou minúsculas, inclusive num spread de objeto literal, em `input` ou numa peça, porque o campo nativo segue o idioma da máquina. Use `DateInput` e, para texto, os formatadores. Um `type` vindo de variável não é lido.
|
|
184
186
|
- **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.
|
|
185
187
|
|
|
186
188
|
### Detalhes
|
package/dist/config.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export interface ComponentsConfig {
|
|
|
14
14
|
classHelpers?: string[];
|
|
15
15
|
exclude?: string[];
|
|
16
16
|
include: string[];
|
|
17
|
+
/** Refuses the native date fields, with a literal type="date" or "datetime-local"; off by default (0.31.0). */
|
|
18
|
+
nativeDates?: boolean;
|
|
17
19
|
sources?: string[];
|
|
18
20
|
}
|
|
19
21
|
export interface ChecksConfig {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { type Node } from "typescript/unstable/ast";
|
|
1
2
|
import type { CheckRule } from "../scan.js";
|
|
2
3
|
export interface ComponentOverrideOptions {
|
|
3
4
|
classHelpers?: string[];
|
|
4
5
|
sources?: string[];
|
|
5
6
|
}
|
|
7
|
+
export declare function propertyKey(node: Node): string | undefined;
|
|
8
|
+
/** Unwraps type assertions, `satisfies` and `!` before reading an expression. */
|
|
9
|
+
export declare function unwrapTypeWrappers(node: Node): Node;
|
|
6
10
|
export declare function createComponentOverrideRule({ classHelpers, sources, }?: ComponentOverrideOptions): CheckRule;
|
|
@@ -79,7 +79,7 @@ function tagParts(tagName) {
|
|
|
79
79
|
}
|
|
80
80
|
return isIdentifier(current) ? { path, root: current.text } : undefined;
|
|
81
81
|
}
|
|
82
|
-
function propertyKey(node) {
|
|
82
|
+
export function propertyKey(node) {
|
|
83
83
|
if (isIdentifier(node) || isStringLiteralLikeNode(node))
|
|
84
84
|
return node.text;
|
|
85
85
|
return isComputedPropertyName(node) && isStringLiteralLikeNode(node.expression)
|
|
@@ -87,7 +87,7 @@ function propertyKey(node) {
|
|
|
87
87
|
: undefined;
|
|
88
88
|
}
|
|
89
89
|
/** Unwraps type assertions, `satisfies` and `!` before reading an expression. */
|
|
90
|
-
function unwrapTypeWrappers(node) {
|
|
90
|
+
export function unwrapTypeWrappers(node) {
|
|
91
91
|
let current = node;
|
|
92
92
|
while (isParenthesizedExpression(current) ||
|
|
93
93
|
isAsExpression(current) ||
|
package/dist/rules/default.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createComponentOverrideRule } from "./componentOverride.js";
|
|
2
|
+
import { createNativeDateRule } from "./nativeDate.js";
|
|
2
3
|
import { createNoLiteralColorRule } from "./noLiteralColor.js";
|
|
3
4
|
import { createTokenCompleteRule, loadThemeContract } from "./tokenComplete.js";
|
|
4
5
|
import { createTokenContrastRule, loadContrastManifest, } from "./tokenContrast.js";
|
|
@@ -24,5 +25,6 @@ export async function createDefaultRules({ components, computed, contract, manif
|
|
|
24
25
|
...(components
|
|
25
26
|
? [createComponentOverrideRule({ classHelpers: components.classHelpers, sources: components.sources })]
|
|
26
27
|
: []),
|
|
28
|
+
...(components?.nativeDates ? [createNativeDateRule()] : []),
|
|
27
29
|
];
|
|
28
30
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { isJsxAttribute, isJsxExpression, isJsxOpeningElement, isJsxSelfClosingElement, isJsxSpreadAttribute, isObjectLiteralExpression, isPropertyAssignment, isStringLiteralLikeNode, } from "typescript/unstable/ast";
|
|
2
|
+
import { propertyKey, unwrapTypeWrappers } from "./componentOverride.js";
|
|
3
|
+
const nativeDateTypes = new Set(["date", "datetime-local"]);
|
|
4
|
+
function literalOf(initializer) {
|
|
5
|
+
const expression = initializer && isJsxExpression(initializer) ? initializer.expression : initializer;
|
|
6
|
+
const written = expression && unwrapTypeWrappers(expression);
|
|
7
|
+
return written && isStringLiteralLikeNode(written) ? written.text : undefined;
|
|
8
|
+
}
|
|
9
|
+
// The native date fields follow the machine's language, so the same screen
|
|
10
|
+
// shows mm/dd on one and dd/mm on another: the house's are DateInput, and the
|
|
11
|
+
// formatters for text. Optional, since an app turns it on when its fields have
|
|
12
|
+
// moved (0.31.0). Only a literal type is read, in any case, as HTML reads it,
|
|
13
|
+
// and a literal spread with it: a type from a variable is the app's own field.
|
|
14
|
+
export function createNativeDateRule() {
|
|
15
|
+
return {
|
|
16
|
+
name: "components/native-date",
|
|
17
|
+
scope: "components",
|
|
18
|
+
check(file) {
|
|
19
|
+
const result = { uncovered: [], violations: [] };
|
|
20
|
+
const sourceFile = file.sourceFile;
|
|
21
|
+
if (!sourceFile)
|
|
22
|
+
return result;
|
|
23
|
+
const typeOf = (attribute) => {
|
|
24
|
+
if (isJsxAttribute(attribute)) {
|
|
25
|
+
return attribute.name.getText(sourceFile) === "type" ? literalOf(attribute.initializer) : undefined;
|
|
26
|
+
}
|
|
27
|
+
if (!isJsxSpreadAttribute(attribute) || !isObjectLiteralExpression(attribute.expression))
|
|
28
|
+
return undefined;
|
|
29
|
+
// The last key is the one that holds.
|
|
30
|
+
const property = [...attribute.expression.properties]
|
|
31
|
+
.reverse()
|
|
32
|
+
.find((entry) => isPropertyAssignment(entry) && propertyKey(entry.name) === "type");
|
|
33
|
+
return property && isPropertyAssignment(property) ? literalOf(property.initializer) : undefined;
|
|
34
|
+
};
|
|
35
|
+
const visit = (node) => {
|
|
36
|
+
if (isJsxOpeningElement(node) || isJsxSelfClosingElement(node)) {
|
|
37
|
+
for (const attribute of node.attributes.properties) {
|
|
38
|
+
const type = typeOf(attribute)?.toLowerCase();
|
|
39
|
+
if (type === undefined || !nativeDateTypes.has(type))
|
|
40
|
+
continue;
|
|
41
|
+
const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
42
|
+
result.violations.push({
|
|
43
|
+
anchor: `${node.tagName.getText(sourceFile)}|${type}`,
|
|
44
|
+
column: position.character + 1,
|
|
45
|
+
line: position.line + 1,
|
|
46
|
+
message: `${node.tagName.getText(sourceFile)} has the native type "${type}": use DateInput from @ksmv/ui-react, which shows dd/mm/aaaa on every machine.`,
|
|
47
|
+
nodeKind: "native-date",
|
|
48
|
+
path: file.path,
|
|
49
|
+
rule: "components/native-date",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
node.forEachChild(visit);
|
|
54
|
+
};
|
|
55
|
+
visit(sourceFile);
|
|
56
|
+
return result;
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -87,10 +87,15 @@ const arrangementUtilities = [
|
|
|
87
87
|
// panels and frames the apps lay out as the house does (a request of the
|
|
88
88
|
// conselhos).
|
|
89
89
|
export const arrangingComponents = new Set(["Card", "Surface"]);
|
|
90
|
+
// The Surface's own height, for a panel or a frame that fills its room
|
|
91
|
+
// (0.31.0, a request of the conselhos).
|
|
92
|
+
const surfaceHeight = new RegExp(`^h-(?:${num}|${frac}|px|auto|full|screen|svh|lvh|dvh|min|max|fit|${arb})$`, "u");
|
|
90
93
|
export function classifyClass(token, component) {
|
|
91
94
|
const utility = utilityOf(token);
|
|
92
95
|
if (freeUtilities.some((pattern) => pattern.test(utility)))
|
|
93
96
|
return "free";
|
|
97
|
+
if (component === "Surface" && surfaceHeight.test(utility))
|
|
98
|
+
return "free";
|
|
94
99
|
if (component !== undefined && arrangingComponents.has(component) && arrangementUtilities.some((pattern) => pattern.test(utility)))
|
|
95
100
|
return "free";
|
|
96
101
|
return "visual";
|
|
@@ -120,6 +125,8 @@ export function classifyStyleProperty(name, component, value) {
|
|
|
120
125
|
const camel = name.replace(/-([a-z])/gu, (_, letter) => letter.toUpperCase());
|
|
121
126
|
if (freeStyleProperties.has(camel))
|
|
122
127
|
return "free";
|
|
128
|
+
if (component === "Surface" && camel === "height")
|
|
129
|
+
return "free";
|
|
123
130
|
if (component !== undefined && arrangingComponents.has(component) && arrangementStyleProperties.has(camel)) {
|
|
124
131
|
if (camel === "display" && !(value !== undefined && arrangementDisplays.has(value.trim())))
|
|
125
132
|
return "visual";
|
package/dist/scan.js
CHANGED
|
@@ -172,15 +172,17 @@ function validateRuntimeConfig(value) {
|
|
|
172
172
|
if (!components ||
|
|
173
173
|
typeof components !== "object" ||
|
|
174
174
|
Array.isArray(components) ||
|
|
175
|
-
Object.keys(components).some((key) => !["classHelpers", "exclude", "include", "sources"].includes(key)) ||
|
|
175
|
+
Object.keys(components).some((key) => !["classHelpers", "exclude", "include", "nativeDates", "sources"].includes(key)) ||
|
|
176
176
|
!isList(components.include, true) ||
|
|
177
177
|
(components.exclude !== undefined &&
|
|
178
178
|
!isList(components.exclude, false)) ||
|
|
179
179
|
(components.sources !== undefined &&
|
|
180
180
|
!isList(components.sources, true)) ||
|
|
181
|
+
(components.nativeDates !== undefined &&
|
|
182
|
+
typeof components.nativeDates !== "boolean") ||
|
|
181
183
|
(components.classHelpers !== undefined &&
|
|
182
184
|
!isList(components.classHelpers, false))) {
|
|
183
|
-
return "components needs include patterns
|
|
185
|
+
return "components needs include patterns, optional exclude, sources and classHelpers string lists, and an optional nativeDates boolean.";
|
|
184
186
|
}
|
|
185
187
|
}
|
|
186
188
|
return undefined;
|
|
@@ -296,7 +298,7 @@ export async function runChecks(config, options = {}) {
|
|
|
296
298
|
// computed evidence too: the shape rule and the declarations read the file.
|
|
297
299
|
for (const theme of config.themes ?? []) {
|
|
298
300
|
for (const file of theme.files) {
|
|
299
|
-
const absolute = resolve(root, file);
|
|
301
|
+
const absolute = resolve(root, toPosixPath(file));
|
|
300
302
|
if (selected.has(absolute))
|
|
301
303
|
continue;
|
|
302
304
|
const exists = await stat(absolute).then((found) => found.isFile(), () => false);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ksmv/ui-checks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.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",
|