Makuco UI
Geral

Dark / Light

Sistema de dark/light mode embutido no Makuco UI — como funciona, as funções disponíveis e exemplos de integração com React, Angular e HTML puro.

O Makuco UI traz um sistema de dark/light mode embutido, exportado diretamente de @db1/makuco-ui-core. É framework-agnóstico — funciona em qualquer ambiente JavaScript sem dependências externas.

Conteúdo


Como funciona

O sistema de temas opera exclusivamente via CSS. Ao chamar setTheme() ou init(), o atributo data-theme é escrito no elemento <html>. Os tokens --db1-* do Makuco UI alteram seus valores de acordo com esse atributo, trocando a paleta de todos os componentes sem nenhum re-render.

html[data-theme="light"]  →  tokens de modo claro (padrão)
html[data-theme="dark"]   →  tokens de modo escuro

A preferência do usuário é persistida no localStorage sob a chave mk-theme. Quando o valor é "system", o Makuco UI detecta a preferência do sistema operacional via prefers-color-scheme e reage a mudanças em tempo real.

Funções

Todas exportadas diretamente de @db1/makuco-ui-core:

import { init, getTheme, setTheme, subscribe } from '@db1/makuco-ui-core';

Tipos

type Theme = 'light' | 'dark' | 'system';

interface ThemeState {
  /** Preferência explícita do usuário — 'system' quando não foi definida. */
  preference: Theme;
  /** Valor efetivo após resolver 'system'. Sempre 'light' ou 'dark'. */
  resolved: 'light' | 'dark';
}

Referência

FunçãoAssinaturaDescrição
init() => () => voidInicializa o sistema. Aplica data-theme no <html> e inicia o listener de mudanças do SO. Retorna uma função de cleanup.
getTheme() => ThemeStateRetorna o estado atual: preferência salva e valor resolvido.
setTheme(theme: Theme) => void'light' e 'dark' persistem no localStorage; 'system' remove a preferência e volta a seguir o SO.
subscribe(cb: (state: ThemeState) => void) => () => voidRegistra um callback para mudanças de tema. Retorna uma função de unsubscribe.

Inicialização

Chame init() uma única vez na inicialização da aplicação. O retorno é uma função de cleanup que remove listeners e subscribers — chame-a quando a aplicação for destruída.

import { init } from '@db1/makuco-ui-core';

const cleanup = init();

// ao destruir a aplicação:
cleanup();

init() realiza três ações:

  1. Lê a preferência salva no localStorage (chave mk-theme)
  2. Sem preferência salva, usa prefers-color-scheme como fallback
  3. Aplica data-theme no <html> e registra o listener de mudanças do SO

Lendo o tema atual

import { getTheme } from '@db1/makuco-ui-core';

const { preference, resolved } = getTheme();
// preference → 'light' | 'dark' | 'system'
// resolved   → 'light' | 'dark'  (nunca 'system')

Use resolved para qualquer lógica de renderização — ele sempre contém o valor efetivo.

Alterando o tema

import { setTheme } from '@db1/makuco-ui-core';

setTheme('dark');    // força escuro, persiste no localStorage
setTheme('light');   // força claro, persiste no localStorage
setTheme('system');  // remove preferência; segue o sistema operacional

Reagindo a mudanças

Use subscribe() para ser notificado de qualquer mudança — inclusive as originadas pelo SO quando o tema está em 'system':

import { subscribe } from '@db1/makuco-ui-core';

const unsubscribe = subscribe(({ preference, resolved }) => {
  console.log('tema resolvido:', resolved); // 'light' | 'dark'
});

// ao finalizar:
unsubscribe();

Uso em React

É JavaScript puro — pode ser integrado num hook sem dependências adicionais.

init() deve ser chamado uma única vez na raiz

O cleanup de init() chama subscribers.clear() internamente, o que removeria todos os subscribers ao desmontar o componente. Por isso, init() não deve ficar dentro de um hook usado em múltiplos componentes. Chame-o uma única vez na raiz e use subscribe nos hooks.

Inicializar na raiz

// App.tsx / root layout
import { init } from '@db1/makuco-ui-core';
import { useEffect } from 'react';

export function AppRoot({ children }: { children: React.ReactNode }) {
  useEffect(() => init(), []); // init() retorna o cleanup — usado como cleanup do effect

  return <>{children}</>;
}

Hook useTheme

O hook assina o sistema de temas, sem inicializá-lo:

// use-theme.ts
import { useEffect, useState } from 'react';
import { getTheme, setTheme, subscribe, type ThemeState } from '@db1/makuco-ui-core';

export function useTheme() {
  const [state, setState] = useState<ThemeState>(getTheme);

  useEffect(() => {
    setState(getTheme()); // sincroniza caso o tema tenha mudado antes do efeito montar
    return subscribe(setState);
  }, []);

  return { ...state, setTheme };
}

setTheme é uma referência estável (função de módulo) — não precisa de useCallback.

Usando o hook

import { useTheme } from './use-theme';

export function ThemeToggle() {
  const { resolved, setTheme } = useTheme();

  return (
    <button onClick={() => setTheme(resolved === 'dark' ? 'light' : 'dark')}>
      {resolved === 'dark' ? '☀ Modo claro' : '☾ Modo escuro'}
    </button>
  );
}

Next.js / next-themes

Se o projeto já usa next-themes, sincronize os dois sistemas num componente wrapper: ao detectar mudanças do next-themes, chame setTheme() do Makuco UI. Evite chamar init() em paralelo ao next-themes — use um único ponto de entrada para o atributo data-theme.

Uso em Angular

Em Angular, exponha o sistema de temas como um Injectable reativo.

ThemeService

// theme.service.ts
import { Injectable, OnDestroy, signal } from '@angular/core';
import {
  init,
  getTheme,
  setTheme as coreSetTheme,
  subscribe,
  type Theme,
  type ThemeState,
} from '@db1/makuco-ui-core';

@Injectable({ providedIn: 'root' })
export class ThemeService implements OnDestroy {
  readonly state = signal<ThemeState>(getTheme());

  private readonly cleanup: () => void;
  private readonly unsubscribe: () => void;

  constructor() {
    this.cleanup = init();
    this.unsubscribe = subscribe((state) => this.state.set(state));
  }

  setTheme(theme: Theme): void {
    coreSetTheme(theme);
  }

  ngOnDestroy(): void {
    this.unsubscribe();
    this.cleanup();
  }
}

Usando o serviço

// theme-toggle.component.ts
import { Component, inject } from '@angular/core';
import { ThemeService } from './theme.service';

@Component({
  selector: 'app-theme-toggle',
  standalone: true,
  template: `
    <button (click)="toggle()">
      {{ theme.state().resolved === 'dark' ? '☀ Modo claro' : '☾ Modo escuro' }}
    </button>
  `,
})
export class ThemeToggleComponent {
  readonly theme = inject(ThemeService);

  toggle(): void {
    const next = this.theme.state().resolved === 'dark' ? 'light' : 'dark';
    this.theme.setTheme(next);
  }
}

APP_INITIALIZER

Para garantir que data-theme seja aplicado antes do primeiro render, registre init como inicializador da aplicação:

// main.ts
import { APP_INITIALIZER } from '@angular/core';
import { init } from '@db1/makuco-ui-core';

bootstrapApplication(AppComponent, {
  providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: () => init,
      multi: true,
    },
  ],
});

Sem framework

Funciona também diretamente em qualquer ambiente JavaScript moderno:

import { init, setTheme, subscribe } from '@db1/makuco-ui-core';

document.addEventListener('DOMContentLoaded', () => {
  const cleanup = init();

  document.querySelector('#btn-dark')
    ?.addEventListener('click', () => setTheme('dark'));

  document.querySelector('#btn-light')
    ?.addEventListener('click', () => setTheme('light'));

  document.querySelector('#btn-system')
    ?.addEventListener('click', () => setTheme('system'));

  subscribe(({ resolved }) => {
    document.querySelector('#theme-indicator')!.textContent = resolved;
  });
});

On this page