// 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 }) => (
);
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,
});