@ksmv/ui-checks 0.1.5 → 0.2.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 +6 -0
- package/README.md +2 -0
- package/dist/color.js +2 -0
- package/dist/density.d.ts +22 -0
- package/dist/density.js +100 -0
- package/dist/rules/tokenComplete.js +33 -0
- package/dist/theme-contract.json +26 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0 - 2026-09-29
|
|
4
|
+
|
|
5
|
+
- **Contrato de tema 2.0.0** empacotado, com os 20 tokens de densidade obrigatórios e os 3 derivados. Pontes da 0.1.x ficam incompletas em `tokens/complete`, e evidência capturada antes da mudança deixa de conferir com a identidade do contrato.
|
|
6
|
+
- **Validação de valor dos tokens de densidade** em `tokens/complete`, pela lista fechada de tokens (nunca por padrão de nome): fonte e altura aceitam comprimento maior que zero em `px`, `rem` ou `em`; respiro e espaçamento aceitam também `0`; entrelinha, número sem unidade maior que zero; nos três, também `calc()`, `clamp()`, `min()` ou `max()`; peso, inteiro de 1 a 1000, `normal` ou `bold`. A conferência usa o valor da evidência calculada quando existe; sem evidência, um `var()` é conferido pelo token do mesmo tema ou pelo fallback que ele alcança. Valor inválido é cobertura obrigatória: sai com código 2 e não entra em baseline, e a mensagem diz o formato esperado, sem repetir o valor.
|
|
7
|
+
- `css/no-literal-color` deixa de ler como cor uma palavra só com dígitos hexadecimais e sem `#`, como o peso `600` numa custom property fora do seletor de tema.
|
|
8
|
+
|
|
3
9
|
## 0.1.5 - 2026-09-28
|
|
4
10
|
|
|
5
11
|
- **Fingerprints de baseline estáveis.** O fingerprint passa a seguir o conteúdo do achado e a ordem entre achados idênticos no mesmo arquivo, e não mais a linha e a coluna. Na 0.1.4, uma linha inserida acima de dívida conhecida fazia toda a dívida seguinte reaparecer como achado novo; numa aplicação consumidora real, um comentário no topo do CSS gerou 115 falsos achados novos. O conteúdo entra no fingerprint só como hash, e nenhum trecho é gravado ou relatado.
|
package/README.md
CHANGED
|
@@ -88,6 +88,8 @@ A evidência não é uma exceção manual. O arquivo é recusado, com código `2
|
|
|
88
88
|
|
|
89
89
|
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.
|
|
90
90
|
|
|
91
|
+
Os tokens de densidade têm o valor conferido por `tokens/complete`: comprimento maior que zero em `px`, `rem` ou `em` para fonte e altura; também `0` para respiro e espaçamento; número sem unidade maior que zero para a entrelinha; em todos esses, também `calc()`, `clamp()`, `min()` ou `max()`; inteiro de 1 a 1000, `normal` ou `bold` para o peso. Com evidência, vale o valor capturado no navegador. Sem evidência, um valor que chega por `var()` é conferido pelo que ele alcança: o token declarado no mesmo tema ou, se não houver, o fallback do próprio `var()`; uma referência que não chega a valor nenhum fica como cobertura pendente. Valor inválido é cobertura obrigatória e não entra em baseline.
|
|
92
|
+
|
|
91
93
|
A captura usa a API pública deste pacote e um navegador à sua escolha. Com Playwright, e a aplicação servida localmente:
|
|
92
94
|
|
|
93
95
|
```js
|
package/dist/color.js
CHANGED
|
@@ -100,6 +100,8 @@ export function containsLiteralColor(value) {
|
|
|
100
100
|
if (node.type === "word" &&
|
|
101
101
|
node.value.toLowerCase() !== "currentcolor" &&
|
|
102
102
|
node.value.toLowerCase() !== "transparent" &&
|
|
103
|
+
// culori parses bare hex digits ("600", "fed") as colors; CSS never does.
|
|
104
|
+
!/^[0-9a-f]{3,8}$/iu.test(node.value) &&
|
|
103
105
|
parsedColor(node.value)) {
|
|
104
106
|
literal = true;
|
|
105
107
|
return false;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type DensityKind = "control-height" | "font-size" | "font-weight" | "line-height" | "spacing";
|
|
2
|
+
/**
|
|
3
|
+
* The closed list of density tokens whose values the checker validates. It is
|
|
4
|
+
* matched by exact name, never by pattern: `--ui-font-body` or
|
|
5
|
+
* `--ui-shadow-sm` must not be read as lengths.
|
|
6
|
+
*/
|
|
7
|
+
export declare const densityTokenKinds: Readonly<Record<string, DensityKind>>;
|
|
8
|
+
export declare function isValidDensityValue(kind: DensityKind, raw: string): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Human-readable description of what a valid value looks like for the given
|
|
11
|
+
* density kind, used in checker diagnostics. Never echoes the offending
|
|
12
|
+
* value itself.
|
|
13
|
+
*/
|
|
14
|
+
export declare function densityExpectation(kind: DensityKind): string;
|
|
15
|
+
/**
|
|
16
|
+
* Resolves a whole-value `var()` reference to the literal it ends in, using
|
|
17
|
+
* the theme's own declarations (and the evidence) first and the reference's
|
|
18
|
+
* fallback second. Returns undefined when the chain does not end in a literal
|
|
19
|
+
* (the reference check reports that) or when it passes through another
|
|
20
|
+
* density token, which is validated on its own.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveDensityReference(value: string, tokens: ReadonlyMap<string, string>, seen?: ReadonlySet<string>): string | undefined;
|
package/dist/density.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import valueParser from "postcss-value-parser";
|
|
2
|
+
/**
|
|
3
|
+
* The closed list of density tokens whose values the checker validates. It is
|
|
4
|
+
* matched by exact name, never by pattern: `--ui-font-body` or
|
|
5
|
+
* `--ui-shadow-sm` must not be read as lengths.
|
|
6
|
+
*/
|
|
7
|
+
export const densityTokenKinds = Object.freeze({
|
|
8
|
+
"--ui-font-size-xs": "font-size",
|
|
9
|
+
"--ui-font-size-sm": "font-size",
|
|
10
|
+
"--ui-font-size-md": "font-size",
|
|
11
|
+
"--ui-font-size-lg": "font-size",
|
|
12
|
+
"--ui-font-size-xl": "font-size",
|
|
13
|
+
"--ui-font-weight-strong": "font-weight",
|
|
14
|
+
"--ui-line-height-control": "line-height",
|
|
15
|
+
"--ui-control-height-sm": "control-height",
|
|
16
|
+
"--ui-control-height-md": "control-height",
|
|
17
|
+
"--ui-control-height-lg": "control-height",
|
|
18
|
+
"--ui-control-padding-inline-sm": "spacing",
|
|
19
|
+
"--ui-control-padding-inline-md": "spacing",
|
|
20
|
+
"--ui-control-padding-inline-lg": "spacing",
|
|
21
|
+
"--ui-space-1": "spacing",
|
|
22
|
+
"--ui-space-1-5": "spacing",
|
|
23
|
+
"--ui-space-2": "spacing",
|
|
24
|
+
"--ui-space-3": "spacing",
|
|
25
|
+
"--ui-space-4": "spacing",
|
|
26
|
+
"--ui-space-6": "spacing",
|
|
27
|
+
"--ui-space-8": "spacing",
|
|
28
|
+
"--ui-field-height": "control-height",
|
|
29
|
+
"--ui-dialog-title-font-size": "font-size",
|
|
30
|
+
"--ui-button-font-weight": "font-weight",
|
|
31
|
+
});
|
|
32
|
+
const lengthPattern = /^(?:\d+(?:\.\d+)?|\.\d+)(?:px|rem|em)$/iu;
|
|
33
|
+
const math = /^(?:calc|clamp|min|max)\(.+\)$/iu;
|
|
34
|
+
const unitless = /^(?:\d+(?:\.\d+)?|\.\d+)$/u;
|
|
35
|
+
export function isValidDensityValue(kind, raw) {
|
|
36
|
+
const value = raw.trim();
|
|
37
|
+
switch (kind) {
|
|
38
|
+
case "font-weight": {
|
|
39
|
+
if (/^(?:normal|bold)$/iu.test(value))
|
|
40
|
+
return true;
|
|
41
|
+
if (!/^\d+$/u.test(value))
|
|
42
|
+
return false;
|
|
43
|
+
const weight = Number(value);
|
|
44
|
+
return weight >= 1 && weight <= 1000;
|
|
45
|
+
}
|
|
46
|
+
case "line-height":
|
|
47
|
+
return math.test(value) || (unitless.test(value) && Number(value) > 0);
|
|
48
|
+
case "spacing":
|
|
49
|
+
return value === "0" || math.test(value) || lengthPattern.test(value);
|
|
50
|
+
case "control-height":
|
|
51
|
+
case "font-size":
|
|
52
|
+
return math.test(value) || (lengthPattern.test(value) && Number.parseFloat(value) > 0);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Human-readable description of what a valid value looks like for the given
|
|
57
|
+
* density kind, used in checker diagnostics. Never echoes the offending
|
|
58
|
+
* value itself.
|
|
59
|
+
*/
|
|
60
|
+
export function densityExpectation(kind) {
|
|
61
|
+
switch (kind) {
|
|
62
|
+
case "control-height":
|
|
63
|
+
case "font-size":
|
|
64
|
+
return "a length above zero in px, rem or em, or calc()/clamp()/min()/max()";
|
|
65
|
+
case "spacing":
|
|
66
|
+
return "zero or a length in px, rem or em, or calc()/clamp()/min()/max()";
|
|
67
|
+
case "line-height":
|
|
68
|
+
return "a unitless number above zero, or calc()/clamp()/min()/max()";
|
|
69
|
+
case "font-weight":
|
|
70
|
+
return "an integer from 1 to 1000, normal or bold";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Resolves a whole-value `var()` reference to the literal it ends in, using
|
|
75
|
+
* the theme's own declarations (and the evidence) first and the reference's
|
|
76
|
+
* fallback second. Returns undefined when the chain does not end in a literal
|
|
77
|
+
* (the reference check reports that) or when it passes through another
|
|
78
|
+
* density token, which is validated on its own.
|
|
79
|
+
*/
|
|
80
|
+
export function resolveDensityReference(value, tokens, seen = new Set()) {
|
|
81
|
+
const trimmed = value.trim();
|
|
82
|
+
const nodes = valueParser(trimmed).nodes;
|
|
83
|
+
if (nodes.length !== 1)
|
|
84
|
+
return undefined;
|
|
85
|
+
const [node] = nodes;
|
|
86
|
+
if (node.type !== "function" || node.value.toLowerCase() !== "var")
|
|
87
|
+
return trimmed;
|
|
88
|
+
const comma = node.nodes.findIndex((child) => child.type === "div" && child.value === ",");
|
|
89
|
+
const name = valueParser.stringify(comma === -1 ? node.nodes : node.nodes.slice(0, comma)).trim();
|
|
90
|
+
const fallback = comma === -1 ? undefined : valueParser.stringify(node.nodes.slice(comma + 1)).trim();
|
|
91
|
+
if (name in densityTokenKinds || seen.has(name))
|
|
92
|
+
return undefined;
|
|
93
|
+
const next = new Set([...seen, name]);
|
|
94
|
+
const declared = tokens.get(name);
|
|
95
|
+
if (declared !== undefined)
|
|
96
|
+
return resolveDensityReference(declared, tokens, next);
|
|
97
|
+
if (fallback)
|
|
98
|
+
return resolveDensityReference(fallback, tokens, next);
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { unresolvedTokenReferences } from "../color.js";
|
|
2
2
|
import { parseThemeContract, } from "../contracts.js";
|
|
3
|
+
import { densityExpectation, densityTokenKinds, isValidDensityValue, resolveDensityReference, } from "../density.js";
|
|
3
4
|
import { createThemeCollector } from "./theme.js";
|
|
4
5
|
export async function loadThemeContract(url) {
|
|
5
6
|
const { readFile } = await import("node:fs/promises");
|
|
@@ -116,6 +117,38 @@ export function createTokenCompleteRule({ computed = {}, contract, themes, }) {
|
|
|
116
117
|
}
|
|
117
118
|
}
|
|
118
119
|
}
|
|
120
|
+
// Density values are checked on the value the browser resolved when the
|
|
121
|
+
// evidence carries it; without evidence, a var() is checked through
|
|
122
|
+
// what it resolves to in this theme or its own fallback, and one that
|
|
123
|
+
// resolves to nothing stays with the reference check above. An invalid
|
|
124
|
+
// value is mandatory coverage, so no baseline can absorb it.
|
|
125
|
+
for (const [token, kind] of Object.entries(densityTokenKinds)) {
|
|
126
|
+
if (!evidenceTokens.has(token))
|
|
127
|
+
continue;
|
|
128
|
+
const declaration = theme.declarations.get(token);
|
|
129
|
+
// A derived token that is not declared here is validated through
|
|
130
|
+
// its own source (the fallback token it derives from); reporting
|
|
131
|
+
// it again from this theme would just duplicate that diagnostic.
|
|
132
|
+
if (token in contract.derived && !declaration)
|
|
133
|
+
continue;
|
|
134
|
+
const raw = computedTheme[token] ?? declaration?.value;
|
|
135
|
+
if (raw === undefined)
|
|
136
|
+
continue;
|
|
137
|
+
const value = /var\s*\(/iu.test(raw) ? resolveDensityReference(raw, tokens) : raw;
|
|
138
|
+
if (value === undefined)
|
|
139
|
+
continue;
|
|
140
|
+
if (!isValidDensityValue(kind, value)) {
|
|
141
|
+
result.uncovered.push({
|
|
142
|
+
...(declaration
|
|
143
|
+
? { column: declaration.column, line: declaration.line, path: declaration.path }
|
|
144
|
+
: location),
|
|
145
|
+
message: `Theme ${theme.name} has an invalid value for ${token}; expected ${densityExpectation(kind)}.`,
|
|
146
|
+
nodeKind: "theme-density-value",
|
|
147
|
+
rule: "tokens/complete",
|
|
148
|
+
required: true,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
119
152
|
}
|
|
120
153
|
return result;
|
|
121
154
|
},
|
package/dist/theme-contract.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"contractVersion": "
|
|
2
|
+
"contractVersion": "2.0.0",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
4
|
"prefix": "--ui-",
|
|
5
5
|
"required": [
|
|
@@ -62,7 +62,27 @@
|
|
|
62
62
|
"--ui-radius-pill",
|
|
63
63
|
"--ui-shadow-sm",
|
|
64
64
|
"--ui-shadow-md",
|
|
65
|
-
"--ui-shadow-lg"
|
|
65
|
+
"--ui-shadow-lg",
|
|
66
|
+
"--ui-font-size-xs",
|
|
67
|
+
"--ui-font-size-sm",
|
|
68
|
+
"--ui-font-size-md",
|
|
69
|
+
"--ui-font-size-lg",
|
|
70
|
+
"--ui-font-size-xl",
|
|
71
|
+
"--ui-font-weight-strong",
|
|
72
|
+
"--ui-line-height-control",
|
|
73
|
+
"--ui-control-height-sm",
|
|
74
|
+
"--ui-control-height-md",
|
|
75
|
+
"--ui-control-height-lg",
|
|
76
|
+
"--ui-control-padding-inline-sm",
|
|
77
|
+
"--ui-control-padding-inline-md",
|
|
78
|
+
"--ui-control-padding-inline-lg",
|
|
79
|
+
"--ui-space-1",
|
|
80
|
+
"--ui-space-1-5",
|
|
81
|
+
"--ui-space-2",
|
|
82
|
+
"--ui-space-3",
|
|
83
|
+
"--ui-space-4",
|
|
84
|
+
"--ui-space-6",
|
|
85
|
+
"--ui-space-8"
|
|
66
86
|
],
|
|
67
87
|
"derived": {
|
|
68
88
|
"--ui-action-primary-border": "--ui-action-primary-bg",
|
|
@@ -71,7 +91,10 @@
|
|
|
71
91
|
"--ui-status-success-border": "--ui-status-success",
|
|
72
92
|
"--ui-status-warning-border": "--ui-status-warning",
|
|
73
93
|
"--ui-status-danger-border": "--ui-status-danger",
|
|
74
|
-
"--ui-status-info-border": "--ui-status-info"
|
|
94
|
+
"--ui-status-info-border": "--ui-status-info",
|
|
95
|
+
"--ui-field-height": "--ui-control-height-md",
|
|
96
|
+
"--ui-dialog-title-font-size": "--ui-font-size-xl",
|
|
97
|
+
"--ui-button-font-weight": "--ui-font-weight-strong"
|
|
75
98
|
},
|
|
76
99
|
"brandComposition": [
|
|
77
100
|
"--ui-brand-primary",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ksmv/ui-checks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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",
|