@hibrit/moldura
v0.1.0
Published
Escola Moldura e Painel da HIBRIT: moldura do app, folhas, campos pt-BR e componentes do DS, sem cor de produto (tudo por variáveis CSS).
Readme
@hibrit/moldura
Escola Moldura e Painel da HIBRIT (decisão Crystal 1d4fa02e): a moldura do app, folhas e popovers,
campos pt-BR e os componentes do DS, extraídos do Obrita (hbt-obra) para serem usados também pelo
Spectra9 e pelo STK LITE. Decisão de pacote compartilhado: Crystal 08d00709 (nada de cópia entre repos).
Design System de referência: https://claude.ai/artifact/DZoGWtD79HhdLB8egzmX6v
O pacote não traz cor de produto nenhuma. Tudo sai de variáveis CSS que cada produto define no próprio
tokens.css.
Instalar
npm install @hibrit/molduraPeer dependencies: react e react-dom 19+, next 15+, lucide-react 1+. Publicado no npm público
(escopo @hibrit), então o build na Vercel não precisa de token.
CSS: a ordem importa
No CSS global do app (ex.: app/globals.css), nesta ordem:
@import "@hibrit/moldura/tokens.base.css"; /* estrutura neutra: raios, toque, sombras, botão do Google */
@import "./tokens.css"; /* as cores do SEU produto (contrato abaixo) */
@import "@hibrit/moldura/moldura.css"; /* classes da escola */
@import "./ui.css"; /* classes do seu produto */
@import "@hibrit/moldura/base.css"; /* reset, utilitários de tipografia e anel de foco: POR ÚLTIMO */base.css vem por último de propósito: os utilitários (.lab, .sm, .muted...) e o :focus-visible
precisam vencer as classes de componente de mesma especificidade (é o que mantém o anel de foco do teclado
na lista de escolha, por exemplo). Inverter a ordem muda o visual.
Os componentes são client components ("use client" preservado no dist): podem ser importados direto de
server components do App Router.
Contrato de tokens
O produto define em :root (claro) e redefine no escuro (@media (prefers-color-scheme: dark) com
:root:not([data-theme="light"]), e :root[data-theme="dark"] para a escolha manual):
| token | papel |
|---|---|
| --shell | moldura (fundo do app, topo, menu lateral, barra inferior) |
| --pane | painel do conteúdo, folhas, popovers |
| --container | cartões e campos dentro do painel |
| --field-shell | trilhos, hover, superfícies neutras mais escuras |
| --ink / --ink-muted / --ink-disabled | texto principal / secundário / desabilitado e placeholder |
| --icon | ícones e botões de ícone |
| --accent / --on-accent | a UMA cor do tenant (ação primária, foco, logo) e o texto sobre ela |
| --accent-tint / --on-accent-tint | pílula tingida do item ativo e o texto sobre ela |
| --on-inverse-accent | ação do toast ("Desfazer") sobre --inverse |
| --error / --error-container / --on-error-container | erro de campo e estado de dinheiro/prazo |
| --warning-container / --on-warning-container | atenção |
| --success-container / --on-success-container | sucesso |
| --inverse / --on-inverse | toast |
| --scrim | fundo atrás de folhas e diálogos |
| --font-sans | família tipográfica (o pacote só tem system-ui de reserva) |
Raios (--radius-sm 10, --radius-md 14, --radius-lg 20, --radius-xl 28, --radius-pill), alvo de toque
(--touch 44px), sombras (--shadow-float, --shadow-dialog, --shadow-toast) e as cores do botão do Google
(--google-bg, --google-ink) já vêm do tokens.base.css; o produto pode sobrescrever.
Exemplo: tokens do Obrita (cimento claro + Tijolo)
:root {
color-scheme: light;
--shell: #eceae6;
--pane: #fdfcfa;
--container: #f4f2ee;
--field-shell: #e3e0da;
--ink: #26231f;
--ink-muted: #6a655d;
--icon: #4a463f;
--ink-disabled: #9a948b;
--accent: #ae5631;
--on-accent: #ffffff;
--accent-tint: color-mix(in srgb, var(--accent) 10%, white);
--on-accent-tint: color-mix(in srgb, var(--accent) 45%, black);
--on-inverse-accent: color-mix(in srgb, var(--accent) 30%, white);
--error: #b3261e;
--error-container: #fce8e6;
--on-error-container: #8c1d18;
--warning-container: #fff1d6;
--on-warning-container: #5c3800;
--success-container: #e3f4e8;
--on-success-container: #146c2e;
--inverse: #2f2b27;
--on-inverse: #ffffff;
--scrim: rgba(38, 35, 31, 0.42);
--shadow-float: 0 12px 36px rgba(38, 35, 31, 0.18), 0 2px 6px rgba(38, 35, 31, 0.08);
--font-sans: var(--font-google-sans-flex), var(--font-outfit), "Google Sans Flex", Outfit, system-ui, sans-serif;
}
/* + o mesmo bloco no escuro (accent #e0906a, shell #161513, pane #201e1b...). Arquivo completo:
hibritoficial/hbt-obra, app/tokens.css */As 7 leis
- Moldura sem conteúdo.
- Conteúdo em painel com raio 28, a 8px das bordas.
- Zero bordas e divisórias: a hierarquia vem do tom.
- Uma cor do tenant.
- Item ativo é pílula tingida.
- Sombra só no que flutua.
- No máximo um alerta por coluna.
E mais: alvos de toque ≥ 44px; menu lateral a partir de 900px, abaixo disso barra inferior de 5 itens;
estado (verde/âmbar/vermelho) sempre com ícone + texto, nunca só cor; nada de <select> nem input de data
nativos.
Componentes
1. Moldura: AppShell, Pane, Marca, SeletorTopo, Avatar
import { AppShell, Avatar, Marca, type ItemNav } from "@hibrit/moldura";
import { House, LayoutGrid, Layers, ReceiptText, ShoppingCart } from "lucide-react";
const NAV: ItemNav[] = [
{ href: "/", rotulo: "Início", Icone: House },
{ href: "/gastos", rotulo: "Gastos", Icone: ReceiptText },
{ href: "/compras", rotulo: "Compras", Icone: ShoppingCart },
{ href: "/etapas", rotulo: "Etapas", Icone: Layers },
{ href: "/mais", rotulo: "Mais", Icone: LayoutGrid },
];
<AppShell
topo={<Marca href="/" nome="Obrita" icone={<House size={20} strokeWidth={2.2} aria-hidden />} />}
acoes={<Avatar href="/conta" nome={pessoa.nome} foto={pessoa.foto} />}
nav={NAV} // null = só o topo (.frame.bare), para listas e assistentes
navExtra={<div className="navsec lab">Seus projetos</div>}
flutuante={<button className="fab">Novo gasto</button>}
>
{children}
</AppShell>O item ativo usa usePathname (raiz só em /; os outros também nas sub-rotas). SeletorTopo é a troca de
contexto no topo (ponto de cor + nome + seta). Pane é o painel sozinho (main.pane > .inner).
2. Folha e popover responsivos: Folha, Flutuante
<Folha titulo="Novo gasto" onFechar={fechar} rodape={<button className="btn full">Lançar gasto</button>}>
…campos…
</Folha>Abaixo de 900px sobe da base; a partir de 900px vira diálogo centralizado (dialogo força o diálogo também no
celular). Esc e o fundo fecham; o foco vai para o campo com autoFocus e volta ao fechar.
Flutuante é o popover ancorado (desktop) / folha acima de outra folha (celular) usado pela lista e pelo
calendário; use para os seus próprios popovers (PONTO_DESKTOP = (min-width: 900px)).
3. Lista de escolha: Escolha, ListaEscolha
<Escolha rotulo="Etapa" valor={etapa} opcoes={[{ valor: "e1", rotulo: "Fundação", detalhe: "mês 2" }]} onChange={setEtapa} />role="listbox", teclado (setas, Home, End, Enter), busca acima de 8 opções (sem acento, sem caixa), item
escolhido em pílula tingida com marca. ListaEscolha é só a lista, para usar dentro de outro flutuante.
4. Calendário pt-BR: SeletorData, Calendario, Meses
<SeletorData rotulo="Data" valor={data} onChange={setData} /> // "AAAA-MM-DD"
<SeletorData rotulo="Do mês" modo="mes" valor={mes} onChange={setMes} limpavel /> // "AAAA-MM"Semana começando no domingo, atalho "Hoje" (ou "Este mês"), "Limpar" com limpavel, grade navegável por
setas/PageUp/PageDown/Home/End. "Hoje" é o dia em São Paulo (Calendario e Meses aceitam hoje fixo).
5. Dinheiro e número: CampoMoeda, MoedaInline, CampoNumero
<CampoMoeda rotulo="Valor" valor={valor} onChange={setValor} grande autoFocus /> // R$ 1.234,56, digitação por centavos
<MoedaInline ariaLabel="Preço por m³" valor={v} onChange={setV} /> // sem rótulo, dentro de linha
<CampoNumero rotulo="Área construída" valor={area} onChange={setArea} sufixo="m²" casas={2} /> // 1.240,5Colar "1.234,56", "1234.56" ou "R$ 1.234" lê o valor como está escrito. As funções puras estão em
@hibrit/moldura/formato (formatarMoeda, moedaDigitada, moedaColada, numeroDigitado, formatarNumero,
lerNumero, dataCurta, rotuloMes, hojeNoFuso, chaveBusca), sem React, usáveis no servidor.
6. Cidade do IBGE: @hibrit/moldura/ibge
import { CampoCidade, type Municipio } from "@hibrit/moldura/ibge";
<CampoCidade valor={municipio} onChange={setMunicipio} /> // "lond" → "Londrina · PR"; sem texto livreSubexportação separada: quem não importa daqui não carrega a lista. A lista (5.571 municípios, ≈170 KB) fica
num chunk próprio, baixado no primeiro foco do campo. No servidor, confie no código, nunca no texto:
municipioPorCodigo(codigo) (assíncrona) ou a lista crua em @hibrit/moldura/ibge/dados.
7. Cartões de escolha: Opcao (rádio), Marcar (seleção), Chips, Segmentado
<div className="pick" role="radiogroup" aria-label="Objetivo">
<Opcao on={obj === "venda"} titulo="Construir para vender" descricao="Mostra o lucro previsto." onClick={() => setObj("venda")} />
</div>
<Marcar on={feito} rotulo="Comprado" onClick={alternar} />
<Chips valor={cat} opcoes={[["material", "Material"], ["mo", "Mão de obra"]]} onChange={setCat} rotulo="Categoria" />
<Segmentado valor={tema} opcoes={[["", "Automática"], ["light", "Clara"], ["dark", "Escura"]]} onChange={setTema} rotulo="Aparência" />.pick.cartoes põe os cartões em duas colunas a partir de 640px. Entrada (texto) e Campo (rótulo + erro/dica
em volta de qualquer controle) completam o formulário.
8. Assistente em passos: AssistentePassos + rascunho injetável
<AssistentePassos
passo={passo} total={4}
onVoltar={voltar} // no passo 1 o botão diz "Cancelar"
onAvancar={avancar} rotuloAvancar={passo === 4 ? "Criar projeto" : "Continuar"}
onDepois={passo >= 3 ? preencherDepois : undefined} // sem ele, não há "Preencher depois"
ocupado={ocupado}
>
…conteúdo do passo…
</AssistentePassos>O rascunho guarda onde o produto mandar:
import { salvarRascunho, apagarRascunho, useRascunho, type ArmazenamentoRascunho } from "@hibrit/moldura";
salvarRascunho(`novo-projeto:${userId}`, { passo, dados }); // padrão: localStorage deste aparelho
const r = useRascunho<Dados>(`novo-projeto:${userId}`); // { dados, salvoEm } | null (SSR: null)
const noServidor: ArmazenamentoRascunho = { ler, gravar, apagar, ouvir }; // ex.: sessionStorage, IndexedDB, API
salvarRascunho(chave, dados, noServidor);9. Toast com "Desfazer": useToast + Toast
const { toast, aviso, fechar } = useToast();
aviso("Gasto excluído.", { rotulo: "Desfazer", fn: desfazer }); // 5 s com ação, 2,8 s sem
<Toast toast={toast} onFechar={fechar} /> // monte uma vez, no provider do approle="status", aria-live="polite". Sobe para o topo quando há folha aberta (body.lock). A lógica de
"apagar só depois de 5 s sem Desfazer" é do produto (no Obrita: excluirComDesfazer).
10. Botão do Google: BotaoGoogle, GoogleG
<BotaoGoogle onClick={entrarComGoogle} disabled={pendente} aria-busy={pendente || undefined} className={roboto.className} />Só o visual do guia de marca do Google ("G" oficial colorido, fundo neutro, sem borda, pílula). O fluxo de login (Google Identity Services, OAuth, server action) fica em cada produto.
O que NÃO é do pacote
- Regra de negócio de qualquer produto: cálculos, comandos,
has_feature, planos, permissões. - KPIs, selos de estado, linhas de lista, gráficos e ilustrações de cada produto (ficam no CSS e nos componentes do produto, sobre os tokens).
- Cores de produto (
tokens.cssé de cada produto). - Fluxo de login, sessão, dados e rotas.
- A chave de comparação de nomes que espelha índice de banco (ex.:
chave_nomedo Obrita): ochaveBuscadaqui é só para busca de tela.
Desenvolvimento
npm install
npm run typecheck && npm test && npm run buildPublicação: tag vX.Y.Z igual à versão do package.json dispara .github/workflows/publish.yml
(npm publish --access public com o secret NPM_TOKEN do repositório, mesmo padrão do @hibrit/connect).
Semver: mudança visual ou de API pública é minor (0.x) e vai no CHANGELOG.
