@aurea-uds/react 0.18.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/actions.d.ts CHANGED
@@ -39,9 +39,8 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>, Re
39
39
  rel?: string;
40
40
  download?: boolean | string;
41
41
  /**
42
- * @deprecated Use `<Toggle>` instead. Pesquisado em 18/08/2026 nas doze referências (MUI,
43
- * Fluent 2, React Aria, Spectrum, Carbon, Radix/Base UI, shadcn, ReUI, PrimeReact, HeroUI,
44
- * Cedar, APG): **nenhuma** põe o estado de pressionado no botão comum — todas têm um
42
+ * @deprecated Use `<Toggle>` instead. Pesquisado em 18/08/2026 nas doze referências (entre
43
+ * elas o motor Base UI e o APG): **nenhuma** põe o estado de pressionado no botão comum — todas têm um
45
44
  * componente separado, e o nosso é o `Toggle`. Manter os dois é dois caminhos para a mesma
46
45
  * coisa, que é o "qual eu uso?" que denuncia recurso duplicado.
47
46
  * A diferença de verdade: aqui VOCÊ guarda o estado e isto só pinta e anuncia; o `Toggle`
package/dist/actions.js CHANGED
@@ -58,8 +58,8 @@ export const Button = forwardRef(function Button({ variant, appearance, tone, si
58
58
  // 🔴 OCUPADO TROCA, NÃO ACRESCENTA (01/10/2026, achado pelo Victor no aureauds.dev): a rodinha
59
59
  // entrava ANTES do ícone, e no `IconButton` — 36 de largura fixa — rodinha e glifo não cabiam:
60
60
  // medido no catálogo, a rodinha começava 4px FORA do botão, à esquerda, com o disquete ainda ao
61
- // lado. O HeroUI troca (exemplo "Loading State" da doc do Button 3.2.6:
62
- // `isPending ? <Spinner/> : <Paperclip/>`). Aqui também: ocupado, a rodinha toma o lugar do ícone
61
+ // lado. A referência troca (no exemplo de estado ocupado da doc do botão dela, a rodinha
62
+ // entra no lugar do ícone). Aqui também: ocupado, a rodinha toma o lugar do ícone
63
63
  // da frente; e sem conteúdo (o `IconButton` ocupado) o invólucro vazio some, senão o `gap` dele
64
64
  // empurraria a rodinha para fora do centro. Fora do estado ocupado o HTML é o mesmo de antes.
65
65
  const temConteudo = children != null && children !== false;
@@ -67,7 +67,7 @@ export const Button = forwardRef(function Button({ variant, appearance, tone, si
67
67
  // INERTE ≠ DESABILITADO, e a diferença é medida (M4, 13/08/2026): `disabled` tira o botão da
68
68
  // ordem de foco, então quem navega por teclado nunca alcança a explicação de POR QUE não dá — e
69
69
  // "não dá porque você não tem permissão" é justamente o caso em que a explicação é tudo. O
70
- // embrulho de <span> que o MUI documenta resolve o ponteiro e não resolve o teclado (span nasce
70
+ // embrulho de <span> que uma das referências documenta resolve o ponteiro e não resolve o teclado (span nasce
71
71
  // com tabIndex -1, medido). Com `aria-disabled` o botão continua focável e anunciado como
72
72
  // desabilitado, e é ESTE componente que tem de barrar a ativação — o atributo é só semântica.
73
73
  // Mesmo remendo do AUD-0004, que já barrava o link desabilitado; aqui ele alcança o <button>.
@@ -118,7 +118,7 @@ function temConteudoVisivel(children) {
118
118
  // ESTE é o toggle da Aurea. O `pressed` do `Button` está DEPRECIADO em favor dele (0.4.0,
119
119
  // sai na 1.0): as doze referências pesquisadas em 18/08/2026 têm componente separado, e
120
120
  // nenhuma põe o estado no botão comum.
121
- // E a REGRA QUE FALTAVA ESTAR ESCRITA AQUI, do Adobe Spectrum e do APG: **o rótulo não muda
121
+ // E a REGRA QUE FALTAVA ESTAR ESCRITA AQUI, de uma das referências e do APG: **o rótulo não muda
122
122
  // entre os estados**. Se o texto vira "Mute"/"Unmute" ou "Play"/"Pause", não é toggle — é
123
123
  // botão de ação, porque quem lê tela ouve o rótulo NOVO e o estado ao mesmo tempo e não sabe
124
124
  // se o botão descreve o que é ou o que fará.
@@ -145,7 +145,7 @@ export const ThemeToggle = forwardRef(function ThemeToggle({ className, ...props
145
145
  const s = useAureaStrings();
146
146
  const escuro = theme === "dark";
147
147
  // O corpo do `IconButton`, escrito aqui só para o glifo sair na forma CHEIA (ADR-0053: a lua e o
148
- // sol cheios, como os do Carbon aprovados pela imagem em 25/09/2026). O `IconButton` público não
148
+ // sol cheios, como os do conjunto de ícones anterior, aprovados pela imagem em 25/09/2026). O `IconButton` público não
149
149
  // tem peso de ícone.
150
150
  return _jsx(Button, { ref: ref, variant: "ghost", className: cx("btn-icon", escuro ? "theme-toggle-sun" : "theme-toggle-moon", className), "aria-label": escuro ? s.themeToLight : s.themeToDark, onClick: toggleTheme, ...props, children: _jsx(Icon, { name: escuro ? "sun" : "moon", weight: "fill" }) });
151
151
  });
package/dist/agents.js CHANGED
@@ -9,13 +9,13 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
9
9
  // `DIRECTION.md` se declara "ilustrativo, não-vinculante", então tirar dali uma
10
10
  // categoria é decisão, não dedução.
11
11
  //
12
- // PROCEDÊNCIA (BUILDING.md passo 2): estes três saem de `agents-kit-main`
13
- // (`components/agents-ui/agent-card.tsx`, `agent-status-panel.tsx`) e do `DetailsView/`
14
- // do `agent-prism-main`. As sete referências antigas NÃO têm esta família — foi por
12
+ // PROCEDÊNCIA (BUILDING.md passo 2): estes três saem de uma referência de agentes (o cartão
13
+ // de agente e o painel de estado) e da vista de detalhes
14
+ // de outra. As sete referências antigas NÃO têm esta família — foi por
15
15
  // isso que a Parte H ficou bloqueada até 08/08/2026.
16
16
  //
17
- // Escopo menor que o da referência (passo 5): o `AgentCard` de lá despacha ações por
18
- // STRING (`onAction: (action: string) => void`), o que troca o compilador por um acordo
17
+ // Escopo menor que o da referência (passo 5): o cartão de lá despacha ações por
18
+ // STRING (uma função que recebe o nome da ação como texto), o que troca o compilador por um acordo
19
19
  // verbal. Aqui as ações entram como nó, e quem monta o botão sabe o que ele faz.
20
20
  import React from "react";
21
21
  import { cx, useAureaStrings } from "./internal.js";
@@ -45,7 +45,7 @@ export function AgentCard({ name, description, avatarSrc, model, state, capabili
45
45
  return _jsxs("article", { className: cx("card", "agent-card", className), ...props, children: [_jsxs("header", { className: "agent-card-head", children: [_jsx(Avatar, { src: avatarSrc, alt: name, fallback: name.slice(0, 2), size: "md" }), _jsxs("div", { className: "agent-card-id", children: [_jsx("strong", { children: name }), model && _jsx("span", { className: "hint", children: model })] }), state && _jsx(AgentStatus, { state: state })] }), description && _jsx("p", { className: "agent-card-desc", children: description }), !!capabilities?.length && _jsx("ul", { className: "agent-caps", children: capabilities.map(c => _jsxs("li", { className: "agent-cap", title: c.description, children: [c.icon && _jsx(Icon, { name: c.icon, size: "sm" }), _jsx("span", { children: c.name })] }, c.name)) }), actions && _jsx("footer", { className: "agent-card-actions", children: actions })] });
46
46
  }
47
47
  // AgentInspector — o detalhe: configuração, ferramentas, instruções.
48
- // A anatomia é a do `DetailsView/` do `agent-prism`: cabeçalho + seções rotuladas, com
48
+ // A anatomia é a da vista de detalhes de uma das referências de agentes: cabeçalho + seções rotuladas, com
49
49
  // o valor cru disponível. Aqui as seções são `DataList`, que já existe e já tem pele —
50
50
  // e o corpo livre entra como filho, para inspecionar o que a ficha não previu.
51
51
  //
@@ -64,10 +64,10 @@ export function AgentInspector({ title, sections, children, className, ...props
64
64
  //
65
65
  // O passo que tem corpo é um `<details>` nativo, o mesmo idioma do `Accordion`: dobrar
66
66
  // e desdobrar já é comportamento de plataforma, com teclado e ARIA, e a referência
67
- // gasta um `Collapsible` de biblioteca para chegar no mesmo lugar.
67
+ // gasta um componente dobrável de biblioteca para chegar no mesmo lugar.
68
68
  //
69
69
  // `running` marca `aria-busy` na região INTEIRA em vez de animar o texto. A referência
70
- // usa um `TextShimmer` — animação em cima do rótulo — e isso é decoração que um leitor
70
+ // usa um brilho que corre pelo texto — animação em cima do rótulo — e isso é decoração que um leitor
71
71
  // de tela não alcança e que `prefers-reduced-motion` teria de desfazer.
72
72
  //
73
73
  // Entrada e saída são `group` NOMEADO, e não `<h3>` — mesmo defeito e mesma correção do
@@ -86,10 +86,10 @@ const TAREFA = {
86
86
  };
87
87
  // TaskQueue — a fila de trabalho.
88
88
  //
89
- // ESCOPO MENOR QUE O DA REFERÊNCIA, e por fronteira, não por preguiça: o `AgentTask` de
89
+ // ESCOPO MENOR QUE O DA REFERÊNCIA, e por fronteira, não por preguiça: a tarefa de
90
90
  // lá tem 15 campos, e cinco deles pertencem a OUTROS componentes desta mesma parte —
91
- // `metrics.tokens` e `metrics.cost` são o `ModelUsage` (H11) e o `CostMeter` (H12),
92
- // `checkpoints` é o `TraceTimeline` (H9), `assignee` é o `AgentCard` (H1). Absorvê-los
91
+ // a contagem de tokens e o custo são o `ModelUsage` (H11) e o `CostMeter` (H12),
92
+ // os pontos de controle são o `TraceTimeline` (H9), o responsável é o `AgentCard` (H1). Absorvê-los
93
93
  // aqui faria a fila responder por quatro contratos e nenhum deles direito.
94
94
  export function TaskQueue({ tasks, label, onRetry, className, ...props }) {
95
95
  const s = useAureaStrings();
@@ -150,7 +150,7 @@ export function EventStream({ events, label, follow, className, ...props }) {
150
150
  // (end-start)/(max-min) de largura.
151
151
  //
152
152
  // O que NÃO entrou: as ONZE cores por categoria de span da referência
153
- // (`llm_call`, `tool_execution`, `retrieval`…). Cor por categoria numa paleta travada é
153
+ // (chamada de modelo, execução de ferramenta, busca…). Cor por categoria numa paleta travada é
154
154
  // o mesmo defeito que o H.a recusou; `kind` entra como TEXTO, que é legível também por
155
155
  // quem não distingue as cores.
156
156
  export function TraceTimeline({ spans, label, className, ...props }) {
@@ -174,24 +174,24 @@ export function HealthMatrix({ entries, label, className, ...props }) {
174
174
  // ─────────────────────────────────────────────────────────────────────────────
175
175
  // H.e — custo e memória: quanto se consumiu, contra que teto, e o que ficou guardado.
176
176
  //
177
- // PROCEDÊNCIA: `langfuse-main/web/src/features/dashboard/components/` — o
178
- // `ModelUsageChart.tsx` (a métrica total no topo e a quebra por modelo), o
179
- // `ModelCostTable.tsx` (as colunas modelo × tokens × custo, com o número à direita), o
180
- // `TotalMetric.tsx` (número grande + descrição, e é UMA linha de arranjo) e o
181
- // `cards/BarListChartArea.tsx` (a lista de barras horizontais com rótulo de valor).
182
- // Mais `agent-prism-main/packages/ui/src/components/{TokensBadge,PriceBadge}.tsx` — o par
183
- // de unidades desta família, contagem e preço — e `agents-kit-main/components/agents-ui/
184
- // agent-status-panel.tsx`, que traz a tarifa por 1k tokens com entrada e saída separadas.
177
+ // PROCEDÊNCIA: o painel de uma referência de observabilidade de LLM — o
178
+ // gráfico de uso por modelo (a métrica total no topo e a quebra por modelo), a
179
+ // tabela de custo por modelo (as colunas modelo × tokens × custo, com o número à direita), a
180
+ // métrica total (número grande + descrição, e é UMA linha de arranjo) e a
181
+ // lista de barras (a lista de barras horizontais com rótulo de valor).
182
+ // Mais os selos de tokens e de preço de uma referência de agentes — o par
183
+ // de unidades desta família, contagem e preço — e o painel de estado de agente de outra,
184
+ // que traz a tarifa por 1k tokens com entrada e saída separadas.
185
185
  //
186
186
  // O `MemoryLedger` NÃO TEM REFERÊNCIA, e é o caso declarado no `BUILDING.md` §1: nasce de
187
187
  // pesquisa registrada no `REFERENCES.md` e de composição. A pesquisa está lá com data.
188
188
  // Formatação de número: `Intl` é a plataforma, e o locale entra como prop com default
189
189
  // FIXO. O default fixo é o que mantém a saída determinística no SSR do catálogo e no gate
190
190
  // de pixel — locale do ambiente mudaria o texto entre a máquina e a CI. A referência faz
191
- // igual: o `costFormatter` do langfuse é `Intl.NumberFormat("en-US")` pregado, medido em
192
- // `web/src/utils/numbers.ts`.
191
+ // igual: o formatador de custo dela é `Intl.NumberFormat("en-US")` pregado, medido no
192
+ // fonte dela.
193
193
  const compacto = (n, locale) => new Intl.NumberFormat(locale, { notation: "compact", maximumFractionDigits: 2 }).format(n);
194
- // Duas casas fixas é o que o `costFormatter` da referência usa, e aqui isso MENTE: custo de
194
+ // Duas casas fixas é o que o formatador de custo da referência usa, e aqui isso MENTE: custo de
195
195
  // modelo cai em fração de centavo o tempo todo, e "$0.00" para um gasto real de US$ 0,004
196
196
  // apaga o número justamente no componente que existe para vigiá-lo.
197
197
  const dinheiro = (n, currency, locale) => new Intl.NumberFormat(locale, { style: "currency", currency, minimumFractionDigits: 2, maximumFractionDigits: n !== 0 && Math.abs(n) < 0.01 ? 6 : 2 }).format(n);
@@ -202,8 +202,8 @@ const dinheiro = (n, currency, locale) => new Intl.NumberFormat(locale, { style:
202
202
  // levou quanto". No `CostMeter` o teto vem de fora, e a pergunta é "quanto ainda cabe".
203
203
  // Trocar um pelo outro é mostrar orçamento onde não existe orçamento nenhum.
204
204
  //
205
- // ESCOPO MENOR QUE O DA REFERÊNCIA (passo 5): as quatro abas de lá (`Cost by model`,
206
- // `Cost by type`, `Usage by model`, `Usage by type`) existem porque são quatro CONSULTAS
205
+ // ESCOPO MENOR QUE O DA REFERÊNCIA (passo 5): as quatro abas de lá (custo e uso,
206
+ // cada um por modelo e por tipo) existem porque são quatro CONSULTAS
207
207
  // diferentes no produto dela. Aqui a dimensão é uma prop e os dados chegam prontos. Também
208
208
  // ficaram fora o seletor de modelos em popover e a série temporal — a série é o `Chart`,
209
209
  // que já existe, e embutir um segundo gráfico aqui duplicaria aquele contrato.
@@ -212,7 +212,7 @@ export function ModelUsage({ entries, metric = "tokens", label, currency = "USD"
212
212
  const valor = (e) => (metric === "cost" ? e.cost : metric === "requests" ? e.requests : e.tokens) ?? 0;
213
213
  const fmt = (n) => metric === "cost" ? dinheiro(n, currency, locale) : compacto(n, locale);
214
214
  const total = entries.reduce((a, e) => a + valor(e), 0);
215
- // Ordem decrescente pela métrica, como a referência (`orderBy` em `ModelCostTable`): uma
215
+ // Ordem decrescente pela métrica, como a referência (a ordenação da tabela de custo dela): uma
216
216
  // lista de barras fora de ordem obriga a varrer tudo para achar o maior, que é a única
217
217
  // coisa que ela existe para responder de relance. Cópia, não `sort` no array de quem chamou.
218
218
  const linhas = [...entries].sort((a, b) => valor(b) - valor(a));
@@ -223,10 +223,10 @@ export function ModelUsage({ entries, metric = "tokens", label, currency = "USD"
223
223
  }
224
224
  // CostMeter — o gasto contra um TETO.
225
225
  //
226
- // Os dois limites são os do LiteLLM, que é o padrão de mercado de controle de custo de LLM
226
+ // Os dois limites são os do padrão de mercado de controle de custo de LLM,
227
227
  // medido em 09/08/2026 (pesquisa registrada no `REFERENCES.md`): o limite RÍGIDO bloqueia,
228
- // o limite BRANDO só avisa. Nenhuma das nove referências locais tem orçamento — o langfuse
229
- // OSS mostra custo e não teto —, então isto veio pelo passo 4 do `BUILDING.md`, pesquisado
228
+ // o limite BRANDO só avisa. Nenhuma das nove referências locais tem orçamento — a referência
229
+ // de observabilidade, na versão aberta, mostra custo e não teto —, então isto veio pelo passo 4 do `BUILDING.md`, pesquisado
230
230
  // antes de escrever, e não de memória.
231
231
  //
232
232
  // Sem `softLimit` NÃO EXISTE estado de aviso. A tentação era derivar um em 80% do teto;
@@ -244,9 +244,9 @@ const LANCAMENTO = {
244
244
  };
245
245
  // MemoryLedger — o que o agente guardou, esqueceu ou lembrou, e DE ONDE veio.
246
246
  //
247
- // É o único da Parte H sem referência no mundo aberto: o ADE do Letta só abre o servidor, o
248
- // OpenMemory do mem0 está sendo descontinuado e o painel do mem0 é só na nuvem. Medido em
249
- // 08/08/2026 e reconfirmado em 09/08.
247
+ // É o único da Parte H sem referência no mundo aberto: dos produtos de memória de agentes
248
+ // pesquisados, um só abre o servidor, e o outro tem a versão aberta sendo descontinuada e o
249
+ // painel só na nuvem. Medido em 08/08/2026 e reconfirmado em 09/08.
250
250
  //
251
251
  // Livro-razão é APENDE-SÓ, e é isso que o separa de uma lista de memórias: `forgotten` não
252
252
  // tira a linha, registra o esquecimento. Uma tela que apaga a linha ao esquecer perde
@@ -271,8 +271,8 @@ const RECADO = {
271
271
  // tem um remetente e um destinatário, e é a passagem entre os dois que se está lendo. Um
272
272
  // `MessageList` com dois autores não diz quem entregou o quê a quem.
273
273
  //
274
- // O que NÃO entrou: as seis cores por intenção do `agent-routing-hub` (`technical` ciano,
275
- // `billing` âmbar, `sales` ardósia…). É a TERCEIRA vez que esta parte recusa a mesma coisa —
274
+ // O que NÃO entrou: as seis cores por intenção do painel de encaminhamento da referência (ciano,
275
+ // âmbar, ardósia…). É a TERCEIRA vez que esta parte recusa a mesma coisa —
276
276
  // depois das onze de span no H.d e das seis de estado no H.a. Cor por categoria numa paleta
277
277
  // travada não é legível para quem não separa as cores. A `kind` entra como palavra, num selo.
278
278
  export function InterAgentMessage({ messages, label, className, ...props }) {
@@ -284,7 +284,7 @@ export function InterAgentMessage({ messages, label, className, ...props }) {
284
284
  }
285
285
  // AutomationCard — a regra que roda sozinha: QUANDO isto, ENTÃO aquilo.
286
286
  //
287
- // A anatomia é a do `AutomationSidebar` do langfuse, medida: nome, gatilho (fonte de evento) e
287
+ // A anatomia é a da lateral de automações da referência de observabilidade, medida: nome, gatilho (fonte de evento) e
288
288
  // ação (tipo), mais o estado ativo/inativo. É isso que separa este cartão do `AgentCard`, que é
289
289
  // identidade, e da `TaskQueue`, que é trabalho enfileirado: uma automação é uma REGRA, e regra
290
290
  // é uma condição e uma consequência.
package/dist/calendar.js CHANGED
@@ -13,9 +13,9 @@ import { cx } from "./internal.js";
13
13
  const NOMES = { months: "calendar-months", month_caption: "calendar-caption" };
14
14
  export function Calendar({ label, className, classNames, ...props }) {
15
15
  const grade = _jsx(DayPicker
16
- // Grade cheia por padrão, como o shadcn: mês com buraco nas pontas treme ao trocar de mês.
16
+ // Grade cheia por padrão, como naquela referência: mês com buraco nas pontas treme ao trocar de mês.
17
17
  , {
18
- // Grade cheia por padrão, como o shadcn: mês com buraco nas pontas treme ao trocar de mês.
18
+ // Grade cheia por padrão, como naquela referência: mês com buraco nas pontas treme ao trocar de mês.
19
19
  showOutsideDays: true, ...props, classNames: { ...NOMES, root: cx("calendar", className), ...classNames } });
20
20
  // O motor já nomeia a GRADE com o mês ("August 2026"). O que ele não sabe é para que serve
21
21
  // ESTE calendário — "de" e "até" lado a lado soariam os dois "August 2026". É o que `label`
package/dist/chart.js CHANGED
@@ -4,17 +4,17 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  // componente que precisa do `recharts`, que é peer OPCIONAL. Se morasse em data-display.tsx,
5
5
  // quem importa Table baixaria um motor de gráfico inteiro — que é o achado A5 visto de perto.
6
6
  //
7
- // NÃO ESCREVEMOS MOTOR DE GRÁFICO. Duas das quatro referências (shadcn/ui e Untitled UI React)
7
+ // NÃO ESCREVEMOS MOTOR DE GRÁFICO. Duas das quatro referências
8
8
  // envelopam o MESMO motor, o Recharts, e nenhuma das quatro desenha eixo à mão. O que é nosso
9
9
  // aqui é só a pele: a caixa, o tooltip e a legenda. Escala, eixo, curva e interação são dele.
10
10
  //
11
11
  // O QUE FICOU DE FORA, e por quê (BUILDING.md passo 5 — escopo menor que o da referência):
12
- // • o `ChartConfig` do shadcn (mapa dataKey → {label, color, theme}). MEDIDO em 01/08/2026: o
12
+ // • o `ChartConfig` de uma delas (mapa dataKey → {label, color, theme}). MEDIDO em 01/08/2026: o
13
13
  // payload que o Recharts entrega ao `content` já traz `name` (do prop `name` da série) e
14
14
  // `color` resolvido. O objeto de configuração é indireção para reconstruir o que já chega.
15
15
  // E o eixo `theme:{light,dark}` não existe aqui: `var(--chart-2)` já troca com o data-theme.
16
16
  // • o par `ChartTooltip`/`ChartTooltipContent`. No Recharts 2 o filho tinha de ser o
17
- // componente DELES, o que forçava o shadcn a reexportar o primitivo e pôr a pele no
17
+ // componente DELES, o que forçava essa referência a reexportar o primitivo e pôr a pele no
18
18
  // `content`. MEDIDO no 3.10.1: um componente NOSSO como filho é reconhecido. Então é uma
19
19
  // peça só, não duas.
20
20
  // • prop de altura. Dimensão não é número (check 23): a caixa mede pelo CSS (`.chart`), e
@@ -1,6 +1,17 @@
1
1
  import React, { type ReactNode } from "react";
2
2
  import { type IconName } from "./system.js";
3
3
  import { type BadgeVariant } from "./feedback.js";
4
+ export type ChatDirection = "incoming" | "outgoing";
5
+ export interface MessageAttachment {
6
+ id: string;
7
+ kind: "image" | "video" | "file";
8
+ src?: string;
9
+ alt?: string;
10
+ name?: string;
11
+ bytes?: number;
12
+ duration?: number;
13
+ href?: string;
14
+ }
4
15
  export interface ChatMessage {
5
16
  id: string;
6
17
  body: ReactNode;
@@ -14,18 +25,60 @@ export interface ChatMessage {
14
25
  label: ReactNode;
15
26
  variant?: BadgeVariant;
16
27
  };
28
+ direction?: ChatDirection;
29
+ attachments?: MessageAttachment[];
30
+ replyTo?: {
31
+ id: string;
32
+ author?: ReactNode;
33
+ body: ReactNode;
34
+ };
35
+ forwardedFrom?: ReactNode;
36
+ edited?: boolean;
37
+ day?: string;
17
38
  }
18
- export declare function MessageList({ messages, label, className }: {
39
+ export interface MessageListProps {
19
40
  messages: ChatMessage[];
20
41
  label?: string;
21
42
  className?: string;
22
- }): React.JSX.Element;
23
- export declare function MessageComposer({ onSend, placeholder, label, icon, sendLabel, disabled, className }: {
24
- onSend: (text: string) => void;
43
+ hasMoreBefore?: boolean;
44
+ hasMoreAfter?: boolean;
45
+ loadingBefore?: boolean;
46
+ loadingAfter?: boolean;
47
+ onReachStart?: () => void;
48
+ onReachEnd?: () => void;
49
+ onOpenAttachment?: (message: ChatMessage, attachment: MessageAttachment) => void;
50
+ }
51
+ export declare function MessageList({ messages, label, className, hasMoreBefore, hasMoreAfter, loadingBefore, loadingAfter, onReachStart, onReachEnd, onOpenAttachment }: MessageListProps): React.JSX.Element;
52
+ export interface ComposerReply {
53
+ id: string;
54
+ author?: ReactNode;
55
+ body: ReactNode;
56
+ }
57
+ export interface ComposerEdit {
58
+ id: string;
59
+ body: string;
60
+ }
61
+ export interface ComposerAttach {
62
+ accept?: string;
63
+ maxSize?: number;
64
+ multiple?: boolean;
65
+ }
66
+ export interface MessageSendDetails {
67
+ files: File[];
68
+ replyToId?: string;
69
+ editingId?: string;
70
+ }
71
+ export declare function MessageComposer({ onSend, placeholder, label, icon, sendLabel, disabled, className, attach, replyTo, onCancelReply, editing, onCancelEdit }: {
72
+ onSend: (text: string, details?: MessageSendDetails) => void;
25
73
  placeholder?: string;
26
74
  label?: string;
27
75
  icon?: IconName;
28
76
  sendLabel?: string;
29
77
  disabled?: boolean;
30
78
  className?: string;
79
+ attach?: boolean | ComposerAttach;
80
+ replyTo?: ComposerReply | null;
81
+ onCancelReply?: () => void;
82
+ editing?: ComposerEdit | null;
83
+ onCancelEdit?: () => void;
31
84
  }): React.JSX.Element;
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  // Fase 9 (achado A5): este arquivo saiu do index.tsx de 970 linhas. Um módulo por categoria
4
4
  // do registry — a taxonomia já existia e é gateada. A ordem de import entre eles é um DAG:
5
5
  // internal → system → actions → feedback → inputs → navigation → layout → data-display → resto.
@@ -9,21 +9,244 @@ import { InputGroup, InputGroupAddon } from "./inputs.js";
9
9
  import { Icon } from "./system.js";
10
10
  import { IconButton } from "./actions.js";
11
11
  import { Avatar } from "./identity.js";
12
- import { Badge } from "./feedback.js";
13
- export function MessageList({ messages, label, className }) {
12
+ import { Badge, Spinner } from "./feedback.js";
13
+ import { formatSize, matchesAccept } from "./file-input.js";
14
+ // A AN-02 põe álbum na mensagem com a `Gallery` que já existe. `media` mora no "resto" do DAG,
15
+ // como este módulo, e não importa `communication` — não há ciclo.
16
+ import { Gallery } from "./media.js";
17
+ // O contêiner que rola é o ancestral com `overflow` de rolagem, ou a página.
18
+ function rolador(el) {
19
+ for (let p = el.parentElement; p; p = p.parentElement) {
20
+ const o = getComputedStyle(p).overflowY;
21
+ if (o === "auto" || o === "scroll" || o === "overlay")
22
+ return p;
23
+ }
24
+ return document.scrollingElement ?? document.documentElement;
25
+ }
26
+ const ehPagina = (sc) => sc === document.scrollingElement || sc === document.documentElement;
27
+ function useVigia(alvo, ligado, avisar, deps) {
28
+ const cb = React.useRef(avisar);
29
+ cb.current = avisar;
30
+ React.useEffect(() => {
31
+ const el = alvo.current;
32
+ if (!el || !ligado || typeof IntersectionObserver === "undefined")
33
+ return;
34
+ const sc = rolador(el);
35
+ const obs = new IntersectionObserver(es => { if (es.some(e => e.isIntersecting))
36
+ cb.current?.(); }, { root: ehPagina(sc) ? null : sc });
37
+ obs.observe(el);
38
+ return () => obs.disconnect();
39
+ // eslint-disable-next-line react-hooks/exhaustive-deps
40
+ }, [ligado, ...deps]);
41
+ }
42
+ export function MessageList({ messages, label, className, hasMoreBefore, hasMoreAfter, loadingBefore, loadingAfter, onReachStart, onReachEnd, onOpenAttachment }) {
14
43
  const s = useAureaStrings();
15
- return _jsx("div", { className: cx("stack", className), role: "log", "aria-live": "polite", "aria-label": label ?? s.chatLabel, children: messages.map(m => _jsxs("div", { className: "message", children: [m.avatar ? _jsx(Avatar, { src: m.avatar.src, fallback: m.avatar.fallback }) : _jsx("span", {}), _jsxs("div", { className: "message-bubble", children: [(m.author || m.time) && _jsxs("div", { className: "label", children: [_jsx("span", { children: m.author }), m.time && _jsx("span", { className: "hint", children: m.time })] }), m.body, m.status && _jsx("div", { className: "message-status", children: _jsxs(Badge, { variant: m.status.variant, children: [_jsx("i", { className: "status-dot" }), m.status.label] }) })] })] }, m.id)) });
44
+ const janela = !!onReachStart || !!onReachEnd;
45
+ const raiz = React.useRef(null);
46
+ const topo = React.useRef(null);
47
+ const fim = React.useRef(null);
48
+ const ancora = React.useRef(null);
49
+ const noFim = React.useRef(true);
50
+ const ultimoAntes = React.useRef(undefined);
51
+ const montou = React.useRef(false);
52
+ const [anuncio, setAnuncio] = React.useState("");
53
+ const hasMoreAfterRef = React.useRef(hasMoreAfter);
54
+ hasMoreAfterRef.current = hasMoreAfter;
55
+ // A posição de quem está no alto da tela, e se a última está à vista. Lida a cada rolagem e
56
+ // depois de cada troca: entre uma troca e outra, só a rolagem move as mensagens.
57
+ const medir = React.useCallback(() => {
58
+ const el = raiz.current;
59
+ if (!el)
60
+ return;
61
+ const sc = rolador(el), pag = ehPagina(sc);
62
+ const alto = pag ? 0 : sc.getBoundingClientRect().top, baixo = pag ? window.innerHeight : sc.getBoundingClientRect().bottom;
63
+ const itens = el.querySelectorAll("[data-message-id]");
64
+ // A âncora é a primeira que COMEÇA dentro da tela, e não a primeira que aparece: medido no banco
65
+ // em 04/10/2026, um álbum cortado no alto cresceu 480 px quando as fotos chegaram — para baixo
66
+ // dele —, o topo dele não andou, e a conversa inteira desceu sem a conta perceber. Se nenhuma
67
+ // começa na tela (uma mensagem maior que ela), vale a que está à vista.
68
+ ancora.current = null;
69
+ let vista;
70
+ for (const it of Array.from(itens)) {
71
+ const r = it.getBoundingClientRect();
72
+ if (r.bottom <= alto)
73
+ continue;
74
+ vista ??= it;
75
+ if (r.top >= alto) {
76
+ ancora.current = { id: it.dataset.messageId, top: r.top };
77
+ break;
78
+ }
79
+ }
80
+ if (!ancora.current && vista)
81
+ ancora.current = { id: vista.dataset.messageId, top: vista.getBoundingClientRect().top };
82
+ const ultimo = itens[itens.length - 1];
83
+ noFim.current = !ultimo || ultimo.getBoundingClientRect().bottom <= baixo + 1;
84
+ }, []);
85
+ React.useEffect(() => {
86
+ if (!janela || !raiz.current)
87
+ return;
88
+ const sc = rolador(raiz.current), alvo = ehPagina(sc) ? window : sc;
89
+ alvo.addEventListener("scroll", medir, { passive: true });
90
+ return () => alvo.removeEventListener("scroll", medir);
91
+ }, [janela, medir]);
92
+ const ultimoId = messages[messages.length - 1]?.id;
93
+ React.useLayoutEffect(() => {
94
+ const el = raiz.current;
95
+ if (!janela || !el) {
96
+ ultimoAntes.current = ultimoId;
97
+ return;
98
+ }
99
+ const sc = rolador(el);
100
+ const irAoFim = () => { sc.scrollTop = sc.scrollHeight; };
101
+ const antes = ultimoAntes.current;
102
+ // Chegou mensagem no fim: a última de antes continua na lista, e não é mais a última.
103
+ const ia = antes === undefined ? -1 : messages.findIndex(m => m.id === antes);
104
+ const chegou = montou.current && ia >= 0 && ia < messages.length - 1;
105
+ if (!montou.current) {
106
+ irAoFim();
107
+ montou.current = true;
108
+ }
109
+ else if (chegou && noFim.current && !hasMoreAfter)
110
+ irAoFim();
111
+ else if (ancora.current) {
112
+ const it = el.querySelector(`[data-message-id="${CSS.escape(ancora.current.id)}"]`);
113
+ if (it)
114
+ sc.scrollTop += it.getBoundingClientRect().top - ancora.current.top;
115
+ }
116
+ if (chegou) {
117
+ const novo = el.querySelector(`[data-message-id="${CSS.escape(ultimoId)}"]`);
118
+ setAnuncio(novo?.textContent ?? "");
119
+ }
120
+ ultimoAntes.current = ultimoId;
121
+ medir();
122
+ // eslint-disable-next-line react-hooks/exhaustive-deps
123
+ }, [messages]);
124
+ // A ALTURA MUDA DEPOIS DE MONTAR — a fonte chega, a foto decodifica, o app troca um texto — e
125
+ // nenhuma rolagem acontece para avisar. Medido no banco em 04/10/2026: a conversa abria na #4992
126
+ // e não na última. Então se observa o tamanho da lista: quem estava no fim continua no fim; quem
127
+ // não estava mantém a mensagem do alto onde ela estava.
128
+ React.useEffect(() => {
129
+ const el = raiz.current;
130
+ if (!janela || !el || typeof ResizeObserver === "undefined")
131
+ return;
132
+ const ro = new ResizeObserver(() => {
133
+ const sc = rolador(el);
134
+ if (noFim.current && !hasMoreAfterRef.current)
135
+ sc.scrollTop = sc.scrollHeight;
136
+ else if (ancora.current) {
137
+ const it = el.querySelector(`[data-message-id="${CSS.escape(ancora.current.id)}"]`);
138
+ if (it)
139
+ sc.scrollTop += it.getBoundingClientRect().top - ancora.current.top;
140
+ }
141
+ medir();
142
+ });
143
+ ro.observe(el);
144
+ return () => ro.disconnect();
145
+ }, [janela, medir]);
146
+ useVigia(topo, janela && !!hasMoreBefore && !loadingBefore, onReachStart, [messages.length]);
147
+ useVigia(fim, janela && !!hasMoreAfter && !loadingAfter, onReachEnd, [messages.length]);
148
+ return _jsxs(_Fragment, { children: [_jsxs("div", { ref: raiz, className: cx("stack", className), role: "log", "aria-live": janela ? "off" : "polite", "aria-busy": (janela && (loadingBefore || loadingAfter)) || undefined, "aria-label": label ?? s.chatLabel, children: [janela && hasMoreBefore && _jsx("div", { ref: topo, className: "message-more", children: loadingBefore && _jsx(Spinner, {}) }), messages.map((m, i) => {
149
+ const lado = m.direction;
150
+ const midia = (m.attachments ?? []).filter(a => a.kind !== "file" && a.src);
151
+ const arquivos = (m.attachments ?? []).filter(a => a.kind === "file");
152
+ const dia = m.day != null && m.day !== messages[i - 1]?.day;
153
+ return _jsxs(React.Fragment, { children: [dia && _jsx("div", { className: "message-day", children: _jsx(Badge, { children: m.day }) }), _jsxs("div", { className: cx("message", lado === "outgoing" && "message-out", lado === "incoming" && "message-in"), "data-message-id": m.id, children: [lado === "outgoing" ? null : m.avatar ? _jsx(Avatar, { src: m.avatar.src, fallback: m.avatar.fallback }) : _jsx("span", {}), _jsxs("div", { className: "message-bubble", children: [(m.author || m.time || m.edited) && _jsxs("div", { className: "label", children: [_jsx("span", { children: m.author }), (m.time || m.edited) && _jsxs("span", { className: "hint", children: [m.edited && _jsxs(_Fragment, { children: [s.chatEdited, m.time != null && " · "] }), m.time] })] }), m.forwardedFrom != null && _jsxs("div", { className: "message-forwarded", children: [_jsx(Icon, { name: "share-fat", size: "sm" }), _jsxs("span", { children: [s.chatForwardedFrom, " ", m.forwardedFrom] })] }), m.replyTo && _jsx("div", { className: "message-quote", children: _jsxs("span", { className: "message-quote-text", children: [_jsx("span", { className: "message-quote-title", children: m.replyTo.author }), _jsx("span", { className: "message-quote-body", children: m.replyTo.body })] }) }), midia.length > 0 && _jsx(Gallery, { className: "message-album", label: s.chatAttachments, zoom: !onOpenAttachment, ratio: midia.length === 1 ? "4/3" : "1/1", items: midia.map(a => ({ id: a.id, src: a.src, alt: a.alt ?? a.name ?? "", kind: a.kind === "video" ? "video" : "image", duration: a.duration })), onSelect: onOpenAttachment ? (id => { const a = midia.find(x => x.id === id); if (a)
154
+ onOpenAttachment(m, a); }) : undefined }), arquivos.length > 0 && _jsx("ul", { className: "file-list", "aria-label": s.chatAttachments, children: arquivos.map(a => _jsxs("li", { className: "file-item", children: [_jsx(Icon, { name: "file-text" }), a.href ? _jsx("a", { className: "file-name", href: a.href, download: a.name ?? true, children: a.name ?? a.href }) : _jsx("span", { className: "file-name", children: a.name }), a.bytes != null && _jsx("span", { className: "file-size", children: formatSize(a.bytes) })] }, a.id)) }), m.body, m.status && _jsx("div", { className: "message-status", children: _jsxs(Badge, { variant: m.status.variant, children: [_jsx("i", { className: "status-dot" }), m.status.label] }) })] })] })] }, m.id);
155
+ }), janela && hasMoreAfter && _jsx("div", { ref: fim, className: "message-more", children: loadingAfter && _jsx(Spinner, {}) })] }), janela && _jsx("span", { className: "sr-only", role: "status", "aria-live": "polite", children: anuncio })] });
16
156
  }
17
- // Composer: <form> sobre InputGroup + .input; Enter envia (submit nativo) e limpa.
18
- // Texto é estado interno (não controlado — controlar de fora só com demanda real,
19
- // precedente MediaPlayer/FileInput). Não envia texto vazio/só-espaço; o botão de
20
- // envio desabilita enquanto vazio. O input sempre tem nome acessível (aria-label),
21
- // pois placeholder não serve de nome. ponytail: input de uma linha; textarea +
22
- // Shift+Enter quando surgir demanda de multilinha.
23
- export function MessageComposer({ onSend, placeholder, label, icon, sendLabel, disabled, className }) {
157
+ export function MessageComposer({ onSend, placeholder, label, icon, sendLabel, disabled, className, attach, replyTo, onCancelReply, editing, onCancelEdit }) {
24
158
  const s = useAureaStrings();
25
159
  const [text, setText] = React.useState("");
26
- const submit = (e) => { e.preventDefault(); const t = text.trim(); if (!t)
27
- return; onSend(t); setText(""); };
28
- return _jsxs("form", { className: cx("message-composer", className), onSubmit: submit, children: [_jsxs(InputGroup, { children: [icon && _jsx(InputGroupAddon, { children: _jsx(Icon, { name: icon }) }), _jsx("input", { className: "input", value: text, disabled: disabled, placeholder: placeholder, "aria-label": label ?? s.chatMessage, onChange: e => setText(e.target.value) })] }), _jsx(IconButton, { type: "submit", variant: "primary", icon: "paper-plane-tilt", label: sendLabel ?? s.chatSend, disabled: disabled || !text.trim() })] });
160
+ const [anexos, setAnexos] = React.useState([]);
161
+ const [recusados, setRecusados] = React.useState([]);
162
+ const [aviso, setAviso] = React.useState("");
163
+ const campo = React.useRef(null);
164
+ const seletor = React.useRef(null);
165
+ const seq = React.useRef(0);
166
+ const opcoes = attach === true ? {} : attach || undefined;
167
+ const editandoId = editing?.id;
168
+ const respondendoId = replyTo?.id;
169
+ // O texto antigo entra quando a edição COMEÇA (muda o id), e sai quando ela acaba — sem isso o
170
+ // texto da mensagem editada ficava no campo como rascunho de mensagem nova.
171
+ const editouAntes = React.useRef(undefined);
172
+ React.useEffect(() => {
173
+ if (editandoId !== undefined) {
174
+ setText(editing?.body ?? "");
175
+ campo.current?.focus();
176
+ }
177
+ else if (editouAntes.current !== undefined)
178
+ setText("");
179
+ editouAntes.current = editandoId;
180
+ // eslint-disable-next-line react-hooks/exhaustive-deps
181
+ }, [editandoId]);
182
+ React.useEffect(() => { if (respondendoId !== undefined)
183
+ campo.current?.focus(); }, [respondendoId]);
184
+ // As URLs das miniaturas vivem enquanto o anexo vive; desmontar solta todas (o FileInput faz igual).
185
+ const anexosRef = React.useRef(anexos);
186
+ anexosRef.current = anexos;
187
+ React.useEffect(() => () => anexosRef.current.forEach(a => a.url && URL.revokeObjectURL(a.url)), []);
188
+ const soltar = (lista) => lista.forEach(a => a.url && URL.revokeObjectURL(a.url));
189
+ const escolher = (lista) => {
190
+ if (!lista)
191
+ return;
192
+ const novos = [], fora = [];
193
+ for (const file of Array.from(lista)) {
194
+ if (!matchesAccept(file, opcoes?.accept))
195
+ fora.push({ name: file.name, reason: s.fileWrongType });
196
+ else if (opcoes?.maxSize != null && file.size > opcoes.maxSize)
197
+ fora.push({ name: file.name, reason: s.fileTooLarge });
198
+ else
199
+ novos.push({ id: `a${seq.current++}`, file, url: file.type.startsWith("image/") && typeof URL.createObjectURL === "function" ? URL.createObjectURL(file) : undefined });
200
+ }
201
+ setRecusados(fora);
202
+ if (novos.length) {
203
+ if (opcoes?.multiple === false) {
204
+ soltar(anexos);
205
+ setAnexos(novos.slice(0, 1));
206
+ }
207
+ else
208
+ setAnexos(prev => [...prev, ...novos]);
209
+ setAviso(`${s.fileAdded}: ${novos.map(a => a.file.name).join(", ")}`);
210
+ }
211
+ if (seletor.current)
212
+ seletor.current.value = "";
213
+ };
214
+ const remover = (a) => { soltar([a]); setAnexos(prev => prev.filter(x => x.id !== a.id)); setAviso(`${s.fileRemoved}: ${a.file.name}`); campo.current?.focus(); };
215
+ const podeAnexar = !!opcoes && editandoId === undefined;
216
+ const vazio = !text.trim() && !(podeAnexar && anexos.length);
217
+ const submit = (e) => {
218
+ e.preventDefault();
219
+ if (vazio)
220
+ return;
221
+ const t = text.trim();
222
+ // O segundo argumento só vai quando o app usa uma prop nova: quem chamava `onSend(texto)` e
223
+ // conferia a chamada exata continua recebendo exatamente isso.
224
+ if (!opcoes && replyTo == null && editing == null)
225
+ onSend(t);
226
+ else
227
+ onSend(t, { files: podeAnexar ? anexos.map(a => a.file) : [], replyToId: editandoId === undefined ? respondendoId : undefined, editingId: editandoId });
228
+ setText("");
229
+ soltar(anexos);
230
+ setAnexos([]);
231
+ setRecusados([]);
232
+ };
233
+ const aoTeclar = (e) => {
234
+ if (e.key !== "Escape")
235
+ return;
236
+ if (editandoId !== undefined && onCancelEdit) {
237
+ e.preventDefault();
238
+ onCancelEdit();
239
+ }
240
+ else if (respondendoId !== undefined && onCancelReply) {
241
+ e.preventDefault();
242
+ onCancelReply();
243
+ }
244
+ };
245
+ const citacao = editing
246
+ ? _jsxs("div", { className: "message-quote", children: [_jsx(Icon, { name: "pencil-simple", size: "sm" }), _jsxs("span", { className: "message-quote-text", children: [_jsx("span", { className: "message-quote-title", children: s.chatEditing }), _jsx("span", { className: "message-quote-body", children: editing.body })] }), onCancelEdit && _jsx(IconButton, { variant: "ghost", size: "sm", icon: "x", label: s.chatEditCancel, onClick: onCancelEdit })] })
247
+ : replyTo
248
+ ? _jsxs("div", { className: "message-quote", children: [_jsx(Icon, { name: "arrow-bend-up-left", size: "sm" }), _jsxs("span", { className: "message-quote-text", children: [_jsxs("span", { className: "message-quote-title", children: [s.chatReplyTo, replyTo.author != null && _jsxs(_Fragment, { children: [" ", replyTo.author] })] }), _jsx("span", { className: "message-quote-body", children: replyTo.body })] }), onCancelReply && _jsx(IconButton, { variant: "ghost", size: "sm", icon: "x", label: s.chatReplyCancel, onClick: onCancelReply })] })
249
+ : null;
250
+ const temFaixa = !!citacao || (podeAnexar && (anexos.length > 0 || recusados.length > 0));
251
+ return _jsxs("form", { className: cx("message-composer", className), onSubmit: submit, children: [_jsxs(InputGroup, { children: [icon && _jsx(InputGroupAddon, { children: _jsx(Icon, { name: icon }) }), temFaixa && _jsxs("div", { className: "input-group-addon input-group-addon-start input-group-addon-block message-composer-context", children: [citacao, podeAnexar && anexos.length > 0 && _jsx("ul", { className: "file-list", "aria-label": s.chatAttachments, children: anexos.map(a => _jsxs("li", { className: "file-item", children: [a.url ? _jsx("img", { className: "file-thumb", src: a.url, alt: "" }) : _jsx(Icon, { name: "file-text" }), _jsx("span", { className: "file-name", children: a.file.name }), _jsx("span", { className: "file-size", children: formatSize(a.file.size) }), _jsx(IconButton, { variant: "ghost", size: "sm", icon: "x", label: `${s.fileRemove} ${a.file.name}`, onClick: () => remover(a) })] }, a.id)) }), podeAnexar && recusados.map((r, n) => _jsxs("span", { className: "field-error", children: [r.reason, ": ", r.name] }, n))] }), _jsx("input", { ref: campo, className: "input", value: text, disabled: disabled, placeholder: placeholder, "aria-label": label ?? s.chatMessage, onChange: e => setText(e.target.value), onKeyDown: aoTeclar }), podeAnexar && _jsx(InputGroupAddon, { side: "end", children: _jsx(IconButton, { variant: "ghost", size: "sm", icon: "paperclip", label: s.chatAttach, disabled: disabled, onClick: () => seletor.current?.click() }) })] }), podeAnexar && _jsx("input", { ref: seletor, type: "file", hidden: true, accept: opcoes?.accept, multiple: opcoes?.multiple !== false, onChange: e => escolher(e.target.files) }), _jsx(IconButton, { type: "submit", variant: "primary", icon: editing ? "check" : "paper-plane-tilt", label: editing ? s.chatSave : (sendLabel ?? s.chatSend), disabled: disabled || vazio }), podeAnexar && _jsx("span", { className: "sr-only", role: "status", "aria-live": "polite", children: aviso })] });
29
252
  }