// Shared UI primitives const { useState, useEffect, useMemo, useRef, useLayoutEffect, useCallback } = React; const cls = (...xs) => xs.filter(Boolean).join(' '); const Card = ({ children, className, ...rest }) => (
{children}
); const Stat = ({ label, value, delta, deltaTone = 'emerald', icon, sub, onClick, hint }) => { const clickable = !!onClick; return (
{ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onClick(); } } : undefined} >
{label} {icon && {icon}}
{value}
{delta && ( {delta} )} {sub && {sub}}
{clickable && hint && {hint} }
); }; const Pill = ({ tone = 'fg', children, className }) => ( {children} ); const Badge = ({ tone = 'fg', children, dot = true, className }) => ( {dot && } {children} ); const Btn = ({ kind = 'ghost', size = 'md', icon, iconRight, children, className, ...rest }) => ( ); const IconBtn = ({ icon, kind = 'ghost', size = 'md', className, title, ...rest }) => ( ); const Input = ({ icon, ...props }) => ( ); const Toggle = ({ checked, onChange, label }) => ( ); const SectionHead = ({ title, sub, right }) => (

{title}

{sub &&

{sub}

}
{right &&
{right}
}
); const Avatar = ({ name, tone = 'emerald', size = 32 }) => { const initials = (name || '?').split(' ').slice(0, 2).map(s => s[0]).join('').toUpperCase(); return ( {initials} ); }; // Right-side drawer with portal — used for entity detail panels const Drawer = ({ open, onClose, title, eyebrow, headerRight, children, footer, width = 480 }) => { useEffect(() => { if (!open) return; const onKey = (e) => { if (e.key === 'Escape') onClose && onClose(); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [open, onClose]); if (!open) return null; return ReactDOM.createPortal((
), document.body); }; // === Quotation status mapping === const QSTATUS = { aberta: { label: 'Aberta', tone: 'sky' }, analise: { label: 'Em Análise', tone: 'amber' }, fechada: { label: 'Fechada', tone: 'emerald' }, cancelada: { label: 'Cancelada', tone: 'muted' }, }; const OSTATUS = { 'Emitido': { tone: 'sky' }, 'Confirmado': { tone: 'violet' }, 'Em Trânsito': { tone: 'amber' }, 'Entregue': { tone: 'emerald' }, 'Cancelado': { tone: 'muted' }, }; // inject global ui css (function injectUICss() { if (document.getElementById('scp-ui-css')) return; const s = document.createElement('style'); s.id = 'scp-ui-css'; s.textContent = ` .scp-card { background: var(--surface); border: 1px solid var(--border-soft); border-radius: var(--radius-lg); box-shadow: var(--shadow-soft); } .scp-sec-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; margin-bottom: 14px; } .scp-sec-head h3 { margin: 0; font-size: 16px; font-weight: 600; letter-spacing: -0.01em; } .scp-sec-head p { margin: 4px 0 0; font-size: 13px; color: var(--fg-3); } .scp-sec-right { display: flex; gap: 8px; align-items: center; } /* Stat card */ .scp-stat { background: var(--surface); border: 1px solid var(--border-soft); border-radius: var(--radius-lg); padding: 18px 18px 16px; display: flex; flex-direction: column; gap: 8px; position: relative; overflow: hidden; } .scp-stat::after { content: ""; position: absolute; inset: -1px; background: radial-gradient(120% 80% at 100% 0%, oklch(0.4 0.05 160 / .12), transparent 60%); pointer-events: none; } .scp-stat-head { display: flex; justify-content: space-between; align-items: center; } .scp-stat-label { font-size: 12px; color: var(--fg-3); text-transform: uppercase; letter-spacing: .08em; font-weight: 600; } .scp-stat-icon { color: var(--fg-3); transition: color .15s; } .scp-stat-value { font-size: 28px; font-weight: 600; letter-spacing: -0.02em; font-family: var(--font-sans); font-feature-settings: "ss01","tnum"; } .scp-stat-foot { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } .scp-stat-sub { font-size: 12px; color: var(--fg-3); } .scp-stat.clickable { cursor: pointer; transition: border-color .15s, transform .12s, background .15s; } .scp-stat.clickable:hover { border-color: oklch(0.42 0.08 160 / .7); transform: translateY(-1px); } .scp-stat.clickable:hover .scp-stat-icon { color: oklch(0.86 0.14 160); } .scp-stat.clickable:hover .scp-stat-hint { opacity: 1; transform: translateX(0); } .scp-stat.clickable:focus-visible { outline: none; border-color: oklch(0.55 0.12 160); box-shadow: 0 0 0 3px oklch(0.55 0.12 160 / .25); } .scp-stat-hint { position: absolute; right: 14px; bottom: 14px; display: inline-flex; align-items: center; gap: 4px; font-size: 11px; font-weight: 600; color: oklch(0.86 0.14 160); opacity: 0; transform: translateX(-4px); transition: opacity .18s, transform .18s; pointer-events: none; } /* Pill / Badge */ .scp-pill { display: inline-flex; align-items: center; gap: 4px; padding: 3px 8px; border-radius: 999px; font-size: 11px; font-weight: 600; letter-spacing: .01em; line-height: 1; } .scp-pill.tone-emerald { background: oklch(0.28 0.07 160 / .6); color: oklch(0.86 0.14 160); } .scp-pill.tone-amber { background: oklch(0.28 0.06 78 / .6); color: oklch(0.88 0.13 78); } .scp-pill.tone-coral { background: oklch(0.28 0.08 25 / .6); color: oklch(0.86 0.16 25); } .scp-pill.tone-sky { background: oklch(0.28 0.07 230 / .6); color: oklch(0.86 0.12 230); } .scp-pill.tone-violet { background: oklch(0.28 0.07 285 / .6); color: oklch(0.86 0.12 285); } .scp-pill.tone-muted { background: oklch(0.26 0.01 250 / .8); color: var(--fg-3); } .scp-pill.tone-fg { background: oklch(0.30 0.01 250 / .8); color: var(--fg-2); } .scp-pill.tone-solid-emerald { background: var(--emerald); color: oklch(0.16 0.04 160); } .scp-badge { display: inline-flex; align-items: center; gap: 6px; padding: 3px 9px 3px 8px; border-radius: 999px; font-size: 11px; font-weight: 600; letter-spacing: .02em; border: 1px solid transparent; } .scp-badge-dot { width: 6px; height: 6px; border-radius: 999px; background: currentColor; } .scp-badge.tone-emerald { background: oklch(0.24 0.05 160 / .55); color: oklch(0.84 0.14 160); border-color: oklch(0.36 0.06 160 / .55); } .scp-badge.tone-amber { background: oklch(0.24 0.05 78 / .55); color: oklch(0.86 0.13 78); border-color: oklch(0.36 0.06 78 / .55); } .scp-badge.tone-coral { background: oklch(0.24 0.06 25 / .55); color: oklch(0.84 0.16 25); border-color: oklch(0.36 0.07 25 / .55); } .scp-badge.tone-sky { background: oklch(0.24 0.05 230 / .55); color: oklch(0.84 0.12 230); border-color: oklch(0.36 0.06 230 / .55); } .scp-badge.tone-violet { background: oklch(0.24 0.05 285 / .55); color: oklch(0.84 0.12 285); border-color: oklch(0.36 0.06 285 / .55); } .scp-badge.tone-muted { background: oklch(0.24 0.01 250 / .55); color: var(--fg-3); border-color: var(--border); } .scp-badge.tone-fg { background: oklch(0.28 0.01 250 / .55); color: var(--fg-2); border-color: var(--border); } /* Buttons */ .scp-btn { display: inline-flex; align-items: center; gap: 8px; border-radius: 10px; font-weight: 600; letter-spacing: -0.005em; transition: background .12s ease, border-color .12s ease, color .12s ease, transform .08s ease; white-space: nowrap; } .scp-btn.size-sm { padding: 6px 11px; font-size: 12.5px; border-radius: 8px; } .scp-btn.size-md { padding: 9px 14px; font-size: 13.5px; } .scp-btn.size-lg { padding: 12px 18px; font-size: 14.5px; border-radius: 12px; } .scp-btn:active { transform: translateY(1px); } .scp-btn:disabled, .scp-btn[disabled] { opacity: .5; cursor: not-allowed; filter: grayscale(.3); } .scp-btn:disabled:hover, .scp-btn[disabled]:hover { filter: grayscale(.3); } .scp-btn-icon { display: inline-flex; } .scp-btn.kind-primary { background: linear-gradient(180deg, oklch(0.78 0.16 160), oklch(0.66 0.16 160)); color: oklch(0.16 0.04 160); box-shadow: 0 1px 0 oklch(0.92 0.16 160 / .6) inset, 0 8px 22px -10px oklch(0.5 0.16 160 / .7); } .scp-btn.kind-primary:hover { filter: brightness(1.04); } .scp-btn.kind-secondary { background: var(--surface-2); border: 1px solid var(--border); color: var(--fg); } .scp-btn.kind-secondary:hover { background: var(--surface-3); border-color: var(--border-strong); } .scp-btn.kind-ghost { color: var(--fg-2); border: 1px solid transparent; } .scp-btn.kind-ghost:hover { color: var(--fg); background: var(--surface-2); } .scp-btn.kind-danger { background: oklch(0.28 0.07 25); border: 1px solid oklch(0.42 0.10 25); color: oklch(0.92 0.12 25); } .scp-btn.kind-danger:hover { background: oklch(0.34 0.08 25); } .scp-icon-btn { width: 34px; height: 34px; display: inline-grid; place-items: center; border-radius: 9px; color: var(--fg-2); transition: background .12s, color .12s, border-color .12s; border: 1px solid transparent; } .scp-icon-btn.size-sm { width: 28px; height: 28px; border-radius: 7px; } .scp-icon-btn.kind-ghost:hover { background: var(--surface-2); color: var(--fg); } .scp-icon-btn.kind-secondary { background: var(--surface-2); border-color: var(--border); } .scp-icon-btn.kind-secondary:hover { background: var(--surface-3); color: var(--fg); } .scp-icon-btn.kind-soft { background: var(--surface-2); } .scp-icon-btn.kind-soft:hover { background: var(--surface-3); color: var(--fg); } /* Input */ .scp-input { display: inline-flex; align-items: center; gap: 8px; background: var(--surface-2); border: 1px solid var(--border); border-radius: 10px; padding: 8px 12px; color: var(--fg-2); transition: border-color .12s ease, background .12s; } .scp-input:focus-within { border-color: oklch(0.55 0.10 160); background: var(--surface); } .scp-input input { background: transparent; border: 0; outline: 0; flex: 1; min-width: 0; color: var(--fg); font-size: 13.5px; } .scp-input-icon { color: var(--fg-3); display: inline-flex; } .scp-input input::placeholder { color: var(--fg-4); } /* Toggle */ .scp-toggle { display: inline-flex; align-items: center; gap: 10px; cursor: pointer; user-select: none; } .scp-toggle input { display: none; } .scp-toggle .track { width: 32px; height: 18px; border-radius: 999px; background: var(--surface-3); border: 1px solid var(--border); position: relative; transition: background .15s, border-color .15s; } .scp-toggle .thumb { width: 14px; height: 14px; border-radius: 999px; background: var(--fg-3); position: absolute; top: 1px; left: 1px; transition: transform .15s, background .15s; } .scp-toggle input:checked + .track { background: oklch(0.45 0.13 160 / .9); border-color: oklch(0.55 0.14 160); } .scp-toggle input:checked + .track .thumb { transform: translateX(14px); background: oklch(0.94 0.12 160); } .scp-toggle .lbl { font-size: 13px; color: var(--fg-2); } /* Avatar */ .scp-avatar { display: inline-grid; place-items: center; border-radius: 999px; font-weight: 700; letter-spacing: .02em; } .scp-avatar.tone-emerald { background: oklch(0.30 0.08 160); color: oklch(0.94 0.14 160); } .scp-avatar.tone-sky { background: oklch(0.30 0.08 230); color: oklch(0.94 0.12 230); } .scp-avatar.tone-amber { background: oklch(0.30 0.07 78); color: oklch(0.94 0.13 78); } .scp-avatar.tone-coral { background: oklch(0.30 0.08 25); color: oklch(0.94 0.14 25); } .scp-avatar.tone-violet { background: oklch(0.30 0.07 285); color: oklch(0.94 0.12 285); } /* Table */ .scp-table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 13px; } .scp-table thead th { text-align: left; font-weight: 600; color: var(--fg-3); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; padding: 12px 16px; border-bottom: 1px solid var(--border-soft); background: var(--surface); position: sticky; top: 0; z-index: 1; } .scp-table tbody td { padding: 14px 16px; border-bottom: 1px solid var(--border-soft); color: var(--fg-2); vertical-align: middle; } .scp-table tbody tr { transition: background .1s; } .scp-table tbody tr:hover td { background: var(--surface-2); } .scp-table tbody tr.clickable { cursor: pointer; } .scp-table .num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; } .scp-table .em { color: var(--fg); font-weight: 600; } .scp-table tbody tr:last-child td { border-bottom: 0; } /* Tabs */ .scp-tabs { display: inline-flex; gap: 4px; background: var(--surface-2); border: 1px solid var(--border-soft); border-radius: 10px; padding: 4px; } .scp-tabs button { padding: 7px 12px; border-radius: 7px; font-size: 13px; color: var(--fg-3); font-weight: 600; transition: background .12s, color .12s; } .scp-tabs button:hover { color: var(--fg-2); } .scp-tabs button.active { background: var(--surface); color: var(--fg); box-shadow: 0 1px 0 rgba(255,255,255,.04) inset; } /* Generic chip */ .scp-chip { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 999px; background: var(--surface-2); border: 1px solid var(--border-soft); font-size: 12px; color: var(--fg-2); font-weight: 500; } .scp-chip.active { background: oklch(0.30 0.07 160 / .5); color: oklch(0.92 0.13 160); border-color: oklch(0.45 0.08 160 / .5); } /* Scrollable surface */ .scp-scroll { overflow: auto; } /* Animation helpers */ @keyframes fadein { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } } @keyframes scaleIn { from { opacity: 0; transform: scale(.97); } to { opacity: 1; transform: scale(1); } } @keyframes slideInRight { from { transform: translateX(100%); } to { transform: translateX(0); } } .scp-fade-in { animation: fadein .35s ease both; } /* ===== Mobile · ajustes globais ===== */ @media (max-width: 900px) { .scp-screen { padding: 18px 16px 32px !important; gap: 14px !important; } .scp-dash-grid { grid-template-columns: repeat(2, 1fr) !important; gap: 10px !important; } .scp-row-2-1, .scp-row-1-1 { grid-template-columns: 1fr !important; } .scp-hero { grid-template-columns: 1fr !important; padding: 22px !important; } .scp-hero-left h2 { font-size: 22px !important; } .scp-stat { padding: 14px !important; } .scp-stat-value { font-size: 22px !important; } .scp-list-head { flex-direction: column !important; align-items: stretch !important; padding: 14px !important; } .scp-list-head .scp-tabs { overflow-x: auto; } .scp-list-head .scp-input { width: 100% !important; } .scp-modal { width: 95vw !important; max-width: 95vw !important; } .scp-drawer { width: 100vw !important; max-width: 100vw !important; } table.scp-table { font-size: 12px; } table.scp-table thead th, table.scp-table tbody td { padding: 10px 8px; } } @media (max-width: 560px) { .scp-dash-grid { grid-template-columns: 1fr !important; } .scp-screen { padding: 14px 12px 24px !important; } } /* Drawer */ .scp-drawer-mask { position: fixed; inset: 0; z-index: 60; background: oklch(0.05 0.005 250 / .55); backdrop-filter: blur(3px); animation: fadein .18s ease both; } .scp-drawer { position: fixed; top: 0; right: 0; bottom: 0; background: var(--surface); border-left: 1px solid var(--border); box-shadow: -24px 0 80px -20px rgba(0,0,0,0.6); display: flex; flex-direction: column; animation: slideInRight .22s cubic-bezier(.2,.7,.2,1) both; } .scp-drawer-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 18px 20px 14px; border-bottom: 1px solid var(--border-soft); } .scp-drawer-titles { min-width: 0; display: flex; flex-direction: column; gap: 2px; } .scp-drawer-eyebrow { font-size: 10.5px; color: var(--fg-3); text-transform: uppercase; letter-spacing: .14em; font-weight: 700; } .scp-drawer-head h3 { margin: 0; font-size: 18px; font-weight: 600; letter-spacing: -0.01em; } .scp-drawer-head-right { display: flex; align-items: center; gap: 8px; } .scp-drawer-body { flex: 1; overflow-y: auto; padding: 18px 20px; display: flex; flex-direction: column; gap: 18px; } .scp-drawer-foot { padding: 14px 20px; border-top: 1px solid var(--border-soft); display: flex; gap: 8px; justify-content: flex-end; align-items: center; background: oklch(0.18 0.012 250 / .6); } /* Detail building blocks */ .scp-detail-section { display: flex; flex-direction: column; gap: 8px; } .scp-detail-section > h4 { margin: 0; font-size: 11px; color: var(--fg-3); text-transform: uppercase; letter-spacing: .12em; font-weight: 700; } .scp-detail-kv { display: grid; grid-template-columns: 110px 1fr; gap: 4px 14px; font-size: 13px; } .scp-detail-kv dt { color: var(--fg-3); padding: 4px 0; } .scp-detail-kv dd { margin: 0; color: var(--fg); padding: 4px 0; font-weight: 500; } .scp-detail-kv dd.num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; } .scp-detail-metrics { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; } .scp-detail-metric { background: var(--surface-2); border: 1px solid var(--border-soft); border-radius: 10px; padding: 12px 14px; display: flex; flex-direction: column; gap: 2px; } .scp-detail-metric .l { font-size: 10.5px; color: var(--fg-3); text-transform: uppercase; letter-spacing: .1em; font-weight: 600; } .scp-detail-metric .v { font-size: 20px; font-weight: 600; letter-spacing: -0.01em; font-feature-settings: "tnum"; } .scp-detail-metric .v.em { color: oklch(0.86 0.14 160); } .scp-detail-metric .s { font-size: 11px; color: var(--fg-3); } .scp-detail-hero { display: flex; align-items: center; gap: 14px; padding: 14px; border-radius: 12px; background: oklch(0.22 0.04 160 / .25); border: 1px solid oklch(0.34 0.06 160 / .35); } .scp-detail-hero .name { font-size: 17px; font-weight: 600; color: var(--fg); letter-spacing: -0.01em; } .scp-detail-hero .meta { font-size: 12px; color: var(--fg-3); margin-top: 2px; } .scp-detail-cats { display: flex; gap: 4px; flex-wrap: wrap; } .scp-detail-list { display: flex; flex-direction: column; background: var(--surface-2); border: 1px solid var(--border-soft); border-radius: 10px; overflow: hidden; } .scp-detail-list-row { display: grid; grid-template-columns: 1fr auto; gap: 10px; align-items: center; padding: 10px 12px; border-bottom: 1px solid var(--border-soft); font-size: 12.5px; } .scp-detail-list-row:last-child { border-bottom: 0; } .scp-detail-list-row .n { color: var(--fg); font-weight: 500; } .scp-detail-list-row .s { color: var(--fg-3); font-size: 11px; } .scp-detail-list-row .v { color: var(--fg); font-family: var(--font-mono); font-weight: 600; } .scp-detail-empty { font-size: 12.5px; color: var(--fg-3); padding: 16px; text-align: center; background: var(--surface-2); border-radius: 10px; } /* Toast */ .scp-toast-host { position: fixed; right: 22px; bottom: 22px; z-index: 200; display: flex; flex-direction: column-reverse; gap: 8px; pointer-events: none; } .scp-toast { pointer-events: auto; display: grid; grid-template-columns: 4px 1fr auto; gap: 12px; align-items: center; min-width: 280px; max-width: 380px; padding: 12px 14px 12px 0; background: var(--surface); border: 1px solid var(--border); border-radius: 12px; box-shadow: var(--shadow-pop); font-size: 13px; color: var(--fg); animation: scpToastIn .22s cubic-bezier(.2,.7,.2,1) both; overflow: hidden; } .scp-toast.leaving { animation: scpToastOut .26s ease both; } .scp-toast .scp-toast-glow { display: block; width: 4px; align-self: stretch; } .scp-toast.tone-emerald .scp-toast-glow { background: oklch(0.78 0.16 160); box-shadow: 4px 0 24px oklch(0.78 0.16 160 / .35); } .scp-toast.tone-amber .scp-toast-glow { background: oklch(0.84 0.14 78); box-shadow: 4px 0 24px oklch(0.84 0.14 78 / .35); } .scp-toast.tone-coral .scp-toast-glow { background: oklch(0.74 0.18 25); box-shadow: 4px 0 24px oklch(0.74 0.18 25 / .35); } .scp-toast.tone-sky .scp-toast-glow { background: oklch(0.76 0.12 230); box-shadow: 4px 0 24px oklch(0.76 0.12 230 / .35); } .scp-toast .scp-toast-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; } .scp-toast .scp-toast-body p { margin: 0; font-size: 13.5px; font-weight: 600; color: var(--fg); line-height: 1.25; } .scp-toast .scp-toast-body span { font-size: 11.5px; color: var(--fg-3); line-height: 1.3; } .scp-toast .scp-toast-x { width: 24px; height: 24px; border-radius: 6px; display: grid; place-items: center; color: var(--fg-3); font-size: 18px; line-height: 1; transition: background .12s, color .12s; } .scp-toast .scp-toast-x:hover { background: var(--surface-2); color: var(--fg); } @keyframes scpToastIn { from { opacity: 0; transform: translateY(8px) scale(.98); } to { opacity: 1; transform: none; } } @keyframes scpToastOut { to { opacity: 0; transform: translateX(60px); } } `; document.head.appendChild(s); })(); // === Toast (imperative) === function ensureToastHost() { let host = document.getElementById('scp-toast-host'); if (!host) { host = document.createElement('div'); host.id = 'scp-toast-host'; host.className = 'scp-toast-host'; document.body.appendChild(host); } return host; } function scpToast(message, opts = {}) { const { kind = 'emerald', duration = 3500, sub = null } = opts; const host = ensureToastHost(); const el = document.createElement('div'); el.className = `scp-toast tone-${kind}`; el.innerHTML = `

${message}

${sub ? `${sub}` : ''}
`; host.appendChild(el); const remove = () => { el.classList.add('leaving'); setTimeout(() => el.remove(), 260); }; el.querySelector('.scp-toast-x').addEventListener('click', remove); setTimeout(remove, duration); } window.scpToast = scpToast; Object.assign(window, { cls, Card, Stat, Pill, Badge, Btn, IconBtn, Input, Toggle, SectionHead, Avatar, Drawer, QSTATUS, OSTATUS, scpToast, });