medesign 0.0.0-stage → 1.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/LICENSE +21 -0
- package/README.md +128 -2
- package/bin/medesign.js +200 -0
- package/integrations/claude-code/SKILL.md +20 -0
- package/integrations/cursor/medesign.mdc +26 -0
- package/medesign.config.js +46 -0
- package/package.json +62 -4
- package/presets/apple/design-tokens.css +115 -0
- package/presets/apple/guidelines.md +19 -0
- package/presets/apple/preset.json +53 -0
- package/presets/metha/design-tokens.css +127 -0
- package/presets/metha/guidelines.md +23 -0
- package/presets/metha/preset.json +53 -0
- package/presets/schema.json +102 -0
- package/presets/tesla/design-tokens.css +112 -0
- package/presets/tesla/guidelines.md +17 -0
- package/presets/tesla/preset.json +53 -0
- package/references/ai-tells.md +127 -0
- package/references/appendices.md +220 -0
- package/references/architecture.md +39 -0
- package/references/argon-design-archetypes.md +317 -0
- package/references/banner-sizes-and-styles.md +118 -0
- package/references/block-library.md +61 -0
- package/references/brand-design-tokens.md +42 -0
- package/references/catalog.json +108 -0
- package/references/cip-deliverable-guide.md +95 -0
- package/references/cip-design.md +121 -0
- package/references/cip-prompt-engineering.md +84 -0
- package/references/cip-style-guide.md +68 -0
- package/references/cli-architecture.md +146 -0
- package/references/components.md +240 -0
- package/references/dark-mode.md +23 -0
- package/references/design-core.md +42 -0
- package/references/design-md-protocol.md +75 -0
- package/references/design-routing.md +207 -0
- package/references/eng-imagery-content.md +71 -0
- package/references/eng-layout.md +28 -0
- package/references/eng-other.md +26 -0
- package/references/eng-states-theme.md +22 -0
- package/references/eng-type-color.md +44 -0
- package/references/icon-design.md +122 -0
- package/references/image-mockup-direction.md +53 -0
- package/references/image-to-code.md +46 -0
- package/references/impeccable-polish.md +85 -0
- package/references/intake.md +90 -0
- package/references/logo-color-psychology.md +101 -0
- package/references/logo-design.md +110 -0
- package/references/logo-prompt-engineering.md +158 -0
- package/references/logo-style-guide.md +109 -0
- package/references/mesurv-dna.md +104 -0
- package/references/micro-interactions-and-auto-animate.md +71 -0
- package/references/modern-craft-invariants.md +332 -0
- package/references/motion-tokens.md +99 -0
- package/references/preflight.md +110 -0
- package/references/proactive-motion.md +167 -0
- package/references/quality-gate.md +147 -0
- package/references/redesign-audit.md +38 -0
- package/references/redesign.md +52 -0
- package/references/slides-copywriting-formulas.md +84 -0
- package/references/slides-create.md +4 -0
- package/references/slides-html-template.md +295 -0
- package/references/slides-layout-patterns.md +137 -0
- package/references/slides-strategies.md +94 -0
- package/references/slides.md +42 -0
- package/references/social-photos-design.md +329 -0
- package/references/spatial-canvas-engine.md +160 -0
- package/references/stitch-system.md +184 -0
- package/references/style-extraction.md +17 -0
- package/references/styles/brutalist.md +88 -0
- package/references/styles/minimalist.md +81 -0
- package/references/styles/soft.md +94 -0
- package/references/threeui-spatial-engine.md +224 -0
- package/references/ui-styling-shadcn.md +336 -0
- package/references/ui-ux-vocabulary-vault.json +1223 -0
- package/references/ui-ux-vocabulary-vault.md +438 -0
- package/references/vocab-protocol.md +52 -0
- package/references/vocabulary.md +78 -0
- package/scripts/export-tokens.js +403 -0
- package/tests/browser-audit.js +442 -0
- package/tests/linter.js +412 -0
- package/tests/schemas/sarif-2.1.0.schema.json +2915 -0
- package/tests/visual-testbed.html +681 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ME-DESIGN Phase 2 — Rendered DOM & Headless Browser Verification Engine
|
|
5
|
+
* Direct Chrome DevTools Protocol (CDP) client over native WebSocket.
|
|
6
|
+
* Zero external npm dependencies.
|
|
7
|
+
*
|
|
8
|
+
* Verifies:
|
|
9
|
+
* 1. Real Computed Contrast Ratio across backgrounds
|
|
10
|
+
* 2. Real Keyboard Tab Navigation & Focus Ring rendering (:focus-visible)
|
|
11
|
+
* 3. Windows High Contrast Mode (forced-colors: active) physical outline persistence
|
|
12
|
+
* 4. Hit-Target Bounding Dimensions (44px min touch standard)
|
|
13
|
+
* 5. Responsive Viewports (Mobile 390px, Tablet 820px, Desktop 1440px)
|
|
14
|
+
* 6. Theme Modes (Dark & Light)
|
|
15
|
+
*
|
|
16
|
+
* Usage: node tests/browser-audit.js <filepath> [--json]
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const { spawn } = require('child_process');
|
|
22
|
+
|
|
23
|
+
const ANSI_RESET = '\x1b[0m';
|
|
24
|
+
const ANSI_BOLD = '\x1b[1m';
|
|
25
|
+
const ANSI_GREEN = '\x1b[32m';
|
|
26
|
+
const ANSI_RED = '\x1b[31m';
|
|
27
|
+
const ANSI_YELLOW = '\x1b[33m';
|
|
28
|
+
const ANSI_CYAN = '\x1b[36m';
|
|
29
|
+
const ANSI_GRAY = '\x1b[90m';
|
|
30
|
+
|
|
31
|
+
const args = process.argv.slice(2);
|
|
32
|
+
let targetFile = null;
|
|
33
|
+
let isJson = false;
|
|
34
|
+
|
|
35
|
+
for (let i = 0; i < args.length; i++) {
|
|
36
|
+
if (args[i] === '--json') {
|
|
37
|
+
isJson = true;
|
|
38
|
+
} else if (!targetFile && !args[i].startsWith('--')) {
|
|
39
|
+
targetFile = args[i];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
targetFile = targetFile || path.join(__dirname, 'fixtures', 'valid', 'metha-cadastre-dashboard.html');
|
|
44
|
+
|
|
45
|
+
if (!fs.existsSync(targetFile)) {
|
|
46
|
+
console.error(`Error: File not found: ${targetFile}`);
|
|
47
|
+
process.exit(2);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const resolvedPath = path.resolve(targetFile);
|
|
51
|
+
const fileUrl = 'file:///' + resolvedPath.replace(/\\/g, '/');
|
|
52
|
+
|
|
53
|
+
// Locate Chromium executable across Windows, Linux, and macOS
|
|
54
|
+
function findChromium() {
|
|
55
|
+
const envPath = process.env.CHROME_PATH || process.env.CHROME_BIN;
|
|
56
|
+
if (envPath && fs.existsSync(envPath)) return envPath;
|
|
57
|
+
|
|
58
|
+
const localApp = process.env.LOCALAPPDATA || '';
|
|
59
|
+
const candidate = path.join(localApp, 'ms-playwright', 'chromium-1248', 'chrome-win64', 'chrome.exe');
|
|
60
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
61
|
+
|
|
62
|
+
const programFiles = process.env['ProgramFiles'] || 'C:\\Program Files';
|
|
63
|
+
const programFilesX86 = process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)';
|
|
64
|
+
|
|
65
|
+
const alternatives = [
|
|
66
|
+
// Windows
|
|
67
|
+
path.join(programFiles, 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
68
|
+
path.join(programFilesX86, 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
69
|
+
path.join(programFilesX86, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
|
|
70
|
+
// Linux (CI & Debian/Ubuntu)
|
|
71
|
+
'/usr/bin/google-chrome',
|
|
72
|
+
'/usr/bin/google-chrome-stable',
|
|
73
|
+
'/usr/bin/chromium',
|
|
74
|
+
'/usr/bin/chromium-browser',
|
|
75
|
+
'/snap/bin/chromium',
|
|
76
|
+
// macOS
|
|
77
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
78
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium'
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
for (const p of alternatives) {
|
|
82
|
+
if (fs.existsSync(p)) return p;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const chromePath = findChromium();
|
|
88
|
+
if (!chromePath) {
|
|
89
|
+
console.error('Error: Chromium/Chrome binary not found on system.');
|
|
90
|
+
process.exit(2);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Minimal JSON-RPC CDP Client over Node's native global WebSocket
|
|
94
|
+
class CDPClient {
|
|
95
|
+
constructor(port) {
|
|
96
|
+
this.port = port;
|
|
97
|
+
this.ws = null;
|
|
98
|
+
this.msgId = 0;
|
|
99
|
+
this.callbacks = new Map();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async connect(targetUrl) {
|
|
103
|
+
const res = await fetch(`http://127.0.0.1:${this.port}/json/new`, { method: 'PUT' });
|
|
104
|
+
const target = await res.json();
|
|
105
|
+
this.targetId = target.id;
|
|
106
|
+
this.ws = new WebSocket(target.webSocketDebuggerUrl);
|
|
107
|
+
|
|
108
|
+
await new Promise((resolve, reject) => {
|
|
109
|
+
this.ws.onopen = resolve;
|
|
110
|
+
this.ws.onerror = reject;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
this.ws.onmessage = (event) => {
|
|
114
|
+
const msg = JSON.parse(event.data);
|
|
115
|
+
if (msg.id && this.callbacks.has(msg.id)) {
|
|
116
|
+
const { resolve, reject } = this.callbacks.get(msg.id);
|
|
117
|
+
this.callbacks.delete(msg.id);
|
|
118
|
+
if (msg.error) reject(msg.error);
|
|
119
|
+
else resolve(msg.result);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
send(method, params = {}) {
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
const id = ++this.msgId;
|
|
127
|
+
this.callbacks.set(id, { resolve, reject });
|
|
128
|
+
this.ws.send(JSON.stringify({ id, method, params }));
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async close() {
|
|
133
|
+
if (this.ws) {
|
|
134
|
+
try { this.ws.close(); } catch (e) {}
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
await fetch(`http://127.0.0.1:${this.port}/json/close/${this.targetId}`, { method: 'PUT' });
|
|
138
|
+
} catch (e) {}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function runAudit() {
|
|
143
|
+
const cdpPort = 9444 + Math.floor(Math.random() * 200);
|
|
144
|
+
const chromeProcess = spawn(chromePath, [
|
|
145
|
+
'--headless=new',
|
|
146
|
+
`--remote-debugging-port=${cdpPort}`,
|
|
147
|
+
'--no-first-run',
|
|
148
|
+
'--disable-gpu',
|
|
149
|
+
'--no-sandbox',
|
|
150
|
+
'--disable-extensions',
|
|
151
|
+
'--mute-audio',
|
|
152
|
+
'--hide-scrollbars'
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
// Wait for CDP readiness
|
|
156
|
+
let ready = false;
|
|
157
|
+
for (let i = 0; i < 25; i++) {
|
|
158
|
+
try {
|
|
159
|
+
const r = await fetch(`http://127.0.0.1:${cdpPort}/json/version`);
|
|
160
|
+
if (r.ok) { ready = true; break; }
|
|
161
|
+
} catch (e) {}
|
|
162
|
+
await new Promise(res => setTimeout(res, 100));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!ready) {
|
|
166
|
+
chromeProcess.kill();
|
|
167
|
+
console.error('Failed to connect to headless Chromium instance.');
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const client = new CDPClient(cdpPort);
|
|
172
|
+
await client.connect();
|
|
173
|
+
|
|
174
|
+
// Full 7-Condition Test Matrix:
|
|
175
|
+
// 3 Viewports (Mobile, Tablet, Desktop) × 2 Themes (Dark, Light) + Windows High Contrast (forced-colors: active)
|
|
176
|
+
const testMatrix = [
|
|
177
|
+
{ viewport: { name: 'Mobile', width: 390, height: 844, isMobile: true }, theme: 'dark', forcedColors: false },
|
|
178
|
+
{ viewport: { name: 'Mobile', width: 390, height: 844, isMobile: true }, theme: 'light', forcedColors: false },
|
|
179
|
+
{ viewport: { name: 'Tablet', width: 820, height: 1180, isMobile: true }, theme: 'dark', forcedColors: false },
|
|
180
|
+
{ viewport: { name: 'Tablet', width: 820, height: 1180, isMobile: true }, theme: 'light', forcedColors: false },
|
|
181
|
+
{ viewport: { name: 'Desktop', width: 1440, height: 900, isMobile: false }, theme: 'dark', forcedColors: false },
|
|
182
|
+
{ viewport: { name: 'Desktop', width: 1440, height: 900, isMobile: false }, theme: 'light', forcedColors: false },
|
|
183
|
+
// Condition 7: Windows High Contrast Emulation (forced-colors: active)
|
|
184
|
+
{ viewport: { name: 'Desktop', width: 1440, height: 900, isMobile: false }, theme: 'forced-colors', forcedColors: true }
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
const auditResults = [];
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
await client.send('Page.enable');
|
|
191
|
+
await client.send('DOM.enable');
|
|
192
|
+
await client.send('Runtime.enable');
|
|
193
|
+
|
|
194
|
+
for (const testCase of testMatrix) {
|
|
195
|
+
const vp = testCase.viewport;
|
|
196
|
+
const th = testCase.theme;
|
|
197
|
+
|
|
198
|
+
// Set Device Metrics
|
|
199
|
+
await client.send('Emulation.setDeviceMetricsOverride', {
|
|
200
|
+
width: vp.width,
|
|
201
|
+
height: vp.height,
|
|
202
|
+
deviceScaleFactor: 2,
|
|
203
|
+
mobile: vp.isMobile
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Set Emulated Color Scheme and Forced Colors
|
|
207
|
+
const features = [
|
|
208
|
+
{ name: 'prefers-color-scheme', value: testCase.forcedColors ? 'dark' : th }
|
|
209
|
+
];
|
|
210
|
+
if (testCase.forcedColors) {
|
|
211
|
+
features.push({ name: 'forced-colors', value: 'active' });
|
|
212
|
+
} else {
|
|
213
|
+
features.push({ name: 'forced-colors', value: 'none' });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
await client.send('Emulation.setEmulatedMedia', {
|
|
217
|
+
media: 'screen',
|
|
218
|
+
features
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Navigate
|
|
222
|
+
await client.send('Page.navigate', { url: fileUrl });
|
|
223
|
+
await new Promise(res => setTimeout(res, 250)); // Wait for render and styles to settle
|
|
224
|
+
|
|
225
|
+
// Dispatch real keyboard Tab navigation event
|
|
226
|
+
await client.send('Input.dispatchKeyEvent', { type: 'rawKeyDown', key: 'Tab', code: 'Tab', windowsVirtualKeyCode: 9 });
|
|
227
|
+
await client.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Tab', code: 'Tab', windowsVirtualKeyCode: 9 });
|
|
228
|
+
await new Promise(res => setTimeout(res, 50));
|
|
229
|
+
|
|
230
|
+
// Perform in-page inspection
|
|
231
|
+
const inspectExpr = `
|
|
232
|
+
(() => {
|
|
233
|
+
// Relative luminance calculator
|
|
234
|
+
function getLuminance(r, g, b) {
|
|
235
|
+
const [rs, gs, bs] = [r, g, b].map(c => {
|
|
236
|
+
c = c / 255;
|
|
237
|
+
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
238
|
+
});
|
|
239
|
+
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Shared 1x1 canvas for hardware-exact pixel color rasterization
|
|
243
|
+
const pixelCanvas = document.createElement('canvas');
|
|
244
|
+
pixelCanvas.width = 1;
|
|
245
|
+
pixelCanvas.height = 1;
|
|
246
|
+
const pixelCtx = pixelCanvas.getContext('2d', { willReadFrequently: true });
|
|
247
|
+
|
|
248
|
+
function parseRgba(colorStr) {
|
|
249
|
+
if (!colorStr) return [0, 0, 0, 1];
|
|
250
|
+
try {
|
|
251
|
+
pixelCtx.clearRect(0, 0, 1, 1);
|
|
252
|
+
pixelCtx.fillStyle = colorStr;
|
|
253
|
+
pixelCtx.fillRect(0, 0, 1, 1);
|
|
254
|
+
const d = pixelCtx.getImageData(0, 0, 1, 1).data;
|
|
255
|
+
return [d[0], d[1], d[2], d[3] / 255];
|
|
256
|
+
} catch (e) {
|
|
257
|
+
return [255, 255, 255, 1];
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function getContrastRatio(fgRgb, bgRgb) {
|
|
262
|
+
const l1 = getLuminance(fgRgb[0], fgRgb[1], fgRgb[2]);
|
|
263
|
+
const l2 = getLuminance(bgRgb[0], bgRgb[1], bgRgb[2]);
|
|
264
|
+
const lighter = Math.max(l1, l2);
|
|
265
|
+
const darker = Math.min(l1, l2);
|
|
266
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function isDemoAntiPattern(el) {
|
|
270
|
+
return el.closest('.con') || el.classList.contains('con') || el.closest('[data-anti-pattern]') || el.classList.contains('btn-slop') || el.closest('.cmp-col:has(.btn-slop)') || el.closest('.cmp-col:has(.thai-crop-bad)');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Test 1: Hit-Target Dimensions (min 44x44px for touch)
|
|
274
|
+
const buttons = Array.from(document.querySelectorAll('button, [role="button"], a.btn, .action-btn, .pill-btn-container, .telemetry-btn, .btn-ring')).filter(b => !isDemoAntiPattern(b));
|
|
275
|
+
const hitTargetViolations = [];
|
|
276
|
+
buttons.forEach(b => {
|
|
277
|
+
const rect = b.getBoundingClientRect();
|
|
278
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
279
|
+
if (rect.width < 43.5 || rect.height < 43.5) {
|
|
280
|
+
hitTargetViolations.push({
|
|
281
|
+
tag: b.tagName.toLowerCase(),
|
|
282
|
+
text: b.textContent.trim().slice(0, 20),
|
|
283
|
+
width: Math.round(rect.width),
|
|
284
|
+
height: Math.round(rect.height)
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Test 2: Real Computed Contrast Ratio against actual background
|
|
291
|
+
const texts = Array.from(document.querySelectorAll('h1, h2, h3, p, td, th, span, label, .visual-surface')).filter(el => {
|
|
292
|
+
return !isDemoAntiPattern(el) && el.children.length === 0 && el.textContent.trim().length > 0;
|
|
293
|
+
}).slice(0, 20);
|
|
294
|
+
|
|
295
|
+
let minContrast = 21.0;
|
|
296
|
+
const debugContrast = [];
|
|
297
|
+
|
|
298
|
+
texts.forEach(el => {
|
|
299
|
+
const cs = window.getComputedStyle(el);
|
|
300
|
+
if (cs.display === 'none' || cs.visibility === 'hidden' || parseFloat(cs.opacity) === 0) return;
|
|
301
|
+
|
|
302
|
+
const fg = parseRgba(cs.color);
|
|
303
|
+
let solidBg = [9, 9, 11];
|
|
304
|
+
const overlays = [];
|
|
305
|
+
|
|
306
|
+
let p = el;
|
|
307
|
+
while (p && p !== document) {
|
|
308
|
+
const bgStr = window.getComputedStyle(p).backgroundColor;
|
|
309
|
+
if (bgStr && bgStr !== 'transparent') {
|
|
310
|
+
const rgba = parseRgba(bgStr);
|
|
311
|
+
if (rgba[3] >= 0.90) {
|
|
312
|
+
solidBg = [rgba[0], rgba[1], rgba[2]];
|
|
313
|
+
break;
|
|
314
|
+
} else if (rgba[3] > 0) {
|
|
315
|
+
overlays.unshift(rgba);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
p = p.parentElement;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
let compositeBg = [...solidBg];
|
|
322
|
+
overlays.forEach(([r, g, b, a]) => {
|
|
323
|
+
compositeBg[0] = Math.round(r * a + compositeBg[0] * (1 - a));
|
|
324
|
+
compositeBg[1] = Math.round(g * a + compositeBg[1] * (1 - a));
|
|
325
|
+
compositeBg[2] = Math.round(b * a + compositeBg[2] * (1 - a));
|
|
326
|
+
});
|
|
327
|
+
const cr = getContrastRatio(fg, compositeBg);
|
|
328
|
+
debugContrast.push({ tag: el.tagName, text: el.textContent.trim().slice(0, 20), fg: fg.slice(0,3), bg: compositeBg, cr: Math.round(cr * 10) / 10 });
|
|
329
|
+
if (cr < minContrast) minContrast = cr;
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Test 3: Active Element Tab Focus Outline
|
|
333
|
+
let focusVisibleRendered = false;
|
|
334
|
+
let highContrastOutlineRendered = false;
|
|
335
|
+
const targetEl = (document.activeElement && document.activeElement !== document.body)
|
|
336
|
+
? document.activeElement
|
|
337
|
+
: buttons[0];
|
|
338
|
+
|
|
339
|
+
if (targetEl) {
|
|
340
|
+
targetEl.focus();
|
|
341
|
+
const fs = window.getComputedStyle(targetEl);
|
|
342
|
+
const hasOutline = fs.outlineStyle !== 'none' && parseFloat(fs.outlineWidth) > 0;
|
|
343
|
+
const hasShadow = fs.boxShadow && fs.boxShadow !== 'none';
|
|
344
|
+
focusVisibleRendered = hasOutline || hasShadow;
|
|
345
|
+
// Physical outline required for forced-colors: active
|
|
346
|
+
highContrastOutlineRendered = hasOutline;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
buttonCount: buttons.length,
|
|
351
|
+
hitTargetViolations,
|
|
352
|
+
minContrastRatio: Math.round(minContrast * 10) / 10,
|
|
353
|
+
focusVisibleRendered,
|
|
354
|
+
highContrastOutlineRendered,
|
|
355
|
+
debugContrast
|
|
356
|
+
};
|
|
357
|
+
})()
|
|
358
|
+
`;
|
|
359
|
+
|
|
360
|
+
const evalRes = await client.send('Runtime.evaluate', {
|
|
361
|
+
expression: inspectExpr,
|
|
362
|
+
returnByValue: true
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const data = evalRes.result.value || {};
|
|
366
|
+
auditResults.push({
|
|
367
|
+
viewport: vp.name,
|
|
368
|
+
dimensions: `${vp.width}x${vp.height}`,
|
|
369
|
+
theme: th,
|
|
370
|
+
...data
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
} finally {
|
|
374
|
+
await client.close();
|
|
375
|
+
chromeProcess.kill();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
let overallPass = true;
|
|
379
|
+
auditResults.forEach(r => {
|
|
380
|
+
const isForced = r.theme === 'forced-colors';
|
|
381
|
+
const contrastPass = isForced ? true : r.minContrastRatio >= 4.5;
|
|
382
|
+
const hitTargetPass = r.hitTargetViolations.length === 0;
|
|
383
|
+
const focusPass = isForced
|
|
384
|
+
? (r.highContrastOutlineRendered || r.buttonCount === 0)
|
|
385
|
+
: (r.focusVisibleRendered || r.buttonCount === 0);
|
|
386
|
+
|
|
387
|
+
if (!contrastPass || !hitTargetPass || !focusPass) overallPass = false;
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
if (isJson) {
|
|
391
|
+
console.log(JSON.stringify({
|
|
392
|
+
target: targetFile,
|
|
393
|
+
passed: overallPass,
|
|
394
|
+
matrix: auditResults
|
|
395
|
+
}, null, 2));
|
|
396
|
+
process.exit(overallPass ? 0 : 1);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Render Matrix Card
|
|
400
|
+
console.log(`\n${ANSI_BOLD}ME-DESIGN Phase 2 — Browser Verification Matrix${ANSI_RESET}`);
|
|
401
|
+
console.log(`${ANSI_CYAN}Headless Chromium Runtime Engine (CDP v156)${ANSI_RESET}`);
|
|
402
|
+
console.log(`${ANSI_GRAY}────────────────────────────────────────────────────────────────────────${ANSI_RESET}`);
|
|
403
|
+
console.log(`Target: ${path.basename(targetFile)}\n`);
|
|
404
|
+
|
|
405
|
+
console.log(`${'VIEWPORT'.padEnd(10)} | ${'MODE'.padEnd(14)} | ${'CONTRAST'.padEnd(10)} | ${'HIT-TARGET (≥44px)'.padEnd(20)} | ${'TAB FOCUS'.padEnd(10)}`);
|
|
406
|
+
console.log(`${ANSI_GRAY}------------------------------------------------------------------------${ANSI_RESET}`);
|
|
407
|
+
|
|
408
|
+
auditResults.forEach(r => {
|
|
409
|
+
const isForced = r.theme === 'forced-colors';
|
|
410
|
+
const contrastPass = isForced ? true : r.minContrastRatio >= 4.5;
|
|
411
|
+
const hitTargetPass = r.hitTargetViolations.length === 0;
|
|
412
|
+
const focusPass = isForced
|
|
413
|
+
? (r.highContrastOutlineRendered || r.buttonCount === 0)
|
|
414
|
+
: (r.focusVisibleRendered || r.buttonCount === 0);
|
|
415
|
+
|
|
416
|
+
const contrastStr = isForced
|
|
417
|
+
? ANSI_CYAN + 'SYS-HIGH' + ANSI_RESET
|
|
418
|
+
: (contrastPass ? ANSI_GREEN : ANSI_RED) + `${r.minContrastRatio}:1` + ANSI_RESET;
|
|
419
|
+
|
|
420
|
+
const hitStr = hitTargetPass
|
|
421
|
+
? ANSI_GREEN + 'OK (All ≥44px)' + ANSI_RESET
|
|
422
|
+
: ANSI_RED + `FAIL (${r.hitTargetViolations.length} under 44px)` + ANSI_RESET;
|
|
423
|
+
|
|
424
|
+
const focusStr = (focusPass ? ANSI_GREEN : ANSI_RED) + (focusPass ? (isForced ? 'OUTLINE OK' : 'VERIFIED') : (isForced ? 'NO OUTLINE' : 'NO FOCUS')) + ANSI_RESET;
|
|
425
|
+
|
|
426
|
+
console.log(`${r.viewport.padEnd(10)} | ${r.theme.padEnd(14)} | ${contrastStr.padEnd(19)} | ${hitStr.padEnd(31)} | ${focusStr}`);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
console.log(`${ANSI_GRAY}────────────────────────────────────────────────────────────────────────${ANSI_RESET}`);
|
|
430
|
+
if (overallPass) {
|
|
431
|
+
console.log(`${ANSI_GREEN}${ANSI_BOLD}BROWSER MATRIX VERIFICATION: PASS (Exit Code: 0)${ANSI_RESET}\n`);
|
|
432
|
+
process.exit(0);
|
|
433
|
+
} else {
|
|
434
|
+
console.log(`${ANSI_RED}${ANSI_BOLD}BROWSER MATRIX VERIFICATION: FAIL (Exit Code: 1)${ANSI_RESET}\n`);
|
|
435
|
+
process.exit(1);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
runAudit().catch(err => {
|
|
440
|
+
console.error('Browser Audit Error:', err);
|
|
441
|
+
process.exit(1);
|
|
442
|
+
});
|