@designesy/score 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/engine.js +48 -10
- package/package.json +1 -1
package/dist/engine.js
CHANGED
|
@@ -485,7 +485,7 @@ const REMEDIATION = {
|
|
|
485
485
|
v34: 'If the page hosts an AI chatbot or agent, tell people they are talking to an AI no later than their first interaction, in a form assistive technology announces. EU AI Act Article 50(1), which applies from 2026-08-02, places this duty on the provider of the AI system (the organisation that develops it and places it on the market or puts it into service under its own name); it does not apply where the AI nature is obvious from the context, and Article 50(5) asks for the information to be clear and accessible. The check reads any one of: visible "AI assistant" or "Chatbot" text, aria-label="AI assistant" on the chat container, or a data-ai-disclosure attribute on the widget. A live chat answered by people needs no AI disclosure: if the check flagged one for review, confirm who answers. US laws differ in scope: California\'s bot-disclosure law (SB 1001, Bus. & Prof. Code §§17940-17943) covers bots that mislead people about being bots to drive a sale or influence a vote; Colorado SB 24-205 was repealed and re-enacted by SB 26-189 (signed 2026-05-14, obligations from 2027-01-01), which governs automated decision-making in consequential decisions. This is not legal advice.',
|
|
486
486
|
v35: 'Add a forced-colors readiness block: @media (forced-colors: active) { ... } with forced-color-adjust: none on elements that must preserve brand identity (logos, charts, semantic-color indicators). Windows High Contrast Mode and Chrome forced-colors recolor the page: without this media query, critical UI becomes illegible. Also ensure borders/outlines use currentColor or system colors so they adapt. Test with Windows HCM (Settings > Accessibility > Contrast themes).',
|
|
487
487
|
v36: 'Remove UTS #39 confusable characters from CSS identifiers and token names. Confusables are Unicode characters from different scripts (Cyrillic, Greek, fullwidth) that look identical to ASCII letters; Cyrillic а (U+0430), for example, looks like Latin a (U+0061). In token names they enable shadowing attacks (--соlor-bg with Cyrillic с vs --color-bg). Audit all custom property names, class names, and url() paths for non-ASCII characters using a Unicode confusable detector. Provenance: Unicode Technical Standard #39, Unicode 16.0.0. Designesy is the only design verification engine that checks this surface.',
|
|
488
|
-
v37: 'Publish a DESIGN.md file at /DESIGN.md in your repo root and serve it publicly.
|
|
488
|
+
v37: 'Publish a DESIGN.md file at /DESIGN.md in your repo root and serve it publicly. The DESIGN.md format\'s linter (v0.4.0, Apache-2.0) validates the file: 11 lint rules covering broken token refs, missing primary colors, WCAG contrast, orphaned tokens, section order, and more. Designesy integrates Google\'s linter as the spec layer and runs its own 45-check contract verification as the layer above. Install the CLI: npm install -g @google/design.md. Lint locally: npx @google/design.md lint DESIGN.md. Export to W3C DTCG: npx @google/design.md export --format dtcg DESIGN.md. Note: DESIGN.md uses sRGB hex only; for OKLCH/Display P3 color spaces, use the W3C DTCG JSON format directly.',
|
|
489
489
|
v38: 'Rewrite button labels to start with a verb or recognized command. NN/g: "Lead with verbs or verb phrases that clearly outline what will happen after the command is selected." Use "Save changes" not "Changes", "Delete file" not "File". Recognized commands: Save, Cancel, Delete, Edit, Share, Close, Back, Next, etc. This is a WARN (heuristic): review flagged buttons manually.',
|
|
490
490
|
v39: 'Remove trailing periods from button text, labels, and tab text. Microsoft Fluent: "Don\'t end text for buttons, radio buttons, labels, or checkboxes with a period." Periods are for full sentences in tooltips, error messages, and dialog bodies only.',
|
|
491
491
|
v40: 'Replace non-descriptive link text with destination-revealing text. WCAG 2.4.4 Link Purpose: link text should describe the destination. Use "Read the typography guide" not "Click here". Use "View the leaderboard" not "Learn more". NN/g: non-descriptive links force users to read surrounding context to understand the destination.',
|
|
@@ -1139,6 +1139,39 @@ function isFieldSelector(selector) {
|
|
|
1139
1139
|
|| /\.(?:input|field)(?![\w-])/i.test(subject);
|
|
1140
1140
|
});
|
|
1141
1141
|
}
|
|
1142
|
+
/**
|
|
1143
|
+
* v27 helper (engine 1.4.1): the selector of the rule whose body holds the
|
|
1144
|
+
* declaration at `offset`, read backwards from that body's opening brace to
|
|
1145
|
+
* the previous closing brace, opening brace or semicolon. Null when the offset
|
|
1146
|
+
* is not inside a rule body (a stray declaration, or a brace closed in
|
|
1147
|
+
* between). Linear: a few lastIndexOf scans per call, no whole-sheet regex.
|
|
1148
|
+
*/
|
|
1149
|
+
function ruleSelectorAt(css, offset) {
|
|
1150
|
+
const open = css.lastIndexOf('{', offset);
|
|
1151
|
+
if (open < 0 || css.lastIndexOf('}', offset) > open)
|
|
1152
|
+
return null;
|
|
1153
|
+
const start = Math.max(css.lastIndexOf('}', open - 1), css.lastIndexOf('{', open - 1), css.lastIndexOf(';', open - 1)) + 1;
|
|
1154
|
+
return css.slice(start, open);
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* v27 helper (engine 1.4.1): does the font-size declaration at `offset` sit in
|
|
1158
|
+
* a rule whose own selector names a field keyword (input, textarea, select,
|
|
1159
|
+
* .input, .field), the same words the literal scan looks for? v27's literal
|
|
1160
|
+
* regexes find a keyword and then the next rule body, and their `[^{]*` runs
|
|
1161
|
+
* across a closing brace. So the word "select" in `.chip { user-select: none }`
|
|
1162
|
+
* carried into `.chip-label { font-size: 12px }` and FAILed a page whose
|
|
1163
|
+
* fields were all 16px. The regexes still find the candidates; this reads the
|
|
1164
|
+
* selector of the rule each matched declaration actually sits in, and drops a
|
|
1165
|
+
* candidate whose own selector carries no keyword. Class names stay matched
|
|
1166
|
+
* as before (`.search-input`, `.form-select`): many sites style their fields
|
|
1167
|
+
* through such classes, and measured on the leaderboard cohort the stricter
|
|
1168
|
+
* isFieldSelector test dropped real fields (a 14px textarea, a 16px search
|
|
1169
|
+
* input's floor).
|
|
1170
|
+
*/
|
|
1171
|
+
function inFieldRule(css, offset) {
|
|
1172
|
+
const selector = ruleSelectorAt(css, offset);
|
|
1173
|
+
return selector !== null && /input|textarea|select|\.field/i.test(selector.replace(/\/\*[\s\S]*?\*\//g, ' '));
|
|
1174
|
+
}
|
|
1142
1175
|
/**
|
|
1143
1176
|
* v27 helper (engine 1.3.1): in each rule whose selector styles a field
|
|
1144
1177
|
* (isFieldSelector), every `font-size: var(...)` declaration is rewritten to
|
|
@@ -1160,12 +1193,9 @@ function resolveFontSizeVars(css, tokens) {
|
|
|
1160
1193
|
if (css.indexOf('var(') < 0)
|
|
1161
1194
|
return css;
|
|
1162
1195
|
return css.replace(/(font-size\s*:\s*)(var\([^;{}]*\))/gi, (decl, lead, value, offset) => {
|
|
1163
|
-
const open = css.lastIndexOf('{', offset);
|
|
1164
1196
|
// Not inside a rule body (a stray declaration, or a brace closed between).
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
const start = Math.max(css.lastIndexOf('}', open - 1), css.lastIndexOf('{', open - 1), css.lastIndexOf(';', open - 1)) + 1;
|
|
1168
|
-
if (!isFieldSelector(css.slice(start, open)))
|
|
1197
|
+
const selector = ruleSelectorAt(css, offset);
|
|
1198
|
+
if (selector === null || !isFieldSelector(selector))
|
|
1169
1199
|
return decl;
|
|
1170
1200
|
const sub = substituteCssVars(value, [tokens]);
|
|
1171
1201
|
return sub === null ? decl : lead + sub.trim();
|
|
@@ -1211,6 +1241,9 @@ function checkInputFontFloor(css, html, tokens = {}) {
|
|
|
1211
1241
|
const below = [];
|
|
1212
1242
|
let m;
|
|
1213
1243
|
while ((m = inputRe.exec(sheet)) !== null) {
|
|
1244
|
+
// Engine 1.4.1: only a declaration whose own rule styles a field counts.
|
|
1245
|
+
if (!inFieldRule(sheet, m.index + m[0].lastIndexOf('font-size')))
|
|
1246
|
+
continue;
|
|
1214
1247
|
const val = parseFloat(m[1]);
|
|
1215
1248
|
const unit = m[2].toLowerCase();
|
|
1216
1249
|
const px = unit === 'rem' ? val * 16 : val;
|
|
@@ -1221,8 +1254,13 @@ function checkInputFontFloor(css, html, tokens = {}) {
|
|
|
1221
1254
|
// Also check: is there a global input font-size ≥16px? (e.g. `input { font-size: 1rem }`)
|
|
1222
1255
|
// The strict form `input\s*\{` matches standalone rules; the grouped form
|
|
1223
1256
|
// `input,...,{` matches selectors merged by CSS minifiers (e.g. `input,textarea,select{font-size:1rem}`).
|
|
1224
|
-
|
|
1225
|
-
|
|
1257
|
+
// Engine 1.4.1: the floor rule must itself style a field, as above.
|
|
1258
|
+
const floorRe = /input\s*(?:,[^{]*)?\{[^}]*font-size\s*:\s*(?:1rem|16px|1\.0(?:\d+)?rem|[2-9]\dpx)/gi;
|
|
1259
|
+
let hasGlobalFloor = false;
|
|
1260
|
+
let f;
|
|
1261
|
+
while (!hasGlobalFloor && (f = floorRe.exec(sheet)) !== null) {
|
|
1262
|
+
hasGlobalFloor = inFieldRule(sheet, f.index + f[0].lastIndexOf('font-size'));
|
|
1263
|
+
}
|
|
1226
1264
|
if (hasGlobalFloor)
|
|
1227
1265
|
return { id: 'v27', item: 'Input font-size ≥16px (prevents iOS Safari auto-zoom)', category: 'accessibility', status: 'PASS', detail: 'input font-size floor detected' };
|
|
1228
1266
|
if (!hasZoomableField(html))
|
|
@@ -4074,7 +4112,7 @@ function checkRemScale(css) {
|
|
|
4074
4112
|
return { id: 'v16', item: 'Rem-based scale: all text sizes in rem, root at 16px confirmed', category: 'cadence', status: 'WARN', detail: `${pxMatches} px vs ${remMatches} rem` };
|
|
4075
4113
|
}
|
|
4076
4114
|
async function checkDesignMdSpec(targetUrl) {
|
|
4077
|
-
const ITEM = 'DESIGN.md spec-layer validation (
|
|
4115
|
+
const ITEM = 'DESIGN.md spec-layer validation (DESIGN.md format lint)';
|
|
4078
4116
|
const CATEGORY = 'spec';
|
|
4079
4117
|
const parsed = new URL(targetUrl);
|
|
4080
4118
|
const designMdUrl = `${parsed.protocol}//${parsed.host}/DESIGN.md`;
|
|
@@ -4490,7 +4528,7 @@ export async function scoreFromParts(input) {
|
|
|
4490
4528
|
checks.push(input.offline
|
|
4491
4529
|
? {
|
|
4492
4530
|
id: 'v37',
|
|
4493
|
-
item: 'DESIGN.md spec-layer validation (
|
|
4531
|
+
item: 'DESIGN.md spec-layer validation (DESIGN.md format lint)',
|
|
4494
4532
|
category: 'spec',
|
|
4495
4533
|
status: 'SKIP',
|
|
4496
4534
|
detail: 'not probed: offline fixture run (no network); excluded from the score',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@designesy/score",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Design quality gate for AI-generated UI: score any URL on 45 deterministic checks, A–F grade. Runs locally; browser checks return MANUAL. No required dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Designesy <hello@designesy.org>",
|