# QueryBoundary

Os quatro finais de uma consulta (carregando, erro, vazio e dados) em volta de
qualquer conteúdo.

O `DataTable` e o `ChartContainer` já os têm embutidos, e o resto da tela não:
cada cartão de resumo, cada folha de detalhes e cada lista desenhada à mão
reescreve a mesma escada de `if`. Esta peça é essa escada, com os mesmos nomes
de prop das duas: `isLoading`, `isError`, `onRetry`, `errorTitle`,
`errorMessage` e `empty`.

```tsx
<QueryBoundary
  data={query.data}
  isLoading={query.isLoading}
  isError={query.isError}
  onRetry={query.refetch}
  empty={{
    title: 'Nenhuma nota por aqui',
    description: 'Quando você emitir a primeira, ela aparece nesta lista.',
  }}
>
  {(invoices) => <Invoices invoices={invoices} />}
</QueryBoundary>
```

Não conhece React Query, e isso é de propósito: entram três booleanos e uma
resposta, e funciona igual com `fetch` na mão, com SWR ou com server component.

**A ordem é a mesma das duas irmãs:** erro vence carregando, e vazio só vale
depois que a resposta chegou. Sem isso, uma nova busca sobre um erro pisca
"nenhum resultado" antes de mostrar o problema.

## O filho pode ser função

Como função, ela só é chamada depois que o dado chegou, e recebe o `data` sem o
`undefined`, que é exatamente o `!` que toda tela escrevia aqui. Como nó,
serve a quem não precisa do dado para desenhar:

```tsx
<QueryBoundary data={customer}>
  {(customer) => <DescriptionItem label="Cliente">{customer.name}</DescriptionItem>}
</QueryBoundary>
```

Com filho em função, `data` indefinido continua sendo espera mesmo com
`isLoading={false}`. Não há o que entregar à função, e é a regra do
`DataTable`. Com filho em nó, `isLoading` manda sozinho quando você o passa: o
nó não depende do dado para existir.

A função não atravessa a fronteira de um server component: ela não é
serializável. De um server component, passe o filho como nó.

**Os filhos saem como você os escreveu, sem embrulho.** A peça não põe uma
`<div>` em volta do que ela devolve: uma moldura invisível quebraria o `grid`
ou o `flex` de quem está por fora, e o defeito só apareceria no navegador.

## A espera

O padrão são três linhas de `Skeleton`, e elas seguram altura sem prometer
forma nenhuma. `skeletonRows` muda quantas, o mesmo nome do `DataTable`.

Quando a forma do que vem importa (e ela quase sempre importa), passe o seu
molde em `skeleton`:

```tsx
<QueryBoundary
  isLoading={query.isLoading}
  skeleton={
    <div className="flex flex-col gap-2">
      <Skeleton className="h-4 w-40" />
      <Skeleton className="h-4 w-24" />
    </div>
  }
>
```

Não é `Spinner` de propósito, e é a mesma escolha das duas irmãs: o giro no meio
do vazio não reserva altura, então a página pula quando o conteúdo chega. O
`Spinner` continua sendo o certo para a espera que não tem forma: o botão que
envia, a ação que não desenha nada.

O nó do carregando sai com `aria-busy="true"`. O `Skeleton` se esconde do leitor
de tela de propósito, e é no contêiner que o aviso de carregamento pertence.

**A espera se anuncia em voz alta.** `aria-busy` num nó sem papel não é lido por
leitor de tela nenhum: ele descreve o estado de uma região, e só chega a quem já
está dentro dela. Quem esperava ouvia silêncio, e a chegada do dado, que troca a
tela inteira, também não dizia nada. As quatro irmãs publicam a mesma região viva
(`role="status" aria-live="polite"`, marcada com `data-rc-status`), que diz
"Carregando…" enquanto a consulta não volta e "Conteúdo carregado" quando ela
volta. Ela existe antes de o texto mudar e é o mesmo nó do primeiro ao último
estado: região que nasce já com o texto dentro não dispara anúncio nenhum.

## O que ela nao trata: dado velho enquanto revalida

`isLoading` e `isError` descrevem duas situacoes, e o caso mais comum de tela
real e uma terceira: **ja ha dado na tela e uma nova busca esta correndo.** Se
voce passar `isFetching` em `isLoading`, o esqueleto cobre o que a pessoa
estava lendo; se nao passar nada, a atualizacao acontece sem sinal nenhum.

A peca nao resolve isso hoje, e a escolha e sua:

```tsx
<QueryBoundary isLoading={query.isLoading} isError={query.isError} data={query.data}>
  {(rows) => (
    <div aria-busy={query.isFetching}>
      <DataTable rows={rows} />
    </div>
  )}
</QueryBoundary>
```

`isLoading` do TanStack Query e verdadeiro so na primeira busca, que e o que a
peca espera. `isFetching` e verdadeiro em toda busca, inclusive a que revalida
- entao ele nao serve para `isLoading`, e serve para `aria-busy`.

## Quem decide o vazio

A peça decide sozinha, pelo `data`: **lista de tamanho zero e `null` são
vazio**, e `undefined` é "ainda não chegou". Sem `isLoading`, é ele quem liga o
carregando.

Para a resposta que não é uma lista (`{ items: [], total: 0 }` é a paginada de
sempre), quem responde é `isEmpty`, que vence a contagem quando vem:

```tsx
<QueryBoundary data={page} isEmpty={page.total === 0} empty={{ ... }}>
```

Se `empty` chegar sem que nada consiga decidir, a peça avisa no console em
desenvolvimento. O estado vazio que nunca aparece é silencioso: os filhos
desenham sobre o nada, e ninguém descobre até um cliente abrir a tela sem dados.

Sem `empty`, não há estado vazio. A resposta vazia cai nos filhos, e eles
desenham o vazio deles. A exceção é o `null` com filho em função: sem dado para
entregar e sem vazio configurado, a peça não desenha nada.

A descrição é obrigatória pelo mesmo motivo do `DataTable`: "nenhum resultado"
transfere para a pessoa o trabalho de descobrir por quê, e ela quase nunca
descobre.

## Os textos que a peça escreve

`errorTitle` (padrão "Não foi possível carregar") e `errorMessage` (padrão
"Tente de novo em alguns minutos.") são o par do estado de erro, com os mesmos
nomes e o mesmo papel que têm no `DataTable` e no `ChartContainer`: uma tela que
carrega três blocos precisa dizer qual deles falhou, e um produto que não fala
português precisa dizer isso em outra língua.

`retryLabel` (padrão "Tentar de novo") é o nome do botão que executa o
`onRetry`, e existe pelo mesmo motivo: sem ele, a tela traduzida saía com o
título em inglês e o botão em português, que é pior do que tudo em português. O
nome e o padrão são os mesmos nas quatro peças de consulta.

Sem `onRetry` não há botão de nova tentativa. Aviso com botão que não leva a
lugar nenhum é pior que aviso sem botão.

## Movimento

O `Alert` do erro e o `EmptyState` do vazio entram pelo movimento deles. O conteúdo que você entrega não ganha entrada daqui: a moldura não embrulha os seus filhos numa caixa, porque uma caixa a mais muda o layout de quem usa (o filho que era `flex-1`, o item de grade). Quem tem caixa própria entra sozinho: o `DataTable` esmaece o corpo, e o gráfico se desenha.

## Partes

`classNames` veste cada final: `loading`, `error`, `empty`. O `className` veste
os três de uma vez, que é onde mora a moldura que reserva a altura
(`className="min-h-40"`). E não veste os filhos, que são seus.

## Quando não usar

**Não embrulhe `DataTable` nem `ChartContainer`.** As duas já recebem os quatro
finais, com estas mesmas props, e desenham a espera no formato do que elas
mesmas mostram: linhas falsas de tabela, barras falsas de gráfico. Por fora,
uma moldura só teria um esqueleto genérico para oferecer, e os dois estados de
erro empilhados apareceriam juntos no dia em que a consulta falhasse.

Para o estado vazio sozinho (uma tela que nunca carrega nada, um resultado que
já está na mão), use `EmptyState`. Para o aviso de erro que não é o fim de uma
consulta, `Alert`. Para a marca de lugar solta dentro de um bloco que já tem os
outros finais tratados, `Skeleton`. Esta peça existe para os quatro juntos, e na
ordem; um só deles não paga o embrulho.

Ela também não captura exceção de renderização: `QueryBoundary` mostra o erro
que a consulta reportou em `isError`, e não o que estourou dentro dos filhos.
Para esse, o que existe é o error boundary do React.

## No React Native

Traduz com os mesmos nomes de prop e a mesma ordem: **erro vence carregando**, e vazio só vale depois que a resposta chegou. O `children` também aceita função aqui, que é o que justifica a peça existir: ela entrega o dado já sem `undefined`, e mata o `!` que a tela escrevia.

Cinco diferenças de tipo, todas porque texto no nativo mora dentro de um `Text`: `errorTitle`, `errorMessage`, `retryLabel`, `empty.title` e `empty.description` são `string`. O `empty.icon` atravessa, e aceita também a função do `EmptyState` nativo, que entrega a cor e o tamanho. É a mesma nota que o `ChartContainer` já carrega.

**`classNames` não porta, e a razão não é preguiça:** a prop existe no web para que ninguém alcance o nó interno por `[&_div]` e acople a tela à árvore da peça. No React Native não há seletor de descendente, então essa escotilha não existe e a prop não teria o que evitar. O `className` veste os três finais, como no web.

O esqueleto genérico fica na peça, e não vem de quem chama: sem ele, `isLoading` sem `skeleton` colapsaria a tela para altura zero e ela pularia quando o dado chegasse. No celular isso dói mais, porque não há barra de rolagem nem indicador de rede para explicar a espera.

## Importação

```tsx
import { QueryBoundary } from '@rivocode/ui'
```

## Exemplos

### Com dados

```tsx
import { useState } from 'react'
import { FileText } from 'lucide-react'
import {
  Badge,
  Button,
  Card,
  CardContent,
  CardHeader,
  CardTitle,
  DescriptionItem,
  DescriptionList,
  QueryBoundary,
  Skeleton,
} from '@rivocode/ui'

type Invoice = {
  id: string
  number: string
  customer: string
  amount: string
  status: 'Paga' | 'Aberta'
}

const INVOICES: Invoice[] = [
  { id: '1', number: '4813', customer: 'Clínica São Lucas', amount: 'R$ 2.480,00', status: 'Paga' },
  { id: '2', number: '4814', customer: 'Ótica Central', amount: 'R$ 940,00', status: 'Aberta' },
]

function Invoices({ invoices }: { invoices: Invoice[] }) {
  return (
    <ul className="flex flex-col gap-2">
      {invoices.map((invoice) => (
        <li key={invoice.id} className="flex items-center justify-between gap-3">
          <span className="text-base text-fg">{invoice.customer}</span>
          <Badge tone={invoice.status === 'Paga' ? 'success' : 'neutral'}>{invoice.status}</Badge>
        </li>
      ))}
    </ul>
  )
}

export function WithData() {
  return (
    <Card className="max-w-lg">
      <CardHeader>
        <CardTitle>Últimas notas</CardTitle>
      </CardHeader>
      <CardContent>
        <QueryBoundary data={INVOICES} className="min-h-40">
          {(invoices) => <Invoices invoices={invoices} />}
        </QueryBoundary>
      </CardContent>
    </Card>
  )
}
```

### Carregando

```tsx
import { useState } from 'react'
import { FileText } from 'lucide-react'
import {
  Badge,
  Button,
  Card,
  CardContent,
  CardHeader,
  CardTitle,
  DescriptionItem,
  DescriptionList,
  QueryBoundary,
  Skeleton,
} from '@rivocode/ui'

type Invoice = {
  id: string
  number: string
  customer: string
  amount: string
  status: 'Paga' | 'Aberta'
}

function Invoices({ invoices }: { invoices: Invoice[] }) {
  return (
    <ul className="flex flex-col gap-2">
      {invoices.map((invoice) => (
        <li key={invoice.id} className="flex items-center justify-between gap-3">
          <span className="text-base text-fg">{invoice.customer}</span>
          <Badge tone={invoice.status === 'Paga' ? 'success' : 'neutral'}>{invoice.status}</Badge>
        </li>
      ))}
    </ul>
  )
}

export function Loading() {
  return (
    <Card className="max-w-lg">
      <CardHeader>
        <CardTitle>Últimas notas</CardTitle>
      </CardHeader>
      <CardContent>
        <QueryBoundary<Invoice[]> isLoading className="min-h-40">
          {(invoices) => <Invoices invoices={invoices} />}
        </QueryBoundary>
      </CardContent>
    </Card>
  )
}
```

### Carregando com o molde da tela

```tsx
import { useState } from 'react'
import { FileText } from 'lucide-react'
import {
  Badge,
  Button,
  Card,
  CardContent,
  CardHeader,
  CardTitle,
  DescriptionItem,
  DescriptionList,
  QueryBoundary,
  Skeleton,
} from '@rivocode/ui'

type Invoice = {
  id: string
  number: string
  customer: string
  amount: string
  status: 'Paga' | 'Aberta'
}

function Invoices({ invoices }: { invoices: Invoice[] }) {
  return (
    <ul className="flex flex-col gap-2">
      {invoices.map((invoice) => (
        <li key={invoice.id} className="flex items-center justify-between gap-3">
          <span className="text-base text-fg">{invoice.customer}</span>
          <Badge tone={invoice.status === 'Paga' ? 'success' : 'neutral'}>{invoice.status}</Badge>
        </li>
      ))}
    </ul>
  )
}

export function LoadingWithSkeleton() {
  return (
    <Card className="max-w-lg">
      <CardHeader>
        <CardTitle>Últimas notas</CardTitle>
      </CardHeader>
      <CardContent>
        <QueryBoundary<Invoice[]>
          isLoading
          className="min-h-40"
          skeleton={
            <div className="flex flex-col gap-2">
              {[0, 1, 2].map((line) => (
                <div key={line} className="flex items-center justify-between gap-3">
                  <Skeleton className="h-4 w-40" />
                  <Skeleton className="h-5 w-16 rounded-pill" />
                </div>
              ))}
            </div>
          }
        >
          {(invoices) => <Invoices invoices={invoices} />}
        </QueryBoundary>
      </CardContent>
    </Card>
  )
}
```

### Erro

```tsx
import { useState } from 'react'
import { FileText } from 'lucide-react'
import {
  Badge,
  Button,
  Card,
  CardContent,
  CardHeader,
  CardTitle,
  DescriptionItem,
  DescriptionList,
  QueryBoundary,
  Skeleton,
} from '@rivocode/ui'

type Invoice = {
  id: string
  number: string
  customer: string
  amount: string
  status: 'Paga' | 'Aberta'
}

function Invoices({ invoices }: { invoices: Invoice[] }) {
  return (
    <ul className="flex flex-col gap-2">
      {invoices.map((invoice) => (
        <li key={invoice.id} className="flex items-center justify-between gap-3">
          <span className="text-base text-fg">{invoice.customer}</span>
          <Badge tone={invoice.status === 'Paga' ? 'success' : 'neutral'}>{invoice.status}</Badge>
        </li>
      ))}
    </ul>
  )
}

export function Failed() {
  return (
    <div className="max-w-lg">
      <QueryBoundary<Invoice[]>
        isError
        onRetry={() => {}}
        errorTitle="Não foi possível carregar as notas"
        errorMessage="A prefeitura não respondeu. Tente de novo em alguns minutos."
      >
        {(invoices) => <Invoices invoices={invoices} />}
      </QueryBoundary>
    </div>
  )
}
```

### Vazio

```tsx
import { useState } from 'react'
import { FileText } from 'lucide-react'
import {
  Badge,
  Button,
  Card,
  CardContent,
  CardHeader,
  CardTitle,
  DescriptionItem,
  DescriptionList,
  QueryBoundary,
  Skeleton,
} from '@rivocode/ui'

type Invoice = {
  id: string
  number: string
  customer: string
  amount: string
  status: 'Paga' | 'Aberta'
}

function Invoices({ invoices }: { invoices: Invoice[] }) {
  return (
    <ul className="flex flex-col gap-2">
      {invoices.map((invoice) => (
        <li key={invoice.id} className="flex items-center justify-between gap-3">
          <span className="text-base text-fg">{invoice.customer}</span>
          <Badge tone={invoice.status === 'Paga' ? 'success' : 'neutral'}>{invoice.status}</Badge>
        </li>
      ))}
    </ul>
  )
}

type Page = { items: Invoice[]; total: number }

const PAGE: Page = { items: [], total: 0 }

export function Empty() {
  return (
    <Card className="max-w-lg">
      <CardContent>
        <QueryBoundary
          data={[] as Invoice[]}
          empty={{
            icon: <FileText aria-hidden="true" />,
            title: 'Nenhuma nota por aqui',
            description: 'Quando você emitir a primeira, ela aparece nesta lista.',
            action: <Button size="sm">Emitir nota</Button>,
          }}
        >
          {(invoices) => <Invoices invoices={invoices} />}
        </QueryBoundary>
      </CardContent>
    </Card>
  )
}

type Page = { items: Invoice[]; total: number }

const PAGE: Page = { items: [], total: 0 }
```

### Resposta que não é lista

```tsx
import { useState } from 'react'
import { FileText } from 'lucide-react'
import {
  Badge,
  Button,
  Card,
  CardContent,
  CardHeader,
  CardTitle,
  DescriptionItem,
  DescriptionList,
  QueryBoundary,
  Skeleton,
} from '@rivocode/ui'

type Invoice = {
  id: string
  number: string
  customer: string
  amount: string
  status: 'Paga' | 'Aberta'
}

function Invoices({ invoices }: { invoices: Invoice[] }) {
  return (
    <ul className="flex flex-col gap-2">
      {invoices.map((invoice) => (
        <li key={invoice.id} className="flex items-center justify-between gap-3">
          <span className="text-base text-fg">{invoice.customer}</span>
          <Badge tone={invoice.status === 'Paga' ? 'success' : 'neutral'}>{invoice.status}</Badge>
        </li>
      ))}
    </ul>
  )
}

type Page = { items: Invoice[]; total: number }

const PAGE: Page = { items: [], total: 0 }

type Stage = 'carregando' | 'erro' | 'vazio' | 'dados'

const STAGES: { id: Stage; label: string }[] = [
  { id: 'carregando', label: 'Carregando' },
  { id: 'erro', label: 'Erro' },
  { id: 'vazio', label: 'Vazio' },
  { id: 'dados', label: 'Dados' },
]

export function NotAList() {
  return (
    <Card className="max-w-lg">
      <CardContent>
        <QueryBoundary
          data={PAGE}
          isEmpty={PAGE.total === 0}
          empty={{
            title: 'Nenhuma nota no período',
            description: 'Amplie o intervalo de datas para ver notas mais antigas.',
          }}
        >
          {(page) => <Invoices invoices={page.items} />}
        </QueryBoundary>
      </CardContent>
    </Card>
  )
}

type Stage = 'carregando' | 'erro' | 'vazio' | 'dados'

const STAGES: { id: Stage; label: string }[] = [
  { id: 'carregando', label: 'Carregando' },
  { id: 'erro', label: 'Erro' },
  { id: 'vazio', label: 'Vazio' },
  { id: 'dados', label: 'Dados' },
]
```

### Os quatro finais

```tsx
import { useState } from 'react'
import { FileText } from 'lucide-react'
import {
  Badge,
  Button,
  Card,
  CardContent,
  CardHeader,
  CardTitle,
  DescriptionItem,
  DescriptionList,
  QueryBoundary,
  Skeleton,
} from '@rivocode/ui'

type Invoice = {
  id: string
  number: string
  customer: string
  amount: string
  status: 'Paga' | 'Aberta'
}

const INVOICES: Invoice[] = [
  { id: '1', number: '4813', customer: 'Clínica São Lucas', amount: 'R$ 2.480,00', status: 'Paga' },
  { id: '2', number: '4814', customer: 'Ótica Central', amount: 'R$ 940,00', status: 'Aberta' },
]

function Invoices({ invoices }: { invoices: Invoice[] }) {
  return (
    <ul className="flex flex-col gap-2">
      {invoices.map((invoice) => (
        <li key={invoice.id} className="flex items-center justify-between gap-3">
          <span className="text-base text-fg">{invoice.customer}</span>
          <Badge tone={invoice.status === 'Paga' ? 'success' : 'neutral'}>{invoice.status}</Badge>
        </li>
      ))}
    </ul>
  )
}

type Stage = 'carregando' | 'erro' | 'vazio' | 'dados'

const STAGES: { id: Stage; label: string }[] = [
  { id: 'carregando', label: 'Carregando' },
  { id: 'erro', label: 'Erro' },
  { id: 'vazio', label: 'Vazio' },
  { id: 'dados', label: 'Dados' },
]

type Customer = { name: string; document: string; city: string }

const CUSTOMER: Customer = {
  name: 'Clínica São Lucas',
  document: '12.345.678/0001-90',
  city: 'João Pessoa, PB',
}

export function FourEndings() {
  const [stage, setStage] = useState<Stage>('carregando')

  return (
    <div className="flex w-full max-w-lg flex-col gap-3">
      <div className="flex flex-wrap gap-2">
        {STAGES.map((option) => (
          <Button
            key={option.id}
            size="sm"
            variant={option.id === stage ? 'primary' : 'secondary'}
            onClick={() => setStage(option.id)}
          >
            {option.label}
          </Button>
        ))}
      </div>

      <Card>
        <CardContent>
          <QueryBoundary
            data={stage === 'dados' ? INVOICES : stage === 'vazio' ? [] : undefined}
            isLoading={stage === 'carregando'}
            isError={stage === 'erro'}
            onRetry={() => setStage('dados')}
            errorMessage="A prefeitura não respondeu. Tente de novo em alguns minutos."
            className="min-h-40"
            empty={{
              icon: <FileText aria-hidden="true" />,
              title: 'Nenhuma nota por aqui',
              description: 'Quando você emitir a primeira, ela aparece nesta lista.',
              action: <Button size="sm">Emitir nota</Button>,
            }}
          >
            {(invoices) => <Invoices invoices={invoices} />}
          </QueryBoundary>
        </CardContent>
      </Card>
    </div>
  )
}

type Customer = { name: string; document: string; city: string }

const CUSTOMER: Customer = {
  name: 'Clínica São Lucas',
  document: '12.345.678/0001-90',
  city: 'João Pessoa, PB',
}
```

### Uma folha, e não uma lista

```tsx
import { useState } from 'react'
import { FileText } from 'lucide-react'
import {
  Badge,
  Button,
  Card,
  CardContent,
  CardHeader,
  CardTitle,
  DescriptionItem,
  DescriptionList,
  QueryBoundary,
  Skeleton,
} from '@rivocode/ui'

type Customer = { name: string; document: string; city: string }

const CUSTOMER: Customer = {
  name: 'Clínica São Lucas',
  document: '12.345.678/0001-90',
  city: 'João Pessoa, PB',
}

export function SingleRecord() {
  return (
    <Card className="max-w-lg">
      <CardContent>
        <QueryBoundary<Customer>
          data={CUSTOMER}
          errorTitle="Não foi possível carregar o cliente"
          skeletonRows={4}
        >
          {(customer) => (
            <DescriptionList>
              <DescriptionItem label="Cliente">{customer.name}</DescriptionItem>
              <DescriptionItem label="CNPJ">{customer.document}</DescriptionItem>
              <DescriptionItem label="Cidade">{customer.city}</DescriptionItem>
            </DescriptionList>
          )}
        </QueryBoundary>
      </CardContent>
    </Card>
  )
}
```

## Props

| Prop | Tipo | Obrigatória | Desde | O que faz |
| --- | --- | --- | --- | --- |
| `classNames` | `Partial<Record<"empty" \| "error" \| "loading", string>>` |  | - | Classe por parte: `loading`, `error`, `empty`. |
| `data` | `Data` |  | - | A resposta da consulta. |
| `empty` | `{ title: ReactNode; description: ReactNode; action?: ReactNode; icon?: ReactNode; }` |  | - | O que aparece quando a consulta volta vazia. |
| `errorMessage` | `ReactNode` |  | - |  |
| `errorTitle` | `ReactNode` |  | - | O titulo do aviso de erro. |
| `isEmpty` | `boolean` |  | - | Diz o vazio no lugar do `data`, para a resposta que nao e uma lista: `isEmpty={page.total === 0}`. |
| `isError` | `boolean` |  | - |  |
| `isLoading` | `boolean` |  | - |  |
| `onRetry` | `(() => void)` |  | - | Sem isto, o erro nao oferece nova tentativa. |
| `retryLabel` | `ReactNode` |  | - | O nome do botao que executa o `onRetry`. |
| `skeleton` | `ReactNode` |  | - | O desenho da espera, no formato do que vem depois - a lista de tres linhas, o cartao, a folha de campos. |
| `skeletonRows` | `number` |  | - | Quantas linhas falsas a espera generica mostra. |

Além dessas: repassa `className`, `style`, `id` e os demais atributos do elemento raiz.

## Ver também

- [Alert](/componentes/alert.md)
- [Badge](/componentes/badge.md)
- [EmptyState](/componentes/empty-state.md)
- [Indicator](/componentes/indicator.md)
- [Kbd](/componentes/kbd.md)
- [Meter](/componentes/meter.md)
- [Convenções da biblioteca](/convencoes.md): Provider, tokens e as regras que valem para toda peça
- [Índice completo](/llms.txt)
