rharuow-ds
v4.1.0
Published
Modern React Design System with 20 components and auto color system. Define only 2 colors (primary/secondary) and get automatic variations (hover, light, dark) with proper text contrast (WCAG AA). Includes: Table, Card, Button, Chip, Pagination, Input, Te
Maintainers
Keywords
Readme
rharuow-ds
Um Design System moderno em React com integração completa ao React Hook Form, estilizado com Tailwind CSS e baseado em shadcn/ui.
🌟 Características
- ⚛️ React 18+ com TypeScript
- 🧩 24 componentes prontos para uso (Input, ColorInput, Textarea, Select, AsyncSelect, MultiSelect, MultiAsyncSelect, RadioGroup, Button, Switch, Chip, Pagination, Card, Table, Tooltip, Accordion, AsideSheet, Sidebar, BottomSheet, BottomTabNavigator, Modal, Toaster, ImageInput)
- 💡 Filtro digitável em componentes Select - Digite para encontrar opções rapidamente
- 🔗 Integração nativa com React Hook Form
- 🎨 Sistema de cores automático - Defina apenas 2 cores e todas as variações são calculadas automaticamente
- 🎯 Contraste automático para textos (WCAG AA compliance)
- 🌓 Dark mode com ajustes automáticos de cores
- 🎯 Componentes acessíveis (ARIA)
- 📱 Responsivo por padrão
- 🎭 Animações suaves e modernas
- 📚 Documentação interativa com Storybook
📚 Documentação
Acesse a documentação interativa dos componentes em: https://rharuow.github.io/rharuow-ds-docs/
🚀 Instalação
Adicione o pacote ao seu projeto:
npm install rharuow-dsDependências necessárias
Se você for usar componentes com formulários, instale também:
npm install react-hook-form💡 Atenção:
Não é necessário instalar ou configurar Tailwind CSS no seu projeto consumidor para usar os estilos do rharuow-ds. O CSS já vem pronto no pacote!
Compatibilidade
- ⚛️ React 16.8+ (hooks)
- 📋 React Hook Form 7.0+
- 🌐 Navegadores modernos (ES2018+)
📖 Como usar
Importe o CSS do design system
No seu arquivo de entrada (ex:src/main.tsx,src/index.tsxou_app.tsxno Next.js):import "rharuow-ds/dist/styles.css";Use os componentes normalmente
import { Card, Table, Button, Chip, Pagination, Input, Textarea, Select, AsyncSelect, MultiSelect, RadioGroup, Tooltip, Accordion, AsideSheet, Sidebar, BottomSheet, BottomTabNavigator, Switch, Modal, Toaster, ImageInput, ColorInput, } from "rharuow-ds"; function App() { return ( <div> {/* Exemplo básico do Card */} <Card variant="default"> <Card.Header> <h3>Título do Card</h3> <p>Subtítulo ou descrição</p> </Card.Header> <Card.Body> <p>Conteúdo principal do card</p> </Card.Body> <Card.Footer> <Button>Ação Principal</Button> </Card.Footer> </Card> {/* Exemplo da Table */} <Table variant="striped" size="md"> <Table.Header> <Table.Row> <Table.Cell as="th">Nome</Table.Cell> <Table.Cell as="th">Email</Table.Cell> <Table.Cell as="th">Ações</Table.Cell> </Table.Row> </Table.Header> <Table.Body> <Table.Row> <Table.Cell>João Silva</Table.Cell> <Table.Cell>[email protected]</Table.Cell> <Table.Cell> <Button variant="outline">Editar</Button> </Table.Cell> </Table.Row> </Table.Body> </Table> {/* Outros componentes */} <Input label="E-mail" name="email" type="email" /> <Input label="Senha" name="password" type="password" /> <Textarea label="Comentários" name="comments" rows={4} /> <Select label="País" name="country" options={[ { label: "Brasil", value: "br" }, { label: "Estados Unidos", value: "us" }, ]} /> <RadioGroup label="Tamanho" name="size" options={[ { label: "Pequeno", value: "sm" }, { label: "Médio", value: "md" }, { label: "Grande", value: "lg" }, ]} /> <Tooltip content="Clique para enviar o formulário" position="top"> <Button variant="default">Enviar</Button> </Tooltip> </div> ); }Para componentes com React Hook Form
import { useForm, FormProvider } from "react-hook-form"; import { Card, Table, Input, Textarea, Select, AsyncSelect, MultiAsyncSelect, RadioGroup, Button, Tooltip, Accordion, AsideSheet, Sidebar, BottomSheet, BottomTabNavigator, Switch, Modal, Toaster, ImageInput, ColorInput, } from "rharuow-ds"; function FormExample() { const methods = useForm(); const loadCountries = async (search?: string) => { // Simular chamada à API const countries = [ { label: "Brasil", value: "br" }, { label: "Argentina", value: "ar" }, { label: "Estados Unidos", value: "us" }, { label: "Chile", value: "cl" }, { label: "Peru", value: "pe" }, ]; if (!search) return countries; return countries.filter((c) => c.label.toLowerCase().includes(search.toLowerCase()) ); }; return ( <Card variant="default" size="lg"> <Card.Header> <h2>Formulário de Cadastro</h2> <p>Preencha os dados abaixo</p> </Card.Header> <Card.Body> <FormProvider {...methods}> <form onSubmit={methods.handleSubmit(console.log)}> <Input label="Nome" name="name" /> <Input label="E-mail" name="email" type="email" /> <Input label="Senha" name="password" type="password" /> <Textarea label="Observações" name="notes" rows={3} /> <AsyncSelect label="País" name="country" loadOptions={loadCountries} searchable isClearable /> <MultiAsyncSelect label="Países favoritos" name="favoriteCountries" loadOptions={loadCountries} searchable isClearable maxVisibleItems={2} /> <RadioGroup label="Tamanho" name="size" options={[ { label: "Pequeno", value: "sm" }, { label: "Médio", value: "md" }, { label: "Grande", value: "lg" }, ]} /> </form> </FormProvider> </Card.Body> <Card.Footer> <div className="flex space-x-2"> <Button variant="outline">Cancelar</Button> <Button type="submit">Enviar</Button> </div> </Card.Footer> </Card> ); }
Componentes Disponíveis
� Card
Componente flexível para exibir conteúdo organizado em seções:
- ✅ Estrutura modular: Header, Body e Footer independentes
- ✅ Múltiplas variantes: default, outlined, elevated, flat
- ✅ Largura flexível: Por padrão, cresce para ocupar largura disponível
- ✅ Controle de largura: Use
constrainWidth=truepara aplicar limitações por tamanho - ✅ Tamanhos configuráveis: sm, md, lg (aplicados apenas com
constrainWidth) - ✅ Suporte ao tema dark: Variáveis CSS para light/dark mode
- ✅ Elementos semânticos: Props
aspara acessibilidade (header, main, footer) - ✅ Flexibilidade total: Use apenas as seções necessárias
- ✅ Customização completa: Padding, bordas arredondadas e estilos
📊 Table
Componente completo para exibição de dados tabulares:
- ✅ Estrutura modular: Table, Header, Body, Footer, Row, Cell
- ✅ Múltiplas variantes: default, striped, bordered
- ✅ Tamanhos configuráveis: sm, md, lg
- ✅ Responsividade: Scroll horizontal automático
- ✅ Header fixo: Para tabelas com muitos dados
- ✅ Suporte ao tema dark: Variáveis CSS para light/dark mode
- ✅ Alinhamento de células: left, center, right
- ✅ Colspan e rowspan: Células que ocupam múltiplas colunas/linhas
- ✅ Elementos semânticos: Props
aspara acessibilidade (th/td, thead/tbody/tfoot) - ✅ Linhas interativas: Hover e estados de seleção
�
🎯 Button
Botão customizável com diferentes variantes, incluindo uma variante exclusiva para ícones.
- ✅ Variante
default— fundo primário com texto contrastante - ✅ Variante
outline— borda primária com fundo transparente - ✅ Variante
secondary— fundo secundário com texto contrastante - ✅ Variante
icon— botão quadrado que centraliza um ícone filho
import { Button } from "rharuow-ds";
// Botão padrão
<Button variant="default">Enviar</Button>
// Botão outline
<Button variant="outline">Cancelar</Button>
// Botão secundário
<Button variant="secondary">Mais opções</Button>
// Botão apenas com ícone (ícone é passado como filho)
<Button variant="icon" aria-label="Adicionar">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
</Button>Dica: Na variante
icon, o ícone é passado comochildrendo botão e fica automaticamente centralizado. Use semprearia-labelpara descrever a ação do botão para acessibilidade.
🏷️ Chip
Elemento pill interativo para filtros, tags e seleções toggleáveis:
- ✅ Estado ativo/inativo com visual distinto
- ✅ Ícone opcional à esquerda
- ✅
onChange(active: boolean)— callback de toggle - ✅ Estado
disabledcom visual reduzido - ✅ Acessível (
role="switch",aria-checked) - ✅ Totalmente estilizável via
className
import { Chip } from "rharuow-ds";
// Toggle simples
const [active, setActive] = useState(false);
<Chip label="Filtro" active={active} onChange={setActive} />
// Grupo de filtros
const filters = ["React", "TypeScript", "Tailwind"];
const [selected, setSelected] = useState<string[]>([]);
const toggle = (filter: string) =>
setSelected(prev =>
prev.includes(filter) ? prev.filter(f => f !== filter) : [...prev, filter]
);
{filters.map(filter => (
<Chip
key={filter}
label={filter}
active={selected.includes(filter)}
onChange={() => toggle(filter)}
/>
))}
// Com ícone
<Chip
label="Favorito"
active={isFavorite}
onChange={setIsFavorite}
icon={<StarIcon />}
/>� Pagination
Componente de paginação com janela deslizante e ellipsis:
- ✅ Exibe janela de 3 páginas centrada na página atual
- ✅ Ellipsis (
…) + última página quando o total ultrapassa a janela visível - ✅ Seta esquerda (← anterior) oculta na primeira página
- ✅ Seta direita (→ próxima) oculta na última página
- ✅ Estado
disabledpara toda a navegação - ✅ Acessível (
role="navigation",aria-label,aria-current)
import { Pagination } from "rharuow-ds";
const [page, setPage] = useState(1);
<Pagination
totalPages={20}
currentPage={page}
onPageChange={setPage}
/>�📝 Input
Campo de texto versátil com label flutuante e integração com React Hook Form:
- ✅ Label flutuante animada
- ✅ Suporte a múltiplos tipos (text, email, password, number, tel, url)
- ✅ Variante de CEP com máscara automática (
00000-000) e validação de formato - ✅ Funcionalidade de password com botão mostrar/ocultar
- ✅ Variante de moeda com máscara e formatação automática (BRL por padrão)
- ✅ Saída configurável no React Hook Form como string formatada ou number
- ✅
onChangeexposto para integrações como busca de endereço por CEP - ✅ Ícones customizados opcionais
- ✅ Estados de erro integrados
- ✅ Totalmente acessível (ARIA)
Exemplo com CEP e onChange para busca de endereço:
import React from "react";
import { Input } from "rharuow-ds";
function AddressForm() {
const [cep, setCep] = React.useState("");
const handleCepChange: React.ChangeEventHandler<HTMLInputElement> = async (event) => {
const maskedCep = event.target.value;
setCep(maskedCep);
const normalizedCep = maskedCep.replace(/\D/g, "");
if (normalizedCep.length === 8) {
// Exemplo: chamar servico externo quando CEP estiver completo
// const endereco = await buscarEnderecoPorCep(normalizedCep);
// preencher campos do formulario...
}
};
return (
<Input
name="cep"
label="CEP"
cep
value={cep}
onChange={handleCepChange}
/>
);
}Veja a story de CEP no Storybook para exemplos completos:
� Textarea
Campo de texto multilinha com as mesmas funcionalidades do Input:
- ✅ Label flutuante animada
- ✅ Altura ajustável (propriedade
rows) - ✅ Redimensionamento vertical permitido
- ✅ Ícones customizados opcionais
- ✅ Estados de erro integrados
- ✅ Integração completa com React Hook Form
- ✅ Mesma consistência visual do Input
�📋 Select
Seletor customizado com opções estáticas e suporte a busca.
🔄 AsyncSelect
Seletor com carregamento assíncrono de opções:
- ✅ Carregamento de dados via API
- ✅ Busca em tempo real (searchable)
- ✅ Debounce configurável
- ✅ Estados de loading e "sem opções"
- ✅ Integração completa com React Hook Form
🎛️ MultiSelect
Seletor múltiplo para escolha de várias opções:
- ✅ Seleção múltipla com checkboxes
- ✅ Filtro digitável (
searchable) - Digite para encontrar opções - ✅ Tags visuais para itens selecionados
- ✅ Remoção individual de itens
- ✅ Filtro case-sensitive configurável
- ✅ Função de filtro customizável
- ✅ Botão de limpeza geral (
isClearable) - ✅ Integração completa com React Hook Form
// MultiSelect básico
<MultiSelect
name="fruits"
label="Escolha suas frutas favoritas"
options={fruitOptions}
/>
// MultiSelect com filtro
<MultiSelect
name="fruits"
label="Escolha suas frutas favoritas"
searchable
filterPlaceholder="Digite para filtrar frutas..."
caseSensitive={false}
isClearable
options={fruitOptions}
/>🔄🎛️ MultiAsyncSelect
Seletor múltiplo com carregamento assíncrono:
- ✅ Todas as funcionalidades do AsyncSelect
- ✅ Seleção múltipla com tags visuais
- ✅ Remoção individual de itens selecionados
- ✅ Limite configurável de itens exibidos
- ✅ Contador de itens extras (+X mais)
🎯 RadioGroup
Radio buttons modernos e criativos:
- ✅ Design de botões estilizados (não radio tradicional)
- ✅ Ícones customizados opcionais
- ✅ Layout horizontal ou vertical
- ✅ Diferentes tamanhos (sm, md, lg)
- ✅ Animações ao selecionar
💡 Tooltip
Componente de tooltip inteligente e acessível:
- ✅ Posicionamento automático: top, bottom, left, right
- ✅ Detecção de bordas: Ajusta posição automaticamente se não couber na tela
- ✅ Acessibilidade completa: Suporte a navegação por teclado e screen readers
- ✅ Animações suaves: Transições de entrada e saída elegantes
- ✅ Seta indicativa: Aponta para o elemento que ativou o tooltip
- ✅ Suporte a temas: Variáveis CSS para light/dark mode
- ✅ Flexível: Funciona com qualquer elemento como trigger
- ✅ Controle de estado: Pode ser desabilitado conforme necessário
- ✅ Largura configurável (
maxWidth): Limita a largura e permite quebra de linha no conteúdo
// Tooltip básico
<Tooltip content="Informação útil" position="top">
<Button>Passe o mouse aqui</Button>
</Tooltip>
// Tooltip com texto
<Tooltip content="Clique para mais detalhes" position="right">
<span className="underline cursor-help">
Texto com tooltip
</span>
</Tooltip>
// Tooltip personalizado
<Tooltip
content="Tooltip customizado"
position="bottom"
className="bg-red-500 text-white"
>
<Button variant="outline">Hover aqui</Button>
</Tooltip>
// Tooltip desabilitado
<Tooltip content="Este não aparece" disabled>
<Button>Tooltip desabilitado</Button>
</Tooltip>
// Tooltip com largura máxima (texto longo quebra em múltiplas linhas)
// Aceita número (convertido para px) ou qualquer string CSS válida
<Tooltip
content="Este é um texto longo que vai quebrar em múltiplas linhas quando a largura for limitada."
position="top"
maxWidth={200}
>
<Button>maxWidth numérico (200px)</Button>
</Tooltip>
<Tooltip
content="Também é possível usar valores CSS como rem, em ou percentagem."
position="bottom"
maxWidth="12rem"
>
<Button>maxWidth string (12rem)</Button>
</Tooltip>Props do Tooltip
| Prop | Tipo | Padrão | Descrição |
|------|------|--------|-----------|
| content | string | — | Texto exibido no tooltip |
| position | "top" \| "bottom" \| "left" \| "right" | "top" | Posição preferencial do tooltip |
| disabled | boolean | false | Desabilita o tooltip |
| maxWidth | string \| number | undefined | Largura máxima. Número é convertido para px. Quando definido, o texto quebra em múltiplas linhas |
| className | string | "" | Classes CSS adicionais para o balão do tooltip |
🪗 Accordion
Componente de accordion (acordeão) flexível e acessível para expandir e colapsar seções de conteúdo:
- ✅ Modo single: Apenas um item aberto por vez
- ✅ Modo multiple: Vários itens podem estar abertos simultaneamente
- ✅ Animações suaves: Transições de altura com ease-in-out
- ✅ Variantes visuais: default, bordered, separated
- ✅ Acessibilidade completa: ARIA labels e navegação por teclado
- ✅ Itens desabilitados: Suporte para itens que não podem ser expandidos
- ✅ Ícones customizados: Adicione ícones aos títulos
- ✅ Collapsible configurável: Controle se todos os itens podem ser fechados
- ✅ DefaultOpen: Items podem iniciar abertos
- ✅ Customização total: Classes CSS para header e content
// Accordion básico
<Accordion>
<Accordion.Item title="O que é React?">
<p>React é uma biblioteca JavaScript para construir interfaces de usuário.</p>
</Accordion.Item>
<Accordion.Item title="O que é TypeScript?">
<p>TypeScript é um superset de JavaScript que adiciona tipagem estática.</p>
</Accordion.Item>
</Accordion>
// Accordion com múltiplos itens abertos
<Accordion type="multiple">
<Accordion.Item title="Seção 1" defaultOpen>
<p>Esta seção inicia aberta.</p>
</Accordion.Item>
<Accordion.Item title="Seção 2" defaultOpen>
<p>Esta seção também inicia aberta.</p>
</Accordion.Item>
</Accordion>
// Accordion com variant bordered
<Accordion variant="bordered">
<Accordion.Item title="Recursos do Produto">
<ul>
<li>Interface intuitiva</li>
<li>Integração com múltiplas plataformas</li>
<li>Suporte 24/7</li>
</ul>
</Accordion.Item>
</Accordion>
// Accordion com ícones customizados
<Accordion variant="separated" type="multiple">
<Accordion.Item
title="Documentação"
icon={<DocumentIcon />}
>
<p>Acesse a documentação completa.</p>
</Accordion.Item>
<Accordion.Item
title="Suporte"
icon={<SupportIcon />}
>
<p>Entre em contato com nossa equipe.</p>
</Accordion.Item>
</Accordion>
// Accordion não collapsible (sempre mantém um aberto)
<Accordion collapsible={false}>
<Accordion.Item title="Passo 1" defaultOpen>
<p>Configure seu ambiente.</p>
</Accordion.Item>
<Accordion.Item title="Passo 2">
<p>Desenvolva sua aplicação.</p>
</Accordion.Item>
</Accordion>🪟 AsideSheet
Componente tipo painel deslizante (sheet) que abre a partir das bordas da tela.
- ✅ Suporta controle programático (controlled) e estado interno (uncontrolled)
- ✅ Abre da direita para a esquerda ou da esquerda para a direita (
side: 'left' | 'right') - ✅ Largura configurável via
sizeouclassName - ✅ Acessível: foco gerenciado e comportamento esperado ao fechar (Esc)
Props principais:
isOpen?: boolean— controla visibilidade (quando usado como controlled)defaultOpen?: boolean— estado inicial (uncontrolled)onClose?: () => void— callback chamado ao fecharside?: 'left' | 'right'— lado de abertura (padrão: 'right')size?: 'sm' | 'md' | 'lg' | 'full'— tamanho pré-definido do sheetclassName?: string— classes adicionais para o containertitle?: string | React.ReactNode— título opcional do painel
Exemplo de uso (controlado):
import React from 'react';
import { AsideSheet, Button } from 'rharuow-ds';
function Example() {
const [open, setOpen] = React.useState(false);
return (
<div>
<Button onClick={() => setOpen(true)}>Abrir Aside</Button>
<AsideSheet
isOpen={open}
onClose={() => setOpen(false)}
side="right"
size="md"
>
<AsideSheet.Header>
<h3>Detalhes</h3>
</AsideSheet.Header>
<AsideSheet.Body>
<p>Conteúdo do painel.</p>
</AsideSheet.Body>
<AsideSheet.Footer>
<Button variant="outline" onClick={() => setOpen(false)}>
Fechar
</Button>
</AsideSheet.Footer>
</AsideSheet>
</div>
);
}Veja a story do componente no Storybook para demonstrações e variações (left/right, controlled/uncontrolled):
🗂️ Sidebar
Painel lateral persistente para telas md+ (≥ 768px). Diferente do AsideSheet, a Sidebar não usa overlay nem portal — é parte do layout fixo da página.
- ✅ Exclusivo para telas médias e grandes — oculto automaticamente em mobile (
hidden md:flex) - ✅ Lado configurável:
leftouright - ✅ Larguras predefinidas:
sm(256px),md(288px),lg(384px) - ✅ Largura customizada com qualquer valor CSS (ex:
"320px","20rem") - ✅ Animação suave de slide in/out
- ✅ Sem overlay — não bloqueia o conteúdo principal
- ✅ Variável CSS
--sidebar-bg(fallback para--aside-bge depois#ffffff) - ✅ Acessível:
role="navigation",aria-expanded
Props principais:
open: boolean— controla visibilidade (slide in/out)side?: 'left' | 'right'— lado de abertura (padrão:'left')size?: 'sm' | 'md' | 'lg' | string— tamanho pré-definido ou valor CSS customizado (padrão:'md')className?: string— classes adicionais para o container
Exemplo de uso:
import React from 'react';
import { Sidebar, Button } from 'rharuow-ds';
function Layout() {
const [sidebarOpen, setSidebarOpen] = React.useState(true);
return (
<div className="flex min-h-screen">
<Sidebar open={sidebarOpen} side="left" size="md">
<div className="p-6 flex flex-col gap-4">
<h3 className="text-lg font-semibold">Menu</h3>
<nav className="flex flex-col gap-2 text-sm">
<a href="/dashboard">Dashboard</a>
<a href="/users">Usuários</a>
<a href="/settings">Configurações</a>
</nav>
</div>
</Sidebar>
{/* Conteúdo principal com margem para não ficar atrás da sidebar */}
<main className="flex-1 md:ml-72 p-6">
<Button onClick={() => setSidebarOpen((v) => !v)}>
{sidebarOpen ? 'Fechar Sidebar' : 'Abrir Sidebar'}
</Button>
<p>Conteúdo da página.</p>
</main>
</div>
);
}Exemplo com largura customizada:
<Sidebar open={open} side="left" size="320px">
{/* conteúdo */}
</Sidebar>Variável CSS para customizar a cor de fundo:
:root {
--sidebar-bg: #1e293b; /* escuro */
}Veja as stories do componente no Storybook:
🔀 Switch
Componente de alternancia (on/off) com foco em acessibilidade e controle simples de estado.
- ✅ Funciona em modo controlado com
checked+onChange - ✅ Label opcional com posicao configuravel (
leftouright) - ✅ Tamanhos predefinidos:
sm,md,lg - ✅ Estado
disabledcom estilo e comportamento apropriados - ✅ Acessivel com
role="switch"earia-checked
Props principais:
checked?: boolean- Estado atual do switchonChange?: (checked: boolean) => void- Callback ao alternarlabel?: string- Texto exibido ao lado do controlelabelPosition?: 'left' | 'right'- Posicao do label (padrao:'right')size?: 'sm' | 'md' | 'lg'- Tamanho visual (padrao:'md')disabled?: boolean- Desabilita interacao
Exemplo de uso:
import React from 'react';
import { Switch } from 'rharuow-ds';
function Settings() {
const [notifications, setNotifications] = React.useState(false);
return (
<Switch
checked={notifications}
onChange={setNotifications}
label={notifications ? 'Notificacoes ativas' : 'Notificacoes inativas'}
labelPosition="right"
size="md"
/>
);
}Storybook:
📱 BottomTabNavigator
Navegacao inferior fixa para mobile, ideal para areas principais do app.
- ✅ Exibido somente em telas pequenas (
md:hidden) - ✅ Suporte a modo controlado (
value) e nao controlado (defaultValue) - ✅ Cada item pode ter icone, badge e estado desabilitado
- ✅ Layout responsivo com colunas dinamicas conforme quantidade de tabs
- ✅ Acessivel com
role="tablist"erole="tab"
Props principais:
items: BottomTabItem[]- Lista de abasvalue?: string- Valor selecionado (modo controlado)defaultValue?: string- Valor inicial (modo nao controlado)onValueChange?: (value: string) => void- Callback de troca de aba
Exemplo de uso:
import React from 'react';
import { BottomTabNavigator } from 'rharuow-ds';
function MobileLayout() {
const [active, setActive] = React.useState('home');
return (
<BottomTabNavigator
value={active}
onValueChange={setActive}
items={[
{ id: 'home', label: 'Inicio', icon: '🏠' },
{ id: 'search', label: 'Buscar', icon: '🔎' },
{ id: 'wallet', label: 'Carteira', icon: '💳', badge: '●' },
{ id: 'profile', label: 'Perfil', icon: '👤' },
]}
/>
);
}Storybook:
Storybook — BottomTabNavigator
🧲 BottomSheet
Painel deslizante que abre de baixo para cima em mobile, util para filtros, acoes e formularios curtos.
- ✅ Exibido apenas em mobile (
md:hidden) - ✅ Portal no
document.bodycom overlay - ✅ Fechamento por overlay e tecla ESC (configuravel)
- ✅ Bloqueio de scroll do body enquanto aberto (configuravel)
- ✅ Suporte a arrastar para baixo para fechar (
closeOnDragDown) - ✅ Tamanhos predefinidos (
sm,md,lg,full) ou valor customizado
Props principais:
open: boolean- Controla visibilidade do painelonClose: () => void- Callback ao fecharsize?: 'sm' | 'md' | 'lg' | 'full' | string- Altura maxima do painelcloseOnOverlayClick?: boolean- Fecha ao clicar fora (padrao:true)closeOnEscape?: boolean- Fecha ao pressionar ESC (padrao:true)lockBodyScroll?: boolean- Bloqueia scroll da pagina (padrao:true)closeOnDragDown?: boolean- Habilita gesto de arrastar para fechar (padrao:true)dragCloseThreshold?: number- Distancia minima de arraste para fechar (padrao:100)
Exemplo de uso:
import React from 'react';
import { BottomSheet, Button } from 'rharuow-ds';
function Filters() {
const [open, setOpen] = React.useState(false);
return (
<>
<Button onClick={() => setOpen(true)}>Abrir filtros</Button>
<BottomSheet open={open} onClose={() => setOpen(false)} size="md">
<div className="px-4 pb-6 pt-4">
<h3 className="text-lg font-semibold">Filtros rapidos</h3>
<p className="mt-2 text-sm">Escolha os filtros e aplique.</p>
</div>
</BottomSheet>
</>
);
}Storybook:
🎭 Modal
Componente de diálogo modal para exibir conteúdo sobreposto à página principal.
- ✅ Overlay com transparência configurável
- ✅ Múltiplos tamanhos: sm, md, lg, xl, full
- ✅ Variantes de cor: default, primary, secondary (usando CSS Variables)
- ✅ Controle de fechamento via overlay, ESC ou botão X
- ✅ Prevenção de scroll do body quando aberto
- ✅ Animações suaves de entrada/saída
- ✅ Sub-componentes para estruturação: Header, Body, Footer
- ✅ Renderização via Portal (React Portal)
- ✅ Acessível: role="dialog", aria-modal
Props principais:
open: boolean— controla visibilidade do modalonClose: () => void— callback chamado ao fecharsize?: 'sm' | 'md' | 'lg' | 'xl' | 'full'— tamanho do modal (padrão: 'md')variant?: 'default' | 'primary' | 'secondary'— variante de cor (padrão: 'default')closeOnOverlayClick?: boolean— fecha ao clicar fora (padrão: true)closeOnEscape?: boolean— fecha ao pressionar ESC (padrão: true)showCloseButton?: boolean— exibe botão X de fechar (padrão: true)className?: string— classes adicionais para o container do modal
Exemplo de uso básico:
import React from 'react';
import { Modal, Button } from 'rharuow-ds';
function Example() {
const [open, setOpen] = React.useState(false);
return (
<div>
<Button onClick={() => setOpen(true)}>Abrir Modal</Button>
<Modal open={open} onClose={() => setOpen(false)}>
<h2>Título do Modal</h2>
<p>Conteúdo do modal aqui.</p>
</Modal>
</div>
);
}Exemplo com estrutura completa:
import React from 'react';
import { Modal, Button } from 'rharuow-ds';
function Example() {
const [open, setOpen] = React.useState(false);
return (
<div>
<Button onClick={() => setOpen(true)}>Confirmar Ação</Button>
<Modal
open={open}
onClose={() => setOpen(false)}
size="md"
>
<Modal.Header>
<h2 className="text-2xl font-bold">Confirmar Exclusão</h2>
<p className="text-sm text-gray-500">Esta ação não pode ser desfeita</p>
</Modal.Header>
<Modal.Body>
<p className="text-gray-700">
Você tem certeza que deseja excluir este item?
Todos os dados associados serão removidos permanentemente.
</p>
</Modal.Body>
<Modal.Footer>
<Button variant="outline" onClick={() => setOpen(false)}>
Cancelar
</Button>
<Button onClick={() => {
// Executar ação
setOpen(false);
}}>
Confirmar
</Button>
</Modal.Footer>
</Modal>
</div>
);
}Exemplo com formulário integrado:
import React from 'react';
import { Modal, Button, Input } from 'rharuow-ds';
import { FormProvider, useForm } from 'react-hook-form';
function FormModal() {
const [open, setOpen] = React.useState(false);
const methods = useForm();
const onSubmit = (data: any) => {
console.log('Form data:', data);
setOpen(false);
methods.reset();
};
return (
<div>
<Button onClick={() => setOpen(true)}>Novo Cadastro</Button>
<Modal
open={open}
onClose={() => setOpen(false)}
size="lg"
closeOnOverlayClick={false}
>
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<Modal.Header>
<h2 className="text-2xl font-bold">Cadastrar Usuário</h2>
</Modal.Header>
<Modal.Body>
<div className="space-y-4">
<Input label="Nome completo" name="name" required />
<Input label="E-mail" name="email" type="email" required />
<Input label="Telefone" name="phone" />
</div>
</Modal.Body>
<Modal.Footer>
<Button
type="button"
variant="outline"
onClick={() => setOpen(false)}
>
Cancelar
</Button>
<Button type="submit">Salvar</Button>
</Modal.Footer>
</form>
</FormProvider>
</Modal>
</div>
);
}Exemplo com variantes de cor:
import React from 'react';
import { Modal, Button } from 'rharuow-ds';
function ColorVariants() {
const [openPrimary, setOpenPrimary] = React.useState(false);
const [openSecondary, setOpenSecondary] = React.useState(false);
return (
<div>
{/* Modal com cor primária */}
<Button onClick={() => setOpenPrimary(true)}>
Modal Primary
</Button>
<Modal
open={openPrimary}
onClose={() => setOpenPrimary(false)}
variant="primary"
>
<Modal.Header>
<h2 className="text-2xl font-bold">Ação Importante</h2>
</Modal.Header>
<Modal.Body>
<p className="opacity-95">
Este modal usa as cores primárias da aplicação,
ideal para destacar ações principais.
</p>
</Modal.Body>
<Modal.Footer>
<Button variant="outline" onClick={() => setOpenPrimary(false)}>
Fechar
</Button>
</Modal.Footer>
</Modal>
{/* Modal com cor secundária */}
<Button onClick={() => setOpenSecondary(true)} variant="secondary">
Modal Secondary
</Button>
<Modal
open={openSecondary}
onClose={() => setOpenSecondary(false)}
variant="secondary"
>
<Modal.Header>
<h2 className="text-2xl font-bold">Aviso</h2>
</Modal.Header>
<Modal.Body>
<p className="opacity-95">
Este modal usa as cores secundárias da aplicação,
ideal para avisos e ações alternativas.
</p>
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={() => setOpenSecondary(false)}>
Fechar
</Button>
</Modal.Footer>
</Modal>
</div>
);
}Customizando o background do Modal (--bg-modal)
O background do Modal na variante default pode ser customizado via a variável CSS --bg-modal.
| Tema | Valor padrão |
|------|-------------|
| Light | #ffffff (branco) |
| Dark | #000000 (preto) |
Para customizar no seu app consumidor, defina a variável no seu CSS global ou diretamente no elemento:
/* CSS global da aplicação */
:root {
--bg-modal: #1e3a5f; /* Azul escuro customizado */
}
/* Ou por tema */
:root {
--bg-modal: #f0f4f8; /* Light */
}
[data-theme="dark"], .dark {
--bg-modal: #1a1a2e; /* Dark */
}// Ou inline via style (para casos específicos)
<div style={{ "--bg-modal": "#1e3a5f" } as React.CSSProperties}>
<Modal open={open} onClose={onClose}>...</Modal>
</div>Nota: A variável
--bg-modalafeta apenas a variantedefault. As variantesprimaryesecondaryusam--primarye--secondaryrespectivamente.
Veja a story do componente no Storybook para mais exemplos e variações:
� Toaster
Sistema completo de notificações toast para feedback ao usuário com múltiplas variantes e posicionamento flexível.
- ✅ 5 variantes de toast: success, error, warning, info, default
- ✅ 6 posições configuráveis na tela
- ✅ Auto-dismiss com duração customizável
- ✅ Toast permanente (duration: 0)
- ✅ Ícones automáticos por variante
- ✅ Animações suaves de entrada e saída
- ✅ Limite de toasts simultâneos (padrão: 5)
- ✅ Callbacks ao fechar
- ✅ Hook
useToastpara uso simplificado - ✅ Gerenciamento via Context API
Configuração inicial:
O Toaster precisa ser configurado uma única vez no nível superior da aplicação:
import React from 'react';
import { ToasterProvider } from 'rharuow-ds';
function App() {
return (
<ToasterProvider position="top-right" maxToasts={5}>
{/* Sua aplicação aqui */}
<YourApp />
</ToasterProvider>
);
}Props do ToasterProvider:
position?: ToastPosition- Posição dos toasts na tela (padrão: 'top-right')- Opções: 'top-left', 'top-center', 'top-right', 'bottom-left', 'bottom-center', 'bottom-right'
maxToasts?: number- Número máximo de toasts simultâneos (padrão: 5)
Uso básico com hook useToast:
import React from 'react';
import { useToast, Button } from 'rharuow-ds';
function MyComponent() {
const toast = useToast();
return (
<div>
<Button onClick={() => toast.success('Operação realizada com sucesso!')}>
Success
</Button>
<Button onClick={() => toast.error('Erro ao processar requisição')}>
Error
</Button>
<Button onClick={() => toast.warning('Atenção: verifique os dados')}>
Warning
</Button>
<Button onClick={() => toast.info('Você tem 3 novas mensagens')}>
Info
</Button>
</div>
);
}Toasts com duração customizada:
import React from 'react';
import { useToast, Button } from 'rharuow-ds';
function CustomDuration() {
const toast = useToast();
return (
<div>
{/* Toast rápido - 2 segundos */}
<Button onClick={() => toast.success('Toast rápido', 2000)}>
2 Segundos
</Button>
{/* Toast longo - 10 segundos */}
<Button onClick={() => toast.info('Toast longo', 10000)}>
10 Segundos
</Button>
{/* Toast permanente - não fecha automaticamente */}
<Button
onClick={() => toast.toast('Toast permanente', { duration: 0 })}
>
Permanente
</Button>
</div>
);
}Toast com callback ao fechar:
import React from 'react';
import { useToaster, Button } from 'rharuow-ds';
function WithCallback() {
const { addToast } = useToaster();
const handleAction = () => {
addToast({
message: 'Processando dados...',
variant: 'info',
duration: 3000,
onClose: () => {
console.log('Toast fechado!');
// Executar ação após fechamento
performNextAction();
},
});
};
return <Button onClick={handleAction}>Iniciar Processo</Button>;
}Exemplo completo em um formulário:
import React from 'react';
import { useForm, FormProvider } from 'react-hook-form';
import { Input, Button, useToast } from 'rharuow-ds';
function FormWithToast() {
const methods = useForm();
const toast = useToast();
const onSubmit = async (data: any) => {
try {
// Simular chamada à API
await saveData(data);
toast.success('Dados salvos com sucesso!');
methods.reset();
} catch (error) {
toast.error('Erro ao salvar dados. Tente novamente.');
console.error(error);
}
};
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<Input label="Nome" name="name" required />
<Input label="E-mail" name="email" type="email" required />
<Button type="submit">Salvar</Button>
</form>
</FormProvider>
);
}API do hook useToast:
const toast = useToast();
// Métodos disponíveis:
toast.success(message: string, duration?: number)
toast.error(message: string, duration?: number)
toast.warning(message: string, duration?: number)
toast.info(message: string, duration?: number)
toast.toast(message: string, options?: ToastOptions)API avançada com useToaster:
const { addToast, removeToast, clearAll, toasts } = useToaster();
// Adicionar toast com controle total
const id = addToast({
message: 'Mensagem personalizada',
variant: 'success',
duration: 5000,
onClose: () => console.log('Fechado'),
});
// Remover toast específico
removeToast(id);
// Limpar todos os toasts
clearAll();Dicas de uso:
- Use
successpara operações bem-sucedidas (save, delete, update) - Use
errorpara falhas e erros - Use
warningpara avisos que requerem atenção - Use
infopara informações gerais - Configure
duration: 0para toasts que precisam de ação manual do usuário - Posicione toasts conforme o contexto: top para notificações gerais, bottom para ações específicas
Veja a story do componente no Storybook para demonstrações interativas:
�📷 ImageInput
Componente para seleção e upload de imagens com preview e ações de confirmação/remoção:
- ✅ Seleção via explorador - Clique para abrir o explorador de arquivos (apenas imagens)
- ✅ Preview da imagem - Visualização imediata após seleção
- ✅ Modo avatar (
avatar={true}) - Formato circular para fotos de perfil - ✅ Ações de confirmação - Botões para confirmar upload ou cancelar
- ✅ Remoção de imagem - Botão para excluir imagem já salva
- ✅ Suporte a URLs externas - Exibe imagens já salvas via
valueprop - ✅ Validação de arquivos - Controle de tipo e tamanho máximo
- ✅ Estados de loading - Indicação visual durante upload/remoção
- ✅ Flexível - Funciona com qualquer serviço (Cloudinary, Firebase, S3, etc.)
- ✅ Integração com React Hook Form - Nome do campo e validação
Props principais:
avatar?: boolean— formato circular (ideal para avatars)value?: string— URL da imagem atual (já salva)onUpload?: (file: File) => Promise<string>— callback para upload (retorna URL)onRemove?: (imageUrl?: string) => Promise<void>— callback para remoçãoaccept?: string— tipos aceitos (padrão: "image/*")maxSize?: number— tamanho máximo em bytessize?: 'sm' | 'md' | 'lg'— tamanho do componenteloading?: boolean— estado de carregamentodisabled?: boolean— desabilitar interações
Exemplo básico:
import React from 'react';
import { ImageInput } from 'rharuow-ds';
function ProfileForm() {
const [avatarUrl, setAvatarUrl] = React.useState('');
const handleUpload = async (file: File): Promise<string> => {
// Upload para seu serviço preferido (Cloudinary, Firebase, etc.)
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
const data = await response.json();
setAvatarUrl(data.url);
return data.url;
};
const handleRemove = async (url?: string) => {
// Remover do serviço se necessário
await fetch(`/api/delete?url=${encodeURIComponent(url || '')}`, {
method: 'DELETE'
});
setAvatarUrl('');
};
return (
<ImageInput
avatar
label="Foto do Perfil"
value={avatarUrl}
onUpload={handleUpload}
onRemove={handleRemove}
size="lg"
maxSize={2 * 1024 * 1024} // 2MB
/>
);
}Exemplo com Cloudinary:
const uploadToCloudinary = async (file: File): Promise<string> => {
const formData = new FormData();
formData.append('file', file);
formData.append('upload_preset', 'seu_preset');
const response = await fetch(
`https://api.cloudinary.com/v1_1/seu_cloud_name/image/upload`,
{
method: 'POST',
body: formData
}
);
const data = await response.json();
return data.secure_url;
};
<ImageInput
onUpload={uploadToCloudinary}
placeholder="Upload para Cloudinary"
/>Veja a story do componente no Storybook para demonstrações completas:
🎨 ColorInput
Componente de seleção de cor (color picker) com label flutuante e integração nativa com React Hook Form:
- ✅ Seletor nativo - Usa
<input type="color">do navegador para máxima compatibilidade - ✅ Valor hexadecimal visível - Exibe o código hex selecionado ao lado do swatch
- ✅ Label flutuante - Mesmo comportamento do componente
Input - ✅ Integração com React Hook Form - Registra automaticamente via
useFormContext - ✅ Controlado ou não-controlado - Funciona com ou sem
FormProvider - ✅ Acessível - Label associado ao input via
htmlFor
Props principais:
name: string— nome do campo (obrigatório, usado no React Hook Form)label?: string— label flutuantecontainerClassName?: string— classe extra no wrapperdisabled?: boolean— desabilita o campo
Exemplo básico:
import { ColorInput } from 'rharuow-ds';
import { FormProvider, useForm } from 'react-hook-form';
function ThemeForm() {
const methods = useForm({ defaultValues: { primaryColor: '#8b5cf6' } });
const onSubmit = (data: { primaryColor: string }) => {
console.log('Cor escolhida:', data.primaryColor);
};
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<ColorInput name="primaryColor" label="Cor primária" />
<button type="submit">Salvar</button>
</form>
</FormProvider>
);
}Exemplo controlado (sem React Hook Form):
import React from 'react';
import { ColorInput } from 'rharuow-ds';
function ColorPicker() {
const [color, setColor] = React.useState('#ec4899');
return (
<ColorInput
name="cor"
label="Escolha uma cor"
value={color}
onChange={(e) => setColor(e.target.value)}
/>
);
}Veja a story do componente no Storybook para demonstrações completas:
---
## 🎨 Customização de Tema
O rharuow-ds utiliza um **sistema de cores inteligente** que permite personalizar todo o design system definindo apenas **duas cores**: primária e secundária. Todas as variações (hover, light, dark) e cores de texto com contraste adequado são **calculadas automaticamente**.
> ⚡ **NOVO**: Sistema de Cálculo Automático de Cores! Veja a [documentação completa](AUTO_COLOR_SYSTEM.md) para detalhes.
### ✨ Modo Simplificado (Recomendado)
**Defina apenas 2 variáveis** e o sistema calcula automaticamente todas as variações:
```css
/* Importar o DS primeiro */
@import 'rharuow-ds/dist/styles.css';
/* Defina APENAS as cores base - o resto é automático! */
:root {
--primary: #8b5cf6; /* Roxo */
--secondary: #ec4899; /* Rosa */
}
/* Para dark mode */
[data-theme="dark"] {
--primary: #a78bfa; /* Versão mais clara para melhor contraste */
--secondary: #f472b6;
}O sistema automaticamente gera:
- ✅
--primary-hover,--primary-light,--primary-dark,--primary-text - ✅
--secondary-hover,--secondary-light,--secondary-dark,--secondary-text - ✅ Contraste adequado para textos (WCAG AA compliance)
- ✅ Ajustes automáticos para dark mode
💻 Uso com JavaScript/TypeScript
Para aplicações que precisam mudar cores dinamicamente:
import { applyThemeColors } from 'rharuow-ds/lib/color.utils';
import 'rharuow-ds/dist/styles.css';
function App() {
useEffect(() => {
// Aplica cores e calcula automaticamente todas as variações
applyThemeColors('#8b5cf6', '#ec4899');
}, []);
return <div>...</div>;
}🎨 Funções Utilitárias
O DS exporta várias funções para cálculos de cor:
import {
generateColorPalette, // Gera paleta completa de uma cor
getContrastingTextColor, // Retorna branco ou preto com melhor contraste
isLightColor, // Verifica se uma cor é clara ou escura
lightenColor, // Clareia uma cor em X%
darkenColor, // Escurece uma cor em X%
hexToRgb, // Converte HEX para RGB
getLuminance, // Calcula luminância relativa
getContrastRatio // Calcula razão de contraste (WCAG)
} from 'rharuow-ds/lib/color.utils';
// Exemplo: Gerar paleta completa
const palette = generateColorPalette('#8b5cf6');
/*
{
base: '#8b5cf6',
hover: '#7c3aed',
light: '#ede9fe',
dark: '#6d28d9',
text: '#ffffff',
textOnLight: '#1f2937'
}
*/🌈 Sistema de Cores
Como os Componentes Usam as Cores
Os componentes derivam automaticamente suas cores das variáveis primárias:
- Card Header: Mescla 5% da cor primária com fundo neutro
- Table Header: Mescla 8% da cor primária com fundo neutro
- Table Hover: Mescla 10% da cor primária com fundo neutro
- Select Selected: Usa diretamente
--primary-light - Button/Modal: Usam cores primária/secundária com texto de alto contraste
- Elementos Selecionados: Consistentemente usam a cor primária clara
💡 Modo Avançado (Controle Total)
Se você precisa de controle total sobre cada variação:
:root {
/* Defina todas as variações manualmente */
--primary: #8b5cf6;
--primary-hover: #7c3aed;
--primary-light: #ede9fe;
--primary-dark: #6d28d9;
--primary-text: #ffffff;
--secondary: #ec4899;
--secondary-hover: #db2777;
--secondary-light: #fce7f3;
--secondary-dark: #be185d;
--secondary-text: #ffffff;
}📖 Documentação Completa
- AUTO_COLOR_SYSTEM.md - Guia completo do sistema de cores automático
- THEME_CUSTOMIZATION.md - Customização detalhada de tema
Método 2: JavaScript/React
// App.tsx ou main.tsx
useEffect(() => {
const root = document.documentElement;
root.style.setProperty('--primary', '#f59e0b');
root.style.setProperty('--primary-hover', '#d97706');
root.style.setProperty('--primary-light', '#fef3c7');
}, []);🎯 Exemplos de Paletas
/* Paleta Corporativa (Azul) */
:root {
--primary: #0ea5e9;
--primary-hover: #0284c7;
--primary-light: #e0f2fe;
}
/* Paleta Moderna (Roxo/Rosa) */
:root {
--primary: #8b5cf6;
--primary-hover: #7c3aed;
--primary-light: #ede9fe;
--secondary: #ec4899;
}
/* Paleta Natureza (Verde) */
:root {
--primary: #10b981;
--primary-hover: #059669;
--primary-light: #d1fae5;
}
/* Paleta Minimalista (Cinza) */
:root {
--primary: #6b7280;
--primary-hover: #4b5563;
--primary-light: #f3f4f6;
}🔧 Customização Avançada
Para controle total, você pode sobrescrever variáveis específicas:
:root {
/* Cores base da marca */
--primary: #8b5cf6;
/* Customização específica de Card */
--card-header-bg: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--card-header-border: #8b5cf6;
/* Customização específica de Table */
--table-header-bg: #f3e8ff;
--table-row-selected: #ede9fe;
}📚 Variáveis Disponíveis por Componente
Card
--card-bg,--card-border,--card-text--card-header-bg,--card-header-border--card-footer-bg,--card-footer-border
Table
--table-bg,--table-border,--table-text--table-header-bg,--table-row-hover,--table-row-selected
Select/AsyncSelect/MultiSelect
--select-dropdown-bg,--select-dropdown-border--select-dropdown-hover,--select-dropdown-selected
Tooltip
--tooltip-bg,--tooltip-text
🌓 Dark Mode
O sistema ajusta automaticamente as cores no dark mode:
/* Ative o dark mode adicionando o atributo */
<html data-theme="dark">
<!-- ou -->
<html class="dark">No Storybook
Na documentação do Storybook, você pode testar diferentes temas na story "Theme Customization".
Para mais detalhes, consulte THEME_CUSTOMIZATION.md.
🛠️ Desenvolvimento
- ✅ Ícones customizados opcionais
- ✅ Três tamanhos: sm, md, lg
- ✅ Layout horizontal ou vertical
- ✅ Animações e estados visuais
- ✅ Label flutuante integrada
Documentação
Acesse a documentação interativa dos componentes em GitHub Pages (link será atualizado após o deploy).
Desenvolvimento
Para contribuir ou rodar localmente:
git clone https://github.com/Rharuow/rharuow-ds-docs.git
cd rharuow-ds-docs
npm installComandos disponíveis:
# Iniciar Storybook para desenvolvimento
npm run storybook
# Build dos componentes
npm run build
# Gerar CSS do Tailwind
npm run build:css
# Executar testes
npm test
# Build do Storybook para produção
npm run build-storybookEstrutura do projeto:
src/
├── components/ # Componentes React
│ ├── Button.tsx
│ ├── Input.tsx
│ ├── Select.tsx
│ ├── AsyncSelect.tsx
│ ├── MultiSelect.tsx
│ ├── types.ts # Tipos compartilhados
│ └── index.ts # Exportações
├── lib/
│ └── utils.ts # Utilitários (cn, etc.)
├── stories/ # Stories do Storybook
└── styles/
└── ds.css # Estilos TailwindTecnologias
- ⚛️ React 18 - Biblioteca base
- 📋 React Hook Form - Gerenciamento de formulários
- 🎨 Tailwind CSS - Estilização
- 📚 Storybook - Documentação interativa
- 📦 Vite - Build tool
- 🔷 TypeScript - Tipagem estática
Desenvolvido por Harysson.
