feat: comprehensive design system overhaul
Applied impeccable design critique, audit, harden, layout, distill, and polish across all pages. DESIGN SYSTEM: - Created PRODUCT.md (5 design principles, WCAG 2.1 AA target) - Created DESIGN.md (34 color tokens, 6 radius scale, full component spec) - Created .impeccable/design.json (sidecar with tonal ramps, shadows, motion) ACCESSIBILITY: - 33 aria-labels across 7 pages (was ~13) - 63 <label> elements (was ~30) - 6 <main> landmarks + role=navigation on all sidebars - Esc to close modals on 4 pages - Keyboard shortcuts (Ctrl+J/K, Ctrl+F) on chat THEMING: - 216 hardcoded colors replaced with var() references - 449 !important removed from dark-mode.css - Dark mode script unified (was duplicated 9x inline) - All modals converted to .modal-overlay + .modal design system classes CHAT: - Error states for messages (.msg.erro, .msg.enviando) - Connection status indicator - Confirmation modal before finalizar - Input bar reorganized (3 visible actions, + menu) - Right panel accordions - Send button disabled when empty - Touch targets 44px on mobile CLIENT DETAIL: - Tabs/badges/sub-tabs use CSS classes instead of inline styles - 'Iniciar Conversa' button now primary action - alert() replaced with inline feedback - Modal converted to design system classes CLIENT LIST: - Modal moved inside <body> (was HTML-invalid) - alert() replaced with inline feedback - Modal uses .show pattern SETTINGS: - 4 modals converted to design system classes - 14 alert() calls replaced with inline feedback - fecharModal() uses classList instead of style.display ROUTES: - border-radius normalized, hardcoded colors fixed Score progression: Chat: 23 → 32/40 Client List: 32 → 35/40 Client Detail: 26 → 30/40 Settings: 29 → 35/40 Routes: 37 → 38/40 Audit (project): 14/20 → 17/20
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
function profileNow() {
|
||||
return typeof performance !== 'undefined' && performance.now
|
||||
? performance.now()
|
||||
: Date.now();
|
||||
}
|
||||
|
||||
function createDetectorProfile() {
|
||||
return { events: [] };
|
||||
}
|
||||
|
||||
function recordProfileEvent(profile, event) {
|
||||
if (!profile) return;
|
||||
const normalized = {
|
||||
engine: event.engine || 'unknown',
|
||||
phase: event.phase || 'unknown',
|
||||
ruleId: event.ruleId || 'unknown',
|
||||
target: event.target || '',
|
||||
ms: Number.isFinite(event.ms) ? event.ms : 0,
|
||||
findings: Number.isFinite(event.findings) ? event.findings : 0,
|
||||
};
|
||||
if (event.detail) normalized.detail = event.detail;
|
||||
if (Array.isArray(event.findingIds) && event.findingIds.length) {
|
||||
normalized.findingIds = event.findingIds;
|
||||
}
|
||||
if (typeof profile === 'function') {
|
||||
profile(normalized);
|
||||
} else if (typeof profile.record === 'function') {
|
||||
profile.record(normalized);
|
||||
} else if (Array.isArray(profile.events)) {
|
||||
profile.events.push(normalized);
|
||||
} else if (Array.isArray(profile)) {
|
||||
profile.push(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
function extractFindingIds(findings) {
|
||||
if (!Array.isArray(findings) || findings.length === 0) return [];
|
||||
return [...new Set(findings.map(f => f?.id || f?.type || f?.antipattern).filter(Boolean))];
|
||||
}
|
||||
|
||||
function profileFindings(profile, meta, callback) {
|
||||
if (!profile) return callback();
|
||||
const started = profileNow();
|
||||
const findings = callback();
|
||||
recordProfileEvent(profile, {
|
||||
...meta,
|
||||
ms: profileNow() - started,
|
||||
findings: Array.isArray(findings) ? findings.length : 0,
|
||||
findingIds: extractFindingIds(findings),
|
||||
});
|
||||
return findings;
|
||||
}
|
||||
|
||||
function profileStep(profile, meta, callback) {
|
||||
if (!profile) return callback();
|
||||
const started = profileNow();
|
||||
try {
|
||||
return callback();
|
||||
} finally {
|
||||
recordProfileEvent(profile, {
|
||||
...meta,
|
||||
ms: profileNow() - started,
|
||||
findings: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function profileFindingsAsync(profile, meta, callback) {
|
||||
if (!profile) return callback();
|
||||
const started = profileNow();
|
||||
const findings = await callback();
|
||||
recordProfileEvent(profile, {
|
||||
...meta,
|
||||
ms: profileNow() - started,
|
||||
findings: Array.isArray(findings) ? findings.length : 0,
|
||||
findingIds: extractFindingIds(findings),
|
||||
});
|
||||
return findings;
|
||||
}
|
||||
|
||||
async function profileStepAsync(profile, meta, callback) {
|
||||
if (!profile) return callback();
|
||||
const started = profileNow();
|
||||
try {
|
||||
return await callback();
|
||||
} finally {
|
||||
recordProfileEvent(profile, {
|
||||
...meta,
|
||||
ms: profileNow() - started,
|
||||
findings: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function percentile(sortedValues, pct) {
|
||||
if (!sortedValues.length) return 0;
|
||||
const idx = Math.min(
|
||||
sortedValues.length - 1,
|
||||
Math.max(0, Math.ceil((pct / 100) * sortedValues.length) - 1),
|
||||
);
|
||||
return sortedValues[idx];
|
||||
}
|
||||
|
||||
function summarizeDetectorProfile(profile) {
|
||||
const events = Array.isArray(profile)
|
||||
? profile
|
||||
: (Array.isArray(profile?.events) ? profile.events : []);
|
||||
const groups = new Map();
|
||||
for (const event of events) {
|
||||
const key = [
|
||||
event.engine || 'unknown',
|
||||
event.phase || 'unknown',
|
||||
event.ruleId || 'unknown',
|
||||
event.target || '',
|
||||
].join('\u0000');
|
||||
let group = groups.get(key);
|
||||
if (!group) {
|
||||
group = {
|
||||
engine: event.engine || 'unknown',
|
||||
phase: event.phase || 'unknown',
|
||||
ruleId: event.ruleId || 'unknown',
|
||||
target: event.target || '',
|
||||
calls: 0,
|
||||
totalMs: 0,
|
||||
findings: 0,
|
||||
samples: [],
|
||||
};
|
||||
groups.set(key, group);
|
||||
}
|
||||
const ms = Number.isFinite(event.ms) ? event.ms : 0;
|
||||
group.calls += 1;
|
||||
group.totalMs += ms;
|
||||
group.findings += Number.isFinite(event.findings) ? event.findings : 0;
|
||||
group.samples.push(ms);
|
||||
}
|
||||
return [...groups.values()]
|
||||
.map(group => {
|
||||
const samples = group.samples.sort((a, b) => a - b);
|
||||
return {
|
||||
engine: group.engine,
|
||||
phase: group.phase,
|
||||
ruleId: group.ruleId,
|
||||
target: group.target,
|
||||
calls: group.calls,
|
||||
totalMs: Number(group.totalMs.toFixed(3)),
|
||||
avgMs: Number((group.totalMs / group.calls).toFixed(3)),
|
||||
p50: Number(percentile(samples, 50).toFixed(3)),
|
||||
p95: Number(percentile(samples, 95).toFixed(3)),
|
||||
findings: group.findings,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.totalMs - a.totalMs);
|
||||
}
|
||||
|
||||
export {
|
||||
profileNow,
|
||||
createDetectorProfile,
|
||||
recordProfileEvent,
|
||||
extractFindingIds,
|
||||
profileFindings,
|
||||
profileStep,
|
||||
profileFindingsAsync,
|
||||
profileStepAsync,
|
||||
percentile,
|
||||
summarizeDetectorProfile,
|
||||
};
|
||||
Reference in New Issue
Block a user