@rase-ui/react
v0.4.0
Published
RaseUI — componentes React sobre Base UI com o modelo de tema do Radix (variante ⊥ cor). Temável por CSS vars, sem domínio.
Downloads
217
Readme
@rase-ui/react
RaseUI — componentes React sobre Base UI (comportamento/a11y) com o modelo de tema do Radix (variante ⊥ cor). Sem domínio, temável pelo host por CSS vars.
O modelo vigente está em
docs/DESIGN-SYSTEM.md— leia esse primeiro. ODESIGN.md/STYLES.mddescrevem o sistema relief/skin antigo, ainda vivo nos componentes não migrados.A doc de uso de cada componente vive co-localizada com o código, no
README.mdda pasta dele (linkado na tabela abaixo) — pra não dessincronizar doc e código.
pnpm --filter @rase-ui/react build # tsup (esm) + tsc (.d.ts) + tailwind (css)
pnpm --filter @rase-ui/react typecheckimport { Modal } from "@rase-ui/react"; // barrel completo
import { Modal } from "@rase-ui/react/modal"; // ...ou granular (tree-shake)
import "@rase-ui/react/styles.css"; // uma vez, na raiz do appInstalação
pnpm add @rase-ui/reactimport "@rase-ui/react/styles.css"; // uma vez, na raiz do appIsso basta. Sem classe nenhuma o app sai no modo dark (o default do kit); o claro é <Theme
appearance="light">, ou a classe light num ancestral — é o que o provider emite. O tema embarcado é o
default (src/foundation/themes/default.json, variantes light e dark), compilado pelo @csstokens/core
no build; a camada de cor põe as escalas do mesmo modo no :root.
Peers
react e react-dom (18 ou 19). recharts e react-grid-layout são opcionais — só quem importa
/chart ou /dashboard precisa deles.
ESM puro
O pacote não publica CJS: um require() não resolve. Use import. Em moduleResolution: "bundler"
(Vite, Next, tsup) está tudo verde; em node16/nodenext também.
Tema — o provider <Theme>
O que transforma "componentes soltos" em produto único. Ele não é um segundo mecanismo: só emite num
escopo os data-* que as camadas de CSS já traduzem — então trocar a marca de um app inteiro é uma prop,
e o mesmo resultado se obtém escrevendo os atributos na mão se você preferir.
import { Theme } from "@rase-ui/react";
<Theme accentColor="tomato" grayColor="auto" radius="large" scaling="105%">
<App />
</Theme>;| prop | → | efeito |
| ------------- | --------------------- | ------------------------------------------------ |
| accentColor | data-accent | reponta --accent-* pra uma das 31 escalas |
| grayColor | data-gray-color | o neutro; auto escolhe o que casa com o accent |
| radius | data-radius | none · small · medium · large · full |
| scaling | data-scaling | zoom global: 90% … 110% |
| appearance | classe light/dark | pinta o fundo do escopo |
É aninhável (herda por Context o que você não redefinir), e os atributos valem em qualquer
elemento — <div data-radius="small"> escopa o raio sem provider nenhum. Detalhes em
src/base/theme/README.md.
Eixos
Variante = estrutura (solid/soft/surface/…) e cor = ortogonal (color="tomato") — o
oposto do shadcn, onde a cor está presa na variante. Tamanho é size="1..4" no componente;
data-scaling é o zoom global; data-radius escopa o raio em qualquer elemento.
Não existe eixo de densidade. O
[data-density]foi removido: era um zoom não-uniforme travestido de eixo de tema. Um painel denso declara as próprias métricas--ui-*localmente.
Fontes — o kit não carrega nenhuma
Ele só declara os stacks (--font-sans/--font-serif/--font-mono). Uma biblioteca de componentes
não deve forçar 300KB de download em ninguém — e o Radix Themes também não embarca fonte.
Quem quer as nossas — Geist (sans) · Newsreader (serif) · Geist Mono (mono), as três variáveis — instala e importa:
pnpm add @fontsource-variable/geist @fontsource-variable/geist-mono @fontsource-variable/newsreaderSem elas o kit cai no stack de sistema e continua funcionando. Por que essas três, e por que trocar a
sans re-calibra as outras duas: DESIGN-SYSTEM.md §8.4.
[!WARNING] O kit aplica a família no
:roote os componentes herdam — nenhum.ui-*declarafont-family. Umbody { font-family: system-ui, sans-serif }no app (o boilerplate de qualquer template) troca a fonte de todos os componentes, e a falha é muda: nada quebra, o layout monta, e o texto passa a sentar ~1px mais baixo em botões, campos e badges — porque a métrica vertical da fonte de sistema é outra. Pra trocar a fonte do kit, redefina--font-sans; pra não trocar, não declarefont-familynobody(ou declarevar(--font-sans)).
O que o kit resolve sozinho — e o que ainda não
O kit traz o próprio reset escopado (não exige Tailwind), o tema default com os dois modos e a
geometria default: import "@rase-ui/react/styles.css" e acabou. Um app que quer o próprio tema ou a
própria geometria compila os dois JSON publicados com o @csstokens/core e importa o components.css no
lugar do styles.css (seção abaixo); a prova rodando é o apps/consumer do monorepo — o app que consome o
dist como um terceiro, sem alias e sem preflight.
O que ainda não fecha sozinho: o components.css sem os defaults ainda precisa dos --theme-* do
sistema relief (skin.css e tooltip.css leem cru). Nada do base/ depende disso; é
o que sobra fora dele, e sai com o relief (TODO.md §2).
[!WARNING] O playground não serve de prova de que o pacote funciona instalado: ele consome o
src(condiçãorase-source) e carrega o preflight do Tailwind. Foi assim que o.d.tssem tipos e o"use client"descartado passaram meses invisíveis. Mediu no playground, meça também noapps/consumer.
Compilar os próprios tokens (o caminho do csstokens)
O styles.css é o feijão com arroz: instalou, importou, funciona. Mas o kit também publica a
FONTE, pra um app fazer o que se faz com Tailwind — puxar o preset, estender, e compilar no próprio
build:
| artefato | o quê |
| --- | --- |
| @rase-ui/react/struct/struct.json | o preset de arquitetura — radius, tipografia, containers, z-index, movimento |
| @rase-ui/react/struct | o compilador (renderStruct) + extendTokens |
| @rase-ui/react/themes/*.json | os temas: default.json (o que o kit embarca) e o catálogo (ocean) |
| @rase-ui/react/components.css | o kit sem os defaults de token — 19,2KB a menos que o styles.css |
| @rase-ui/react/manifest · /manifest/manifest.json | as peças como dado — o descriptor de cada type (props com enum e default, slots, eventos), pra um montador que não é código. Sem React |
| @rase-ui/react/registry | o manifest com os componentes: as tuplas [type, Component, descriptor] que um host registra num motor |
import { extendTokens, renderStruct } from "@rase-ui/react/struct";
import preset from "@rase-ui/react/struct/struct.json" with { type: "json" };
import * as engine from "@csstokens/core";
const { css } = renderStruct(extendTokens(preset, meuPatch), engine);Dá pra estender qualquer nó, inclusive as bases das escalas — que é onde importa, porque aí você muda
o número sem copiar a fórmula (radius.base.3, font.serif-adjust).
Use o
renderStruct, não orenderStylesheetcru. Emitir geometria tem um segundo bloco obrigatório — a recomputação em:where(.rase-theme, [data-radius], [data-scaling]). Sem ele,var()resolve no:roote um<Theme scaling>não faz nada; pior, a geometria do kit volta dentro de qualquer<Theme>. A regra é do sistema, não de cada app.
@csstokens/core é peer opcional: só quem compila precisa. E arquitetura ⊥ tema — são dois
TokenSets, duas compilações, dois arquivos. O apps/consumer do monorepo é o exemplo rodando.
Build & packaging
- ESM (
dist/*.js) →tsup, um bundle por subpath. É ESM puro: não publicamos CJS, então umrequire()não resolve — useimport. - Tipos (
dist/types/**) →tsc -p tsconfig.build.json, não odtsdo tsup: com 51 entries orollup-plugin-dtsestoura a memória e o processo morre sem falhar alto — o ESM saía, o.d.tsnão, e o pacote era publicado sem tipos. Não voltar prodts: true. - Import relativo no
srcleva extensão (from "./x.js",from "./x/index.js"), mesmo o alvo sendo.ts. Otsccopia o specifier pro.d.tscomo está — sem a extensão, o consumidor emnode16/nodenextrecebeTS2307apontando pro nosso tipo. Cobrado pelopnpm check:imports, que roda antes do build. "use client"é reinserido depois dotsup(pnpm build:directives) — o esbuild descarta a diretiva, e sem ela o pacote quebra em consumidor RSC.- Tokens →
pnpm build:theme(cor,themes/default.json) epnpm build:struct(geometria,struct/struct.json), os dois via@csstokens/core;pnpm build:reliefcompila o que resta do relief (fork jsoncss). Obuild:structtambém copia o preset e o catálogo de temas prodist/— é o que faz o caminho "compilo no meu app" existir. - CSS (
dist/*.css) → Tailwind, um entry por camada (styles,components,structure; e os do relief emlegacy/:skin,controls,inspector). Mais a camada de cor fatiada emdist/accent/, que é CSS puro (só custom properties) e por isso vai pro artefato sem passar pelo Tailwind — ver "Enxugar a camada de cor" abaixo. - O
exportstem uma condiçãorase-sourceque aponta prosrc/: é o playground pedindo o source (HMR). Um consumidor normal nunca a pede e cai nodist. É custom de propósito — comdevelopment, qualquer app Vite em dev tentaria carregar nossosrc/*.tsx, que não é publicado (files: ["dist"]).
Enxugar a camada de cor
import "@rase-ui/react/styles.css" traz tudo e continua sendo o caminho certo pra começar.
Só que dentro dele a camada de cor sozinha é 17 KB gzip — mais de um terço do CSS do kit — porque ela
carrega as 31 escalas nomeadas do eixo color. E ela não sai por tree-shaking: CSS não é
tree-shaken, e um [data-accent="tomato"] só é "usado" em runtime.
Quem monta o próprio bundle pode trocar isso pelo piso mais as hues que usa:
@import "@rase-ui/react/accent/base.css"; /* 2,2 KB — accent + gray + indigo */
@import "@rase-ui/react/accent/hues/tomato.css"; /* +0,6 KB, só se usar color="tomato" */O piso já cobre o caso comum: o --accent-* do tema (com fallback indigo), o --gray-* global, os alphas
neutros e os eixos color="gray" / color="indigo" / <Theme grayColor>. Cada hue extra custa ~890 B
gzip. Detalhes em DESIGN-SYSTEM.md §6.1.
Organização do source
O src/ é agrupado por papel (os subpaths públicos não seguem as pastas — são planos e
estáveis: @rase-ui/react/button continua igual):
| Pasta | O quê |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| foundation/ | máquina de tokens/skin/tema + helpers — internal, prop-def, accent, tokens, skin, presets, theme, schema |
| base/ | CONTROLES atômicos e primitivos de apresentação — button, icon-button, badge, checkbox, card, container, textfield, input, select, combobox, dropdown-menu, table, typography, avatar, image, inset, data-list, list-row, segmented-control, skeleton, line-loader, drawer, structure, chat, chat-core, theme |
| effects/ | EFFECTS — decoração pura: backdrop, animated-border. Sem valor, sem estado, sem papel de a11y (§ abaixo) |
| overlays/ | camadas flutuantes ainda não migradas — modal, dynamic-modal, popover, tooltip, context-menu, command |
| layout/ | estrutura & containers — resizable, collapsible, nav |
| forms/ | editores de valor — controls |
| data/ | exibição de dados & widgets — primitives, chart, json-view, json-editor, node, widget, combine, dashboard, signal |
| complex/ | compostos (compõem os primitivos) — data-table, inspector, kanban, presence-rail, app-header, notifications |
| manifest/ | as peças como dado — o descriptor de cada type (derivado dos props.ts + o catálogo declarado) e o registry com os componentes. Ver src/manifest/README.md |
| styles/ | CSS centralizado — tokens, os 3 skins compartilhados (control.css, popup.css, material.css) e um por componente |
Packages próprios (compõem sobre este kit): o grafo node-edge
@rase-ui/flow(flow+connector), o@rase-ui/chat(chat humano) e o@rase-ui/assistant(chat de IA). A fundação compartilhada dos chats fica aqui embase/chat-core(@rase-ui/react/chat-core).
A regra: base (primitivos) ≠ complex (compostos), e foundation isola a máquina que
todos importam. Mover um módulo de pasta não muda seu subpath — só o entry do tsup.config.ts.
effects/ — a terceira categoria
base e complex não davam conta de tudo: um backdrop não é controle (não interage, não tem valor
nem estado nem role) e não é complex (não compõe nada — é folha). O teste: se você remover, a página
continua funcionando igual, só fica mais feia; se remover quebra uma interação, não é effect.
A regra de composição completa (e por que o nome vem do vocabulário que o projeto já tinha) está em
DESIGN-SYSTEM.md §10.
Os compostos vêm em duas camadas
Um componente de complex/ que carrega dados (/kanban, /data-table) vem em duas peças que se
encaixam, nunca numa só:
| | /kanban | /data-table |
| --------------------------- | ----------------------------- | ------------------- |
| o pronto, config-driven | <KanbanBoard columns cards> | <DataTable table> |
| o compound por baixo | KanbanRoot+Column+Card | base/table |
| o estado, puro | moveCard / useKanbanBoard | useDataTable |
Comece pelo pronto; desça pro compound quando ele não der conta. O escape hatch está sempre aberto, e a camada de cima não ganha prop que a de baixo já resolve — é o que impede o componente de 40 props.
Componentes
Um subpath por peça, todos planos (@rase-ui/react/<nome>) — a pasta em que o source mora não
aparece aqui, e mover um módulo de pasta não muda o subpath dele.
Base — controles e primitivos de apresentação
| Subpath | Doc | O quê |
| -------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------ |
| /button | ↗ | botão — variante = estrutura (solid/soft/surface/outline/ghost) · cor ⊥ |
| /icon-button | ↗ | o botão quadrado — mesmos eixos do Button, caixa = altura. aria-label é obrigatório |
| /badge | ↗ | chip de status — solid/soft/surface/outline · cor ⊥ · sizes 1·2·3 |
| /checkbox | ↗ | checkbox — surface/soft/outline · checked/indeterminate |
| /switch | ↗ | o irmão do Checkbox — a trilha que muda de cor deslizando; --radius-thumb |
| /textfield | ↗ | campo de texto — surface/classic/soft · Slots · Field (label/erro) |
| /select | ↗ | Select sobre Base UI — o valor é um item, sem digitar |
| /combobox | ↗ | Combobox — o select BUSCÁVEL (digita pra filtrar; chips no multi) |
| /dropdown-menu | ↗ | menu de AÇÕES sobre Base UI Menu — composição + data-driven (DropdownItem[]) |
| /segmented-control | ↗ | escolher um de N modos — "Grade | Lista | Tabela" |
| /tabs | ↗ | as abas — N vistas do mesmo objeto; underline/soft/outline, o indicador desliza; abas como navegação |
| /breadcrumb | ↗ | a trilha; um segmento com items vira switcher (time › projeto) |
| /dialog | ↗ | o modal centrado sobre Base UI — size 1..4, modal, dismissible; o pronto e o compound |
| /drawer | ↗ | a superfície que entra pela borda — side, size (a espessura), modal={false} |
| /tooltip | ↗ | a anotação flutuante sobre Base UI — invertida, seta fundida; o filho é o gatilho |
| /toast | ↗ | o aviso temporário — <Toaster> + toast() chamável de fora do React |
| /calendar | ↗ | o Calendar do shadcn (react-day-picker), instalado como veio — a receita é pendente |
| /card | ↗ | container — surface/classic/ghost/acrylic/mica · slots · interativo via render |
| /box · /flex · /grid · /spacer | ↗ | os primitivos de arranjo — gap/p na escala de espaço; Grid com span responsivo por container e columns="auto" |
| /container | ↗ | capa a largura de um bloco de conteúdo e centraliza (a escala --container-1..5) |
| /inset | ↗ | o conteúdo sangra até a borda do container (lê o --inset-* que o Card publica) |
| /image | ↗ | aspect-ratio + object-fit, sem wrapper |
| /avatar | ↗ | a foto do usuário — vira ícone ou iniciais quando não há foto |
| /typography | ↗ | Text/Heading/Code/… na escala de 9 passos · eixo font · trim |
| /table | ↗ | o <table> do Radix — apresentação pura (<th scope>), ghost/surface, sticky |
| /data-list | ↗ | pares label/valor num <dl> semântico |
| /list-row | ↗ | a linha de lista com ação principal + ações que aparecem no hover (a vaga, §9.6) |
| /dot | ↗ | o indicador — size 1·2·3, solid/soft, cor ⊥ (o não-lido, a presença, o grau) |
| /separator | ↗ | a divisória do Base UI — orientation, size, color |
| /toolbar | ↗ | a barra de ferramentas — o RITMO: gap de família na barra, de irmãos no grupo, ToolbarSpacer |
| /skeleton | ↗ | o placeholder de carregamento — funde no elemento, herda o raio dele |
| /line-loader | ↗ | barra de progresso indeterminada, dep-free e tokenizada |
| /structure | ↗ | layouts headless por REGIÃO: AppStructure, PageStructure, View, Section |
| /nav | ↗ | o NavMenu data-driven — size com caixa fixa de ícone (16·24·32), badges, grupos, rail |
| /chat | ↗ | primitivas de chat headless (data-driven; não sabem de transporte) |
| /chat-core | ↗ | a fundação compartilhada dos chats (avatar, typing, markdown, auto-scroll) |
| /theme | ↗ | o provider <Theme> (accent · gray · radius · scaling · appearance) |
Effects
| Subpath | Doc | O quê |
| ------------------ | -------------------------------------------- | -------------------------------------------------------------------------------------- |
| /backdrop | ↗ | fundos decorativos (aurora, camadas) — aria-hidden, sem interação |
| /animated-border | ↗ | borda animada correndo (rect SVG, pathLength=100) — comet ou marching ants; active/from |
| /page-transition | ↗ | a entrada de rota, mount-only, em --motion-duration-page + --motion-ease-standard |
Data & widgets
| Subpath | Doc | O quê |
| -------------- | ------------------------------------- | ----------------------------------------------------------------------------- |
| /primitives | ↗ | primitivos de display white-label pra widget (recebem valor pronto) |
| /chart | ↗ | primitivos de gráfico white-label sobre Recharts, nos tokens do kit |
| /node | ↗ | descritores de nó JSON + <NodeRenderer> — o que torna o widget serializável |
| /widget | ↗ | o <Widget> config-driven ({ type, data, template }) |
| /dashboard | ↗ | grid de widgets combináveis, data-driven |
| /combine | ↗ | superfície drag-drop + seleção headless, agnóstica do combiner |
| /signal | ↗ | introspecção de elementos + anotação visual, headless |
| /json-view | ↗ | visualizador de JSON read-only (árvore colapsável) |
| /json-editor | ↗ | editor de JSON sobre o Monaco + modal de edição imperativo |
Complex — compostos sobre os primitivos
| Subpath | Doc | O quê |
| -------------------- | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| /data-table | ↗ | o MOTOR (TanStack) — ordena/filtra/pagina/seleciona. Compõe o /table |
| /kanban | ↗ | board de funil — <KanbanBoard columns cards> pronto e o compound por baixo; arraste por ponteiro e teclado |
| /presence-rail | ↗ | a faixa de presença da equipe — quem está online, não-lidas, a conversa aberta |
| /app-header | ↗ | a barra global em cinco zonas (nav · brand · scope ··· search · primary · tools · account), cada uma dado ou nó; os eventos na raiz |
| /notifications | ↗ | a caixa de entrada sem domínio — NotificationList (itens prontos) e o NotificationsDrawer não-modal |
| /complex/inspector | ↗ | painel de propriedades (Photoshop/Blender) + variante resizable — fora do barrel: compõe o legacy/controls |
legacy/* — o que ainda veste o sistema relief
Estes subpaths não são API do RaseUI: vestem o relief, não saem pelo barrel e não têm garantia de semver.
Cada um sai da lista quando o equivalente no modelo nascer (Popover, Modal, Tooltip → Base UI) ou quando o
relief morrer. O legacy/input (o TextField ocupou o papel) e o legacy/theme-manifest (o applier de manifest
JSON, anterior ao tema compilado) são delete pendente de consumidor.
Overlays — camadas flutuantes (ainda em Radix/cmdk)
| Subpath | Doc | O quê |
| ---------------- | ------------------------------------------- | -------------------------------------------------------------- |
| /legacy/modal | ↗ | overlay controlado (portal, backdrop, focus-trap, scroll-lock) |
| /legacy/dynamic-modal | ↗ | modal imperativo que devolve uma Promise |
| /legacy/popover | ↗ | Radix Popover (camada flutuante interativa) |
| /legacy/tooltip | ↗ | Radix Tooltip (self-contained) |
| /legacy/context-menu | ↗ | menu de contexto estilo programa (mata o do navegador) |
| /legacy/command | ↗ | paleta ⌘K data-driven sobre cmdk |
Layout e formulário
| Subpath | Doc | O quê |
| ------------------ | -------------------------------------------- | -------------------------------------------------------------------------------------- |
| /legacy/resizable | ↗ | re-export do react-resizable-panels + handle nos tokens |
| /legacy/collapsible | ↗ | Radix Collapsible (base do accordion) |
| /legacy/nav | ↗ | menu de navegação data-driven (sidebar/rail) — não conhece router |
| /legacy/controls | ↗ | editores de valor: Number, Range, Select, Toggle, Text, Segmented, Vector, Color, Tags |
Fundação do relief
| Subpath | Doc | O quê |
| ---------- | --------------------------------------- | ------------------------------------------------------------------------------------- |
| /legacy/skin | ↗ | SkinLayer: camada de fundo (folha) no padrão 2 camadas — sistema relief, legado |
| /legacy/presets | ↗ | o catálogo curado de superfícies sobre o skin — sistema relief, legado |
| /legacy/tokens | ↗ | o fork jsoncss que alimenta o relief (pnpm build:relief) — legado, morre com ele |
Tokens e a referência de estilo: docs/STYLES.md.
Schema engine (WIP)
A camada declarativa (JSON → render: inspector / form / página nocode pela mesma engine) tem os
contratos definidos em src/foundation/schema/ — model.ts (dados puros,
sem React) e render.ts (o registry). O runtime e a integração com o state/cook estão pendentes, e por
isso o módulo ainda não tem subpath público: onde a engine vai morar (subpath daqui vs. pacote de
contratos próprio) é decisão em aberto.
Stack
Tailwind v4 (@theme, sem preflight) · Base UI (select, combobox, menu — o comportamento das peças
migradas) · Radix UI (collapsible, dialog, popover, tooltip, slider, switch, toggle-group — o que ainda
não migrou) · TanStack Table (motor do /data-table) · react-resizable-panels v3 · cmdk (paleta ⌘K) ·
Monaco (/json-editor) · Recharts (/chart) · react-colorful + @uiw/react-color (color). Tudo
external no bundle: nada disso é duplicado dentro do artefato publicado.
