Pular para conteúdo

Design System

Identidade visual do projeto: energia em fluxo. Implementado com TailwindCSS no Django Template Language. O mesmo design system é replicado no app Flet com os valores hex equivalentes.

O tema escuro é o padrão. O usuário pode alternar para o tema claro; a preferência é persistida.


Configuração do Tailwind

// tailwind.config.js
module.exports = {
  darkMode: 'class',
  content: ['./templates/**/*.html', './**/templates/**/*.html'],
}

A classe dark em <html> ativa o tema escuro. Classes sem prefixo definem o tema claro.


Paleta — Tema Escuro

Token Hex Classe Tailwind
Fundo base #020617 dark:bg-slate-950
Fundo primário #0F172A dark:bg-slate-900
Fundo de card #1E293B dark:bg-slate-800
Fundo de input #334155 dark:bg-slate-700
Texto principal #F8FAFC dark:text-slate-50
Texto secundário #CBD5E1 dark:text-slate-300
Texto sutil #94A3B8 dark:text-slate-400
Borda #334155 dark:border-slate-700

Paleta — Tema Claro

Token Hex Classe Tailwind
Fundo base #F8FAFC bg-slate-50
Fundo primário #FFFFFF bg-white
Fundo de card #FFFFFF bg-white
Fundo de input #F1F5F9 bg-slate-100
Texto principal #0F172A text-slate-900
Texto secundário #334155 text-slate-700
Texto sutil #64748B text-slate-500
Borda #E2E8F0 border-slate-200

Cores de marca e estado (iguais nos dois temas)

Token Hex Claro Escuro
Primária (energia) #10B981 emerald-600 dark:emerald-500
Primária (fluxo) #06B6D4 cyan-600 dark:cyan-500
Acento IA #8B5CF6 violet-600 dark:violet-500
Online / sucesso #10B981 text-emerald-600 dark:text-emerald-400
Pico / aviso #F59E0B text-amber-600 dark:text-amber-400
Offline / perigo #EF4444 text-red-600 dark:text-red-400

Gradiente de marca:

class="bg-gradient-to-r from-emerald-500 to-cyan-500"

Gradiente IA:

class="bg-gradient-to-r from-violet-500 to-fuchsia-500"

Cores de acento — arquétipos (landing page)

Extraídas do guia de otimização da landing (videos/otimizacao da landing - final.pdf), mapeadas aos dois arquétipos da marca (Mago + Cuidador — ver memória project_brand_archetypes). Não substituem a paleta acima; são usadas pontualmente para marcar dados de natureza específica.

Token Hex Papel Onde usar
brand.teal #2DD4BF (teal-400) Consumo real / clareza números de consumo ao vivo (ex: "R$ 12,50 gasto até agora"), ícones de leitura em tempo real
brand.amberWarm #E8A659 Cuidado avisos proativos que protegem o usuário (ex: "avisamos quando o consumo foge do normal")

brand.violet (#8B5CF6) já cobre o arquétipo Mago/previsão — não precisa de token novo.

Não confundir brand.amberWarm com amber-500/amber-600 (token "Pico / aviso" da tabela acima): o amber padrão sinaliza alerta de tarifa/preço; o amberWarm sinaliza cuidado/proteção. São propositalmente próximos, mas com significados diferentes — usar o contexto do texto para decidir qual aplicar.


Tipografia

Fonte Inter (Google Fonts), pesos 400/500/600/700/800, para corpo de texto e UI.

Fonte Plus Jakarta Sans, pesos 600/700/800, para títulos de destaque (font-display) — usada nos h1/h2 de seção da landing page. Corpo, labels e cards continuam em Inter.

<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Plus+Jakarta+Sans:wght@600;700;800&display=swap" rel="stylesheet">
<h2 class="font-display tracking-tight font-extrabold ...">Título de destaque</h2>
Elemento Classes Tailwind
H1 / Hero text-4xl md:text-5xl font-extrabold tracking-tight text-slate-900 dark:text-slate-50
H2 text-2xl font-bold text-slate-900 dark:text-slate-50
H3 text-lg font-semibold text-slate-800 dark:text-slate-100
Corpo text-base text-slate-700 dark:text-slate-300
Label text-sm font-medium text-slate-500 dark:text-slate-400
KPI / métrica text-3xl font-extrabold text-slate-900 dark:text-slate-50
Caption text-xs text-slate-500 dark:text-slate-500

Componentes

Botão primário

<button class="bg-gradient-to-r from-emerald-500 to-cyan-500 hover:from-emerald-400
               hover:to-cyan-400 text-slate-950 font-semibold px-5 py-2.5 rounded-xl
               shadow-lg shadow-emerald-500/20 transition-all duration-200
               focus:outline-none focus:ring-2 focus:ring-emerald-400 focus:ring-offset-2
               focus:ring-offset-white dark:focus:ring-offset-slate-900">
  Salvar
</button>

Botão IA (violeta)

<button class="bg-gradient-to-r from-violet-500 to-fuchsia-500 hover:from-violet-400
               hover:to-fuchsia-400 text-white font-semibold px-5 py-2.5 rounded-xl
               shadow-lg shadow-violet-500/20 transition-all duration-200">
  Gerar previsão
</button>

Botão secundário

<button class="bg-white hover:bg-slate-100 text-slate-700 border border-slate-300
               dark:bg-slate-800 dark:hover:bg-slate-700 dark:text-slate-100 dark:border-slate-700
               font-medium px-5 py-2.5 rounded-xl transition-colors duration-200">
  Cancelar
</button>

Botão destrutivo

<button class="bg-red-600 hover:bg-red-500 text-white font-semibold px-5 py-2.5
               rounded-xl transition-colors duration-200">
  Excluir
</button>

Input

<div class="flex flex-col gap-1.5">
  <label class="text-sm font-medium text-slate-500 dark:text-slate-400">E-mail</label>
  <input type="email"
         class="bg-white border border-slate-300 text-slate-900 placeholder-slate-400
                dark:bg-slate-800 dark:border-slate-700 dark:text-slate-100 dark:placeholder-slate-500
                rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:ring-2
                focus:ring-emerald-500 focus:border-transparent transition-all duration-200">
</div>

Card base

<div class="bg-white border border-slate-200 shadow-lg shadow-slate-200/50
            dark:bg-slate-800/80 dark:backdrop-blur dark:border-slate-700 dark:shadow-xl dark:shadow-black/20
            rounded-2xl p-6">
</div>

Card de KPI

<div class="bg-white border border-slate-200 dark:bg-slate-800 dark:border-slate-700 rounded-2xl p-6">
  <p class="text-sm font-medium text-slate-500 dark:text-slate-400">Custo estimado do ciclo</p>
  <p class="mt-2 text-3xl font-extrabold bg-gradient-to-r from-emerald-500 to-cyan-500
            dark:from-emerald-400 dark:to-cyan-400 bg-clip-text text-transparent">R$ 187,40</p>
</div>

Badge de status

<!-- Online -->
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium
             bg-emerald-500/10 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-400">
  <span class="w-1.5 h-1.5 rounded-full bg-emerald-500 dark:bg-emerald-400"></span> Online
</span>

<!-- Offline -->
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium
             bg-red-500/10 text-red-700 dark:bg-red-500/15 dark:text-red-400">
  <span class="w-1.5 h-1.5 rounded-full bg-red-500 dark:bg-red-400"></span> Offline
</span>

<!-- Pico -->
<span class="px-2.5 py-1 rounded-full text-xs font-medium
             bg-amber-500/10 text-amber-700 dark:bg-amber-500/15 dark:text-amber-400">Pico</span>

Grid de KPIs

<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
  <!-- cards -->
</div>

A sidebar usa o padrão fixed-overlay em mobile / static em desktop:

<aside id="sidebar"
       class="fixed inset-y-0 left-0 z-40 w-64 flex flex-col min-h-screen
              -translate-x-full transition-transform duration-300 ease-in-out
              lg:static lg:translate-x-0 lg:z-auto lg:shrink-0
              bg-white border-r border-slate-200
              dark:bg-slate-900 dark:border-slate-800">
  • Mobile (< lg): fixed, fora da tela por padrão. Abre com remove('-translate-x-full') + overlay semitransparente.
  • Desktop (lg+): static, participa do flex-row normalmente.
  • Botão hamburger (apenas mobile): fixed top-3.5 left-4 z-50 lg:hidden
  • Conteúdo principal recebe pt-14 lg:pt-8 para compensar o hamburger em mobile.
function openSidebar()  { /* remove -translate-x-full, mostra overlay */ }
function closeSidebar() { /* adiciona -translate-x-full, oculta overlay */ }

Chatbot flutuante (seção Assistente)

  • Botão redondo w-16 h-16 rounded-full, posicionado por JS no canto inferior direito da área de conteúdo.
  • Modal w-96 max-w-[calc(100vw-3rem)] — largura máxima é sempre vw − 48px em qualquer tela.
  • O JS de posicionamento usa Math.min(MOD_W, window.innerWidth - 48) para espelhar o max-w CSS e evitar overflow em smartphones.
  • bounds() desconta automaticamente a sidebar via aside.getBoundingClientRect().right — funciona corretamente quando sidebar está oculta (right ≈ 0) ou aberta (right = 256).

Cuidado com o widget do VLibras: base.html carrega o plugin do VLibras (acessibilidade em Libras) em todas as páginas do site. Ele se posiciona sozinho sempre no centro vertical exato da viewport (top ≈ 50%, testado em alturas de 700 a 1300px), independentemente da altura da tela. Qualquer elemento fixo novo que use top: 50% para se centralizar verticalmente vai colidir com ele. Prefira uma posição fixa a partir do topo ou do rodapé (ex.: top: 7.5rem ou bottom: 2rem), nunca top: 50%.


Favicon

Raio SVG com gradiente de marca em static/img/favicon.svg. Referenciado em base.html:

{% load static %}
<link rel="icon" type="image/svg+xml" href="{% static 'img/favicon.svg' %}">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
  <defs>
    <linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
      <stop offset="0%" stop-color="#10b981"/>
      <stop offset="100%" stop-color="#06b6d4"/>
    </linearGradient>
  </defs>
  <path fill="url(#g)" d="M19 2L5 18h11l-3 12 15-18H17z"/>
</svg>

Tema escuro — Implementação web

Script anti-flash — deve estar no <head> do base.html, antes do CSS:

<script>
  (function () {
    var stored = localStorage.getItem('theme');
    var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
    var isDark = stored ? stored === 'dark' : (prefersDark || stored === null);
    document.documentElement.classList.toggle('dark', isDark);
  })();
</script>

Botão de alternância (sidebar autenticada):

<button id="theme-toggle" type="button" aria-label="Alternar tema"
        class="p-2 rounded-lg text-slate-500 hover:bg-slate-100
               dark:text-slate-400 dark:hover:bg-slate-800 transition-colors">
  <span class="hidden dark:inline">☀️</span>
  <span class="inline dark:hidden">🌙</span>
</button>

<script>
  document.getElementById('theme-toggle').addEventListener('click', function () {
    var isDark = document.documentElement.classList.toggle('dark');
    localStorage.setItem('theme', isDark ? 'dark' : 'light');
  });
</script>

Exceção deliberada na landing pública: templates/core/landing.html não usa este script nem este botão. A landing tem seu próprio mecanismo de alternância (um interruptor de luz, com um botão compacto equivalente em telas estreitas), sempre abre no tema escuro e não persiste a escolha em localStorage entre visitas. É intencional, não um bug a corrigir para bater com o resto do site.


Paridade no app Flet

O Flet não usa Tailwind; usa os mesmos valores hex diretamente.

Token Claro Escuro
Fundo (page.bgcolor) #F8FAFC #020617
Fundo de card #FFFFFF #1E293B
Texto principal #0F172A #F8FAFC
Texto sutil #64748B #94A3B8
Cor primária #10B981 #10B981
Acento IA #8B5CF6 #8B5CF6
import flet as ft

def apply_theme(page: ft.Page, dark: bool) -> None:
    page.theme_mode = ft.ThemeMode.DARK if dark else ft.ThemeMode.LIGHT
    page.bgcolor = '#020617' if dark else '#F8FAFC'
    page.theme = ft.Theme(color_scheme_seed='#10B981', font_family='Inter')
    page.update()

def toggle_theme(page: ft.Page) -> None:
    is_dark = page.theme_mode == ft.ThemeMode.DARK
    apply_theme(page, dark=not is_dark)
    page.client_storage.set('theme_dark', not is_dark)

A preferência do usuário é persistida via client_storage. O tema escuro é o padrão.