vanna-clubpetro/static/vanna-theme.css
leonardosalazar-cp 1d152c0dce Initial commit: Vanna 2.0 deployment for ClubPetro
Wrapper application around upstream Vanna with:
- Tenant-aware ChromaDB memory (per program/store)
- ClickHouse RLS runner with introspection guards
- PT-BR system prompt and chat translations
- Custom Plotly chart generator (ranked bar, datetime coercion)
- Embed bootstrap (theme pierce + i18n + markdown) shared by demo and React app
- Event sink for chat turn observability
2026-04-29 17:22:05 -03:00

393 lines
14 KiB
CSS

/* Vanna chat theme — ClubPetro
* Paleta espelhada do widget de referência (test.html):
* - Brand orange: #F46A1F (primário / interativo)
* - Brand orange dark: #DB5510 (badges, hover, accent forte)
* - Purple deep: #4A1A56 (texto / estrutural)
* - Purple medium: #7B2D8E (info / divisores secundários)
* - Cream bg: #F8EFE2 (header, surfaces warm)
* - Cream border: #EADFCB (borders sobre cream)
* - Meta/muted: #8B7B6E
* - Font: Open Sans
*
* CSS custom properties pierce the Shadow DOM, so overriding them on the
* <vanna-chat> host element retemas tudo dentro do componente.
*
* Reference (todos os tokens disponíveis):
* vanna/frontends/webcomponent/src/styles/vanna-design-tokens.ts
*/
/* Estratégia dual:
* - vanna-chat: matchea o host element no documento (cascade externo).
* - :host: matchea o host de qualquer shadow root onde este sheet
* for adotado via adoptedStyleSheets (override em todo nested
* custom element como vanna-message, plotly-chart, etc., já que
* eles re-declaram os tokens via :host).
*
* Google Fonts é carregado via <link> no HTML — @import não funciona
* em CSSStyleSheet construída via replaceSync().
*/
:host,
vanna-chat {
/* === Brand accent (orange) === */
--vanna-accent-primary-default: #f46a1f;
--vanna-accent-primary-stronger: #db5510;
--vanna-accent-primary-strongest: #b8460e;
--vanna-accent-primary-subtle: rgba(244, 106, 31, 0.12);
--vanna-accent-primary-hover: #e55c13;
/* "Positive" também ancora no laranja pra manter coerência da marca */
--vanna-accent-positive-default: #f46a1f;
--vanna-accent-positive-stronger: #db5510;
--vanna-accent-positive-subtle: rgba(244, 106, 31, 0.12);
/* Negativo continua vermelho (semântica de erro deve diferir da marca) */
--vanna-accent-negative-default: #dc2626;
--vanna-accent-negative-stronger: #b91c1c;
--vanna-accent-negative-subtle: rgba(220, 38, 38, 0.1);
/* Warning em amber pra distinguir do laranja-primário */
--vanna-accent-warning-default: #d97706;
--vanna-accent-warning-stronger: #b45309;
--vanna-accent-warning-subtle: rgba(217, 119, 6, 0.1);
/* === Foreground (texto) — roxo deep do widget de referência === */
--vanna-foreground-default: #4a1a56;
--vanna-foreground-dimmer: #6b4673;
--vanna-foreground-dimmest: #8b7b6e;
/* === Backgrounds (cream warm pra ecoar fundo da logo + página) ===
* root/default ficam branco puro (legibilidade nas bolhas/tabelas);
* higher/subtle/lower puxam #F8EFE2 (cream do test.html) pra dar
* identidade em surfaces secundárias (sidebar, áreas de input,
* separadores).
*/
--vanna-background-root: #ffffff;
--vanna-background-default: #ffffff;
--vanna-background-higher: #f8efe2;
--vanna-background-highest: #f0e2c8;
--vanna-background-subtle: #fbf5ea;
--vanna-background-lower: #f8efe2;
/* === Outlines / borders — cream-tinted pro look warm + brand orange
* pro estado de hover/focus.
*/
--vanna-outline-default: rgba(244, 106, 31, 0.25);
--vanna-outline-dimmer: #eadfcb;
--vanna-outline-dimmest: #f3e8d2;
--vanna-outline-hover: #f46a1f;
/* === Typography === */
--vanna-font-family-default: "Open Sans", system-ui, -apple-system,
"Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
/* === Shape — bolhas um pouco menos arredondadas pra ar mais corporativo === */
--vanna-chat-bubble-radius: 14px;
--vanna-chat-bubble-radius-sm: 8px;
}
/* ============================================================================
* Containment fixes — impedem mensagem/tabela de estourar a largura do chat.
*
* Estes seletores casam elementos *dentro* dos shadow roots onde este sheet
* é adoptado (vanna-message, vanna-chat). Não dependem de :host.
* ========================================================================== */
/* Bolha de mensagem:
* 1) Texto puro mantém o limite original do upstream (~580px) — preserva
* a estética de bolha de chat. Só adicionamos min-width:0 + overflow-wrap
* pra texto longo quebrar dentro da bolha em vez de vazar.
* 2) Quando a mensagem do assistente tem tabela/chart embutido, ampliamos
* o max-width pra 100% — caso contrário a tabela empurraria a bolha
* além do limite (display:flex sem min-width:0 nos children).
*/
.message {
min-width: 0;
box-sizing: border-box;
}
.message-content {
min-width: 0;
max-width: 100%;
overflow-wrap: anywhere;
word-break: break-word;
box-sizing: border-box;
}
/* Headers de markdown dentro do balão (### etc., gerados pelo mdToHtml
do embed-demo.html). Sem essas regras, o browser usa default que é
grande demais e quebra o ritmo do balão. */
.message-content h1,
.message-content h2,
.message-content h3,
.message-content h4,
.message-content h5,
.message-content h6 {
font-weight: 600;
line-height: 1.3;
margin: 0.7em 0 0.3em 0;
}
.message-content h1 { font-size: 1.25em; }
.message-content h2 { font-size: 1.15em; }
.message-content h3 { font-size: 1.05em; }
.message-content h4 { font-size: 1em; }
.message-content h5 { font-size: 0.95em; }
.message-content h6 { font-size: 0.9em; opacity: 0.85; }
/* Primeiro header sem margem-top — evita gap visível na borda do balão. */
.message-content > h1:first-child,
.message-content > h2:first-child,
.message-content > h3:first-child,
.message-content > h4:first-child,
.message-content > h5:first-child,
.message-content > h6:first-child {
margin-top: 0;
}
/* Esconder o FAB nativo quando minimizado — substituímos por um CTA
custom (.vanna-cta) renderizado fora do shadow DOM em embed-demo.html
/ no app React. A regra :host(.minimized) é a única que vence o
posicionamento fixed do componente.
*
* overflow:hidden é cinto-de-segurança: o host fica 0x0 com overflow
* clippando o .chat-layout interno (que mantemos com dimensões naturais
* — vê regra abaixo), evitando o crash do ResizeObserver do Plotly.
*/
:host(.minimized) {
background: transparent !important;
box-shadow: none !important;
width: 0 !important;
height: 0 !important;
overflow: hidden !important;
pointer-events: none !important;
}
:host(.minimized) .minimized-icon {
display: none !important;
}
/* Plotly ResizeObserver crash defense. Upstream (vanna-chat.ts:103-105)
* faz `:host(.minimized) .chat-layout { display: none }` quando o chat
* minimiza. Plotly tem um ResizeObserver no container do chart que dispara
* `_t [as relayout]` em cada mutação de tamanho; quando o container vai
* pra display:none, o `gd._fullLayout` interno fica undefined e o relayout
* quebra com `TypeError: Cannot read properties of undefined (reading
* 'width')` (kt at vanna-components.js:22888).
*
* Mantemos display:grid (cancela o display:none upstream) + dimensões
* naturais — o host já é 0x0 com overflow:hidden, então o chat-layout
* é clippado visualmente sem precisar mudar suas próprias dimensões.
* Plotly não vê o container colapsar pra 0x0 e o ResizeObserver fica
* quieto. pointer-events:none impede interação acidental.
*/
:host(.minimized) .chat-layout {
display: grid !important;
pointer-events: none !important;
}
/* CTA abre direto em maximized; estado intermediário "normal" não é
exposto ao usuário. Esconde os botões de restore (que voltariam de
maximized → normal) e maximize. Sobra só o .minimize, que dispara
windowState='minimized' e o CTA volta a aparecer. */
.window-control-btn.restore,
.window-control-btn.maximize {
display: none !important;
}
.message.assistant:has(.rich-dataframe, .rich-component, .plotly-chart) {
max-width: 100%;
width: 100%;
}
/* Tabela de resultados (rich-dataframe):
* - .dataframe-table-container já tem overflow:auto, mas falta limitar
* o max-width do rich-component (wrapper) ao tamanho da bolha.
* - Forçando 100% + box-sizing, a tabela com N colunas ganha scroll
* horizontal interno em vez de empurrar a bolha.
*/
.rich-component,
.rich-dataframe,
.dataframe-table-container {
max-width: 100%;
width: 100%;
box-sizing: border-box;
}
.dataframe-table-container {
overflow-x: auto;
overflow-y: auto;
}
/* Cabeçalhos longos quebram em duas linhas em vez de forçar coluna larga;
* células de dado não quebram (preserva alinhamento numérico). */
.dataframe-table th {
white-space: normal;
overflow-wrap: anywhere;
}
.dataframe-table td {
white-space: nowrap;
}
/* Avatar do header — substitui as iniciais "AV" pela logo ClubPetro.
* Upstream renderiza <div class="chat-avatar">${initials}</div> num shadow
* root; este sheet é adotado lá dentro via attachShadow patch (ver
* embed-demo.html), então o seletor de classe vence o :host upstream em
* specificity igual + ordem (adotado vai por último). font-size:0 esconde
* as iniciais sem mudar o layout grid 44x44 do upstream.
*/
.chat-avatar {
background-color: transparent;
background-image: url(/clubpetro-logo.svg);
background-repeat: no-repeat;
background-size: 100%;
background-position: center;
backdrop-filter: none;
border: none;
overflow: visible;
font-size: 0;
color: transparent;
}
/* Bolha de mensagem do usuário — laranja brand sólido (sem gradient).
* Cor única alinhada ao #F46A1F do widget de referência. Texto branco
* mantém contraste suficiente em semibold (≈3.4:1 WCAG AA Large).
*
* align-self: flex-end + width: fit-content fazem a bolha encolher ao
* tamanho do conteúdo (em vez de esticar até o max-width default do
* upstream). O parent .chat-messages é flex-direction:column com
* align-items:stretch implícito, então sem o override a bolha enche
* a coluna inteira mesmo com texto curto.
*
* max-width replica o do upstream (vanna-message.ts:57). Necessário
* porque `width: fit-content` sozinho pode empurrar a bolha além do
* limite quando o conteúdo tem strings incompressíveis (URL longa,
* palavra grudada) — overflow-wrap: anywhere já está em .message-content
* mas o cap explícito é cinto + suspensório. min-width: 0 garante que
* flex children obedeçam ao max-width.
*/
.message.user {
background: #f46a1f;
border-color: rgba(255, 255, 255, 0.18);
align-self: flex-end;
width: fit-content;
max-width: min(80%, 500px);
min-width: 0;
box-sizing: border-box;
}
:host([theme="dark"]) .message.user {
background: #f46a1f;
}
/* ============================================================================
* Header — cream bg, roxo deep no título, laranja no subtítulo.
*
* Upstream renderiza o header com gradient laranja cheio + overlay
* radial branco e texto branco. Aqui invertemos: fundo cream #F8EFE2,
* título em roxo #4A1A56 e subtítulo injetado via ::after no .header-text
* (upstream tem property `subtitle` mas nunca renderiza no template —
* pseudo-element é mais simples que rebuild).
* ========================================================================== */
.chat-header {
background: #f8efe2 !important;
border-bottom: 1px solid #eadfcb !important;
color: #4a1a56 !important;
}
.chat-header::before {
display: none;
}
.chat-title {
color: #4a1a56 !important;
font-weight: 700;
}
.header-text {
gap: 2px;
}
.header-text::after {
content: "inteligência para seu posto";
color: #f46a1f;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.01em;
line-height: 1.2;
}
/* Window control buttons (refresh / minimize / maximize / X) — repintar
* com tinta roxa pra contrastar com o cream em vez do laranja original.
*
* .header-top-actions / .window-controls precisam de flex-shrink:0 porque
* upstream (vanna-chat.ts:204-209) só seta `margin-left:auto` no actions,
* sem proteção de shrink. Em larguras justas (chat ocupando metade da
* viewport via :host(.maximized){left:50vw} + título/avatar consumindo o
* .header-left flex:1) o squeeze do flexbox comprime os botões a 0 e o
* minimize some. Travar shrink mantém os 32x32 sempre.
*
* .window-control-btn ganha `flex-shrink:0` pelo mesmo motivo + width
* explícito no shadow (upstream põe width:32px sem flex-basis, então em
* algumas combinações o shrink ainda toca).
*/
.header-top-actions,
.window-controls {
flex-shrink: 0 !important;
}
.window-control-btn {
background: rgba(74, 26, 86, 0.05) !important;
border-color: rgba(74, 26, 86, 0.1) !important;
color: #4a1a56 !important;
flex-shrink: 0 !important;
}
.window-control-btn:hover {
background: rgba(74, 26, 86, 0.12) !important;
}
/* Maximizado ocupa só a metade direita da tela em vez da viewport inteira.
* Upstream (vanna-chat.ts:62-75) usa top/left/right/bottom = space-6 (24px),
* cobrindo full-screen com margem. Override de `left` pra 50vw mantém a
* sensação de "side panel" — usuário ainda vê o app por trás à esquerda.
*
* overflow:hidden no host é cinto-de-segurança contra qualquer descendente
* (bolha, chart, tabela) que rompa as constraints internas — nada visível
* vaza além das bordas arredondadas.
*/
:host(.maximized) {
left: 50vw !important;
overflow: hidden !important;
}
/* Containment do .chat-main — é filho direto do grid .chat-layout
* (1fr quando .compact). min-width:0 + overflow:hidden impedem que um
* descendente largo (ex.: tabela com muitas colunas, chart Plotly que
* recalculou layout estranho) empurre o próprio chat-main além da
* coluna do grid, o que arrastaria o header e cortaria os botões.
*/
.chat-main {
min-width: 0 !important;
overflow: hidden !important;
}
/* Spinner do status bar — repintar de teal upstream pra roxo da marca.
* Upstream (vanna-status-bar.ts:190-198) usa border teal #15A8A8 + glow
* teal via @keyframes spinnerGlow. Como o sheet é adotado em todo shadow
* root (incluindo o do <vanna-status-bar>), tanto a regra de border
* quanto o redefine do @keyframes vencem o upstream (mesma especificity,
* adotado depois).
*/
.spinner {
border-color: rgba(74, 26, 86, 0.18) !important;
border-top-color: #4a1a56 !important;
}
@keyframes spinnerGlow {
0%, 100% {
filter: drop-shadow(0 0 2px rgba(74, 26, 86, 0.45));
}
50% {
filter: drop-shadow(0 0 6px rgba(74, 26, 86, 0.75));
}
}