sync: auto-sync from GURU-5070 at 2026-06-03 20:07:24

Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-03 20:07:24
This commit is contained in:
2026-06-03 20:07:28 -07:00
parent 10e8d7b6bb
commit 37ccc5f35c
12 changed files with 1241 additions and 0 deletions

View File

@@ -0,0 +1,254 @@
#!/usr/bin/env node
/**
* human-flow scanner
*
* Static analysis pass for mouse + keyboard workflow friction.
* Expands the spirit of frontend-design and impeccable with a narrow,
* human-motor-and-expectation focus.
*
* Usage:
* node scripts/scan.mjs --path dashboard/src --format json
* node scripts/scan.mjs --path dashboard/src/features/sessions
*
* It is intentionally lightweight (regex + heuristics) so it can run fast
* inside agent loops. The real intelligence comes from the agent combining
* these findings with full component reading and task-flow understanding.
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const args = process.argv.slice(2);
let targetPath = 'src';
let format = 'text';
let mode = 'friction'; // 'friction' | 'fancy'
for (let i = 0; i < args.length; i++) {
if (args[i] === '--path' || args[i] === '-p') targetPath = args[++i];
if (args[i] === '--format' || args[i] === '-f') format = args[++i];
if (args[i] === '--fancy' || args[i] === '--mode=fancy') mode = 'fancy';
if (args[i] === '--mode' && args[i + 1] === 'fancy') { mode = 'fancy'; i++; }
}
const absTarget = path.resolve(process.cwd(), targetPath);
if (!fs.existsSync(absTarget)) {
console.error(`Target not found: ${absTarget}`);
process.exit(1);
}
const findings = [];
function walk(dir) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (['node_modules', 'dist', 'build', '.git'].includes(entry.name)) continue;
walk(full);
} else if (/\.(tsx|jsx|ts|js|css)$/.test(entry.name)) {
analyzeFile(full);
}
}
}
function analyzeFile(file) {
const content = fs.readFileSync(file, 'utf8');
const rel = path.relative(process.cwd(), file).replace(/\\/g, '/');
const lines = content.split('\n');
if (mode === 'fancy') {
// Fancy / beauty & elegance pass — lighter static signals + prompts for qualitative review
let match;
// Existing transitions / animations (look for opportunities to refine)
const hasTransition = /transition:|transition-\w+:|animate-|@keyframes|ViewTransition|view-transition/i.test(content);
if (hasTransition) {
findings.push({
file: rel,
line: 1,
category: 'fancy-existing',
severity: 'info',
pattern: 'existing-motion',
message: 'This file already contains motion/transition code. Good candidate for the fancy pass to review quality, consistency, and restraint.',
humanImpact: 'Existing fancy elements can feel either premium or cheap/janky depending on execution.',
suggestion: 'In the fancy pass, evaluate easing curves, durations, performance, reduced-motion respect, and whether the motion serves the human workflow or just decorates.'
});
}
// Missing View Transitions API in SPA navigation contexts
if (/(useNavigate|navigate\(|<Link|react-router|next\/|router\.push)/i.test(content) && !/document\.startViewTransition|View Transitions|view-transition/i.test(content)) {
findings.push({
file: rel,
line: 1,
category: 'fancy-opportunity',
severity: 'low',
pattern: 'missing-view-transitions',
message: 'Navigation or view change logic detected without use of the View Transitions API.',
humanImpact: 'Page-like changes can feel abrupt or cheap. Modern "ajax-style" smooth transitions between views feel significantly more premium.',
suggestion: 'Consider wrapping key navigation with document.startViewTransition() + CSS view-transition-name for elegant morphs or fades. Only where it genuinely improves perceived quality.'
});
}
// Basic hover without fancy enhancement
if (/:hover\s*\{[^}]*background|transform|box-shadow|scale|opacity/i.test(content)) {
findings.push({
file: rel,
line: 1,
category: 'fancy-opportunity',
severity: 'low',
pattern: 'basic-hover',
message: 'Hover state exists but may be basic. Opportunity for more elegant micro-interaction.',
humanImpact: 'A merely functional hover feels flat. A refined one (subtle lift + shadow + accent) makes the interface feel alive and high-craft.',
suggestion: 'Layer tasteful depth (shadow + slight scale or translate) with excellent easing. Keep it restrained, especially in dense data views.'
});
}
return; // In fancy mode we mostly collect signals for the agent to do deep qualitative work
}
// === FRICTION MODE (original) ===
// 1. Small / sm button targets in interactive contexts (very common friction)
const smallButton = /size=["']sm["']|<button[^>]*className=.*btn--sm|height:\s*2[0-8]px|min-height:\s*2[0-8]px/g;
let match;
while ((match = smallButton.exec(content)) !== null) {
const lineNo = content.substring(0, match.index).split('\n').length;
findings.push({
file: rel,
line: lineNo,
category: 'target-size',
severity: 'high',
pattern: 'small-button',
message: 'Compact "sm" button or very small height used for an action. Frequent actions (especially in lists) become precision targets.',
humanImpact: 'Operators must slow down and aim carefully for common tasks. High error rate under time pressure.',
suggestion: 'Use default (md) size for primary/frequent actions. For true compact row actions, ensure generous invisible padding or switch to a larger always-visible treatment.'
});
}
// 2. Hover-revealed or low-opacity row actions (the classic operator console anti-pattern)
if (/\.dt__rowactions|\.rowactions|\.actions\s*\{[^}]*opacity:\s*0\.[0-6]/s.test(content) ||
/opacity:\s*0\.[0-6][^}]*hover|hover[^}]*opacity:\s*(1|0\.[7-9])/s.test(content)) {
const lineNo = 1; // best effort
findings.push({
file: rel,
line: lineNo,
category: 'discoverability',
severity: 'high',
pattern: 'hover-only-actions',
message: 'Row or list actions are dimmed or hidden until hover (or only fully visible on hover).',
humanImpact: 'A human scanning a list with eyes + mouse must "paint" every row to discover what they can do. Keyboard users often never see the controls at full strength.',
suggestion: 'Raise resting opacity to 0.71.0 so actions are scannable at a glance. Or move frequent actions into a dedicated, always-visible column or primary row target. Keep hover only for polish, not discovery.'
});
}
// 3. onClick without obvious keyboard support on non-native elements
const clickNoKeyboard = /onClick=\{[^}]+}\s*(?!.*(onKeyDown|tabIndex|role=))/g;
while ((match = clickNoKeyboard.exec(content)) !== null) {
const lineNo = content.substring(0, match.index).split('\n').length;
// Only flag if it looks like a custom interactive (div, span, custom component in list context)
const context = content.substring(Math.max(0, match.index - 80), match.index + 120);
if (/<\s*(div|span|tr|td|li|custom|Card|Row)[^>]*onClick|onClick[^>]*<\s*(div|span|tr|td|li|Card|Row)/.test(context)) {
findings.push({
file: rel,
line: lineNo,
category: 'keyboard-parity',
severity: 'high',
pattern: 'click-without-keyboard',
message: 'Custom element has onClick but no visible tabIndex/onKeyDown/Enter-Space handling in the immediate area.',
humanImpact: 'Keyboard (or mixed mouse+keyboard) users cannot activate the same thing the mouse can without extra workarounds.',
suggestion: 'Add tabIndex={0}, onKeyDown handler for Enter/Space, and strong :focus-visible styles. Prefer native <button> when possible.'
});
}
}
// 4. Icon-only buttons without accessible name (common with small action icons)
const iconButton = /<Button[^>]*>\s*<[^>]+Icon|<\s*button[^>]*>\s*<[^>]+Icon|<[A-Z][^>]*>\s*<[^>]+Icon/g;
while ((match = iconButton.exec(content)) !== null) {
const lineNo = content.substring(0, match.index).split('\n').length;
const nearby = content.substring(Math.max(0, match.index - 30), match.index + 180);
if (!/aria-label|title=/.test(nearby)) {
findings.push({
file: rel,
line: lineNo,
category: 'discoverability',
severity: 'medium',
pattern: 'icon-only-no-label',
message: 'Icon-only button or action with no aria-label or title.',
humanImpact: 'Screen readers and keyboard users (and anyone who forgets what the tiny icon means) have no idea what it does until they activate it.',
suggestion: 'Add aria-label (and preferably a visible label or tooltip that works on focus too).'
});
}
}
// 5. Very narrow status / action columns (precision rail)
if (/width:\s*2[0-9]px|width:\s*30px|padding-left:\s*0 !important/.test(content) && /status|actions|select/i.test(content)) {
findings.push({
file: rel,
line: 1,
category: 'target-size',
severity: 'medium',
pattern: 'narrow-rail',
message: 'Very narrow column (status, select, or actions rail) used for interactive or important visual elements.',
humanImpact: 'Mouse must be extremely precise to hit the control or even read the status comfortably.',
suggestion: 'Widen the rail or make the entire left edge a larger hit area (see dt__checkwrap pattern). Status can be visual + text on hover/focus.'
});
}
// 6. Row that is fully clickable + internal small actions (mis-click risk)
if (/onRowClick|onClick.*row|tr.*onClick/.test(content) && /dt__rowactions|rowactions/.test(content)) {
findings.push({
file: rel,
line: 1,
category: 'workflow',
severity: 'medium',
pattern: 'row-click-plus-internal-actions',
message: 'Whole row is clickable (for detail/open) while also containing small action buttons inside the row.',
humanImpact: 'Easy to accidentally trigger the row action when aiming for the small icon (or vice versa). Classic source of "I didn\'t mean to open that".',
suggestion: 'Make the primary row action very clearly the dominant target (bigger visual weight, different treatment). Or stop making the whole row clickable and use a dedicated primary button + separate secondary actions.'
});
}
}
walk(absTarget);
// Deduplicate similar findings per file
const seen = new Set();
const uniqueFindings = findings.filter(f => {
const key = `${f.file}:${f.pattern}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
if (format === 'json') {
console.log(JSON.stringify({ target: absTarget, mode, findings: uniqueFindings }, null, 2));
} else {
const title = mode === 'fancy'
? `Human-Flow "Fancy as Fuck" Signals for: ${absTarget}`
: `Human-Flow Scan Results for: ${absTarget}`;
console.log(`${title}\n`);
if (uniqueFindings.length === 0) {
if (mode === 'fancy') {
console.log('No obvious static fancy signals detected.\nThis is normal — the real fancy pass is qualitative. Load references/fancy-as-fuck.md and evaluate the target for beauty, elegance, and appropriate delight opportunities.');
} else {
console.log('No obvious mouse/keyboard friction patterns detected by static rules.\nRun a full agent review with the references/heuristics.md for deeper semantic issues.');
}
} else {
uniqueFindings.forEach((f, i) => {
console.log(`${i + 1}. [${f.severity.toUpperCase()}] ${f.category}${f.pattern}`);
console.log(` File: ${f.file}:${f.line}`);
console.log(` ${f.message}`);
console.log(` Human impact: ${f.humanImpact}`);
console.log(` Suggestion: ${f.suggestion}\n`);
});
}
}
const exitCode = mode === 'fancy' ? 0 : (uniqueFindings.length > 0 ? 2 : 0);
process.exit(exitCode);