Diretrizes de código
Como construir com o ranui: qual é o contrato dos componentes, onde a fronteira do Shadow DOM muda as regras a que você está acostumado e quais erros vale conhecer antes de cometê-los.
A metade visual disso são as diretrizes de design; os tokens são o design system.
Use quando estiver ligando componentes do ranui a uma aplicação: escolhendo importações, amarrando eventos, estilizando algo que um seletor não alcança, renderizando num servidor ou escrevendo testes.
Princípios
- O elemento é a API. Atributos, propriedades, eventos, slots e
::part()são todo o contrato. Qualquer outra coisa que se veja de fora é detalhe de implementação e vai mudar de lugar. - O estado tem um dono só. Ou a sua aplicação é dona do valor e o empurra para dentro, ou o componente é dono e avisa quando muda. Espelhar nos dois sentidos é como os valores se desencontram.
- Estilize através da fronteira do Shadow DOM com propriedades personalizadas,
::part(),sheete slots. Seletores comuns não a atravessam; nenhuma dose de especificidade muda isso. - Importe o que você usa. Cada componente tem a própria entrada; o barril é conveniência, não exigência.
- Prefira a plataforma. São elementos personalizados:
addEventListener,setAttributeehiddenfuncionam como especificado, e as abstrações de framework por cima são opcionais.
Pontos de entrada
Cada entrada registra exatamente o que o nome dela diz, nada mais, então uma página que só quer tematização nunca paga pela biblioteca de componentes.
| Importação | Contém |
|---|---|
ranui | Todos os componentes (registra todos os <r-*> como efeito colateral) |
ranui/<component> | Um componente: ranui/button, ranui/select, ranui/modal, … |
ranui/theme | initTheme / setTheme / getTheme e sobrescrita de tokens; sem elementos |
ranui/i18n | O motor de tradução; sem elementos |
ranui/fonts | Geist Sans + Geist Mono hospedadas por você (só o CSS de @font-face) |
ranui/style | A folha de estilos, se a sua configuração não a pegar sozinha |
ranui/builder | O construtor de DOM encadeável com que os componentes são escritos |
ranui/ssr, ranui/ssr-stream | Renderização no servidor |
ranui/testing | Auxiliares para alcançar um shadow root fechado a partir de um teste |
ranui/typings | Tipos ambientais (declarações de elementos para JSX / TS) |
import 'ranui/button'; // um elemento
import 'ranui'; // todos elesImporte pelo efeito colateral. import 'ranui/button' registra o <r-button>; raramente você precisa da classe exportada. A exceção é a renderização no servidor, onde você mesmo a instancia.
O contrato dos componentes
Os atributos, propriedades, eventos (com o formato do detail), slots e parts exatos de cada elemento são gerados do código-fonte para o COMPONENTS.md. As regras abaixo são o que aquela tabela não diz.
Atributos são strings; propriedades têm tipo
Atributos HTML são minúsculos e do tipo string; a propriedade correspondente é camelCase e recebe um valor de verdade. São o mesmo estado, alcançado de duas maneiras:
<r-select showsearch dropdownclass="wide"></r-select>select.showSearch = true; // propriedade — camelCase
select.setAttribute('showsearch', ''); // atributo — minúsculo- Atributos booleanos valem pela presença, como o
disabledde um<button>nativo:disabled=""edisabled="false"estão os dois desabilitados. Remova o atributo (ou coloque a propriedade emfalse) para desligar. - Valores ricos passam por propriedades. Arrays, objetos e
Filenão sobrevivem a um atributo: oattachmentsdor-attachments, por exemplo, é uma propriedade. - Nomes de atributo na marcação não distinguem maiúsculas, e é por isso que o HTML acima diz
showsearchenquanto a propriedade éshowSearch. Em JSX, escreva a forma de atributo.
Escute no próprio elemento
Os componentes do ranui despacham CustomEvent, e a carga vem sempre em detail:
select.addEventListener('change', (event) => {
const { value, label } = event.detail;
});Se um evento borbulha ou não é decisão de cada componente, então amarre ao elemento, não a um contêiner. O núcleo de formulários e camadas (r-input, r-checkbox, r-select, r-modal) despacha eventos que não borbulham sobre si mesmo, de propósito: um change de um select dentro do seu formulário não deveria parecer um change do formulário. Outros borbulham (e são composed, então cruzam fronteiras de shadow): r-theme-switch, r-voice-button, r-attachments, r-conversation, r-tool-card, r-markdown, r-math, r-mermaid, r-router, r-route, r-link, r-colorpicker.
Um ouvinte no elemento funciona nos dois casos; a delegação num ancestral funciona só com o segundo grupo e falha em silêncio com o primeiro. Confira o código ou o COMPONENTS.md antes de confiar na delegação.
Eventos before* são canceláveis. O r-modal despacha beforeopen / beforeclose antes de agir; event.preventDefault() veta a transição. Os pares open / close / afteropen / afterclose relatam o que já aconteceu e não podem ser cancelados.
modal.addEventListener('beforeclose', (event) => {
if (hasUnsavedChanges) event.preventDefault();
});Slots e parts
O conteúdo entra pelos slots (padrão e nomeados) e fica no seu documento, então o CSS da sua página o estiliza normalmente. Só fica fora de alcance o que o componente monta por dentro, e é para isso que existe o ::part().
Estilos através da fronteira do shadow
Todo componente do ranui é desenhado num shadow root fechado. O CSS da página não vaza para dentro, e os seletores não alcançam através dela. Há exatamente quatro caminhos de entrada, em ordem de preferência:
| Mecanismo | Serve para | Exemplo |
|---|---|---|
| Propriedades personalizadas | Tudo que o componente expõe como token | r-button { --ran-btn-background: #7c3aed; } |
::part() | Um ajuste estrutural que os tokens não cobrem | r-card::part(footer) { justify-content: flex-end; } |
O atributo sheet | CSS programático ou dinâmico injetado dentro | el.sheet = '.ran-btn { letter-spacing: .02em }' |
| Conteúdo de slot | Marcação que já é sua de qualquer jeito | <span slot="extra">…</span> |
As propriedades personalizadas são a via preferida porque são herdadas através da fronteira: definir um token no :root, num invólucro ou no elemento funciona igual, e são os mesmos tokens que o tema usa. Parts e sheet prendem você à estrutura interna, então guarde-os para lacunas reais e conte com revisitá-los a cada atualização.
O que não funciona, em nenhuma especificidade: r-select .some-inner-class { … }, !important ou um querySelector para dentro do componente. Um root fechado significa que element.shadowRoot é null tanto para o seu CSS quanto para os seus scripts e para os localizadores do seu executor de testes.
Ser dono do estado
Decida, valor a valor, quem é o dono:
- O componente é dono (não controlado): defina um valor inicial e depois leia o valor do
detaildo evento quando ele mudar. O mais simples, e o padrão para formulários. - A sua aplicação é dona (controlado): defina a propriedade a cada render e trate o evento como um pedido de mudar o seu estado, não como uma mudança que já aconteceu no seu modelo.
O que quebra é fazer as duas coisas: guardar uma cópia do valor do componente no seu estado, escrevê-la de volta a cada evento e redefinir a propriedade a partir desse estado. Os dois se desencontram sob digitação rápida, e escrever durante um evento pode entrar em laço. Escolha uma direção.
// Controlado: o estado é a fonte da verdade, o evento é um pedido
input.value = state.query;
input.addEventListener('input', (event) => {
state.query = event.detail.value;
render(); // que define input.value de novo — mas a partir de um dono só
});Integração com frameworks
São elementos personalizados padrão, então nada específico de framework é exigido, mas três detalhes mordem:
- React (antes da 19) define toda prop de JSX como atributo, então valores ricos não chegam e props no estilo
onChangenão se ligam a eventos personalizados. Use umrefe defina propriedades ou chameaddEventListenerdentro de um efeito. O React 19 define propriedades quando elas existem e ainda assim não liga eventos personalizados pelo nome, então mantenha orefpara os ouvintes. - Vue compila tags desconhecidas como componentes, a menos que seja avisado; acrescente
r-acompilerOptions.isCustomElementna configuração de build. Depois disso,:propliga uma propriedade e@changeliga um ouvinte de evento de verdade, ambos corretamente. - Angular precisa de
CUSTOM_ELEMENTS_SCHEMA; Svelte e Solid passam atributos e ouvinteson:/ondireto e não precisam de nada.
Quem usa TypeScript pode fazer import 'ranui/typings' para as declarações de elementos intrínsecos do JSX.
Renderização no servidor
Os componentes do ranui se serializam em shadow DOM declarativo, então um servidor pode emitir a marcação real e a primeira pintura fica correta antes de qualquer JavaScript rodar:
import 'ranui'; // preenche o registro de SSR
import { renderHTMLToString } from 'ranui/ssr-stream';
const html = await renderHTMLToString(`
<r-button type="primary">Submit</r-button>
<r-progress percent="65"></r-progress>
`);renderToStream(html) é a mesma coisa como gerador assíncrono, para respostas em streaming; renderToString(instance) em ranui/ssr serializa uma instância de componente que você mesmo construiu. Tags desconhecidas passam intactas, então é seguro rodar sobre uma página inteira.
Duas coisas a saber:
- O cliente reconstrói, não reaproveita. Como os roots são fechados, o navegador não consegue reaproveitar a árvore renderizada no servidor para o componente, então, ao atualizar, cada elemento constrói uma idêntica do zero. Você ganha a primeira pintura vinda do servidor; não ganha reaproveitamento na hidratação, e não deve pôr estado na marcação shadow do servidor esperando que o cliente a leia.
- Nada medido está disponível no servidor. Tudo que depende de
getBoundingClientRectou deoffsetWidthse resolve depois da montagem, no navegador.
Desempenho
- Importe por componente em páginas que usam um punhado; o barril é para aplicações que usam quase toda a biblioteca.
- As variantes carregam sob demanda. O
r-icone or-loadingbuscam uma variante pelo nome em tempo de execução, então o custo base não cresce com a quantidade de ícones que você não usa. - Defina propriedades, não reconstrua elementos. Substituir um elemento personalizado roda o construtor dele de novo; definir uma propriedade atualiza no lugar.
- Agrupe as escritas de atributos. Cada escrita pode disparar o
attributeChangedCallback; monte o estado antes de inserir sempre que der.
Testes
Shadow roots fechados também barram os localizadores dos testes. O getByRole, o getByText e o querySelector do Playwright param todos na fronteira e não acham nada, então uma especificação escrita com eles passa enquanto afirma coisas sobre elementos que nunca viu. Duas suítes deste repositório foram escritas assim antes de alguém perceber. O ranui/testing é a costura, com nome e documentada:
import { insideShadow, settlePainted } from 'ranui/testing';
const label = await insideShadow(page, 'r-button', (root) => root.querySelector('[part=content]')?.textContent);Fora isso, teste o contrato, não as entranhas: defina um atributo ou uma propriedade e afirme sobre o evento e sobre o que a pessoa consegue perceber. Afirmações contra nomes de classe internos quebram a cada refatoração e não dizem nada sobre o componente funcionar ou não.
Antipadrões
| Antipadrão | Por que falha |
|---|---|
Delegar change num contêiner para r-input / r-select | Esses eventos não borbulham; o ouvinte nunca dispara. Amarre ao elemento. |
document.querySelector('r-select').shadowRoot | Root fechado: sempre null. Use a API pública, as parts ou o ranui/testing. |
Estilizar as entranhas com r-card .inner { … } | Seletores não cruzam a fronteira em nenhuma especificidade. Use tokens ou ::part(). |
!important para vencer um componente | Não há conflito de cascata a vencer: a regra nunca se aplica. Mesma correção de cima. |
| Espelhar o valor de um componente no seu estado e de volta | Dois donos, um valor; eles se desencontram e podem entrar em laço. |
| Recriar elementos para atualizá-los | Roda o construtor de novo, perde o foco e o estado interno. Defina propriedades. |
| Escrever uma cor à mão ao lado de um componente tematizado | Quebra no instante em que o tema vira. Use tokens semânticos. |
z-index geral num invólucro "caso" uma camada abra | Eleva conteúdo estático acima da sua própria moldura para sempre. Restrinja com :has(). |
Esperar pelo shadowRoot num teste | Como acima: afirme pelo ranui/testing ou sobre comportamento observável. |
Contribuir com o ranui
O repositório carrega padrões próprios, mais rígidos, para o código da biblioteca:
docs/DESIGN.md: o padrão de design executável; nove regras são cobradas pelopnpm -F ranui verify:design.docs/CODING.md: a arquitetura dos componentes, a posse do estado e as regras de teste para o código da biblioteca.docs/BUILDER.md: o construtor de DOM encadeável e as primitivas reativas dele.CLAUDE.mdna raiz do pacote: o arquivo de orientação, que viaja no tarball do npm e que tanto pessoas quanto agentes de código leem primeiro.
Antes de abrir um pull request: pnpm -F ranui test:all, pnpm -F ranui verify:design e pnpm verify:docs (as tabelas de API e de tokens são geradas; a CI falha se estiverem desatualizadas).