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:
2026-06-23 16:58:12 +00:00
parent cf720c37a1
commit 12d419de7c
113 changed files with 52423 additions and 951 deletions
@@ -0,0 +1,94 @@
#!/usr/bin/env node
/**
* Recover the next agent action from the durable live-session journal.
*/
import { createLiveSessionStore } from './live/session-store.mjs';
function manualApplyReplyCommand(eventOrId = 'EVENT_ID') {
const id = typeof eventOrId === 'string' ? eventOrId : eventOrId?.id || 'EVENT_ID';
return `live-poll.mjs --reply ${id} done --data '<json>'`;
}
export function manualApplyResumeHint(event = {}) {
const summary = event.manualApplySummary || summarizeManualApplyEvent(event);
const parts = [];
if (summary.pageUrl) parts.push(`page ${summary.pageUrl}`);
if (summary.chunk) parts.push(`chunk ${summary.chunk.index}/${summary.chunk.total}`);
if (Number.isFinite(summary.opCount)) parts.push(`${summary.opCount} op(s)`);
if (Number.isFinite(summary.entryCount)) parts.push(`${summary.entryCount} entr${summary.entryCount === 1 ? 'y' : 'ies'}`);
if (summary.files?.length) parts.push(`likely files: ${summary.files.join(', ')}`);
const scope = parts.length ? ` (${parts.join(', ')})` : '';
return `Manual Apply pending${scope}. If you have not already leased it, run live-poll.mjs. Apply the source edits from the manual_edit_apply batch, then reply with ${manualApplyReplyCommand(event.id)}. Polling only leases this work item; it does not commit source edits. Do not run live-commit-manual-edits.mjs for this leased event. Do not poll again before replying.`;
}
function summarizeManualApplyEvent(event = {}) {
const entries = Array.isArray(event.batch?.entries) ? event.batch.entries : [];
const opCount = entries.reduce((sum, entry) => sum + (Array.isArray(entry.ops) ? entry.ops.length : 0), 0);
return {
pageUrl: event.pageUrl || null,
chunk: event.chunk || null,
entryCount: entries.length,
opCount,
files: collectManualApplyFiles(event.batch),
};
}
function collectManualApplyFiles(batch) {
const files = [];
for (const entry of batch?.entries || []) {
for (const op of entry.ops || []) files.push(op.sourceHint?.file);
}
for (const candidate of batch?.candidates || []) {
files.push(candidate.sourceHint?.relativeFile, candidate.sourceHint?.file);
for (const item of candidate.textMatches || []) files.push(item.file);
for (const item of candidate.objectKeyMatches || []) files.push(item.file);
for (const item of candidate.locatorMatches || []) files.push(item.file);
for (const item of candidate.contextTextMatches || []) files.push(item.file);
}
return [...new Set(files.filter((file) => typeof file === 'string' && file.length > 0))].sort();
}
function parseArgs(argv) {
const out = { id: null };
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (arg === '--id') out.id = argv[++i];
else if (arg.startsWith('--id=')) out.id = arg.slice('--id='.length);
else if (arg === '--help' || arg === '-h') out.help = true;
}
return out;
}
export async function resumeCli() {
const args = parseArgs(process.argv.slice(2));
if (args.help) {
console.log(`Usage: node live-resume.mjs [--id SESSION_ID]\n\nPrint the active durable session checkpoint and the next safe agent action.`);
return;
}
const store = createLiveSessionStore({ cwd: process.cwd(), sessionId: args.id || undefined });
const snapshot = args.id ? store.getSnapshot(args.id) : store.listActiveSessions()[0] || null;
if (!snapshot) {
console.log(JSON.stringify({ active: false, nextAction: 'No active durable live session found.' }, null, 2));
return;
}
const pending = snapshot.pendingEvent || null;
const nextAction = pending
? pending.type === 'manual_edit_apply'
? manualApplyResumeHint(pending)
: `Run live-poll.mjs, handle ${pending.type} ${pending.id}, then acknowledge with live-poll.mjs --reply ${pending.id} done.`
: snapshot.phase === 'carbonize_required'
? `Finish carbonize cleanup${snapshot.sourceFile ? ` in ${snapshot.sourceFile}` : ''}, then run live-complete.mjs --id ${snapshot.id}.`
: snapshot.phase === 'accept_requested'
? `Run live-complete.mjs --id ${snapshot.id} after verifying the accepted variant is written.`
: `Inspect ${snapshot.id}; no pending agent event is currently queued.`;
console.log(JSON.stringify({ active: true, snapshot, pendingEvent: pending, nextAction }, null, 2));
}
const _running = process.argv[1];
if (_running?.endsWith('live-resume.mjs') || _running?.endsWith('live-resume.mjs/')) {
resumeCli();
}