@ksmv/ui-checks 0.17.0 → 0.19.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 +15 -0
- package/README.md +3 -3
- package/dist/color.d.ts +8 -0
- package/dist/color.js +2 -2
- package/dist/contracts.d.ts +20 -2
- package/dist/contracts.js +44 -8
- package/dist/contrast-contract.json +59 -12
- package/dist/evidence.js +12 -7
- package/dist/rules/default.js +3 -0
- package/dist/rules/themeShape.d.ts +8 -0
- package/dist/rules/themeShape.js +35 -0
- package/dist/rules/tokenComplete.js +1 -34
- package/dist/rules/tokenContrast.d.ts +2 -1
- package/dist/rules/tokenContrast.js +16 -3
- package/dist/theme-contract.json +82 -42
- package/package.json +1 -1
- package/dist/density.d.ts +0 -22
- package/dist/density.js +0 -103
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.19.0 - 2026-10-13
|
|
4
|
+
|
|
5
|
+
- **Contratos de tema 3.0.0 e de contraste 8.0.0** empacotados, os dois em `schemaVersion: 2`; um contrato `schemaVersion: 1` passa a ser recusado. Atualize o `@ksmv/ui-react` junto (ver o CHANGELOG dele).
|
|
6
|
+
- **A forma é da biblioteca:** a regra nova `themes/no-shape` aponta, como violação, cada token da lista `shape` que um seletor de tema declarar. Quem migra pode carregar essas violações na baseline enquanto tira as linhas. A validação de densidade sai.
|
|
7
|
+
- **Derivados tipados:** um derivado aponta para outro token ou tem um padrão literal (`transparent` ou uma cor), usado quando o tema não o declara.
|
|
8
|
+
- **Fundo translúcido:** um par do manifesto pode dizer sobre qual superfície está (`over`); um fundo translúcido é composto sobre ela antes da medida. Sem `over`, ele continua cobertura pendente.
|
|
9
|
+
- **Quem usa `computedEvidence` precisa capturar a evidência de novo:** os dois contratos mudaram.
|
|
10
|
+
|
|
11
|
+
## 0.18.1 - 2026-10-11
|
|
12
|
+
|
|
13
|
+
- **A 0.18.0 não foi publicada.** A verificação da release parou por tempo, antes de a versão chegar ao npm, e nada mudou no pacote desde então: de 0.17.0, atualize direto para a 0.18.1. A tag `checks-v0.18.0` existe no repositório, mas não tem versão no registro.
|
|
14
|
+
- **Contratos de tema 2.5.0 e de contraste 7.0.0** empacotados: 22 derivados opcionais e 27 pares novos, das variantes suaves, do contorno de aviso e do botão de texto (ver o CHANGELOG do `@ksmv/ui-react`). Um par novo pode reprovar um tema que hoje passa. A resolução de derivados já existente avalia os tokens novos pela origem quando o tema não os declara.
|
|
15
|
+
- **Quem usa `computedEvidence` precisa capturar a evidência de novo:** as identidades dos dois contratos mudaram, e a evidência anterior é recusada com código 2, com a causa.
|
|
16
|
+
- Regras e CLI não mudam.
|
|
17
|
+
|
|
3
18
|
## 0.17.0 - 2026-10-09
|
|
4
19
|
|
|
5
20
|
- **Contratos de tema 2.4.0 e de contraste 6.0.0** empacotados: 16 derivados opcionais e 20 pares novos, das variantes de ação e do botão de texto (ver o CHANGELOG do `@ksmv/ui-react`). Um par novo pode reprovar um tema que hoje passa. A resolução de derivados já existente avalia os tokens novos pela origem quando o tema não os declara.
|
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), 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), prefixo dos tokens, cores literais fora dos arquivos de tema e evidência calculada vinculada às fontes.
|
|
4
4
|
|
|
5
5
|
## Antes de depender deste pacote
|
|
6
6
|
|
|
@@ -95,9 +95,9 @@ config Invalid computed theme evidence in artifacts/theme-evidence.json: the the
|
|
|
95
95
|
|
|
96
96
|
Depois de atualizar o pacote React, a mensagem traz a versão do contrato da captura e a do contrato instalado; quando faltam tokens ou resta `var(...)`, traz os nomes dos tokens, em ordem, até dez, e a contagem dos demais. A mensagem não repete valores do produto: aparecem só o caminho da evidência e o nome do tema como estão na configuração, nomes de tokens e versões de contrato. Falha ao resolver os contratos também é relatada com a própria causa (`Unable to resolve the configured contract source.`, `Unsupported theme contract.` ou `Unsupported contrast manifest.`).
|
|
97
97
|
|
|
98
|
-
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. Quando a origem também é um derivado, como no
|
|
98
|
+
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. Quando a origem também é um derivado, como no texto do hover do contorno de aviso (`--ui-action-warning-outline-text-hover` deriva de `--ui-action-warning-outline-text`, que deriva do contorno de perigo), a cadeia é seguida até um token declarado. Um derivado que é ponta de um par de contraste, como `--ui-control-checked-bg`, também pode ficar vazio; a captura precisa então do token em que a cadeia dele termina. Sem evidência, `tokens/contrast` segue a mesma cadeia: um derivado que o tema não declara é avaliado pelo valor da origem, ou pelo padrão literal do contrato (`transparent`, uma cor). Um par com `over` mede um fundo translúcido composto sobre aquela superfície; sem `over`, um fundo translúcido continua cobertura pendente.
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
Desde o contrato de tema `3.0.0`, a forma é da biblioteca: a regra `themes/no-shape` aponta, como violação, cada token da lista `shape` do contrato que um seletor de tema declarar. Quem migra de um tema que declarava forma pode carregar essas violações na baseline enquanto tira as linhas.
|
|
101
101
|
|
|
102
102
|
A captura usa a API pública deste pacote e um navegador à sua escolha. Com Playwright, e a aplicação servida localmente:
|
|
103
103
|
|
package/dist/color.d.ts
CHANGED
|
@@ -6,9 +6,17 @@ export type ColorResolution = {
|
|
|
6
6
|
status: "uncovered";
|
|
7
7
|
};
|
|
8
8
|
export declare function isColorBearingProperty(property: string): boolean;
|
|
9
|
+
export declare function parsedColor(value: string): {
|
|
10
|
+
alpha: number | undefined;
|
|
11
|
+
b: number;
|
|
12
|
+
g: number;
|
|
13
|
+
r: number;
|
|
14
|
+
mode: "rgb";
|
|
15
|
+
} | undefined;
|
|
9
16
|
export declare function containsUnsupportedColorFunction(value: string): boolean;
|
|
10
17
|
export declare function containsLiteralColor(value: string): boolean;
|
|
11
18
|
export declare function referencedTokens(value: string): string[];
|
|
12
19
|
export declare function unresolvedTokenReferences(value: string, tokens: ReadonlyMap<string, string>, stack?: ReadonlySet<string>): string[];
|
|
13
20
|
export declare function resolveColor(value: string | undefined, tokens: ReadonlyMap<string, string>, stack?: ReadonlySet<string>): ColorResolution;
|
|
21
|
+
export declare function composite(foreground: RgbColor, background: RgbColor): RgbColor;
|
|
14
22
|
export declare function contrastRatio(foreground: RgbColor, background: RgbColor): number | undefined;
|
package/dist/color.js
CHANGED
|
@@ -37,7 +37,7 @@ export function isColorBearingProperty(property) {
|
|
|
37
37
|
cssProperty === "text-emphasis" ||
|
|
38
38
|
cssProperty === "column-rule");
|
|
39
39
|
}
|
|
40
|
-
function parsedColor(value) {
|
|
40
|
+
export function parsedColor(value) {
|
|
41
41
|
// culori throws, instead of answering "not a color", on some values Tailwind
|
|
42
42
|
// allows in arbitrary classes, such as calc(100vw-1.5rem).
|
|
43
43
|
let parsed;
|
|
@@ -174,7 +174,7 @@ export function resolveColor(value, tokens, stack = new Set()) {
|
|
|
174
174
|
const color = parsedColor(value.trim());
|
|
175
175
|
return color ? { color, status: "resolved" } : { status: "uncovered" };
|
|
176
176
|
}
|
|
177
|
-
function composite(foreground, background) {
|
|
177
|
+
export function composite(foreground, background) {
|
|
178
178
|
const alpha = foreground.alpha ?? 1;
|
|
179
179
|
return {
|
|
180
180
|
b: foreground.b * alpha + background.b * (1 - alpha),
|
package/dist/contracts.d.ts
CHANGED
|
@@ -1,23 +1,41 @@
|
|
|
1
1
|
export declare const tokenNamePattern: RegExp;
|
|
2
2
|
export type ContractSource = "bundled" | "@ksmv/ui-react";
|
|
3
|
+
/** A derived token's default as the contract file writes it (schema 2). */
|
|
4
|
+
export type DerivedDefault = {
|
|
5
|
+
kind: "token";
|
|
6
|
+
token: string;
|
|
7
|
+
} | {
|
|
8
|
+
kind: "literal";
|
|
9
|
+
value: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* The theme contract as the rules read it: `derived` maps a derived token to
|
|
13
|
+
* its source token, and `defaults` maps one whose default is a literal color
|
|
14
|
+
* to that value. `shape` lists the tokens the library declares itself: a
|
|
15
|
+
* theme does not declare them.
|
|
16
|
+
*/
|
|
3
17
|
export interface ThemeContract {
|
|
4
18
|
brandComposition: string[];
|
|
5
19
|
contractVersion: string;
|
|
20
|
+
defaults: Record<string, string>;
|
|
6
21
|
derived: Record<string, string>;
|
|
7
22
|
prefix: "--ui-";
|
|
8
23
|
required: string[];
|
|
9
|
-
schemaVersion:
|
|
24
|
+
schemaVersion: 2;
|
|
25
|
+
shape: string[];
|
|
10
26
|
}
|
|
11
27
|
export interface ContrastPair {
|
|
12
28
|
background: string;
|
|
13
29
|
context: string;
|
|
14
30
|
foreground: string;
|
|
15
31
|
minimum: number;
|
|
32
|
+
/** The surface under a background that may be translucent (schema 2). */
|
|
33
|
+
over?: string;
|
|
16
34
|
}
|
|
17
35
|
export interface ContrastManifest {
|
|
18
36
|
contractVersion: string;
|
|
19
37
|
pairs: ContrastPair[];
|
|
20
|
-
schemaVersion:
|
|
38
|
+
schemaVersion: 2;
|
|
21
39
|
}
|
|
22
40
|
export interface ContractIdentity {
|
|
23
41
|
contractVersion: string;
|
package/dist/contracts.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
|
+
import { parsedColor, referencedTokens } from "./color.js";
|
|
5
6
|
export const tokenNamePattern = /^--ui-[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
6
7
|
/**
|
|
7
8
|
* A failure whose message this package wrote, so a report may repeat it. The
|
|
@@ -16,6 +17,21 @@ function exactKeys(value, keys) {
|
|
|
16
17
|
function isRecord(value) {
|
|
17
18
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
18
19
|
}
|
|
20
|
+
// A literal default is `transparent` or a color the checker can read.
|
|
21
|
+
function isLiteralColor(value) {
|
|
22
|
+
return typeof value === "string" &&
|
|
23
|
+
(value === "transparent" || (!referencedTokens(value).length && parsedColor(value) !== undefined));
|
|
24
|
+
}
|
|
25
|
+
function isDerivedDefault(value) {
|
|
26
|
+
if (!isRecord(value))
|
|
27
|
+
return false;
|
|
28
|
+
if (value.kind === "token") {
|
|
29
|
+
return exactKeys(value, ["kind", "token"]) &&
|
|
30
|
+
typeof value.token === "string" &&
|
|
31
|
+
tokenNamePattern.test(value.token);
|
|
32
|
+
}
|
|
33
|
+
return value.kind === "literal" && exactKeys(value, ["kind", "value"]) && isLiteralColor(value.value);
|
|
34
|
+
}
|
|
19
35
|
function isTokenList(value) {
|
|
20
36
|
return Array.isArray(value) && value.every((token) => typeof token === "string" && tokenNamePattern.test(token));
|
|
21
37
|
}
|
|
@@ -35,8 +51,9 @@ export function parseThemeContract(content) {
|
|
|
35
51
|
"prefix",
|
|
36
52
|
"required",
|
|
37
53
|
"schemaVersion",
|
|
54
|
+
"shape",
|
|
38
55
|
]) ||
|
|
39
|
-
value.schemaVersion !==
|
|
56
|
+
value.schemaVersion !== 2 ||
|
|
40
57
|
value.prefix !== "--ui-" ||
|
|
41
58
|
typeof value.contractVersion !== "string" ||
|
|
42
59
|
!semverPattern.test(value.contractVersion) ||
|
|
@@ -44,13 +61,30 @@ export function parseThemeContract(content) {
|
|
|
44
61
|
new Set(value.required).size !== value.required.length ||
|
|
45
62
|
!isTokenList(value.brandComposition) ||
|
|
46
63
|
new Set(value.brandComposition).size !== value.brandComposition.length ||
|
|
64
|
+
!isTokenList(value.shape) ||
|
|
65
|
+
new Set(value.shape).size !== value.shape.length ||
|
|
47
66
|
!isRecord(value.derived) ||
|
|
48
|
-
Object.entries(value.derived).some(([token, fallback]) => !tokenNamePattern.test(token) ||
|
|
49
|
-
typeof fallback !== "string" ||
|
|
50
|
-
!tokenNamePattern.test(fallback))) {
|
|
67
|
+
Object.entries(value.derived).some(([token, fallback]) => !tokenNamePattern.test(token) || !isDerivedDefault(fallback))) {
|
|
51
68
|
throw new CheckSetupError("Unsupported theme contract.");
|
|
52
69
|
}
|
|
53
|
-
|
|
70
|
+
const shape = new Set(value.shape);
|
|
71
|
+
const entries = Object.entries(value.derived);
|
|
72
|
+
const derived = {};
|
|
73
|
+
const defaults = {};
|
|
74
|
+
for (const [token, fallback] of entries) {
|
|
75
|
+
if (fallback.kind === "token")
|
|
76
|
+
derived[token] = fallback.token;
|
|
77
|
+
else
|
|
78
|
+
defaults[token] = fallback.value;
|
|
79
|
+
}
|
|
80
|
+
// A shape token is the library's, never the theme's: it cannot be required,
|
|
81
|
+
// derived, or the source of a derived token.
|
|
82
|
+
if (value.required.some((token) => shape.has(token)) ||
|
|
83
|
+
Object.entries(derived).some(([token, source]) => shape.has(token) || shape.has(source)) ||
|
|
84
|
+
Object.keys(defaults).some((token) => shape.has(token))) {
|
|
85
|
+
throw new CheckSetupError("Unsupported theme contract.");
|
|
86
|
+
}
|
|
87
|
+
return { ...value, defaults, derived };
|
|
54
88
|
}
|
|
55
89
|
export function parseContrastManifest(content) {
|
|
56
90
|
let value;
|
|
@@ -62,7 +96,7 @@ export function parseContrastManifest(content) {
|
|
|
62
96
|
}
|
|
63
97
|
if (!isRecord(value) ||
|
|
64
98
|
!exactKeys(value, ["contractVersion", "pairs", "schemaVersion"]) ||
|
|
65
|
-
value.schemaVersion !==
|
|
99
|
+
value.schemaVersion !== 2 ||
|
|
66
100
|
typeof value.contractVersion !== "string" ||
|
|
67
101
|
!semverPattern.test(value.contractVersion) ||
|
|
68
102
|
!Array.isArray(value.pairs)) {
|
|
@@ -71,7 +105,9 @@ export function parseContrastManifest(content) {
|
|
|
71
105
|
const identities = [];
|
|
72
106
|
for (const pair of value.pairs) {
|
|
73
107
|
if (!isRecord(pair) ||
|
|
74
|
-
!exactKeys(pair, ["background", "context", "foreground", "minimum"]) ||
|
|
108
|
+
!(exactKeys(pair, ["background", "context", "foreground", "minimum"]) ||
|
|
109
|
+
(exactKeys(pair, ["background", "context", "foreground", "minimum", "over"]) &&
|
|
110
|
+
typeof pair.over === "string" && tokenNamePattern.test(pair.over))) ||
|
|
75
111
|
typeof pair.background !== "string" ||
|
|
76
112
|
!tokenNamePattern.test(pair.background) ||
|
|
77
113
|
typeof pair.foreground !== "string" ||
|
|
@@ -83,7 +119,7 @@ export function parseContrastManifest(content) {
|
|
|
83
119
|
pair.minimum <= 0) {
|
|
84
120
|
throw new CheckSetupError("Unsupported contrast manifest.");
|
|
85
121
|
}
|
|
86
|
-
identities.push(`${pair.context}\u0000${pair.foreground}\u0000${pair.background}`);
|
|
122
|
+
identities.push(`${pair.context}\u0000${pair.foreground}\u0000${pair.background}\u0000${pair.over ?? ""}`);
|
|
87
123
|
}
|
|
88
124
|
if (new Set(identities).size !== identities.length) {
|
|
89
125
|
throw new CheckSetupError("Unsupported contrast manifest.");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"contractVersion": "
|
|
3
|
-
"schemaVersion":
|
|
2
|
+
"contractVersion": "8.0.0",
|
|
3
|
+
"schemaVersion": 2,
|
|
4
4
|
"pairs": [
|
|
5
5
|
{ "foreground": "--ui-action-primary-text", "background": "--ui-action-primary-bg", "minimum": 4.5, "context": "normal-text" },
|
|
6
6
|
{ "foreground": "--ui-action-primary-text", "background": "--ui-action-primary-bg-hover", "minimum": 4.5, "context": "normal-text-hover" },
|
|
@@ -12,8 +12,12 @@
|
|
|
12
12
|
{ "foreground": "--ui-action-danger-text", "background": "--ui-action-danger-bg-hover", "minimum": 4.5, "context": "normal-text-hover" },
|
|
13
13
|
{ "foreground": "--ui-action-danger-text", "background": "--ui-action-danger-bg-active", "minimum": 4.5, "context": "normal-text-active" },
|
|
14
14
|
{ "foreground": "--ui-action-ghost-text", "background": "--ui-surface", "minimum": 4.5, "context": "normal-text" },
|
|
15
|
-
{ "foreground": "--ui-action-ghost-text", "background": "--ui-action-ghost-bg-hover", "minimum": 4.5, "context": "normal-text-hover" },
|
|
16
|
-
{ "foreground": "--ui-action-ghost-text", "background": "--ui-action-ghost-bg-
|
|
15
|
+
{ "foreground": "--ui-action-ghost-text-hover", "background": "--ui-action-ghost-bg-hover", "over": "--ui-surface", "minimum": 4.5, "context": "normal-text-hover" },
|
|
16
|
+
{ "foreground": "--ui-action-ghost-text-hover", "background": "--ui-action-ghost-bg-hover", "over": "--ui-surface-raised", "minimum": 4.5, "context": "normal-text-hover-raised" },
|
|
17
|
+
{ "foreground": "--ui-action-ghost-text-hover", "background": "--ui-action-ghost-bg-hover", "over": "--ui-surface-subtle", "minimum": 4.5, "context": "normal-text-hover-subtle" },
|
|
18
|
+
{ "foreground": "--ui-action-ghost-text-hover", "background": "--ui-action-ghost-bg-active", "over": "--ui-surface", "minimum": 4.5, "context": "normal-text-active" },
|
|
19
|
+
{ "foreground": "--ui-action-ghost-text-hover", "background": "--ui-action-ghost-bg-active", "over": "--ui-surface-raised", "minimum": 4.5, "context": "normal-text-active-raised" },
|
|
20
|
+
{ "foreground": "--ui-action-ghost-text-hover", "background": "--ui-action-ghost-bg-active", "over": "--ui-surface-subtle", "minimum": 4.5, "context": "normal-text-active-subtle" },
|
|
17
21
|
{ "foreground": "--ui-status-success-text", "background": "--ui-status-success-bg", "minimum": 4.5, "context": "status-text" },
|
|
18
22
|
{ "foreground": "--ui-status-success", "background": "--ui-status-success-bg", "minimum": 3, "context": "status-indicator" },
|
|
19
23
|
{ "foreground": "--ui-status-warning-text", "background": "--ui-status-warning-bg", "minimum": 4.5, "context": "status-text" },
|
|
@@ -26,14 +30,12 @@
|
|
|
26
30
|
{ "foreground": "--ui-status-warning", "background": "--ui-surface", "minimum": 3, "context": "status-indicator-on-surface" },
|
|
27
31
|
{ "foreground": "--ui-status-danger", "background": "--ui-surface", "minimum": 3, "context": "status-indicator-on-surface" },
|
|
28
32
|
{ "foreground": "--ui-status-info", "background": "--ui-surface", "minimum": 3, "context": "status-indicator-on-surface" },
|
|
29
|
-
{ "foreground": "--ui-
|
|
33
|
+
{ "foreground": "--ui-loading-indicator-color", "background": "--ui-border", "minimum": 3, "context": "loading-indicator" },
|
|
30
34
|
{ "foreground": "--ui-status-danger-text", "background": "--ui-surface", "minimum": 4.5, "context": "field-error-text" },
|
|
31
35
|
{ "foreground": "--ui-status-danger", "background": "--ui-surface", "minimum": 3, "context": "invalid-control-boundary" },
|
|
32
36
|
{ "foreground": "--ui-status-info", "background": "--ui-surface-subtle", "minimum": 3, "context": "progress-indicator" },
|
|
33
37
|
{ "foreground": "--ui-text-inverse", "background": "--ui-surface-inverse", "minimum": 4.5, "context": "inverse-text" },
|
|
34
38
|
{ "foreground": "--ui-border-control", "background": "--ui-surface", "minimum": 3, "context": "control-boundary" },
|
|
35
|
-
{ "foreground": "--ui-focus-ring", "background": "--ui-surface", "minimum": 3, "context": "focus-indicator" },
|
|
36
|
-
{ "foreground": "--ui-focus-ring", "background": "--ui-focus-ring-offset", "minimum": 3, "context": "focus-indicator" },
|
|
37
39
|
{ "foreground": "--ui-control-checked-bg", "background": "--ui-surface", "minimum": 3, "context": "checked-control-boundary" },
|
|
38
40
|
{ "foreground": "--ui-control-checked-fg", "background": "--ui-control-checked-bg", "minimum": 3, "context": "checked-indicator" },
|
|
39
41
|
{ "foreground": "--ui-text-primary", "background": "--ui-surface", "minimum": 4.5, "context": "body-text" },
|
|
@@ -46,12 +48,10 @@
|
|
|
46
48
|
{ "foreground": "--ui-link-text", "background": "--ui-surface", "minimum": 4.5, "context": "link-text" },
|
|
47
49
|
{ "foreground": "--ui-link-text", "background": "--ui-surface-raised", "minimum": 4.5, "context": "link-text-raised" },
|
|
48
50
|
{ "foreground": "--ui-link-text", "background": "--ui-surface-subtle", "minimum": 4.5, "context": "link-text-subtle" },
|
|
49
|
-
{ "foreground": "--ui-action-primary-border", "background": "--ui-surface", "minimum": 3, "context": "action-primary-boundary" },
|
|
50
51
|
{ "foreground": "--ui-action-secondary-border", "background": "--ui-surface", "minimum": 3, "context": "action-secondary-boundary" },
|
|
51
|
-
{ "foreground": "--ui-action-danger-border", "background": "--ui-surface", "minimum": 3, "context": "action-danger-boundary" },
|
|
52
52
|
{ "foreground": "--ui-action-accent-text", "background": "--ui-surface", "minimum": 4.5, "context": "accent-text" },
|
|
53
|
-
{ "foreground": "--ui-action-accent-text", "background": "--ui-action-accent-bg-hover", "minimum": 4.5, "context": "accent-text-hover" },
|
|
54
|
-
{ "foreground": "--ui-action-accent-text", "background": "--ui-action-accent-bg-active", "minimum": 4.5, "context": "accent-text-active" },
|
|
53
|
+
{ "foreground": "--ui-action-accent-text", "background": "--ui-action-accent-bg-hover", "over": "--ui-surface", "minimum": 4.5, "context": "accent-text-hover" },
|
|
54
|
+
{ "foreground": "--ui-action-accent-text", "background": "--ui-action-accent-bg-active", "over": "--ui-surface", "minimum": 4.5, "context": "accent-text-active" },
|
|
55
55
|
{ "foreground": "--ui-action-danger-outline-text", "background": "--ui-surface", "minimum": 4.5, "context": "danger-outline-text" },
|
|
56
56
|
{ "foreground": "--ui-action-danger-outline-text", "background": "--ui-action-danger-outline-bg-hover", "minimum": 4.5, "context": "danger-outline-text-hover" },
|
|
57
57
|
{ "foreground": "--ui-action-danger-outline-text", "background": "--ui-action-danger-outline-bg-active", "minimum": 4.5, "context": "danger-outline-text-active" },
|
|
@@ -68,6 +68,53 @@
|
|
|
68
68
|
{ "foreground": "--ui-text-button-muted-text", "background": "--ui-surface-raised", "minimum": 4.5, "context": "text-button-muted-text-raised" },
|
|
69
69
|
{ "foreground": "--ui-text-button-muted-text", "background": "--ui-surface-subtle", "minimum": 4.5, "context": "text-button-muted-text-subtle" },
|
|
70
70
|
{ "foreground": "--ui-action-danger-outline-border", "background": "--ui-surface", "minimum": 3, "context": "action-danger-outline-boundary" },
|
|
71
|
-
{ "foreground": "--ui-action-
|
|
71
|
+
{ "foreground": "--ui-action-soft-accent-text", "background": "--ui-action-soft-accent-bg", "over": "--ui-surface", "minimum": 4.5, "context": "soft-accent-text" },
|
|
72
|
+
{ "foreground": "--ui-action-soft-accent-text-hover", "background": "--ui-action-soft-accent-bg-hover", "over": "--ui-surface", "minimum": 4.5, "context": "soft-accent-text-hover" },
|
|
73
|
+
{ "foreground": "--ui-action-soft-accent-text-hover", "background": "--ui-action-soft-accent-bg-active", "over": "--ui-surface", "minimum": 4.5, "context": "soft-accent-text-active" },
|
|
74
|
+
{ "foreground": "--ui-action-soft-warning-text", "background": "--ui-action-soft-warning-bg", "minimum": 4.5, "context": "soft-warning-text" },
|
|
75
|
+
{ "foreground": "--ui-action-soft-warning-text-hover", "background": "--ui-action-soft-warning-bg-hover", "minimum": 4.5, "context": "soft-warning-text-hover" },
|
|
76
|
+
{ "foreground": "--ui-action-soft-warning-text-hover", "background": "--ui-action-soft-warning-bg-active", "minimum": 4.5, "context": "soft-warning-text-active" },
|
|
77
|
+
{ "foreground": "--ui-action-warning-outline-text", "background": "--ui-surface", "minimum": 4.5, "context": "warning-outline-text" },
|
|
78
|
+
{ "foreground": "--ui-action-warning-outline-text-hover", "background": "--ui-action-warning-outline-bg-hover", "minimum": 4.5, "context": "warning-outline-text-hover" },
|
|
79
|
+
{ "foreground": "--ui-action-warning-outline-text-hover", "background": "--ui-action-warning-outline-bg-active", "minimum": 4.5, "context": "warning-outline-text-active" },
|
|
80
|
+
{ "foreground": "--ui-text-button-strong-text", "background": "--ui-surface", "minimum": 4.5, "context": "text-button-strong-text" },
|
|
81
|
+
{ "foreground": "--ui-text-button-strong-text", "background": "--ui-surface-raised", "minimum": 4.5, "context": "text-button-strong-text-raised" },
|
|
82
|
+
{ "foreground": "--ui-text-button-strong-text", "background": "--ui-surface-subtle", "minimum": 4.5, "context": "text-button-strong-text-subtle" },
|
|
83
|
+
{ "foreground": "--ui-text-button-text-hover", "background": "--ui-surface", "minimum": 4.5, "context": "text-button-text-hover" },
|
|
84
|
+
{ "foreground": "--ui-text-button-text-hover", "background": "--ui-surface-raised", "minimum": 4.5, "context": "text-button-text-hover-raised" },
|
|
85
|
+
{ "foreground": "--ui-text-button-text-hover", "background": "--ui-surface-subtle", "minimum": 4.5, "context": "text-button-text-hover-subtle" },
|
|
86
|
+
{ "foreground": "--ui-text-button-accent-text-hover", "background": "--ui-surface", "minimum": 4.5, "context": "text-button-accent-text-hover" },
|
|
87
|
+
{ "foreground": "--ui-text-button-accent-text-hover", "background": "--ui-surface-raised", "minimum": 4.5, "context": "text-button-accent-text-hover-raised" },
|
|
88
|
+
{ "foreground": "--ui-text-button-accent-text-hover", "background": "--ui-surface-subtle", "minimum": 4.5, "context": "text-button-accent-text-hover-subtle" },
|
|
89
|
+
{ "foreground": "--ui-text-button-muted-text-hover", "background": "--ui-surface", "minimum": 4.5, "context": "text-button-muted-text-hover" },
|
|
90
|
+
{ "foreground": "--ui-text-button-muted-text-hover", "background": "--ui-surface-raised", "minimum": 4.5, "context": "text-button-muted-text-hover-raised" },
|
|
91
|
+
{ "foreground": "--ui-text-button-muted-text-hover", "background": "--ui-surface-subtle", "minimum": 4.5, "context": "text-button-muted-text-hover-subtle" },
|
|
92
|
+
{ "foreground": "--ui-text-button-strong-text-hover", "background": "--ui-surface", "minimum": 4.5, "context": "text-button-strong-text-hover" },
|
|
93
|
+
{ "foreground": "--ui-text-button-strong-text-hover", "background": "--ui-surface-raised", "minimum": 4.5, "context": "text-button-strong-text-hover-raised" },
|
|
94
|
+
{ "foreground": "--ui-text-button-strong-text-hover", "background": "--ui-surface-subtle", "minimum": 4.5, "context": "text-button-strong-text-hover-subtle" },
|
|
95
|
+
{ "foreground": "--ui-action-soft-accent-border", "background": "--ui-surface", "minimum": 3, "context": "action-soft-accent-boundary" },
|
|
96
|
+
{ "foreground": "--ui-action-soft-warning-border", "background": "--ui-surface", "minimum": 3, "context": "action-soft-warning-boundary" },
|
|
97
|
+
{ "foreground": "--ui-action-warning-outline-border", "background": "--ui-surface", "minimum": 3, "context": "action-warning-outline-boundary" },
|
|
98
|
+
{ "foreground": "--ui-icon-button-ghost-text", "background": "--ui-surface", "minimum": 3, "context": "icon-button-ghost-icon" },
|
|
99
|
+
{ "foreground": "--ui-icon-button-ghost-text", "background": "--ui-surface-raised", "minimum": 3, "context": "icon-button-ghost-icon-raised" },
|
|
100
|
+
{ "foreground": "--ui-icon-button-ghost-text", "background": "--ui-surface-subtle", "minimum": 3, "context": "icon-button-ghost-icon-subtle" },
|
|
101
|
+
{ "foreground": "--ui-icon-button-ghost-text-hover", "background": "--ui-action-ghost-bg-hover", "over": "--ui-surface", "minimum": 3, "context": "icon-button-ghost-icon-hover" },
|
|
102
|
+
{ "foreground": "--ui-icon-button-ghost-text-hover", "background": "--ui-action-ghost-bg-hover", "over": "--ui-surface-raised", "minimum": 3, "context": "icon-button-ghost-icon-hover-raised" },
|
|
103
|
+
{ "foreground": "--ui-icon-button-ghost-text-hover", "background": "--ui-action-ghost-bg-hover", "over": "--ui-surface-subtle", "minimum": 3, "context": "icon-button-ghost-icon-hover-subtle" },
|
|
104
|
+
{ "foreground": "--ui-icon-button-ghost-text-hover", "background": "--ui-action-ghost-bg-active", "over": "--ui-surface", "minimum": 3, "context": "icon-button-ghost-icon-active" },
|
|
105
|
+
{ "foreground": "--ui-icon-button-ghost-text-hover", "background": "--ui-action-ghost-bg-active", "over": "--ui-surface-raised", "minimum": 3, "context": "icon-button-ghost-icon-active-raised" },
|
|
106
|
+
{ "foreground": "--ui-icon-button-ghost-text-hover", "background": "--ui-action-ghost-bg-active", "over": "--ui-surface-subtle", "minimum": 3, "context": "icon-button-ghost-icon-active-subtle" },
|
|
107
|
+
{ "foreground": "--ui-text-primary", "background": "--ui-dialog-header-bg", "minimum": 4.5, "context": "dialog-title" },
|
|
108
|
+
{ "foreground": "--ui-text-primary", "background": "--ui-dialog-header-bg-danger", "minimum": 4.5, "context": "dialog-title-danger" },
|
|
109
|
+
{ "foreground": "--ui-text-primary", "background": "--ui-dialog-header-bg-warning", "minimum": 4.5, "context": "dialog-title-warning" },
|
|
110
|
+
{ "foreground": "--ui-text-secondary", "background": "--ui-dialog-header-bg", "minimum": 4.5, "context": "dialog-description" },
|
|
111
|
+
{ "foreground": "--ui-text-secondary", "background": "--ui-dialog-header-bg-danger", "minimum": 4.5, "context": "dialog-description-danger" },
|
|
112
|
+
{ "foreground": "--ui-text-secondary", "background": "--ui-dialog-header-bg-warning", "minimum": 4.5, "context": "dialog-description-warning" },
|
|
113
|
+
{ "foreground": "--ui-dialog-icon", "background": "--ui-dialog-header-bg", "minimum": 3, "context": "dialog-icon" },
|
|
114
|
+
{ "foreground": "--ui-dialog-icon-danger", "background": "--ui-dialog-header-bg-danger", "minimum": 3, "context": "dialog-icon-danger" },
|
|
115
|
+
{ "foreground": "--ui-dialog-icon-warning", "background": "--ui-dialog-header-bg-warning", "minimum": 3, "context": "dialog-icon-warning" },
|
|
116
|
+
{ "foreground": "--ui-icon-button-ghost-text", "background": "--ui-dialog-header-bg", "minimum": 3, "context": "dialog-close" },
|
|
117
|
+
{ "foreground": "--ui-icon-button-ghost-text", "background": "--ui-dialog-header-bg-danger", "minimum": 3, "context": "dialog-close-danger" },
|
|
118
|
+
{ "foreground": "--ui-icon-button-ghost-text", "background": "--ui-dialog-header-bg-warning", "minimum": 3, "context": "dialog-close-warning" }
|
|
72
119
|
]
|
|
73
120
|
}
|
package/dist/evidence.js
CHANGED
|
@@ -59,7 +59,11 @@ function isIdentity(value) {
|
|
|
59
59
|
function requiredEvidenceTokens(contracts) {
|
|
60
60
|
const derived = contracts.theme.derived;
|
|
61
61
|
const required = new Set(contracts.theme.required);
|
|
62
|
+
const defaults = contracts.theme.defaults;
|
|
62
63
|
const origin = (token, seen = new Set()) => {
|
|
64
|
+
// A token with a literal default needs no captured value.
|
|
65
|
+
if (token in defaults)
|
|
66
|
+
return undefined;
|
|
63
67
|
const source = derived[token];
|
|
64
68
|
if (required.has(token) || source === undefined || seen.has(token))
|
|
65
69
|
return token;
|
|
@@ -68,11 +72,10 @@ function requiredEvidenceTokens(contracts) {
|
|
|
68
72
|
return new Set([
|
|
69
73
|
...required,
|
|
70
74
|
// A source that is itself derived is optional too; its chain is followed.
|
|
71
|
-
|
|
72
|
-
...
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
]),
|
|
75
|
+
// A source with a literal default needs no captured value either.
|
|
76
|
+
...Object.values(derived).filter((source) => !(source in derived) && !(source in defaults)),
|
|
77
|
+
...contracts.contrast.pairs.flatMap(({ background, foreground, over }) => [origin(background), origin(foreground), over === undefined ? undefined : origin(over)]
|
|
78
|
+
.filter((token) => token !== undefined)),
|
|
76
79
|
]);
|
|
77
80
|
}
|
|
78
81
|
// Structure only: whether the content agrees with the contracts, the
|
|
@@ -127,7 +130,7 @@ export async function loadComputedEvidence({ contracts, evidenceByPath, now = ne
|
|
|
127
130
|
const requiredTokens = requiredEvidenceTokens(contracts);
|
|
128
131
|
// A token the evidence must carry is never optional, even when the contract
|
|
129
132
|
// also lists it as derived or as a brand token.
|
|
130
|
-
const optionalTokens = new Set([...Object.keys(contracts.theme.derived), ...contracts.theme.brandComposition]
|
|
133
|
+
const optionalTokens = new Set([...Object.keys(contracts.theme.derived), ...Object.keys(contracts.theme.defaults), ...contracts.theme.brandComposition]
|
|
131
134
|
.filter((token) => !requiredTokens.has(token)));
|
|
132
135
|
for (const [path, group] of grouped) {
|
|
133
136
|
const refused = (cause) => new CheckSetupError(`Invalid computed theme evidence in ${path}: ${cause}`);
|
|
@@ -227,13 +230,15 @@ export async function loadComputedEvidence({ contracts, evidenceByPath, now = ne
|
|
|
227
230
|
const value = tokens[token];
|
|
228
231
|
if (value !== undefined)
|
|
229
232
|
return value;
|
|
233
|
+
if (token in contracts.theme.defaults)
|
|
234
|
+
return contracts.theme.defaults[token];
|
|
230
235
|
const source = contracts.theme.derived[token];
|
|
231
236
|
if (source === undefined || seen.has(token))
|
|
232
237
|
return undefined;
|
|
233
238
|
return derivedValue(source, new Set([...seen, token]));
|
|
234
239
|
};
|
|
235
240
|
const underivable = [];
|
|
236
|
-
for (const token of Object.keys(contracts.theme.derived)) {
|
|
241
|
+
for (const token of [...Object.keys(contracts.theme.derived), ...Object.keys(contracts.theme.defaults)]) {
|
|
237
242
|
const value = derivedValue(token, new Set());
|
|
238
243
|
if (value === undefined)
|
|
239
244
|
underivable.push(token);
|
package/dist/rules/default.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createComponentOverrideRule } from "./componentOverride.js";
|
|
|
2
2
|
import { createNoLiteralColorRule } from "./noLiteralColor.js";
|
|
3
3
|
import { createTokenCompleteRule, loadThemeContract } from "./tokenComplete.js";
|
|
4
4
|
import { createTokenContrastRule, loadContrastManifest, } from "./tokenContrast.js";
|
|
5
|
+
import { createThemeShapeRule } from "./themeShape.js";
|
|
5
6
|
import { createTokenPrefixRule } from "./tokenPrefix.js";
|
|
6
7
|
export async function createDefaultRules({ components, computed, contract, manifest, themes, }) {
|
|
7
8
|
const [resolvedContract, resolvedManifest] = await Promise.all([
|
|
@@ -12,8 +13,10 @@ export async function createDefaultRules({ components, computed, contract, manif
|
|
|
12
13
|
createNoLiteralColorRule({ themes }),
|
|
13
14
|
createTokenPrefixRule(),
|
|
14
15
|
createTokenCompleteRule({ computed, contract: resolvedContract, themes }),
|
|
16
|
+
createThemeShapeRule({ contract: resolvedContract, themes }),
|
|
15
17
|
createTokenContrastRule({
|
|
16
18
|
computed,
|
|
19
|
+
defaults: resolvedContract.defaults,
|
|
17
20
|
derived: resolvedContract.derived,
|
|
18
21
|
manifest: resolvedManifest,
|
|
19
22
|
themes,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ThemeContract } from "../contracts.js";
|
|
2
|
+
import type { CheckRule } from "../scan.js";
|
|
3
|
+
import type { ThemeConfig } from "../config.js";
|
|
4
|
+
export interface ThemeShapeOptions {
|
|
5
|
+
contract: ThemeContract;
|
|
6
|
+
themes: ThemeConfig[];
|
|
7
|
+
}
|
|
8
|
+
export declare function createThemeShapeRule({ contract, themes }: ThemeShapeOptions): CheckRule;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createThemeCollector } from "./theme.js";
|
|
2
|
+
// Shape comes from the library (theme contract 3.0.0): a theme declares color.
|
|
3
|
+
// A shape token declared in a theme selector is a violation, which a baseline
|
|
4
|
+
// can carry while a consumer removes the old declarations.
|
|
5
|
+
export function createThemeShapeRule({ contract, themes }) {
|
|
6
|
+
const collector = createThemeCollector(themes);
|
|
7
|
+
return {
|
|
8
|
+
name: "themes/no-shape",
|
|
9
|
+
begin: collector.begin,
|
|
10
|
+
check(file) {
|
|
11
|
+
collector.collect(file);
|
|
12
|
+
return { uncovered: [], violations: [] };
|
|
13
|
+
},
|
|
14
|
+
complete() {
|
|
15
|
+
const violations = [];
|
|
16
|
+
for (const theme of collector.themes) {
|
|
17
|
+
for (const token of contract.shape) {
|
|
18
|
+
const declaration = theme.declarations.get(token);
|
|
19
|
+
if (!declaration)
|
|
20
|
+
continue;
|
|
21
|
+
violations.push({
|
|
22
|
+
anchor: `${theme.name}:${token}`,
|
|
23
|
+
column: declaration.column,
|
|
24
|
+
line: declaration.line,
|
|
25
|
+
message: `Theme ${theme.name} declares shape token ${token}; shape comes from the library.`,
|
|
26
|
+
nodeKind: `theme-shape-token:${token}`,
|
|
27
|
+
path: declaration.path,
|
|
28
|
+
rule: "themes/no-shape",
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { uncovered: [], violations };
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { unresolvedTokenReferences } from "../color.js";
|
|
2
2
|
import { parseThemeContract, } from "../contracts.js";
|
|
3
|
-
import { densityExpectation, densityTokenKinds, isValidDensityValue, resolveDensityReference, } from "../density.js";
|
|
4
3
|
import { createThemeCollector } from "./theme.js";
|
|
5
4
|
export async function loadThemeContract(url) {
|
|
6
5
|
const { readFile } = await import("node:fs/promises");
|
|
@@ -78,7 +77,7 @@ export function createTokenCompleteRule({ computed = {}, contract, themes, }) {
|
|
|
78
77
|
// A derived token falls back to its source, and to the source's own
|
|
79
78
|
// source when that one is derived too, until a declared token.
|
|
80
79
|
const hasFallback = (token, seen) => {
|
|
81
|
-
if (theme.declarations.has(token))
|
|
80
|
+
if (theme.declarations.has(token) || token in contract.defaults)
|
|
82
81
|
return true;
|
|
83
82
|
const source = contract.derived[token];
|
|
84
83
|
if (source === undefined || seen.has(token))
|
|
@@ -126,38 +125,6 @@ export function createTokenCompleteRule({ computed = {}, contract, themes, }) {
|
|
|
126
125
|
}
|
|
127
126
|
}
|
|
128
127
|
}
|
|
129
|
-
// Density values are checked on the value the browser resolved when the
|
|
130
|
-
// evidence carries it; without evidence, a var() is checked through
|
|
131
|
-
// what it resolves to in this theme or its own fallback, and one that
|
|
132
|
-
// resolves to nothing stays with the reference check above. An invalid
|
|
133
|
-
// value is mandatory coverage, so no baseline can absorb it.
|
|
134
|
-
for (const [token, kind] of Object.entries(densityTokenKinds)) {
|
|
135
|
-
if (!evidenceTokens.has(token))
|
|
136
|
-
continue;
|
|
137
|
-
const declaration = theme.declarations.get(token);
|
|
138
|
-
// A derived token that is not declared here is validated through
|
|
139
|
-
// its own source (the fallback token it derives from); reporting
|
|
140
|
-
// it again from this theme would just duplicate that diagnostic.
|
|
141
|
-
if (token in contract.derived && !declaration)
|
|
142
|
-
continue;
|
|
143
|
-
const raw = computedTheme[token] ?? declaration?.value;
|
|
144
|
-
if (raw === undefined)
|
|
145
|
-
continue;
|
|
146
|
-
const value = /var\s*\(/iu.test(raw) ? resolveDensityReference(raw, tokens) : raw;
|
|
147
|
-
if (value === undefined)
|
|
148
|
-
continue;
|
|
149
|
-
if (!isValidDensityValue(kind, value)) {
|
|
150
|
-
result.uncovered.push({
|
|
151
|
-
...(declaration
|
|
152
|
-
? { column: declaration.column, line: declaration.line, path: declaration.path }
|
|
153
|
-
: location),
|
|
154
|
-
message: `Theme ${theme.name} has an invalid value for ${token}; expected ${densityExpectation(kind)}.`,
|
|
155
|
-
nodeKind: "theme-density-value",
|
|
156
|
-
rule: "tokens/complete",
|
|
157
|
-
required: true,
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
128
|
}
|
|
162
129
|
return result;
|
|
163
130
|
},
|
|
@@ -4,9 +4,10 @@ import type { CheckRule } from "../scan.js";
|
|
|
4
4
|
export type { ContrastManifest, ContrastPair } from "../contracts.js";
|
|
5
5
|
export interface TokenContrastOptions {
|
|
6
6
|
computed?: Record<string, Record<string, string>>;
|
|
7
|
+
defaults?: Record<string, string>;
|
|
7
8
|
derived?: Record<string, string>;
|
|
8
9
|
manifest: ContrastManifest;
|
|
9
10
|
themes: ThemeConfig[];
|
|
10
11
|
}
|
|
11
12
|
export declare function loadContrastManifest(url?: URL): Promise<ContrastManifest>;
|
|
12
|
-
export declare function createTokenContrastRule({ computed, derived, manifest, themes, }: TokenContrastOptions): CheckRule;
|
|
13
|
+
export declare function createTokenContrastRule({ computed, defaults, derived, manifest, themes, }: TokenContrastOptions): CheckRule;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isComputedPropertyName, isIdentifier, isJsxAttribute, isJsxExpression, isObjectLiteralExpression, isPropertyAssignment, isJsxSpreadAttribute, isStringLiteralLikeNode, } from "typescript/unstable/ast";
|
|
2
|
-
import { contrastRatio, isColorBearingProperty, resolveColor, } from "../color.js";
|
|
2
|
+
import { composite, contrastRatio, isColorBearingProperty, resolveColor, } from "../color.js";
|
|
3
3
|
import { parseContrastManifest, } from "../contracts.js";
|
|
4
4
|
import { createThemeCollector } from "./theme.js";
|
|
5
5
|
export async function loadContrastManifest(url) {
|
|
@@ -84,7 +84,7 @@ function dynamicStyleResult(file) {
|
|
|
84
84
|
sourceFile.forEachChild(visit);
|
|
85
85
|
return result;
|
|
86
86
|
}
|
|
87
|
-
export function createTokenContrastRule({ computed = {}, derived = {}, manifest, themes, }) {
|
|
87
|
+
export function createTokenContrastRule({ computed = {}, defaults = {}, derived = {}, manifest, themes, }) {
|
|
88
88
|
const collector = createThemeCollector(themes);
|
|
89
89
|
return {
|
|
90
90
|
name: "tokens/contrast",
|
|
@@ -138,9 +138,22 @@ export function createTokenContrastRule({ computed = {}, derived = {}, manifest,
|
|
|
138
138
|
tokens.set(token, `var(${source})`);
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
|
+
// A literal default (theme contract schema 2) is the value itself.
|
|
142
|
+
for (const [token, value] of Object.entries(defaults)) {
|
|
143
|
+
if (!tokens.has(token))
|
|
144
|
+
tokens.set(token, value);
|
|
145
|
+
}
|
|
141
146
|
for (const pair of manifest.pairs) {
|
|
142
|
-
|
|
147
|
+
let background = resolveColor(tokens.get(pair.background), tokens);
|
|
143
148
|
const foreground = resolveColor(tokens.get(pair.foreground), tokens);
|
|
149
|
+
// A translucent background is measured as painted over the surface
|
|
150
|
+
// the pair names (schema 2); without one it stays uncovered below.
|
|
151
|
+
if (pair.over && background.status === "resolved" && (background.color.alpha ?? 1) < 1) {
|
|
152
|
+
const over = resolveColor(tokens.get(pair.over), tokens);
|
|
153
|
+
background = over.status === "resolved" && (over.color.alpha ?? 1) === 1
|
|
154
|
+
? { color: composite(background.color, over.color), status: "resolved" }
|
|
155
|
+
: { status: "uncovered" };
|
|
156
|
+
}
|
|
144
157
|
if (background.status === "uncovered" ||
|
|
145
158
|
foreground.status === "uncovered") {
|
|
146
159
|
result.uncovered.push({
|
package/dist/theme-contract.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"contractVersion": "
|
|
3
|
-
"schemaVersion":
|
|
2
|
+
"contractVersion": "3.0.0",
|
|
3
|
+
"schemaVersion": 2,
|
|
4
4
|
"prefix": "--ui-",
|
|
5
5
|
"required": [
|
|
6
6
|
"--ui-surface-page",
|
|
@@ -18,8 +18,6 @@
|
|
|
18
18
|
"--ui-border-strong",
|
|
19
19
|
"--ui-border-control",
|
|
20
20
|
"--ui-border-disabled",
|
|
21
|
-
"--ui-focus-ring",
|
|
22
|
-
"--ui-focus-ring-offset",
|
|
23
21
|
"--ui-overlay",
|
|
24
22
|
"--ui-action-primary-bg",
|
|
25
23
|
"--ui-action-primary-bg-hover",
|
|
@@ -51,18 +49,83 @@
|
|
|
51
49
|
"--ui-disabled-bg",
|
|
52
50
|
"--ui-disabled-text",
|
|
53
51
|
"--ui-disabled-border",
|
|
52
|
+
"--ui-shadow-sm",
|
|
53
|
+
"--ui-shadow-md",
|
|
54
|
+
"--ui-shadow-lg"
|
|
55
|
+
],
|
|
56
|
+
"derived": {
|
|
57
|
+
"--ui-action-secondary-border": { "kind": "token", "token": "--ui-border-control" },
|
|
58
|
+
"--ui-status-success-border": { "kind": "token", "token": "--ui-status-success" },
|
|
59
|
+
"--ui-status-warning-border": { "kind": "token", "token": "--ui-status-warning" },
|
|
60
|
+
"--ui-status-danger-border": { "kind": "token", "token": "--ui-status-danger" },
|
|
61
|
+
"--ui-status-info-border": { "kind": "token", "token": "--ui-status-info" },
|
|
62
|
+
"--ui-control-checked-bg": { "kind": "token", "token": "--ui-action-primary-bg" },
|
|
63
|
+
"--ui-control-checked-fg": { "kind": "token", "token": "--ui-action-primary-text" },
|
|
64
|
+
"--ui-link-text": { "kind": "token", "token": "--ui-text-primary" },
|
|
65
|
+
"--ui-action-accent-text": { "kind": "token", "token": "--ui-action-ghost-text" },
|
|
66
|
+
"--ui-action-accent-bg-hover": { "kind": "token", "token": "--ui-action-ghost-bg-hover" },
|
|
67
|
+
"--ui-action-accent-bg-active": { "kind": "token", "token": "--ui-action-ghost-bg-active" },
|
|
68
|
+
"--ui-action-danger-outline-text": { "kind": "token", "token": "--ui-status-danger-text" },
|
|
69
|
+
"--ui-action-danger-outline-border": { "kind": "token", "token": "--ui-status-danger" },
|
|
70
|
+
"--ui-action-danger-outline-bg-hover": { "kind": "token", "token": "--ui-status-danger-bg" },
|
|
71
|
+
"--ui-action-danger-outline-bg-active": { "kind": "token", "token": "--ui-status-danger-bg" },
|
|
72
|
+
"--ui-action-warning-bg": { "kind": "token", "token": "--ui-action-danger-bg" },
|
|
73
|
+
"--ui-action-warning-bg-hover": { "kind": "token", "token": "--ui-action-danger-bg-hover" },
|
|
74
|
+
"--ui-action-warning-bg-active": { "kind": "token", "token": "--ui-action-danger-bg-active" },
|
|
75
|
+
"--ui-action-warning-text": { "kind": "token", "token": "--ui-action-danger-text" },
|
|
76
|
+
"--ui-text-button-text": { "kind": "token", "token": "--ui-link-text" },
|
|
77
|
+
"--ui-text-button-accent-text": { "kind": "token", "token": "--ui-text-button-text" },
|
|
78
|
+
"--ui-text-button-muted-text": { "kind": "token", "token": "--ui-text-secondary" },
|
|
79
|
+
"--ui-action-soft-accent-bg": { "kind": "token", "token": "--ui-action-accent-bg-hover" },
|
|
80
|
+
"--ui-action-soft-accent-bg-hover": { "kind": "token", "token": "--ui-action-accent-bg-active" },
|
|
81
|
+
"--ui-action-soft-accent-bg-active": { "kind": "token", "token": "--ui-action-accent-bg-active" },
|
|
82
|
+
"--ui-action-soft-accent-text": { "kind": "token", "token": "--ui-action-accent-text" },
|
|
83
|
+
"--ui-action-soft-accent-border": { "kind": "token", "token": "--ui-action-accent-text" },
|
|
84
|
+
"--ui-action-soft-warning-bg": { "kind": "token", "token": "--ui-status-warning-bg" },
|
|
85
|
+
"--ui-action-soft-warning-bg-hover": { "kind": "token", "token": "--ui-status-warning-bg" },
|
|
86
|
+
"--ui-action-soft-warning-bg-active": { "kind": "token", "token": "--ui-status-warning-bg" },
|
|
87
|
+
"--ui-action-soft-warning-text": { "kind": "token", "token": "--ui-status-warning-text" },
|
|
88
|
+
"--ui-action-soft-warning-border": { "kind": "token", "token": "--ui-status-warning" },
|
|
89
|
+
"--ui-action-warning-outline-text": { "kind": "token", "token": "--ui-action-danger-outline-text" },
|
|
90
|
+
"--ui-action-warning-outline-border": { "kind": "token", "token": "--ui-action-danger-outline-border" },
|
|
91
|
+
"--ui-action-warning-outline-bg-hover": { "kind": "token", "token": "--ui-action-danger-outline-bg-hover" },
|
|
92
|
+
"--ui-action-warning-outline-bg-active": { "kind": "token", "token": "--ui-action-danger-outline-bg-active" },
|
|
93
|
+
"--ui-text-button-strong-text": { "kind": "token", "token": "--ui-text-primary" },
|
|
94
|
+
"--ui-text-button-text-hover": { "kind": "token", "token": "--ui-text-button-text" },
|
|
95
|
+
"--ui-text-button-accent-text-hover": { "kind": "token", "token": "--ui-text-button-accent-text" },
|
|
96
|
+
"--ui-text-button-muted-text-hover": { "kind": "token", "token": "--ui-text-button-muted-text" },
|
|
97
|
+
"--ui-text-button-strong-text-hover": { "kind": "token", "token": "--ui-text-button-strong-text" },
|
|
98
|
+
"--ui-action-soft-accent-text-hover": { "kind": "token", "token": "--ui-action-soft-accent-text" },
|
|
99
|
+
"--ui-action-soft-warning-text-hover": { "kind": "token", "token": "--ui-action-soft-warning-text" },
|
|
100
|
+
"--ui-action-warning-outline-text-hover": { "kind": "token", "token": "--ui-action-warning-outline-text" },
|
|
101
|
+
"--ui-field-disabled-bg": { "kind": "token", "token": "--ui-surface-subtle" },
|
|
102
|
+
"--ui-field-disabled-text": { "kind": "token", "token": "--ui-text-muted" },
|
|
103
|
+
"--ui-button-shadow-color": { "kind": "literal", "value": "transparent" },
|
|
104
|
+
"--ui-action-danger-outline-bg": { "kind": "literal", "value": "transparent" },
|
|
105
|
+
"--ui-action-ghost-text-hover": { "kind": "token", "token": "--ui-action-ghost-text" },
|
|
106
|
+
"--ui-icon-button-ghost-text": { "kind": "token", "token": "--ui-action-ghost-text" },
|
|
107
|
+
"--ui-icon-button-ghost-text-hover": { "kind": "token", "token": "--ui-icon-button-ghost-text" },
|
|
108
|
+
"--ui-loading-indicator-color": { "kind": "token", "token": "--ui-status-info" },
|
|
109
|
+
"--ui-dialog-header-bg": { "kind": "token", "token": "--ui-surface-subtle" },
|
|
110
|
+
"--ui-dialog-header-bg-danger": { "kind": "token", "token": "--ui-status-danger-bg" },
|
|
111
|
+
"--ui-dialog-header-bg-warning": { "kind": "token", "token": "--ui-status-warning-bg" },
|
|
112
|
+
"--ui-dialog-icon": { "kind": "token", "token": "--ui-text-secondary" },
|
|
113
|
+
"--ui-dialog-icon-danger": { "kind": "token", "token": "--ui-status-danger" },
|
|
114
|
+
"--ui-dialog-icon-warning": { "kind": "token", "token": "--ui-status-warning" },
|
|
115
|
+
"--ui-dialog-footer-bg": { "kind": "token", "token": "--ui-surface-subtle" },
|
|
116
|
+
"--ui-dialog-footer-border": { "kind": "token", "token": "--ui-border" }
|
|
117
|
+
},
|
|
118
|
+
"shape": [
|
|
54
119
|
"--ui-font-body",
|
|
55
120
|
"--ui-font-emphasis",
|
|
56
121
|
"--ui-font-mono",
|
|
57
122
|
"--ui-dialog-title-font",
|
|
123
|
+
"--ui-font-display",
|
|
58
124
|
"--ui-dialog-title-weight",
|
|
59
125
|
"--ui-radius-sm",
|
|
60
126
|
"--ui-radius-md",
|
|
61
127
|
"--ui-radius-lg",
|
|
62
128
|
"--ui-radius-pill",
|
|
63
|
-
"--ui-shadow-sm",
|
|
64
|
-
"--ui-shadow-md",
|
|
65
|
-
"--ui-shadow-lg",
|
|
66
129
|
"--ui-font-size-xs",
|
|
67
130
|
"--ui-font-size-sm",
|
|
68
131
|
"--ui-font-size-md",
|
|
@@ -82,42 +145,19 @@
|
|
|
82
145
|
"--ui-space-3",
|
|
83
146
|
"--ui-space-4",
|
|
84
147
|
"--ui-space-6",
|
|
85
|
-
"--ui-space-8"
|
|
148
|
+
"--ui-space-8",
|
|
149
|
+
"--ui-field-height",
|
|
150
|
+
"--ui-dialog-title-font-size",
|
|
151
|
+
"--ui-button-font-weight",
|
|
152
|
+
"--ui-button-font-weight-sm",
|
|
153
|
+
"--ui-button-font-weight-md",
|
|
154
|
+
"--ui-button-font-weight-lg",
|
|
155
|
+
"--ui-icon-button-size-xs",
|
|
156
|
+
"--ui-text-button-font-size-xs",
|
|
157
|
+
"--ui-field-hint-font-size",
|
|
158
|
+
"--ui-font-weight-medium",
|
|
159
|
+
"--ui-font-weight-bold"
|
|
86
160
|
],
|
|
87
|
-
"derived": {
|
|
88
|
-
"--ui-action-primary-border": "--ui-action-primary-bg",
|
|
89
|
-
"--ui-action-secondary-border": "--ui-border-control",
|
|
90
|
-
"--ui-action-danger-border": "--ui-action-danger-bg",
|
|
91
|
-
"--ui-status-success-border": "--ui-status-success",
|
|
92
|
-
"--ui-status-warning-border": "--ui-status-warning",
|
|
93
|
-
"--ui-status-danger-border": "--ui-status-danger",
|
|
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",
|
|
98
|
-
"--ui-button-font-weight-sm": "--ui-button-font-weight",
|
|
99
|
-
"--ui-button-font-weight-md": "--ui-button-font-weight",
|
|
100
|
-
"--ui-button-font-weight-lg": "--ui-button-font-weight",
|
|
101
|
-
"--ui-control-checked-bg": "--ui-action-primary-bg",
|
|
102
|
-
"--ui-control-checked-fg": "--ui-action-primary-text",
|
|
103
|
-
"--ui-link-text": "--ui-text-primary",
|
|
104
|
-
"--ui-action-accent-text": "--ui-action-ghost-text",
|
|
105
|
-
"--ui-action-accent-bg-hover": "--ui-action-ghost-bg-hover",
|
|
106
|
-
"--ui-action-accent-bg-active": "--ui-action-ghost-bg-active",
|
|
107
|
-
"--ui-action-danger-outline-text": "--ui-status-danger-text",
|
|
108
|
-
"--ui-action-danger-outline-border": "--ui-status-danger",
|
|
109
|
-
"--ui-action-danger-outline-bg-hover": "--ui-status-danger-bg",
|
|
110
|
-
"--ui-action-danger-outline-bg-active": "--ui-status-danger-bg",
|
|
111
|
-
"--ui-action-warning-bg": "--ui-action-danger-bg",
|
|
112
|
-
"--ui-action-warning-bg-hover": "--ui-action-danger-bg-hover",
|
|
113
|
-
"--ui-action-warning-bg-active": "--ui-action-danger-bg-active",
|
|
114
|
-
"--ui-action-warning-text": "--ui-action-danger-text",
|
|
115
|
-
"--ui-action-warning-border": "--ui-action-danger-border",
|
|
116
|
-
"--ui-text-button-text": "--ui-link-text",
|
|
117
|
-
"--ui-text-button-accent-text": "--ui-text-button-text",
|
|
118
|
-
"--ui-text-button-muted-text": "--ui-text-secondary",
|
|
119
|
-
"--ui-icon-button-size-xs": "--ui-space-6"
|
|
120
|
-
},
|
|
121
161
|
"brandComposition": [
|
|
122
162
|
"--ui-brand-primary",
|
|
123
163
|
"--ui-brand-on-primary",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ksmv/ui-checks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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",
|
package/dist/density.d.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
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
|
-
"--ui-button-font-weight-sm": "font-weight",
|
|
32
|
-
"--ui-button-font-weight-md": "font-weight",
|
|
33
|
-
"--ui-button-font-weight-lg": "font-weight",
|
|
34
|
-
});
|
|
35
|
-
const lengthPattern = /^(?:\d+(?:\.\d+)?|\.\d+)(?:px|rem|em)$/iu;
|
|
36
|
-
const math = /^(?:calc|clamp|min|max)\(.+\)$/iu;
|
|
37
|
-
const unitless = /^(?:\d+(?:\.\d+)?|\.\d+)$/u;
|
|
38
|
-
export function isValidDensityValue(kind, raw) {
|
|
39
|
-
const value = raw.trim();
|
|
40
|
-
switch (kind) {
|
|
41
|
-
case "font-weight": {
|
|
42
|
-
if (/^(?:normal|bold)$/iu.test(value))
|
|
43
|
-
return true;
|
|
44
|
-
if (!/^\d+$/u.test(value))
|
|
45
|
-
return false;
|
|
46
|
-
const weight = Number(value);
|
|
47
|
-
return weight >= 1 && weight <= 1000;
|
|
48
|
-
}
|
|
49
|
-
case "line-height":
|
|
50
|
-
return math.test(value) || (unitless.test(value) && Number(value) > 0);
|
|
51
|
-
case "spacing":
|
|
52
|
-
return value === "0" || math.test(value) || lengthPattern.test(value);
|
|
53
|
-
case "control-height":
|
|
54
|
-
case "font-size":
|
|
55
|
-
return math.test(value) || (lengthPattern.test(value) && Number.parseFloat(value) > 0);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Human-readable description of what a valid value looks like for the given
|
|
60
|
-
* density kind, used in checker diagnostics. Never echoes the offending
|
|
61
|
-
* value itself.
|
|
62
|
-
*/
|
|
63
|
-
export function densityExpectation(kind) {
|
|
64
|
-
switch (kind) {
|
|
65
|
-
case "control-height":
|
|
66
|
-
case "font-size":
|
|
67
|
-
return "a length above zero in px, rem or em, or calc()/clamp()/min()/max()";
|
|
68
|
-
case "spacing":
|
|
69
|
-
return "zero or a length in px, rem or em, or calc()/clamp()/min()/max()";
|
|
70
|
-
case "line-height":
|
|
71
|
-
return "a unitless number above zero, or calc()/clamp()/min()/max()";
|
|
72
|
-
case "font-weight":
|
|
73
|
-
return "an integer from 1 to 1000, normal or bold";
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Resolves a whole-value `var()` reference to the literal it ends in, using
|
|
78
|
-
* the theme's own declarations (and the evidence) first and the reference's
|
|
79
|
-
* fallback second. Returns undefined when the chain does not end in a literal
|
|
80
|
-
* (the reference check reports that) or when it passes through another
|
|
81
|
-
* density token, which is validated on its own.
|
|
82
|
-
*/
|
|
83
|
-
export function resolveDensityReference(value, tokens, seen = new Set()) {
|
|
84
|
-
const trimmed = value.trim();
|
|
85
|
-
const nodes = valueParser(trimmed).nodes;
|
|
86
|
-
if (nodes.length !== 1)
|
|
87
|
-
return undefined;
|
|
88
|
-
const [node] = nodes;
|
|
89
|
-
if (node.type !== "function" || node.value.toLowerCase() !== "var")
|
|
90
|
-
return trimmed;
|
|
91
|
-
const comma = node.nodes.findIndex((child) => child.type === "div" && child.value === ",");
|
|
92
|
-
const name = valueParser.stringify(comma === -1 ? node.nodes : node.nodes.slice(0, comma)).trim();
|
|
93
|
-
const fallback = comma === -1 ? undefined : valueParser.stringify(node.nodes.slice(comma + 1)).trim();
|
|
94
|
-
if (name in densityTokenKinds || seen.has(name))
|
|
95
|
-
return undefined;
|
|
96
|
-
const next = new Set([...seen, name]);
|
|
97
|
-
const declared = tokens.get(name);
|
|
98
|
-
if (declared !== undefined)
|
|
99
|
-
return resolveDensityReference(declared, tokens, next);
|
|
100
|
-
if (fallback)
|
|
101
|
-
return resolveDensityReference(fallback, tokens, next);
|
|
102
|
-
return undefined;
|
|
103
|
-
}
|