@waler/ui
v3.5.0
Published
Design system Vue 3 da WALER, sobre Reka UI e Tailwind CSS v4
Readme
@waler/ui
Design system Vue 3 da WALER: 31 componentes acessíveis sobre Reka UI, estilizados com Tailwind CSS v4 e pintados pela paleta da marca.
ESM puro, tree-shakeable, tipado. Sem icon set, sem clsx, sem @vueuse/core.
O culori só entra pelo subpath @waler/ui/brand, e o TipTap
só pelo RichTextEditor: quem importa apenas o RichTextContent não leva
editor nenhum no bundle, e o verify:fixture confere isso a cada build.
pnpm add @waler/uiRequer vue@^3.5 e tailwindcss@^4.1 no projeto.
Setup
/* src/main.css */
@import "tailwindcss";
@import "@waler/ui/theme.css";
@source "../node_modules/@waler/ui/dist";A linha
@sourceé obrigatória. O Tailwind v4 ignoranode_modulesna detecção automática de sources. Sem ela os componentes renderizam sem estilo nenhum e sem erro nenhum — a falha é silenciosa, e é o erro de integração mais comum com esta lib.
Se estiver usando pnpm e os estilos não aparecerem, tente a forma que atravessa o store:
@source "../node_modules/.pnpm/**/@waler/ui/dist";Como o Tailwind escaneia o dist por texto, o CSS gerado cobre todos os
componentes do pacote, não só os que você importou. Na prática são utilities
que a aplicação normalmente já produz.
Uso
<script setup lang="ts">
import { Button, Dialog, DialogTrigger, DialogContent, DialogTitle } from '@waler/ui'
</script>
<template>
<Dialog>
<DialogTrigger as-child>
<Button>Abrir</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Confirmar</DialogTitle>
</DialogContent>
</Dialog>
</template>Tooltip exige um TooltipProvider na raiz do app.
Componentes
| | | | | |---|---|---|---| | Avatar | Badge | Breadcrumb | Button | | Calendar | Card | Checkbox | DatePicker | | Dialog | Drawer | FileUpload | Group | | Input | Kbd | Label | LinkCard | | MenuItem | Popover | Progress | RadioGroup | | RichTextContent | RichTextEditor | SectionHeader | Select | | Steps | Surface | Switch | Textarea | | Timeline | Toaster | Tooltip | |
Input, Textarea, Select, DatePicker, FileUpload e RichTextEditor são
a camada de formulário: cada um recebe label, description, error e
required e amarra sozinho o aria-describedby e o aria-invalid ao próprio
controle.
Não há componente de campo separado — o rótulo mora no controle porque é lá
que o id nasce.
O Input cobre ainda prefixo e sufixo (props prefix/suffix ou slots de
mesmo nome) e os tipos formatados currency, decimal e phone; qualquer
outro type passa direto para o DOM.
Todo componente expõe as e asChild quando envolve um primitivo do Reka, e
exporta o próprio tipo de props e de variantes.
Texto formatado
RichTextEditor edita e RichTextContent desenha o mesmo documento, o tipo
RichTextDoc: o JSON do TipTap na forma canônica, sem atributo padrão e com o
link só com href. Guarde o documento como sai do v-model. O renderizador não
usa v-html e confere de novo o protocolo de cada link (http, https, mailto e
tel); fora disso o link vira texto. O leitor desenha o corpo em 14px, peso 400,
linha de 24px e 12px entre parágrafos; o editor escreve no text-body dos
outros campos.
<RichTextEditor
v-model="doc"
label="Mensagem"
placeholder="Escreva o comunicado"
@slash="openInsertMenu"
/>
<RichTextContent :doc="doc" />O slash sai quando "/" é digitado num parágrafo vazio do topo (dentro de lista
ou citação, "/" é texto) e traz uma âncora que o PopoverAnchor aceita em
reference. Listas aninham até três níveis, no Tab e no colar.
Superfícies
Três, e só três: branco sólido, vidro e gradiente. Surface é o primitivo que
carrega as três, com o fallback opaco de backdrop-filter e o scrim do
gradiente já embutidos.
<Surface variant="glass" radius="lg" elevation="card" class="p-6">
conteúdo
</Surface>Texto sobre gradiente herda a tinta certa sozinho, via data-wa-surface no
elemento raiz da superfície.
O scrim melhora o contraste mas não garante: no fim do gradiente do banner o branco fica em 2.25:1, abaixo de AA. Mantenha texto no primeiro terço do eixo.
Customização
Sobrescreva os tokens semânticos depois de importar o tema. Eles têm prefixo
--wa- porque :root é global e last-wins — um --primary sem prefixo
colidiria em silêncio com o do seu app:
@import "@waler/ui/theme.css";
:root {
--wa-primary: #1E3A2F;
--wa-accent: #2C4A3F;
}A utility continua sendo bg-primary. O prefixo fica só na camada de valor.
Para ajuste pontual todo componente aceita class, e o tailwind-merge
resolve o conflito. Para trocar o elemento renderizado, use as ou asChild:
<Button as-child>
<RouterLink to="/planos">Ver planos</RouterLink>
</Button>Cor de tenant
O subpath @waler/ui/brand resolve o caso whitelabel: cada tenant define seis
cores, e elas têm de ser recusadas antes do cadastro em vez de degradarem
em runtime numa tela que ninguém está olhando.
É TypeScript puro — sem Vue e sem CSS — porque quem o consome é o servidor, e servidor não deve arrastar componente nenhum para validar seis hex.
import { checkBrand, brandToCss, BRAND_TOKENS } from '@waler/ui/brand'
const brand = { '--wa-ink': '#16211D', '--wa-deep': '#1E3A2F', /* ... */ }
const report = checkBrand(brand)
if (!report.ok) {
// findings diz o par, a razão medida, o mínimo e quais dos seis a causaram
throw new Error(report.findings.map(f => `${f.pair}: ${f.ratio}`).join('\n'))
}
const css = brandToCss(brand) // bloco :root pronto para servirBRAND_TOKENS é a lista fechada — --wa-ink, --wa-deep, --wa-soft,
--wa-g1, --wa-g2, --wa-g3. Chave fora dela é recusada, não
descartada: um tenant cujo token sumiu em silêncio não tem como distinguir
isso de um token que a lib ignora.
O valor tem de ser hex sem alpha. #2C4A3F00 é transparente e apagaria o anel
de foco do produto inteiro passando por qualquer validação que só olhe formato.
O bloco só repinta aplicado em
:root. A camada de valor é global e last-wins por construção — em um nó interno ele não tem efeito. Sirva o CSS por uma tag<link>noindex.html, não por JavaScript depois do mount: é o que faz a cor chegar antes do primeiro paint, sem flash da cor errada.
report.bands traz, por par do banner, o maior prefixo [0, t] do gradiente
em que o texto ainda passa AA — as bandas do manifesto foram medidas com o
gradiente da Waler, e o de cada tenant é outro.
Cor primária não é o verde
--wa-primary é o ink #16211D, não o verde da marca. No handoff da Waler o
verde é acento, não ação. Componente que assumir "primário = cor de marca"
sai errado.
Licença
MIT © WALER
