@aurea-uds/react 0.8.7 → 0.8.8

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/data-grid.js CHANGED
@@ -149,6 +149,9 @@ export function DataGrid({ data, columns, label, filterable, pageSize, selectabl
149
149
  id: "detail", enableSorting: false, enableHiding: false, header: () => null,
150
150
  cell: ({ row }) => _jsx(IconButton, { icon: "chevron--right", label: s.dataGridDetails, size: "sm", variant: "ghost", "aria-expanded": detailRowId === row.id, onClick: () => setDetailRowId(detailRowId === row.id ? null : row.id) }),
151
151
  }] : allColumns, [allColumns, renderDetail, detailRowId, s]);
152
+ // A função acima precisa da tabela, e a tabela é o que esta chamada devolve: a referência fecha o
153
+ // círculo. Ela é lida só quando o motor filtra, que é sempre depois da primeira montagem.
154
+ const tabelaRef = React.useRef(null);
152
155
  const table = useReactTable({
153
156
  data, columns: colunas, getRowId,
154
157
  state: { sorting, globalFilter, rowSelection, columnFilters, columnVisibility, columnSizing, ...(paginated ? { pagination } : {}) },
@@ -174,6 +177,22 @@ export function DataGrid({ data, columns, label, filterable, pageSize, selectabl
174
177
  getCoreRowModel: getCoreRowModel(),
175
178
  getSortedRowModel: getSortedRowModel(),
176
179
  getFilteredRowModel: getFilteredRowModel(),
180
+ // A BUSCA NÃO DEPENDE DE QUEM VEM PRIMEIRO (A-11, 23/09/2026). A regra do motor (TanStack v8,
181
+ // `GlobalFiltering.js`, `getColumnCanGlobalFilter`) olha SÓ a primeira linha: se o valor dela
182
+ // não for texto nem número, a coluna sai da busca. Medido num app com 298 endereços: o primeiro
183
+ // não tinha fabricante, e procurar "Hikvision" dava zero onde havia 17. A regra daqui é a mesma
184
+ // pergunta — "esta coluna guarda texto ou número?" — respondida pelo primeiro valor PRESENTE,
185
+ // não pela primeira linha. `enableGlobalFilter:false` na coluna continua mandando: o motor
186
+ // confere essa chave antes de chamar esta função.
187
+ getColumnCanGlobalFilter: coluna => {
188
+ for (const linha of tabelaRef.current?.getCoreRowModel().flatRows ?? []) {
189
+ const v = linha.getValue(coluna.id);
190
+ if (v == null || v === "")
191
+ continue;
192
+ return typeof v === "string" || typeof v === "number";
193
+ }
194
+ return false;
195
+ },
177
196
  enableRowSelection: !!selectable,
178
197
  ...(paginated ? { getPaginationRowModel: getPaginationRowModel() } : {}),
179
198
  // só quando há faceta: os dois varrem os dados para montar o mapa de valores
@@ -183,6 +202,7 @@ export function DataGrid({ data, columns, label, filterable, pageSize, selectabl
183
202
  // longe daqui, em `columns` e em `row.original`.
184
203
  ...(facetIds.size ? { getFacetedRowModel: getFacetedRowModel(), getFacetedUniqueValues: getFacetedUniqueValues() } : {}),
185
204
  });
205
+ tabelaRef.current = table;
186
206
  const rows = table.getRowModel().rows;
187
207
  // O TAB STOP ROVING, e o `clamp` não é zelo: filtrar, ordenar ou paginar troca as linhas sob
188
208
  // o foco. Sem ele a grade inteira ficaria `tabIndex=-1` e sumiria da ordem do Tab — é o mesmo
package/dist/graph.d.ts CHANGED
@@ -8,6 +8,7 @@ export interface GraphNodeItem {
8
8
  y?: number;
9
9
  }
10
10
  export interface GraphEdgeItem {
11
+ id?: string;
11
12
  from: string;
12
13
  to: string;
13
14
  label?: string;
package/dist/graph.js CHANGED
@@ -39,7 +39,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
39
39
  // Quem importa a folha é o CONSUMIDOR, uma vez, como já importa o `@aurea-uds/core/css`: um
40
40
  // `import "…css"` dentro deste módulo quebraria em Node puro e o build daqui é `tsc` só.
41
41
  import React from "react";
42
- import { ReactFlow, ReactFlowProvider, Background, Handle, Position, applyNodeChanges } from "@xyflow/react";
42
+ import { ReactFlow, ReactFlowProvider, Background, BaseEdge, Handle, Position, applyNodeChanges, getBezierPath } from "@xyflow/react";
43
43
  import { cx, useAureaStrings } from "./internal.js";
44
44
  // ── O layout ─────────────────────────────────────────────────────────────────────────────
45
45
  // O motor recebe `x`/`y` PRONTOS: ele não posiciona nada. Sem isto, um grafo sem coordenadas
@@ -53,6 +53,17 @@ import { cx, useAureaStrings } from "./internal.js";
53
53
  // subida é o `elkjs`, que é o que o langfuse usa, e ele é DEPENDÊNCIA NOVA: entra com
54
54
  // autorização, não de contrabando.
55
55
  const LARGURA = 180, ALTURA = 52, VAO_X = 90, VAO_Y = 24;
56
+ // O VÃO ENTRE COLUNAS CRESCE COM O RÓTULO (A-09, 23/09/2026). Com o rótulo legível, a primeira
57
+ // imagem mostrou o que o preto escondia: "Te1/1/1 ↔ Te1/0/1" é mais largo que 90px e entrava por
58
+ // baixo dos nós. O layout roda também no servidor, onde não há texto para medir, então a largura
59
+ // é ESTIMADA com números medidos, não chutados: o rótulo é `--text-xs` (0.8125rem = 13px com a
60
+ // raiz de 16px), e no Chromium, com a IBM Plex Sans 400 carregada, o algarismo mede 0,600 da
61
+ // altura da letra e o texto de enlace ("Te1/1/1 ↔ Te1/0/1") mede 0,513 — o algarismo é o teto
62
+ // do texto comum, e é ele que entra. Mais os 2 × 4px do fundo do rótulo (padrão do motor) e uma
63
+ // folga de 16px de cada lado para a linha aparecer antes e depois dele. Nunca encolhe abaixo de
64
+ // `VAO_X`: grafo sem rótulo sai idêntico ao de antes.
65
+ const FONTE_DO_ROTULO = 13, GLIFO_DO_ROTULO = 0.6, FUNDO_X = 8, FOLGA_X = 32;
66
+ const vaoEntreColunas = (edges) => Math.max(VAO_X, ...edges.map(e => e.label ? Math.ceil(e.label.length * FONTE_DO_ROTULO * GLIFO_DO_ROTULO) + FUNDO_X + FOLGA_X : 0));
56
67
  function dispor(nodes, edges) {
57
68
  const entram = new Map();
58
69
  for (const n of nodes)
@@ -79,13 +90,14 @@ function dispor(nodes, edges) {
79
90
  };
80
91
  for (const n of nodes)
81
92
  calcular(n.id);
93
+ const vaoX = vaoEntreColunas(edges);
82
94
  const ocupacao = new Map();
83
95
  const posicoes = new Map();
84
96
  for (const n of nodes) {
85
97
  const d = profundidade.get(n.id) ?? 0;
86
98
  const linha = ocupacao.get(d) ?? 0;
87
99
  ocupacao.set(d, linha + 1);
88
- posicoes.set(n.id, { x: d * (LARGURA + VAO_X), y: linha * (ALTURA + VAO_Y) });
100
+ posicoes.set(n.id, { x: d * (LARGURA + vaoX), y: linha * (ALTURA + VAO_Y) });
89
101
  }
90
102
  // A EXTENSÃO existe para o SERVIDOR: sem ela o `fitView` não tem viewport para caber, e o
91
103
  // grafo renderizado fora do navegador sai ancorado no canto, com metade dos nós do lado de
@@ -97,7 +109,7 @@ function dispor(nodes, edges) {
97
109
  // interno o acionava sem ser prop de ninguém.
98
110
  const colunas = Math.max(...[...ocupacao.keys()], 0) + 1;
99
111
  const linhas = Math.max(...[...ocupacao.values()], 1);
100
- return { posicoes, extensao: { larg: colunas * LARGURA + (colunas - 1) * VAO_X, alt: linhas * ALTURA + (linhas - 1) * VAO_Y } };
112
+ return { posicoes, extensao: { larg: colunas * LARGURA + (colunas - 1) * vaoX, alt: linhas * ALTURA + (linhas - 1) * VAO_Y } };
101
113
  }
102
114
  // O nó é NOSSO, e é o ponto em que este componente deixa de ser "React Flow com outra cor".
103
115
  // O `GraphNode.tsx` do langfuse declara o motivo em uma linha — *"Real-HTML accessibility (the
@@ -115,6 +127,47 @@ function NoAurea({ data }) {
115
127
  : _jsx("span", { className: "graph-node-body", children: corpo }), d.conectavel && _jsx(Handle, { type: "source", position: Position.Right, isConnectable: true, className: "graph-handle" })] });
116
128
  }
117
129
  const TIPOS = { aurea: NoAurea };
130
+ // ── A ARESTA, e por que ela também é nossa (A-09 e A-10, 23/09/2026) ─────────────────────────
131
+ // A-09 · O RÓTULO SAÍA PRETO SOBRE PRETO no tema escuro. O core declara `--xy-edge-label-color` e
132
+ // `--xy-edge-label-background-color` desde o H14, e NINGUÉM AS LIA: quem consome essas duas é o
133
+ // `style.css` do motor — a identidade dele, que esta casa proíbe —, e não o `base.css` que o
134
+ // consumidor importa. Medido em 23/09 num app que instalou a 0.8.7: `fill: rgb(0,0,0)` no texto e
135
+ // no fundo. As variáveis continuam sendo a fonte, e agora quem as lê é o próprio rótulo.
136
+ //
137
+ // A-10 · ARESTAS PARALELAS. Com `id` distinto as duas existem, mas o motor desenha as duas no
138
+ // MESMO caminho e os rótulos se sobrepõem. Cada uma sai de uma altura própria do nó: as `n`
139
+ // paralelas dividem a altura do nó em `n + 1` partes iguais. Uma aresta só fica no meio, que é o
140
+ // desenho de antes. O número vem de `ALTURA`, a do próprio nó — não há distância inventada aqui.
141
+ const ROTULO = { fill: "var(--xy-edge-label-color)", fontSize: "var(--text-xs)" };
142
+ const FUNDO_DO_ROTULO = { fill: "var(--xy-edge-label-background-color)" };
143
+ function ArestaAurea({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, label, data, markerEnd, style }) {
144
+ const dy = data?.deslocamento ?? 0;
145
+ const [caminho, lx, ly] = getBezierPath({ sourceX, sourceY: sourceY + dy, sourcePosition, targetX, targetY: targetY + dy, targetPosition });
146
+ return _jsx(BaseEdge, { id: id, path: caminho, labelX: lx, labelY: ly, label: label, markerEnd: markerEnd, style: style, labelStyle: ROTULO, labelShowBg: true, labelBgStyle: FUNDO_DO_ROTULO });
147
+ }
148
+ const TIPOS_DE_ARESTA = { aurea: ArestaAurea };
149
+ /** Identidade e deslocamento de cada aresta — interno; o teste lê o resultado no DOM. */
150
+ function arestasParalelas(edges) {
151
+ const total = new Map();
152
+ for (const e of edges) {
153
+ const k = `${e.from}->${e.to}`;
154
+ total.set(k, (total.get(k) ?? 0) + 1);
155
+ }
156
+ const vista = new Map();
157
+ const usados = new Set();
158
+ return edges.map(e => {
159
+ const par = `${e.from}->${e.to}`;
160
+ const k = vista.get(par) ?? 0;
161
+ vista.set(par, k + 1);
162
+ const n = total.get(par);
163
+ // Sem `id`, a primeira do par mantém o id de sempre; a segunda em diante ganha o índice.
164
+ let id = e.id ?? (k === 0 ? par : `${par}#${k}`);
165
+ while (usados.has(id))
166
+ id = `${id}#`;
167
+ usados.add(id);
168
+ return { id, deslocamento: n > 1 ? ALTURA * ((k + 1) / (n + 1) - 1 / 2) : 0 };
169
+ });
170
+ }
118
171
  // DependencyGraph — quem depende de quem.
119
172
  //
120
173
  // O vocabulário da API é NOSSO — `{from,to}`, não `{source,target}`. Não é preciosismo: é o que
@@ -150,10 +203,13 @@ export function DependencyGraph({ nodes, edges, label, selectedId, onSelect, con
150
203
  // medido em 29/08/2026, a prop do fluxo não tira a classe `.selectable` da aresta, que é onde
151
204
  // o `base.css` do motor põe `cursor:pointer`. A mão de ponteiro prometia um clique que a API
152
205
  // da Aurea não atende — `onSelect` é do NÓ, e quem o atende é o <button> do corpo dele.
153
- const arestasDoMotor = React.useMemo(() => edges.map(e => ({
154
- id: `${e.from}->${e.to}`, source: e.from, target: e.to, label: e.label, animated: e.animated,
155
- selectable: false,
156
- })), [edges]);
206
+ const arestasDoMotor = React.useMemo(() => {
207
+ const ident = arestasParalelas(edges);
208
+ return edges.map((e, i) => ({
209
+ id: ident[i].id, type: "aurea", source: e.from, target: e.to, label: e.label, animated: e.animated,
210
+ data: { deslocamento: ident[i].deslocamento }, selectable: false,
211
+ }));
212
+ }, [edges]);
157
213
  // ── A MEDIDA TEM DE PODER VOLTAR, e isto foi achado NO NAVEGADOR ───────────────────────
158
214
  // A primeira versão passava `nodes` e nenhum `onNodesChange`. O motor aceita calado e o
159
215
  // resultado é um grafo INVISÍVEL: ele só tira o `visibility:hidden` do nó depois de gravar
@@ -178,6 +234,6 @@ export function DependencyGraph({ nodes, edges, label, selectedId, onSelect, con
178
234
  // que o `fitView` usa ENQUANTO ninguém mediu. Com o prefixo `initial`, o navegador sobrescreve
179
235
  // na primeira medição — passar `width`/`height` direto no ReactFlow fixaria o tamanho e mataria
180
236
  // a fluidez. O valor é a extensão do próprio layout, então no servidor o grafo cabe exato.
181
- return _jsx("div", { className: cx("dependency-graph", className), style: height ? { blockSize: height } : undefined, role: "group", "aria-label": label ?? s.graphLabel, ...props, children: _jsx(ReactFlowProvider, { initialNodes: nosDoMotor, initialEdges: arestasDoMotor, fitView: true, initialWidth: automatico.extensao.larg, initialHeight: automatico.extensao.alt, children: _jsx(ReactFlow, { nodes: nosVivos, onNodesChange: aoMudarNos, edges: arestasDoMotor, nodeTypes: TIPOS, fitView: true, proOptions: { hideAttribution: false }, nodesDraggable: !!onNodeMove, nodesConnectable: !!connectable, elementsSelectable: false, onNodeDragStop: onNodeMove ? (_, no) => onNodeMove(no.id, no.position) : undefined, onConnect: onConnect ? c => { if (c.source && c.target)
237
+ return _jsx("div", { className: cx("dependency-graph", className), style: height ? { blockSize: height } : undefined, role: "group", "aria-label": label ?? s.graphLabel, ...props, children: _jsx(ReactFlowProvider, { initialNodes: nosDoMotor, initialEdges: arestasDoMotor, fitView: true, initialWidth: automatico.extensao.larg, initialHeight: automatico.extensao.alt, children: _jsx(ReactFlow, { nodes: nosVivos, onNodesChange: aoMudarNos, edges: arestasDoMotor, nodeTypes: TIPOS, edgeTypes: TIPOS_DE_ARESTA, fitView: true, proOptions: { hideAttribution: false }, nodesDraggable: !!onNodeMove, nodesConnectable: !!connectable, elementsSelectable: false, onNodeDragStop: onNodeMove ? (_, no) => onNodeMove(no.id, no.position) : undefined, onConnect: onConnect ? c => { if (c.source && c.target)
182
238
  onConnect({ from: c.source, to: c.target }); } : undefined, children: _jsx(Background, {}) }) }) });
183
239
  }
@@ -236,7 +236,11 @@ export function SegmentedControl({ items, value, onChange, label }) {
236
236
  const ehAgrupado = (items) => items != null && items.length > 0 && typeof items[0] === "object" && items[0] != null && "items" in items[0];
237
237
  // UM renderizador e UMA lista para os dois combobox: duas cópias da mesma árvore é como a
238
238
  // segunda fica para trás na mudança seguinte.
239
- const renderComboboxItem = (item) => _jsxs(BaseCombobox.Item, { value: item, className: "menu-item combobox-item", children: [_jsx(BaseCombobox.ItemIndicator, { className: "combobox-check", children: _jsx(Icon, { name: "checkmark", size: "sm" }) }), _jsx("span", { children: item.label })] }, item.value);
239
+ // A COLUNA DO CHECK EXISTE SEMPRE (A-01, 23/09/2026). O `ItemIndicator` do Base UI não põe
240
+ // NADA no DOM quando o item não está escolhido; com um filho só, o texto caía na primeira coluna
241
+ // da grade — a de 16px — e quebrava em duas ou três linhas. O invólucro fica montado sempre, como
242
+ // o `.menu-mark` do `Menu` já faz, e o indicador aparece e some DENTRO dele.
243
+ const renderComboboxItem = (item) => _jsxs(BaseCombobox.Item, { value: item, className: "menu-item combobox-item", children: [_jsx("span", { className: "combobox-check", "aria-hidden": "true", children: _jsx(BaseCombobox.ItemIndicator, { children: _jsx(Icon, { name: "checkmark", size: "sm" }) }) }), _jsx("span", { children: item.label })] }, item.value);
240
244
  const listaCombobox = (items) => _jsx(BaseCombobox.List, { children: ehAgrupado(items)
241
245
  ? (group) => _jsxs(BaseCombobox.Group, { items: group.items, className: "combobox-section", children: [_jsx(BaseCombobox.GroupLabel, { className: "combobox-group-label", children: group.label }), _jsx(BaseCombobox.Collection, { children: renderComboboxItem })] }, group.label)
242
246
  : renderComboboxItem });
@@ -266,7 +270,6 @@ export function MultiCombobox({ items, value, onValueChange, onInputChange, load
266
270
  const portal = usePortalContainer();
267
271
  const autoId = React.useId();
268
272
  const inputId = id ?? autoId;
269
- const renderItem = (item) => _jsxs(BaseCombobox.Item, { value: item, className: "menu-item combobox-item", children: [_jsx(BaseCombobox.ItemIndicator, { className: "combobox-check", children: _jsx(Icon, { name: "checkmark", size: "sm" }) }), _jsx("span", { children: item.label })] }, item.value);
270
273
  return _jsxs(BaseCombobox.Root, { multiple: true, items: items, value: value, onValueChange: soOValor(onValueChange), disabled: disabled, itemToStringLabel: (i) => i.label, filter: onInputChange ? null : undefined, onInputValueChange: onInputChange ? ((v, d) => { if (d.reason !== "item-press")
271
274
  onInputChange(v); }) : undefined, children: [_jsxs("div", { className: cx("field", className), children: [label && _jsx("label", { className: "label", htmlFor: inputId, children: label }), _jsxs(BaseCombobox.InputGroup, { className: "combobox-multi", children: [_jsx(BaseCombobox.Chips, { className: "combobox-chips", children: _jsx(BaseCombobox.Value, { children: (selected) => _jsxs(_Fragment, { children: [selected.map((item) => _jsxs(BaseCombobox.Chip, { className: "combobox-chip", children: [item.label, _jsx(BaseCombobox.ChipRemove, { className: "combobox-chip-remove", "aria-label": `${s.comboboxRemove} ${item.label}`, children: _jsx(Icon, { name: "close", size: "sm" }) })] }, item.value)), _jsx(BaseCombobox.Input, { id: inputId, placeholder: selected.length ? undefined : placeholder, className: "combobox-chip-input", "aria-describedby": descrito, "aria-invalid": invalido })] }) }) }), _jsxs("span", { className: "combobox-actions", children: [_jsx(BaseCombobox.Clear, { render: _jsx(IconButton, { variant: "ghost", size: "sm", icon: "close", label: s.comboboxClear }) }), _jsx(BaseCombobox.Trigger, { render: _jsx(IconButton, { variant: "ghost", size: "sm", icon: "chevron--down", label: s.comboboxOpen }) })] })] })] }), _jsx(BaseCombobox.Portal, { container: portal, children: _jsx(BaseCombobox.Positioner, { sideOffset: 6, children: _jsxs(BaseCombobox.Popup, { className: "menu combobox-popup", children: [_jsx(BaseCombobox.Empty, { className: "combobox-empty", children: loading ? s.comboboxLoading : (empty ?? s.comboboxEmpty) }), listaCombobox(items)] }) }) })] });
272
275
  }
@@ -3,10 +3,11 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
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.
6
- import { useRef } from "react";
6
+ import { useEffect, useRef } from "react";
7
7
  import { Separator as BaseSeparator } from "@base-ui/react/separator";
8
8
  import { cx, useAureaStrings } from "./internal.js";
9
9
  import { useValorResponsivo } from "./responsivo-runtime.js";
10
+ import { ESCALA } from "./escala.js";
10
11
  import { IconButton } from "./actions.js";
11
12
  import { Sidebar, Topbar } from "./navigation.js";
12
13
  // ── Auxiliar de topo: mora ANTES do primeiro export, e a posição é obrigatória ───────────────
@@ -83,8 +84,34 @@ function focoDaGaveta(e) {
83
84
  // então id fixo é estável e não colide. Sem alvo estável não há como o link existir.
84
85
  const SHELL_MAIN_ID = "aurea-shell-main";
85
86
  const SHELL_NAV_ID = "aurea-shell-nav";
87
+ // A GAVETA NÃO SOBREVIVE À SUBIDA PARA O DESKTOP (A-07, 23/09/2026). Aberta em tela estreita e
88
+ // alargada a janela, ela ficava no top layer — que é estado do DOM, e nenhuma media query tira um
89
+ // elemento de lá —, flutuando fora de posição, sem véu, e com o botão que a fecharia escondido pelo
90
+ // CSS acima de lg. Girar um tablet ou encaixar a janela de lado no Windows bastava. O número é o
91
+ // `ESCALA.lg`, gerado dos tokens pela mesma leitura que escreve o `@media (max-width:1023px)` do
92
+ // core: um número só, e o `consumidores-lote1.test` cobra que o do `aurea.js` é o mesmo.
93
+ function useGavetaFechaNoDesktop() {
94
+ useEffect(() => {
95
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function")
96
+ return;
97
+ const mq = window.matchMedia(`(min-width: ${ESCALA.lg}px)`);
98
+ const aoMudar = (e) => {
99
+ if (!e.matches)
100
+ return;
101
+ const gaveta = document.getElementById(SHELL_NAV_ID);
102
+ try {
103
+ if (gaveta?.matches(":popover-open"))
104
+ gaveta.hidePopover();
105
+ }
106
+ catch { /* motor sem popover */ }
107
+ };
108
+ mq.addEventListener?.("change", aoMudar);
109
+ return () => mq.removeEventListener?.("change", aoMudar);
110
+ }, []);
111
+ }
86
112
  export function AppShell({ brand, navigation, topbar, topbarVariant = "floating", sidebarVariant, sidebarCollapsed, children, className, ...props }) {
87
113
  const s = useAureaStrings();
114
+ useGavetaFechaNoDesktop();
88
115
  return _jsxs("div", { className: cx("app-shell", topbarVariant === "flush" && "app-shell-flush", className), ...props, children: [_jsx("a", { className: "skip-link", href: `#${SHELL_MAIN_ID}`, children: s.skipToContent }), _jsx(Topbar, { variant: topbarVariant, brand: _jsxs(_Fragment, { children: [_jsx(IconButton, { className: "nav-toggle", icon: "menu", label: s.navigationToggle, popoverTarget: SHELL_NAV_ID }), brand] }), children: topbar }), _jsx(Sidebar, { id: SHELL_NAV_ID, popover: "auto", variant: sidebarVariant, collapsed: sidebarCollapsed, onToggle: focoDaGaveta, children: navigation }), _jsx("main", { id: SHELL_MAIN_ID, tabIndex: -1, className: "content", children: children })] });
89
116
  }
90
117
  // ── Separator ────────────────────────────────────────────────────────────────────────────────
@@ -72,7 +72,7 @@ export interface SidebarItem {
72
72
  items?: SidebarItem[];
73
73
  }
74
74
  export type SidebarVariant = "floating" | "flush";
75
- export declare function Sidebar({ items, current, collapsed, variant, label, children, className, ...props }: HTMLAttributes<HTMLElement> & RefAttributes<HTMLElement> & {
75
+ export declare function Sidebar({ items, current, collapsed, variant, label, children, className, onClick, ...props }: HTMLAttributes<HTMLElement> & RefAttributes<HTMLElement> & {
76
76
  items?: SidebarItem[];
77
77
  current?: string;
78
78
  collapsed?: boolean;
@@ -193,10 +193,31 @@ function sidebarList(items, ctx, sub, labelledBy) {
193
193
  return _jsxs("li", { children: [ctx.collapsed ? _jsx(Tooltip, { content: it.label, side: "right", children: alvo }) : alvo, filhos && sidebarList(filhos, ctx, true, lid)] }, it.id);
194
194
  }) });
195
195
  }
196
- export function Sidebar({ items, current, collapsed, variant = "floating", label, children, className, ...props }) {
196
+ // A GAVETA FECHA AO ESCOLHER (A-06, 23/09/2026). Abaixo de lg a lateral do `AppShell` é popover,
197
+ // e a biblioteca a abria e NUNCA a fechava por código: `hidePopover` tinha zero ocorrências. O
198
+ // usuário tocava "Filmes", a página trocava por baixo e a gaveta ficava na frente dela. Material 3,
199
+ // Fluent 2 e o guia de gaveta do iOS fecham ao escolher. O fechamento mora NA LATERAL, e não no
200
+ // item, para valer também para a navegação que o consumidor escreve e passa como `children` — o
201
+ // catálogo é um desses. Fora do popover (desktop, lateral solta) o teste de `:popover-open` falha e
202
+ // nada acontece. O `try` é porque `:popover-open` é seletor desconhecido em motor antigo, e lá o
203
+ // `matches` lança em vez de devolver falso.
204
+ function fecharGavetaAoEscolher(e) {
205
+ if (e.defaultPrevented)
206
+ return;
207
+ const gaveta = e.currentTarget;
208
+ const alvo = e.target?.closest?.("a[href], .sidebar-item");
209
+ if (!alvo || !gaveta.contains(alvo))
210
+ return;
211
+ try {
212
+ if (gaveta.matches(":popover-open"))
213
+ gaveta.hidePopover();
214
+ }
215
+ catch { /* motor sem popover: não há gaveta */ }
216
+ }
217
+ export function Sidebar({ items, current, collapsed, variant = "floating", label, children, className, onClick, ...props }) {
197
218
  const s = useAureaStrings();
198
219
  const baseId = React.useId();
199
- return _jsxs("aside", { className: cx("sidebar", variant !== "floating" && `sidebar-${variant}`, collapsed && "sidebar-collapsed", className), ...props, children: [items && items.length > 0 && _jsx("nav", { className: "sidebar-nav", "aria-label": label ?? s.sidebarLabel, children: sidebarList(items, { baseId, current, collapsed }) }), children] });
220
+ return _jsxs("aside", { className: cx("sidebar", variant !== "floating" && `sidebar-${variant}`, collapsed && "sidebar-collapsed", className), onClick: e => { onClick?.(e); fecharGavetaAoEscolher(e); }, ...props, children: [items && items.length > 0 && _jsx("nav", { className: "sidebar-nav", "aria-label": label ?? s.sidebarLabel, children: sidebarList(items, { baseId, current, collapsed }) }), children] });
200
221
  }
201
222
  const LEGADO = {
202
223
  flat: ["edge", "none"], surface: ["floating", "circle"], pill: ["floating", "pill"], dock: ["floating", "circle"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aurea-uds/react",
3
- "version": "0.8.7",
3
+ "version": "0.8.8",
4
4
  "description": "React components for Aurea UDS. Import @aurea-uds/fonts/css and @aurea-uds/core/css alongside.",
5
5
  "keywords": [
6
6
  "aurea",