@inova-lab-ws/ui
v0.14.0
Published
INOVA Lab UI: os componentes, tokens e logos da INOVA Lab Library em React. Fonte única para AmbientAI, Gate e todo produto novo do INOVA Lab.
Readme
INOVA UI
Componentes, tokens e logos do INOVA Lab · Leroy Merlin Brasil
Um desenho no Figma, um pacote em código, a mesma interface em todo produto.
inova-ui é o repositório. @inova-lab-ws/ui é o pacote que os apps instalam.
[!IMPORTANT] 🔒 Fonte única de interface do INOVA Lab. AmbientAI, Gate e todo produto novo instalam este pacote. Nenhum app cria componente de interface próprio nem copia o código daqui. O que faltar é pedido por issue. Dono e único aprovador: @gabriel-moma-lmbr.
Como funciona · Instalar · Componentes · Tokens · Pedir algo novo · Desenvolver
🔆 O caminho de uma mudança
1. Desenhar → o componente ou token nasce na INOVA Lab Library (Figma) e é publicado
2. Exportar → as variáveis viram tokens/tokens.json; o tema é gerado (npm run tokens)
3. Implementar → o componente entra em src/components, com Code Connect para o Figma
4. Lançar → uma release no GitHub publica a nova versão do pacote
5. Atualizar → cada app sobe a versão da dependência; nada é copiadoO Figma desenha, o pacote implementa, a INOVA UI Skill orienta o uso. Valor que não passou pela biblioteca não entra aqui; componente que não está aqui não existe no app.
🎯 Como funciona
flowchart LR
Figma[INOVA Lab Library<br/>Figma] -->|variáveis| Tokens[tokens.json]
Tokens -->|build-tokens| Tema[theme.css]
Figma -->|Code Connect| Comp[Componentes React]
Tema --> Pacote["@inova-lab-ws/ui"]
Comp --> Pacote
Pacote --> AmbientAI
Pacote --> Gate
Pacote --> Novo[Produto novo]
Skill[INOVA UI Skill] -.orienta.-> AmbientAI
Skill -.orienta.-> Gate
Skill -.orienta.-> Novo📦 Instalar num projeto
1. Dependência. Público no npm, sem token nem .npmrc. O projeto precisa de React 19, Tailwind 4
(obrigatório) e lucide-react 1.x (o pacote usa a cópia do app).
npm install @inova-lab-ws/ui lucide-react2. Tema. No CSS global, o Tailwind primeiro e o tema depois, e nenhum outro tema. O tema já aponta para os
componentes (@source), então o app não precisa acertar o caminho de node_modules:
@import "tailwindcss";
@import "@inova-lab-ws/ui/theme.css";Sem Tailwind no app, use @import "@inova-lab-ws/ui/theme-standalone.css";, que já traz o Tailwind.
[!WARNING] O tema muda o tamanho do texto do app inteiro (#59). A biblioteca usa a escala 4/8 nos nomes padrão do Tailwind, então, no momento em que o app importa o tema, toda tela que ainda usa estas classes muda de tamanho:
| Classe | Tailwind | Com o tema | | :--- | ---: | ---: | |
text-lg| 18px | 20px | |text-xl| 20px | 24px | |text-2xl| 24px | 32px | |text-3xl| 30px | 40px | |text-4xl| 36px | 48px |
text-xs,text-smetext-basenão mudam. Antes de importar, liste os usos no app:grep -rnE "text-(lg|xl|[234]xl)\b" app components src.
2b. Na raiz do app (uma vez): KeyboardInsetProvider, para a folha de baixo ficar acima do teclado no celular, e
ToastProvider, para chamar useToast().show({ kind, message }) de qualquer tela.
<KeyboardInsetProvider>
<ToastProvider>{children}</ToastProvider>
</KeyboardInsetProvider>3. Fontes. Com next/font, declare as variáveis --font-inter, --font-instrument-serif e
--font-roboto-mono; o tema lê essas variáveis antes do nome da família.
4. Usar.
import { Button, Alert, Toast, ToastViewport } from "@inova-lab-ws/ui";
<Button variant="primary">Salvar pedido</Button>
<Alert tone="warning" title="Dados de exemplo">Ainda não há eventos reais neste servidor.</Alert>
// Botão que leva a outra página
<Button asChild><Link href="/novo">Adicionar</Link></Button>Página montada no servidor: os componentes são código de navegador. Para dar o visual do pacote a um elemento
numa página do servidor, use import { buttonVariants, cn } from "@inova-lab-ws/ui/variants";.
Projeto sem Tailwind: instale tailwindcss e @tailwindcss/postcss, crie postcss.config.mjs, importe o tema e
isole o CSS antigo numa camada (@import "./legacy.css" layer(components);). O passo a passo está na skill INOVA UI.
🧩 Componentes
Todos os componentes da INOVA Lab Library estão no pacote, cada um com Code Connect em figma/.
Indicadores
| Componente | No Figma |
| :--- | :--- |
| Badge | badge |
| Spinner | spinner |
| Tooltip · TooltipProvider | tooltip |
| Avatar (pessoa: inicial, ícone ou foto; variant="app": logo de serviço) | avatar |
| BotAvatar | bot-avatar |
| Divider | divider |
| Alert | alert |
| Toast · ToastViewport | toast |
| Chip | chip |
| ConnectorCard | connector-card |
Navegação
| Componente | No Figma |
| :--- | :--- |
| NavigationTabBar | navigation-tab-bar |
| Menu | menu |
| PillTab · PillTabs | pill-tab |
Estrutura
| Componente | No Figma |
| :--- | :--- |
| Header | header |
| TopArea | top-area |
| Card | card |
| PageHeader | page-header |
| Accordion · AccordionItem | accordion |
Logos
| Componente | No Figma |
| :--- | :--- |
| LogoAmbientAI | logo / ambientai |
| LogoGate | logo / gate |
| LogoFormaLab | logo / forma-lab |
| LogoInovaUI | logo / inova-ui |
| LogoPlaceholder | logo / placeholder |
Formulário
| Componente | No Figma |
| :--- | :--- |
| Button (destructive-ghost para remoção secundária; action + size="compact" para a pílula verde do Falar) | button |
| Input | input |
| Textarea | textarea |
| Checkbox | checkbox |
| Radio | radio |
| RoleRadio | role-radio |
| Toggle | toggle |
| MultiSelect | multi-select |
| Select | input (com chevron) |
| DateRangePicker · DateRangeDay | date-range-picker |
| LoginForm | login-form |
| CopyField | copy-field |
| Code · codeClassName | code |
Overlay
| Componente | No Figma |
| :--- | :--- |
| Overlay (dialog, drawer, bottom sheet) | overlay |
| ActionMenu · ActionMenuTrigger · ActionMenuContent · ActionMenuItem · ActionMenuSeparator | action-menu |
| ProfileMenu | profile-menu |
Dados e gráficos
| Componente | No Figma |
| :--- | :--- |
| MetricTile · MetricTileGroup | metric-tile |
| TrendBadge | metric-trend |
| MetricChartCard | metric-chart-card |
| AreaChart | area-chart |
| ChartLineKey · ComparisonTooltip · ChartExtremeMarker | primitivas de gráficos |
| ProgressReadout | progress-readout |
| BarChart | bar-chart |
| HorizontalBarChart | horizontal-bar-chart |
| StackedBarChart | stacked-bar-chart |
| DistributionChart | distribution-chart |
| Table · TableRow · TableCell | table |
| ActivityLog · ActivityLogRow | activity-log |
KPIs e gráficos: MetricTile context="embedded" compartilha conteúdo e ajuda com o cartão independente; onActivate seleciona sem alterar o evento DOM legado onSelect. trend={{direction: "up", evaluation: "unfavorable", value: "12%"}} representa custo crescente. A avaliação nunca é deduzida da direção. A tendência ocupa o canto inferior direito do cartão.
MetricChartCard recebe uma tupla de 4 ou 6 metrics com id, label e value. Use activeMetric/onActiveMetricChange para controle externo ou defaultActiveMetric para estado interno; selectable: false exibe uma métrica informativa. O filho pode ser uma função (id) => <AreaChart ... /> para trocar a série sem incorporar regras do produto à biblioteca.
AreaChart recebe data={[{label: "16/out", value: 0, previousValue: 10, date: "16/10", previousDate: "09/10"}]} e label acessível. comparison ativa o período anterior; showExtrema mostra máximo/mínimo. null interrompe linha e preenchimento, enquanto zero permanece medido. parts em cada ponto adiciona linhas ao tooltip; formatValue formata valores. Estados: ready, loading, empty, unavailable. A biblioteca não calcula períodos nem indicadores.
MultiSelectOption.icon aceita um ícone ou logo opcional (ReactNode), decorativo, ao lado do rótulo na lista e no gatilho com uma opção escolhida. Sem ícone, não há espaço reservado. O Select continua nativo.
PillTabs usa um trilho segmentado; PillTab selecionada tem superfície branca e sombra. As APIs navigation e fullWidth permanecem.
BarChart e HorizontalBarChart aceitam selectedLabel e onSelectionChange: clique, Enter ou Espaço selecionam; selecionar novamente limpa (null). Setas, Home e End percorrem as barras.
Previsão e mídia (0.14.0). AreaChart aceita pontos {label, value, projected, low, high}. projectedLabel e rangeLabel personalizam a legenda e a dica; previsão neutra tracejada e faixa provável são independentes da comparação com o período anterior. Limites ausentes ou inválidos deixam lacunas, sem virar zero. ForecastCard recebe period, value, range, target, targetStatus e 2 grupos opcionais em breakdown; valores, avaliação da meta e formatValue pertencem ao consumidor. ForecastCardGroup organiza os cartões em colunas no desktop e rolagem lateral no celular. Badge color="category1" até category5 usa os tons categóricos dos gráficos, claros por padrão; cores antigas e disabled continuam com o mesmo significado.
Thumbnail appearance="frameless" size="compact" aspectRatio="4:3" usa o mesmo padding global de 4px do cartão padrão do chat, mantendo o conteúdo com 8px de respiro. A aparência frameless preserva o fundo transparente sobre a página bege, o hover neutro e o foco acessível, sem borda ou hover verde. Compact ocupa 42vw, até 180px; ThumbnailRail fornece rolagem com snap e gap de 12px. fallbackSrc é tentado uma vez se a imagem principal falhar; trocar src reinicia a tentativa. ThumbnailSkeleton compartilha aparência, tamanho e proporção com o cartão. AuthorLine name="Ana" store="Loja" fornece avatar e texto truncado; SeeAll fornece o link da ação de seção.
<ThumbnailRail>
<Thumbnail asChild appearance="frameless" size="compact" aspectRatio="4:3"
src={photo} fallbackSrc={backupPhoto} caption="Ambiente" overlayAction={<Button onClick={save}>Salvar</Button>}>
<Link href={detailUrl}>Abrir ambiente</Link>
</Thumbnail>
</ThumbnailRail>
<PageHeader size="section" title="Ambientes" action={<SeeAll asChild><Link href="/all">Ver todos</Link></SeeAll>} />O link principal do Thumbnail e as ações são irmãos; não coloque um link envolvendo o cartão inteiro. FilterBar onClearAll={clearFilters} acrescenta “Limpar tudo” ao rodapé móvel sem fechar o painel; showClearAll={false} esconde a ação e clearAllLabel altera seu texto.
DetailLayer é a composição modal de imagem e painel: open, onOpenChange, title, image={{src,alt}} e os filhos do painel. No desktop, a imagem fica contida à esquerda e o painel de 400px rola por dentro; no celular, a camada inteira rola. panelWidth altera a largura desktop. onImageClick oferece a ação de zoom; o consumidor pode abrir ImageViewer, que devolve o foco ao controle que o abriu quando o zoom fecha. initialFocusRef e returnFocusRef permitem escolher o foco inicial e de retorno. Escape, clique no fundo e fechar são gerenciados pela camada. DetailLayout continua sendo a composição lateral no fluxo, sem modal.
Composições e filtros (0.13.0). MetricTile icon={<Icon />} substitui o marcador em layout="variant"; MetricChartCard aceita 2, 4 ou 6 KPIs e bodyPadding="none". O fundo do KPI selecionado acompanha os cantos externos do cartão; foco e ajuda continuam visíveis. Table frameless rowInset permite integrar a tabela sem uma segunda moldura. TableRow interactive onClick={...} também funciona com Enter/Espaço; ações e links dentro da linha continuam independentes.
PageHeader size="section" usa h2 com o mesmo slot action. Thumbnail oferece badge, overlayAction, author e footer; o controle principal e a ação ficam separados. DetailLayout sidebar={...} mantém o detalhe no fluxo, sticky no desktop e abaixo no celular. Breadcrumb recebe items={[{label,href}]}; BreadcrumbBack oferece o retorno dentro de um painel, com no máximo 8px até seus filhos.
PageGrid loadState="idle" onLoadMore={loadNextPage} sinaliza o fim visível sem carregar dados por conta própria. Informe loadKey a cada página consumida, inclusive quando ela retornar zero itens, para rearmar o observador. loadState="loading"|"error"|"end" apresenta progresso, nova tentativa e fim. O consumidor controla a lista, o cursor e os erros.
toast.show({message: "Publicado", action: {label: "Ver", onClick: openDetail}}) adiciona uma ação independente. ProductListRow copySku="123456" oferece cópia compacta e anúncio de sucesso/erro; callbacks onCopySuccess/onCopyError ficam disponíveis.
MultiSelect e DateRangePicker aceitam presentation="popover"|"sheet"|"responsive"; sem a prop, mantêm popover. Responsive usa painel abaixo de 768px. MultiSelectPanel e DateRangePanel são conteúdo controlado sem gatilho/portal, para compor dentro de outro painel. FilterBar recebe primary e filters com id, label, summary, active, field (desktop) e panel (conteúdo do painel móvel); cada painel pode usar esses seletores sem abrir outra modal.
Combobox recebe options em ordem definida pelo consumidor, value/onValueChange, ícone/avatar opcionais e allowCreate/onCreate para criação. Retorne de onCreate um valor ou uma opção, inclusive por Promise. A biblioteca descarta resultados de consultas canceladas; a persistência e a ordenação por uso são do produto. originalValue permite voltar à escolha original; onCancel trata cancelamento. A lista nova usa apresentação responsiva por padrão.
<MenuProvider>
<Header />
<Menu presentation="responsive" navigation={navigation} logo={logo} />
{children}
</MenuProvider>O Header abre o estado compartilhado automaticamente; onMenuClick explícito continua tendo prioridade. Menu open/onOpenChange permite controle externo. O menu responsive é lateral a partir de 768px e tela cheia abaixo disso, fecha ao navegar ou mudar de breakpoint e devolve foco ao gatilho. Os modos anteriores continuam funcionando sem provider.
Avatar variant="logo" src={url} name="Marca" usa borda, superfície clara, object-contain e respiro de 1/6 do tamanho. Recorte o viewBox dos SVGs ao desenho: contain não remove margens vazias gravadas no arquivo; fontes com viewBoxes diferentes continuarão parecendo de tamanhos diferentes.
Conversa
| Componente | No Figma |
| :--- | :--- |
| UserMessage | user-message |
| AssistantMessage | assistant-message |
| Composer | composer |
| InputCard | input-card |
| ListeningBanner | listening-banner |
| Waveform | waveform |
| MentionResult | mention-result |
| SuggestionList | suggestion-list |
| FeedbackPrompt | feedback-prompt |
| ActionCard | action-card |
| MetaRow | meta-row |
| GenerationBoard | generation-board |
| HistoryThumbnail | history-thumbnail |
| Thumbnail | thumbnail |
| ProductListRow | product-list-row |
| ProductHero (topo da folha de detalhe do produto) | product-hero |
🎨 Tokens
Escala 4/8. Tudo é múltiplo de 4, no ritmo de 8.
| Tipo | Valores |
| :--- | :--- |
| Raio | 0 · 4 · 8 · 12 · 16 · 24 · pill |
| Espaçamento | 4 · 8 · 12 · 16 · 20 · 24 · 32 · 40 · 48 · 64 |
| Fonte | 12 · 14 · 16 · 20 · 24 · 32 · 40 · 48 |
| Exceção | chart-axis 10px, só em eixo e rótulo de gráfico |
tokens/tokens.json é exportado da biblioteca e src/styles/theme.css
é gerado a partir dele. Não edite o CSS à mão.
🙋 Pedir algo novo
- Abra uma issue com a tela, o caso de uso e por que nenhum componente atual serve.
- O dono desenha primeiro na biblioteca, publica e só então implementa aqui.
- Sai uma nova versão, e o app atualiza a dependência.
[!WARNING] Variante local é divergência.
classNameserve para posicionar (margem, largura, alinhamento), nunca para mudar cor, tamanho ou borda. Valor solto (text-[15px],rounded-[10px], hex) também não entra no app.
🛠️ Desenvolver
npm install
npm run tokens # regenera src/styles/theme.css a partir de tokens/tokens.json
npm run check # typecheck + build
npx figma connect parse # valida o Code Connect| Caminho | O que é |
| :--- | :--- |
| tokens/tokens.json | os tokens exportados da INOVA Lab Library |
| scripts/build-tokens.mjs | gera o tema (variáveis CSS + mapeamento do Tailwind 4) |
| src/components/ | os componentes React |
| figma/ | o Code Connect de cada componente |
| assets/ | o logo do INOVA UI |
Publicar o pacote: criar uma release no GitHub com a versão do package.json; o workflow publish.yml
publica no npm público com proveniência (trusted publishing, sem token). Publicar o Code Connect: FIGMA_ACCESS_TOKEN=… npx figma connect publish
(exige plano Organization ou Enterprise no Figma).
🧭 INOVA UI Skill
A skill (skills/inova-ui-skill) é o guia de uso: como instalar, qual componente usar em cada caso, os padrões
de tela (área segura, overlays, estados, texto em pt-BR) e o que é proibido. Ela orienta; o pacote decide o visual.
INOVA Lab · Leroy Merlin Brasil
