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>
Sidebar responsiva¶
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 comremove('-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-8para 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 é semprevw − 48pxem qualquer tela. - O JS de posicionamento usa
Math.min(MOD_W, window.innerWidth - 48)para espelhar omax-wCSS e evitar overflow em smartphones. bounds()desconta automaticamente a sidebar viaaside.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.