12d419de7c
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
147 lines
4.2 KiB
JavaScript
147 lines
4.2 KiB
JavaScript
/**
|
|
* Browser-side DOM helpers for Impeccable live mode.
|
|
*
|
|
* Kept separate from live-browser.js so future browser script parts can share
|
|
* chrome mounting, lookup, focus, and picker helpers without depending on the
|
|
* full overlay UI bundle.
|
|
*/
|
|
(function (root) {
|
|
'use strict';
|
|
if (!root) return;
|
|
|
|
function createLiveBrowserDomHelpers({
|
|
prefix,
|
|
skipTags,
|
|
document: doc = root.document,
|
|
css = root.CSS,
|
|
crypto = root.crypto,
|
|
} = {}) {
|
|
if (!prefix) throw new Error('prefix required');
|
|
if (!doc) throw new Error('document required');
|
|
const tagsToSkip = skipTags || new Set();
|
|
|
|
function own(el) {
|
|
return el && (el.id?.startsWith(prefix) || el.closest?.('[id^="' + prefix + '"]'));
|
|
}
|
|
|
|
function pickable(el) {
|
|
if (!el || el.nodeType !== 1) return false;
|
|
if (tagsToSkip.has(String(el.tagName || '').toLowerCase())) return false;
|
|
if (own(el)) return false;
|
|
const r = el.getBoundingClientRect();
|
|
return r.width >= 20 && r.height >= 20;
|
|
}
|
|
|
|
function desc(el) {
|
|
if (!el) return '';
|
|
let s = el.tagName.toLowerCase();
|
|
if (el.id) s += '#' + el.id;
|
|
else if (el.classList.length) s += '.' + [...el.classList].slice(0, 2).join('.');
|
|
return s;
|
|
}
|
|
|
|
function rectIsUsableAnchor(rect) {
|
|
return !!rect && rect.width > 0.5 && rect.height > 0.5;
|
|
}
|
|
|
|
function makeFrozenAnchor(el) {
|
|
if (!el || !el.getBoundingClientRect) return null;
|
|
const r = el.getBoundingClientRect();
|
|
if (!rectIsUsableAnchor(r)) return null;
|
|
const rect = {
|
|
x: r.x, y: r.y,
|
|
top: r.top, left: r.left,
|
|
right: r.right, bottom: r.bottom,
|
|
width: r.width, height: r.height,
|
|
};
|
|
return {
|
|
__impeccableFrozenAnchor: true,
|
|
tagName: el.tagName || 'DIV',
|
|
id: el.id || '',
|
|
classList: el.classList ? [...el.classList] : [],
|
|
hasAttribute: () => false,
|
|
getBoundingClientRect: () => rect,
|
|
};
|
|
}
|
|
|
|
function id8() {
|
|
if (crypto?.randomUUID) return crypto.randomUUID().replace(/-/g, '').slice(0, 8);
|
|
return (Math.random().toString(16).slice(2) + Date.now().toString(16)).slice(0, 8);
|
|
}
|
|
|
|
function cssId(id) {
|
|
if (css?.escape) return css.escape(id);
|
|
return String(id).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1');
|
|
}
|
|
|
|
function liveUiRoot() {
|
|
const uiRoot = root.__IMPECCABLE_LIVE_UI_ROOT__;
|
|
if (uiRoot && typeof uiRoot.appendChild === 'function') return uiRoot;
|
|
return doc.body;
|
|
}
|
|
|
|
function uiAppend(el) {
|
|
liveUiRoot().appendChild(el);
|
|
return el;
|
|
}
|
|
|
|
function uiAppendStyle(styleEl) {
|
|
const uiRoot = liveUiRoot();
|
|
if (uiRoot && uiRoot !== doc.body) uiRoot.appendChild(styleEl);
|
|
else doc.head.appendChild(styleEl);
|
|
return styleEl;
|
|
}
|
|
|
|
function uiGetById(id) {
|
|
const uiRoot = liveUiRoot();
|
|
if (uiRoot?.getElementById) {
|
|
const found = uiRoot.getElementById(id);
|
|
if (found) return found;
|
|
}
|
|
if (uiRoot?.querySelector) {
|
|
const found = uiRoot.querySelector('#' + cssId(id));
|
|
if (found) return found;
|
|
}
|
|
return doc.getElementById(id);
|
|
}
|
|
|
|
function activeElementDeep() {
|
|
let active = doc.activeElement;
|
|
while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
|
|
return active;
|
|
}
|
|
|
|
function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) {
|
|
if (!rootEl) return;
|
|
if (setPointerEvents) {
|
|
rootEl.style.setProperty('pointer-events', 'auto', 'important');
|
|
}
|
|
const stop = (e) => e.stopPropagation();
|
|
rootEl.addEventListener('pointerdown', stop);
|
|
rootEl.addEventListener('mousedown', stop);
|
|
rootEl.addEventListener('focusin', stop);
|
|
}
|
|
|
|
return {
|
|
own,
|
|
pickable,
|
|
desc,
|
|
rectIsUsableAnchor,
|
|
makeFrozenAnchor,
|
|
id8,
|
|
cssId,
|
|
liveUiRoot,
|
|
uiAppend,
|
|
uiAppendStyle,
|
|
uiGetById,
|
|
activeElementDeep,
|
|
defangOutsideHandlers,
|
|
};
|
|
}
|
|
|
|
root.__IMPECCABLE_LIVE_DOM__ = {
|
|
version: 1,
|
|
createLiveBrowserDomHelpers,
|
|
};
|
|
})(typeof window !== 'undefined' ? window : globalThis);
|