Tema claro / escuro

Tema claro/escuro para todas as UIs Koder (web, Flutter mobile/desktop, TV, landing pages): comportamento padrão pós-instalação (ThemeMode.system), persistência da escolha do usuário, anti-flash, CSS vars.

Spec fonte: specs/themes/light-dark.kmd

Tokens semânticos de cor

Toda UI Koder expõe as mesmas variáveis CSS semânticas. As páginas declaram os valores light em :root e sobrescrevem em [data-theme="dark"]. Os valores abaixo são a baseline canônica; produtos podem tighten.

Token Claro Escuro Para
--bg #ffffff #0b1120 page background
--text #0f172a #f1f5f9 primary text
--text-muted #475569 #94a3b8 secondary text
--surface #f8fafc #111827 card / panel surface
--surface-2 #e2e8f0 #1f2937 raised surface
--accent #1d4ed8 #60a5fa primary action
--accent-on #ffffff #0b1120 text on accent
--border #cbd5e1 #334155 divider / border
--focus #1e3a8a #bfdbfe focus ring

Comportamento obrigatório

  1. Apenas dois modos — Claro e Escuro — sem terceiro estado "sistema" no toggle.
  2. Seleção inicial — Honrar prefers-color-scheme do SO no primeiro load (ou após localStorage limpo).
  3. Persistência da escolha do usuário — Salvar o toggle explícito em localStorage["theme"]; isso sobrescreve a preferência do SO em visitas futuras.
  4. Propagação live do SO — Sem preferência salva, seguir mudanças do SO via matchMedia(...).addEventListener("change", …).
  5. Sem flash de tema errado — Aplicar o tema salvo inline antes do primeiro render, no <head> antes do link de CSS.

Estrutura CSS obrigatória

:root {
  --bg: #ffffff;
  --text: #0f172a;
  /* ... other semantic tokens ... */
  color-scheme: light;
}

[data-theme="dark"] {
  --bg: #0b1120;
  --text: #f1f5f9;
  /* ... */
  color-scheme: dark;
}

Script inline anti-flash

Coloque este snippet dentro do <head>, antes de qualquer stylesheet externo. É a menor implementação correta.

<script>
  (function(){
    const s = localStorage.getItem('theme');
    const dark = s ? s === 'dark' : matchMedia('(prefers-color-scheme:dark)').matches;
    if (dark) document.documentElement.setAttribute('data-theme','dark');
  })();
</script>

JavaScript do toggle

function toggleTheme() {
  const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
  const next = isDark ? 'light' : 'dark';
  localStorage.setItem('theme', next);
  applyTheme();
}
function applyTheme() {
  const saved = localStorage.getItem('theme');
  const isDark = saved
    ? saved === 'dark'
    : matchMedia('(prefers-color-scheme:dark)').matches;
  document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
}
matchMedia('(prefers-color-scheme:dark)').addEventListener('change', () => {
  if (!localStorage.getItem('theme')) applyTheme();
});
applyTheme();

Apps Flutter / nativos

Apps nativos seguem o mesmo contrato. Até o widget KoderTheme aparecer no koder_kit v0.6.0, use o pattern inline abaixo — e nunca hardcode ThemeMode.dark ou ThemeMode.light antes de ler a preferência salva.

// main.dart — until KoderTheme widget ships in koder_kit v0.6.0+
Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final prefs = await SharedPreferences.getInstance();
  final saved = prefs.getString('theme'); // 'light' | 'dark' | null
  runApp(MyApp(initialTheme: saved));
}

ThemeMode _resolve(String? saved) {
  if (saved == 'dark') return ThemeMode.dark;
  if (saved == 'light') return ThemeMode.light;
  return ThemeMode.system; // follow OS when no preference saved
}

Checklist de auditoria

/k-housekeep e linters equivalentes verificam que cada surface web cumpre todos os itens.

  1. <head> contém o script anti-flash que lê localStorage["theme"] e seta data-theme.
  2. CSS contém um seletor [data-theme="dark"] com overrides.
  3. color-scheme: light em :root e color-scheme: dark em [data-theme="dark"].
  4. Um botão toggle referenciando toggleTheme() (ou equivalente) vive na navbar.
  5. JavaScript expõe as funções toggleTheme e applyTheme com o comportamento documentado.
  6. Dois ícones (sol / lua) trocam de visibilidade quando o toggle dispara.
  7. matchMedia('(prefers-color-scheme:dark)') é usado e reagido.
  8. Valores salvos são exatamente "light" ou "dark" — nunca outra string.