@aurea-uds/react 0.54.1 → 0.59.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.
@@ -70,13 +70,17 @@ function linhasDoCodigo(codigo, language, highlight) {
70
70
  // arquivo à esquerda e, com `copyable`, o "Copy" ESCRITO (o botão secundário, com o ícone antes do nome; vira "Copied").
71
71
  // O bloco vira o cartão (canto 22, a cor do cartão) e a linha longa DESCE em vez de rolar. Sem `filename`, o de sempre.
72
72
  export function CodeBlock({ children, language = "text", copyable, preview, previewLayout = "tabs", lineNumbers, highlight, tabs, filename, className }) {
73
+ // Os hooks ANTES de todo `return` (auditoria externa, 05/11/2026): moravam depois dos três retornos da caixa e dos dois do
74
+ // bloco sem cópia, e trocar `copyable` entre renderizações mudava a ordem dos hooks — o React avisava e o "copiado" se
75
+ // perdia. Regra dos hooks do React: mesma ordem, em toda renderização.
76
+ const s = useAureaStrings();
77
+ const [copiado, setCopiado] = React.useState(false);
73
78
  if (tabs && tabs.length > 0)
74
79
  return _jsx(CodigoEmAbas, { tabs: tabs, language: language, copyable: copyable, highlight: highlight, className: className });
75
80
  if (preview !== undefined && previewLayout === "stacked")
76
81
  return _jsx(CaixaEmpilhada, { preview: preview, className: className, linhas: (children ?? "").split("\n").length, codigo: _jsx(CodeBlock, { language: language, copyable: copyable, lineNumbers: lineNumbers, highlight: highlight, children: children }) });
77
82
  if (preview !== undefined)
78
83
  return _jsx(CaixaDeExemplo, { preview: preview, className: className, codigo: _jsx(CodeBlock, { language: language, copyable: copyable, lineNumbers: lineNumbers, highlight: highlight, children: children }) });
79
- const s = useAureaStrings();
80
84
  // tabIndex no <pre>: `.code-block` é `overflow:auto` por construção, então é REGIÃO ROLÁVEL
81
85
  // e não tem nada focável dentro — quem navega por teclado não alcança o código que passa da
82
86
  // largura. É a regra `scrollable-region-focusable` do axe, e o mesmo defeito que o painel de
@@ -97,7 +101,6 @@ export function CodeBlock({ children, language = "text", copyable, preview, prev
97
101
  return _jsxs("div", { className: cx("code-block-wrap", "code-block-file", className), children: [_jsx("div", { className: "code-block-head", children: _jsx("span", { className: "code-block-title", children: filename }) }), pre] });
98
102
  // 0.28.0: o "copiado" é dito ao leitor de tela. Com o `aurea.js` na página, é ele quem diz (pelo
99
103
  // texto do `data-aurea-copy-done`, na língua do app); sem ele, a região de status daqui.
100
- const [copiado, setCopiado] = React.useState(false);
101
104
  const onCopy = (e) => {
102
105
  e.stopPropagation();
103
106
  const vanilla = window.Aurea;
@@ -17,8 +17,11 @@ export interface TableProps extends HTMLAttributes<HTMLTableElement>, RefAttribu
17
17
  /** As linhas ímpares do corpo com fundo (o site B, 02/11/2026, a "B" na imagem): a 1ª, a 3ª… — o olho segue a linha
18
18
  * numa tabela larga. Nos dois arranjos (`default` e `rows`). */
19
19
  striped?: boolean;
20
+ /** MNT-38: uma linha clara entre as fileiras do corpo, menos depois da última (a forma "A", aprovada em 05/11/2026). Uma
21
+ * exceção nomeada ao sem linha (ADR-0072): só aparece quando o app escolhe. Sem efeito no arranjo `rows` e na Ember. */
22
+ dividers?: boolean;
20
23
  }
21
- export declare function Table({ caption, stickyHeader, stickyFirstColumn, fit, differencesOnly, striped, variant, children, className, ...props }: TableProps): React.JSX.Element;
24
+ export declare function Table({ caption, stickyHeader, stickyFirstColumn, fit, differencesOnly, striped, dividers, variant, children, className, ...props }: TableProps): React.JSX.Element;
22
25
  export interface SortableItem {
23
26
  id: string;
24
27
  label: ReactNode;
@@ -6,7 +6,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
6
6
  import React from "react";
7
7
  import { cx, useAureaStrings, useReorder } from "./internal.js";
8
8
  import { Icon } from "./system.js";
9
- export function Table({ caption, stickyHeader, stickyFirstColumn, fit, differencesOnly, striped, variant = "default", children, className, ...props }) { const s = useAureaStrings(); return _jsx("div", { className: cx("table-region", stickyHeader && "table-sticky-header", stickyFirstColumn && "table-sticky-first", variant === "rows" && "table-region-rows"), role: "region", "aria-label": typeof caption === "string" ? caption : s.tableLabel, tabIndex: 0, children: _jsxs("table", { className: cx("table", fit && "table-fit", differencesOnly && "table-differences-only", variant === "rows" && "table-rows", striped && "table-striped", className), ...props, children: [caption && _jsx("caption", { children: caption }), children] }) }); }
9
+ export function Table({ caption, stickyHeader, stickyFirstColumn, fit, differencesOnly, striped, dividers, variant = "default", children, className, ...props }) { const s = useAureaStrings(); return _jsx("div", { className: cx("table-region", stickyHeader && "table-sticky-header", stickyFirstColumn && "table-sticky-first", variant === "rows" && "table-region-rows"), role: "region", "aria-label": typeof caption === "string" ? caption : s.tableLabel, tabIndex: 0, children: _jsxs("table", { className: cx("table", fit && "table-fit", differencesOnly && "table-differences-only", variant === "rows" && "table-rows", striped && "table-striped", dividers && "table-dividers", className), ...props, children: [caption && _jsx("caption", { children: caption }), children] }) }); }
10
10
  // O protocolo de teclado e de ponteiro saiu daqui para o `useReorder` do `internal` no item N1,
11
11
  // quando o `BlockEditor` virou o segundo dono dele. A DOM abaixo não mudou uma vírgula na
12
12
  // extração — é o que o gate de pixel e o `skin.spec` continuam medindo.
@@ -12,7 +12,7 @@ export interface GridFilterSpec {
12
12
  label: string;
13
13
  }>;
14
14
  }
15
- export declare function DataGrid<T>({ data, columns, label, filterable, pageSize, selectable, onSelectionChange, getRowId, className, sorting: sortingProp, onSortingChange, globalFilter: globalFilterProp, onGlobalFilterChange, page, onPageChange, rowSelection: rowSelectionProp, onRowSelectionChange, manualSorting, manualFiltering, manualPagination, rowCount, filters, columnFilters: columnFiltersProp, onColumnFiltersChange, bulkActions, stickyHeader, hideableColumns, columnVisibility: columnVisibilityProp, onColumnVisibilityChange, resizableColumns, columnSizing: columnSizingProp, onColumnSizingChange, state, stateMessage, renderDetail, detailRowId: detailRowIdProp, onDetailRowIdChange, onExport }: {
15
+ export declare function DataGrid<T>({ data, columns, label, filterable, pageSize, selectable, onSelectionChange, getRowId, className, sorting: sortingProp, onSortingChange, globalFilter: globalFilterProp, onGlobalFilterChange, page, onPageChange, rowSelection: rowSelectionProp, onRowSelectionChange, manualSorting, manualFiltering, manualPagination, rowCount, filters, columnFilters: columnFiltersProp, onColumnFiltersChange, bulkActions, stickyHeader, dividers, fit, hideableColumns, columnVisibility: columnVisibilityProp, onColumnVisibilityChange, resizableColumns, columnSizing: columnSizingProp, onColumnSizingChange, state, stateMessage, renderDetail, detailRowId: detailRowIdProp, onDetailRowIdChange, onExport }: {
16
16
  data: T[];
17
17
  columns: Array<ColumnDef<T, any>>;
18
18
  label?: string;
@@ -43,7 +43,9 @@ export declare function DataGrid<T>({ data, columns, label, filterable, pageSize
43
43
  icon?: IconName;
44
44
  onAction: (rows: T[], clear: () => void) => void;
45
45
  }>;
46
- stickyHeader?: boolean;
46
+ stickyHeader?: boolean | "page";
47
+ dividers?: boolean;
48
+ fit?: boolean;
47
49
  hideableColumns?: boolean;
48
50
  columnVisibility?: VisibilityState;
49
51
  onColumnVisibilityChange?: (v: VisibilityState) => void;
package/dist/data-grid.js CHANGED
@@ -95,7 +95,67 @@ function useMaybe(outer, onChange, initial) {
95
95
  return [value, u => { const next = typeof u === "function" ? u(value) : u; if (!controlled)
96
96
  setInner(next); onChange?.(next); }];
97
97
  }
98
- export function DataGrid({ data, columns, label, filterable, pageSize, selectable, onSelectionChange, getRowId, className, sorting: sortingProp, onSortingChange, globalFilter: globalFilterProp, onGlobalFilterChange, page, onPageChange, rowSelection: rowSelectionProp, onRowSelectionChange, manualSorting, manualFiltering, manualPagination, rowCount, filters, columnFilters: columnFiltersProp, onColumnFiltersChange, bulkActions, stickyHeader, hideableColumns, columnVisibility: columnVisibilityProp, onColumnVisibilityChange, resizableColumns, columnSizing: columnSizingProp, onColumnSizingChange, state, stateMessage, renderDetail, detailRowId: detailRowIdProp, onDetailRowIdChange, onExport }) {
98
+ /** O que fica parado no topo da página na coluna da grade: o fundo da pilha de elementos no alto da tela, subindo até o
99
+ * primeiro ancestral `fixed` ou `sticky` que não é da própria grade. Devolve onde ele termina (0 se não houver). */
100
+ function topoParado(caixa) {
101
+ const r = caixa.getBoundingClientRect();
102
+ const x = Math.min(Math.max(r.left + r.width / 2, 0), innerWidth - 1);
103
+ let fundo = 0;
104
+ // Sem `elementsFromPoint` (um ambiente sem desenho), não há o que medir: o cabeçalho fica onde está.
105
+ for (const el of document.elementsFromPoint?.(x, 0) ?? []) {
106
+ if (caixa.contains(el))
107
+ continue;
108
+ for (let a = el; a && a !== document.documentElement; a = a.parentElement) {
109
+ const pos = getComputedStyle(a).position;
110
+ if (pos === "fixed" || pos === "sticky") {
111
+ fundo = Math.max(fundo, a.getBoundingClientRect().bottom);
112
+ break;
113
+ }
114
+ }
115
+ }
116
+ return fundo;
117
+ }
118
+ /** S-10: o cabeçalho da grade anda com a página. Cada `th` do `thead` ganha um `translate` — a caixa continua rolando de
119
+ * lado, e o cabeçalho, que está dentro dela, vai junto. Para no fim da caixa, com a última fileira ainda à vista. */
120
+ function usePresoNaPagina(ligado, grade, montagem) {
121
+ React.useLayoutEffect(() => {
122
+ const tabela = grade.current;
123
+ if (!ligado || !tabela)
124
+ return;
125
+ const caixa = tabela.closest(".table-wrap");
126
+ if (!caixa)
127
+ return;
128
+ let quadro = 0;
129
+ const medir = () => {
130
+ quadro = 0;
131
+ const cab = tabela.tHead;
132
+ if (!cab)
133
+ return;
134
+ const r = caixa.getBoundingClientRect();
135
+ const ultima = tabela.tBodies[0]?.rows[tabela.tBodies[0].rows.length - 1]?.offsetHeight ?? 0;
136
+ const y = Math.round(Math.max(0, Math.min(topoParado(caixa) - r.top, r.height - cab.offsetHeight - ultima)));
137
+ for (const th of cab.querySelectorAll(":scope > tr > th"))
138
+ th.style.translate = y ? `0 ${y}px` : "";
139
+ };
140
+ const pedir = () => { if (!quadro)
141
+ quadro = requestAnimationFrame(medir); };
142
+ medir();
143
+ addEventListener("scroll", pedir, { passive: true, capture: true });
144
+ addEventListener("resize", pedir);
145
+ const vigia = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(pedir);
146
+ vigia?.observe(tabela);
147
+ return () => {
148
+ cancelAnimationFrame(quadro);
149
+ removeEventListener("scroll", pedir, { capture: true });
150
+ removeEventListener("resize", pedir);
151
+ vigia?.disconnect();
152
+ for (const th of tabela.querySelectorAll("thead th"))
153
+ th.style.translate = "";
154
+ };
155
+ // `montagem`: a linha aberta do painel de detalhe muda o lugar da tabela na árvore, e a tabela nasce de novo.
156
+ }, [ligado, grade, montagem]);
157
+ }
158
+ export function DataGrid({ data, columns, label, filterable, pageSize, selectable, onSelectionChange, getRowId, className, sorting: sortingProp, onSortingChange, globalFilter: globalFilterProp, onGlobalFilterChange, page, onPageChange, rowSelection: rowSelectionProp, onRowSelectionChange, manualSorting, manualFiltering, manualPagination, rowCount, filters, columnFilters: columnFiltersProp, onColumnFiltersChange, bulkActions, stickyHeader, dividers, fit, hideableColumns, columnVisibility: columnVisibilityProp, onColumnVisibilityChange, resizableColumns, columnSizing: columnSizingProp, onColumnSizingChange, state, stateMessage, renderDetail, detailRowId: detailRowIdProp, onDetailRowIdChange, onExport }) {
99
159
  const s = useAureaStrings();
100
160
  const [sorting, setSorting] = useMaybe(sortingProp, onSortingChange, []);
101
161
  const [globalFilter, setGlobalFilter] = useMaybe(globalFilterProp, onGlobalFilterChange, "");
@@ -400,7 +460,12 @@ export function DataGrid({ data, columns, label, filterable, pageSize, selectabl
400
460
  // O marcador vai na GRADE, não no recado: quem está obsoleto é a tabela, e o `Alert` é só
401
461
  // como ela conta isso. Achado pelo check 30 na primeira execução dele — eu tinha marcado o
402
462
  // recado, e com `state="loading"` não há recado nenhum, então o estado não chegava ao DOM.
403
- return _jsxs("div", { className: cx("datagrid", stickyHeader && "datagrid-sticky", className), "data-state": state, children: [emLote.length > 0 && _jsxs(Toolbar, { label: s.dataGridBulkLabel, children: [_jsxs("span", { className: "hint", children: [emLote.length, " ", s.dataGridSelected] }), _jsx(ToolbarSeparator, {}), bulkActions?.map(a => _jsx(ToolbarButton, { size: "sm", leadingIcon: a.icon, onClick: () => a.onAction(emLote, limparSelecao), children: a.label }, a.id)), _jsx(ToolbarButton, { size: "sm", onClick: limparSelecao, children: s.dataGridClearSelection })] }), controles.length > 1 ? _jsx(Cluster, { children: controles }) : controles[0] ?? null, recado && _jsx(Alert, { variant: state === "error" ? "danger" : universal ? stateSeverity(universal) : "warning", children: recado }), (() => {
463
+ // S-10 (o MNT-40, aprovado em 05/11/2026 como valor novo): com `stickyHeader="page"` a caixa não ganha teto nem rolagem
464
+ // de cima para baixo, e o cabeçalho anda com a PÁGINA até encostar embaixo do que fica parado no topo dela (a barra do
465
+ // `AppShell`, o cabeçalho do site). Só com CSS não dá: a caixa rola de lado, e um elemento grudento gruda no contêiner de
466
+ // rolagem mais próximo — a caixa, não a página. Então o deslocamento é medido aqui, a cada rolagem.
467
+ usePresoNaPagina(stickyHeader === "page", grade, detailRowId);
468
+ return _jsxs("div", { className: cx("datagrid", stickyHeader === true && "datagrid-sticky", stickyHeader === "page" && "datagrid-sticky-page", dividers && "datagrid-dividers", fit && "datagrid-fit", className), "data-state": state, children: [emLote.length > 0 && _jsxs(Toolbar, { label: s.dataGridBulkLabel, children: [_jsxs("span", { className: "hint", children: [emLote.length, " ", s.dataGridSelected] }), _jsx(ToolbarSeparator, {}), bulkActions?.map(a => _jsx(ToolbarButton, { size: "sm", leadingIcon: a.icon, onClick: () => a.onAction(emLote, limparSelecao), children: a.label }, a.id)), _jsx(ToolbarButton, { size: "sm", onClick: limparSelecao, children: s.dataGridClearSelection })] }), controles.length > 1 ? _jsx(Cluster, { children: controles }) : controles[0] ?? null, recado && _jsx(Alert, { variant: state === "error" ? "danger" : universal ? stateSeverity(universal) : "warning", children: recado }), (() => {
404
469
  // F9: o embrulho de duas colunas só existe COM o painel aberto — fechado, a
405
470
  // marcação é a de sempre. É a lição do F7: não mudar a estrutura de quem não
406
471
  // pediu nada. A largura do painel é `--datagrid-detail-w`, e quem decide se
@@ -59,10 +59,12 @@ const ICONE_DA_VARIANTE = { info: "info",
59
59
  // na cor do texto. Sem ele (`"card"`), o cartão de sempre.
60
60
  export function Alert({ variant, state, title, icon, onDismiss, action, appearance, children, className, ...props }) { const s = useAureaStrings(); const v = variant ?? (state ? stateSeverity(state) : "info"); return _jsxs("div", { className: cx("alert", `alert-${v}`, appearance === "soft" && "alert-soft", action != null && "alert-has-action", className), role: v === "danger" ? "alert" : "status", "data-state": state, ...props, children: [_jsx(Icon, { name: icon ?? proprio(ICONE_DA_VARIANTE, v) ?? "info", weight: icon ? undefined : "fill" }), _jsxs("div", { children: [title && _jsx("strong", { children: title }), children ?? (state ? proprio(s.universalState, state) : null)] }), action != null && _jsx("div", { className: "alert-action", children: action }), onDismiss ? _jsx(CloseButton, { onClick: onDismiss }) : _jsx("span", {})] }); }
61
61
  export function Banner(props) {
62
+ // O hook ANTES do `return` do `consent` (auditoria externa, 05/11/2026): trocar a variante entre renderizações mudava a
63
+ // ordem dos hooks.
64
+ const s = useAureaStrings();
62
65
  if (props.variant === "consent")
63
66
  return _jsx(BannerConsent, { ...props });
64
67
  const { variant, state, title, icon, onDismiss, children, className, categories: _c, value: _v, onConsent: _o, ...rest } = props;
65
- const s = useAureaStrings();
66
68
  const v = variant ?? (state ? stateSeverity(state) : "info");
67
69
  return _jsxs("div", { className: cx("banner", `banner-${v}`, className), role: v === "danger" ? "alert" : "status", "data-state": state, ...rest, children: [icon ? _jsx(Icon, { name: icon }) : _jsx("span", {}), _jsxs("div", { children: [title && _jsx("strong", { children: title }), children ?? (state ? proprio(s.universalState, state) : null)] }), onDismiss ? _jsx(IconButton, { variant: "ghost", size: "sm", icon: "x", label: s.close, onClick: onDismiss }) : _jsx("span", {})] });
68
70
  }
@@ -15,7 +15,7 @@ import { Slider as BaseSlider } from "@base-ui/react/slider";
15
15
  import { RadioGroup as BaseRadioGroup } from "@base-ui/react/radio-group";
16
16
  import { Radio as BaseRadio } from "@base-ui/react/radio";
17
17
  import { useDirection } from "@base-ui/react/direction-provider";
18
- import { cx, useAureaStrings, usePortalContainer, useReorder, controlesRotulaveis } from "./internal.js";
18
+ import { cx, useAureaStrings, usePortalContainer, useReorder, useDeslize, controlesRotulaveis } from "./internal.js";
19
19
  import { COLOR_CHANNELS, areaBackground, channelRange, colorName, corToHex, formatChannel, getChannel, hsbToRgb, parseColor, pureHue, setChannel, spaceOf, trackBackground } from "./color.js";
20
20
  import { Popover as BasePopover } from "@base-ui/react/popover";
21
21
  // Os seis controles abaixo saíram deste arquivo no item O1 e moram no `markup.tsx`, sem a
@@ -734,11 +734,15 @@ export function OTPField({ length, value, defaultValue, onValueChange, mask, dis
734
734
  // cheia), `size="2xl"` (segmento de 48), `wrap` (quebra linha, o trilho com canto 22) e `orientation="vertical"` (em pé).
735
735
  export function SegmentedControl({ items, value, onChange, label, fullWidth, size = "md", selectedStyle, wrap, orientation = "horizontal" }) {
736
736
  const s = useAureaStrings();
737
+ // S-04 (05/11/2026): o escolhido DESLIZA de um segmento ao outro, em vez de pular (`useDeslize`, no `internal`). Só na
738
+ // escolha: com `href` a troca é de página, e a página nova já nasce com o escolhido no lugar.
739
+ const fila = React.useRef(null);
740
+ useDeslize(fila, ".active", value);
737
741
  const classe = cx("segmented", fullWidth && "segmented-full", size === "sm" && "segmented-sm", size === "lg" && "segmented-lg", size === "xl" && "segmented-xl", size === "2xl" && "segmented-2xl", wrap && orientation !== "vertical" && "segmented-wrap", orientation === "vertical" && "segmented-vertical");
738
742
  const miolo = (i) => i.icon ? _jsxs(_Fragment, { children: [_jsx(Icon, { name: i.icon, weight: i.value === value ? "fill" : "regular" }), i.label] }) : i.label;
739
743
  if (items.length > 0 && items.every(i => i.href != null))
740
744
  return _jsx("nav", { className: classe, "data-segment-style": selectedStyle, "aria-label": label ?? s.optionsLabel, children: items.map(i => _jsx("a", { href: i.href, className: i.value === value ? "active" : undefined, "aria-current": i.value === value ? "page" : undefined, children: miolo(i) }, i.value)) });
741
- return _jsx(BaseRadioGroup, { className: classe, "data-segment-style": selectedStyle, "aria-label": label ?? s.optionsLabel, "aria-orientation": orientation === "vertical" ? "vertical" : undefined, value: value, onValueChange: v => onChange?.(String(v)), children: items.map(i => _jsx(BaseRadio.Root, { value: i.value, className: i.value === value ? "active" : undefined, nativeButton: true, render: _jsx("button", { type: "button" }), children: miolo(i) }, i.value)) });
745
+ return _jsx(BaseRadioGroup, { ref: fila, className: classe, "data-segment-style": selectedStyle, "aria-label": label ?? s.optionsLabel, "aria-orientation": orientation === "vertical" ? "vertical" : undefined, value: value, onValueChange: v => onChange?.(String(v)), children: items.map(i => _jsx(BaseRadio.Root, { value: i.value, className: i.value === value ? "active" : undefined, nativeButton: true, render: _jsx("button", { type: "button" }), children: miolo(i) }, i.value)) });
742
746
  }
743
747
  const ehAgrupado = (items) => items != null && items.length > 0 && typeof items[0] === "object" && items[0] != null && "items" in items[0];
744
748
  // UM renderizador e UMA lista para os dois combobox: duas cópias da mesma árvore é como a
@@ -33,6 +33,12 @@ export declare const DentroDoProviderContext: React.Context<boolean>;
33
33
  export declare const avisosAncorados: import("@base-ui/react").ToastManager<any>;
34
34
  export declare const PortalContext: React.Context<HTMLElement | null | undefined>;
35
35
  export declare const usePortalContainer: () => HTMLElement | null | undefined;
36
+ export declare const reduzMovimento: () => boolean;
37
+ export declare function tempoDoToken(el: Element, duracao: string, curva: string): {
38
+ duration: number;
39
+ easing: string;
40
+ } | null;
41
+ export declare function useDeslize(ref: React.RefObject<HTMLElement | null>, seletor: string, chave: unknown): void;
36
42
  export interface Reordenavel<E extends HTMLElement> {
37
43
  ref: React.RefObject<E | null>;
38
44
  pego: number | null;
package/dist/internal.js CHANGED
@@ -14,7 +14,7 @@
14
14
  // NÃO é subpath — `@aurea-uds/react/internal` não existe, de propósito.
15
15
  // Kbd mora aqui por dependência, não por categoria: o Button precisa dele e ele não precisa de
16
16
  // ninguém. A casa pública dele continua sendo `/data-display`, que o reexporta.
17
- import { createContext, useContext, useEffect, useRef, useState } from "react";
17
+ import React, { createContext, useContext, useEffect, useLayoutEffect, useRef, useState } from "react";
18
18
  import { Toast as BaseToast } from "@base-ui/react/toast";
19
19
  import { defaultStrings, defaultSpriteUrl } from "./pure.js";
20
20
  export { cx, fundirRender, defaultStrings, ptBR, defaultSpriteUrl, universalStates, stateSeverity } from "./pure.js";
@@ -71,6 +71,111 @@ export const DentroDoProviderContext = createContext(false);
71
71
  export const avisosAncorados = BaseToast.createToastManager();
72
72
  export const PortalContext = createContext(undefined);
73
73
  export const usePortalContainer = () => useContext(PortalContext);
74
+ // direction informa o Base UI (lado dos popovers, setas do teclado). O CSS
75
+ // espelha sozinho por propriedade lógica, mas depende do atributo dir no DOM:
76
+ // quem consome precisa pôr dir="rtl" no <html>. Este provider não mexe no DOM.
77
+ // ── O MOVIMENTO (S-04 e S-05, 05/11/2026) ─────────────────────────────────────────────────────
78
+ // O Victor achou a Aurea "estática": o escolhido do `SegmentedControl` e do `Tabs` PULAVA, e o arrasto do
79
+ // `SortableList` e do `BlockEditor` ia de casa em casa. Decidido por ele (05/11/2026, "Construir nós"): sem biblioteca
80
+ // nova, CSS e a animação do próprio navegador (`Element.animate`). O tempo e a curva NÃO se escrevem aqui: saem dos
81
+ // tokens da folha (`--duration-*`, `--ease-*`), lidos no elemento — o mesmo número que o CSS usa. Sem a folha, não há
82
+ // token, e não há animação: a troca acontece de uma vez, como antes. E quem pediu "reduzir movimento" também vê de
83
+ // uma vez — a regra global do CSS não alcança a animação feita por código, então quem pergunta é este arquivo.
84
+ export const reduzMovimento = () => typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
85
+ export function tempoDoToken(el, duracao, curva) {
86
+ const cs = getComputedStyle(el);
87
+ const d = cs.getPropertyValue(duracao).trim(), easing = cs.getPropertyValue(curva).trim();
88
+ const duration = d.endsWith("ms") ? parseFloat(d) : d.endsWith("s") ? parseFloat(d) * 1000 : NaN;
89
+ return Number.isFinite(duration) && duration > 0 && easing ? { duration, easing } : null;
90
+ }
91
+ // O nome das animações deste arquivo: para cancelar só as nossas, e não a transição de CSS da mesma peça.
92
+ const ANIMACAO = "aurea-movimento";
93
+ // ── useDeslize (S-04) ─────────────────────────────────────────────────────────────────────────
94
+ // O ESCOLHIDO DESLIZA. A referência tem uma peça separada atrás dos itens. Aqui o escolhido continua se pintando — as
95
+ // sete cores do segmento, as treze marcas e os dois temas são regras de CSS que este gancho NÃO repete —, e só na troca
96
+ // entra uma peça de passagem: ela COPIA a pintura do novo escolhido (o fundo, a sombra, o aro e o fio de baixo, que na
97
+ // aba secundária é o único sinal), esconde a do item e anda da posição de antes até a nova. No fim, sai, e o item volta
98
+ // a se pintar. Copiar o que o navegador calculou, em vez de reescrever as cores, é o que garante que o escolhido em
99
+ // repouso fique IDÊNTICO ao de hoje. Troca no meio do caminho parte de onde a peça está, e não do item de antes.
100
+ // `seletor` acha o escolhido dentro de `ref`; `chave` é o valor escolhido.
101
+ export function useDeslize(ref, seletor, chave) {
102
+ const anterior = useRef(null);
103
+ const emCurso = useRef(null);
104
+ React.useLayoutEffect(() => () => { emCurso.current?.(); }, []);
105
+ React.useLayoutEffect(() => {
106
+ const raiz = ref.current;
107
+ if (!raiz)
108
+ return;
109
+ const novo = raiz.querySelector(seletor);
110
+ const velho = anterior.current;
111
+ anterior.current = novo;
112
+ const corrente = emCurso.current;
113
+ emCurso.current = null;
114
+ const de = corrente ? corrente() : velho && velho !== novo && velho.isConnected ? velho.getBoundingClientRect() : null;
115
+ if (!novo || !de || !de.width || reduzMovimento() || typeof novo.animate !== "function")
116
+ return;
117
+ const tempo = tempoDoToken(raiz, "--duration-slow", "--ease-emphasized");
118
+ if (!tempo)
119
+ return;
120
+ const para = novo.getBoundingClientRect();
121
+ if (!para.width)
122
+ return;
123
+ const cs = getComputedStyle(novo), fio = getComputedStyle(novo, "::after");
124
+ const passagem = document.createElement("span");
125
+ passagem.className = "selection-indicator";
126
+ passagem.setAttribute("aria-hidden", "true");
127
+ Object.assign(passagem.style, { left: "0px", top: "0px", borderRadius: cs.borderRadius, backgroundColor: cs.backgroundColor,
128
+ backgroundImage: cs.backgroundImage, boxShadow: cs.boxShadow });
129
+ if (fio.content && fio.content !== "none" && fio.content !== "normal" && fio.display !== "none") {
130
+ const linha = document.createElement("span");
131
+ Object.assign(linha.style, { left: fio.left, right: fio.right, bottom: fio.bottom, height: fio.height,
132
+ borderRadius: fio.borderRadius, backgroundColor: fio.backgroundColor });
133
+ passagem.append(linha);
134
+ }
135
+ // A peça mora DENTRO da fila, atrás dos itens (`z-index:-1` num contexto de empilhamento só dela), e a fila só
136
+ // ganha posição e contexto durante a passagem — em repouso, nada nela muda.
137
+ const antes = { position: raiz.style.position, isolation: raiz.style.isolation };
138
+ if (getComputedStyle(raiz).position === "static")
139
+ raiz.style.position = "relative";
140
+ raiz.style.isolation = "isolate";
141
+ const rr = raiz.getBoundingClientRect();
142
+ const quadro = (r) => ({ transform: `translate(${r.left - rr.left - raiz.clientLeft + raiz.scrollLeft}px,${r.top - rr.top - raiz.clientTop + raiz.scrollTop}px)`,
143
+ width: `${r.width}px`, height: `${r.height}px` });
144
+ const pintura = ["background-color", "background-image", "box-shadow"].map(p => [p, novo.style.getPropertyValue(p), novo.style.getPropertyPriority(p)]);
145
+ for (const [p] of pintura)
146
+ novo.style.setProperty(p, p === "background-color" ? "transparent" : "none", "important");
147
+ novo.setAttribute("data-sliding", "");
148
+ raiz.prepend(passagem);
149
+ const a = passagem.animate([quadro(de), quadro(para)], { ...tempo, fill: "forwards", id: ANIMACAO });
150
+ let feito = false;
151
+ const encerra = () => {
152
+ const onde = passagem.getBoundingClientRect();
153
+ if (feito)
154
+ return onde;
155
+ feito = true;
156
+ a.cancel();
157
+ passagem.remove();
158
+ for (const [p, v, pr] of pintura) {
159
+ if (v)
160
+ novo.style.setProperty(p, v, pr);
161
+ else
162
+ novo.style.removeProperty(p);
163
+ }
164
+ if (!novo.getAttribute("style"))
165
+ novo.removeAttribute("style");
166
+ novo.removeAttribute("data-sliding");
167
+ raiz.style.position = antes.position;
168
+ raiz.style.isolation = antes.isolation;
169
+ if (!raiz.getAttribute("style"))
170
+ raiz.removeAttribute("style");
171
+ if (emCurso.current === encerra)
172
+ emCurso.current = null;
173
+ return onde;
174
+ };
175
+ emCurso.current = encerra;
176
+ a.onfinish = () => { encerra(); };
177
+ }, [chave]);
178
+ }
74
179
  export function useReorder({ count, order, onReorder, rowSelector, handleSelector }) {
75
180
  const s = useAureaStrings();
76
181
  const ref = useRef(null);
@@ -79,16 +184,55 @@ export function useReorder({ count, order, onReorder, rowSelector, handleSelecto
79
184
  const [aviso, setAviso] = useState("");
80
185
  const origem = useRef(null);
81
186
  const arrasto = useRef(null);
187
+ // S-05: a MÃO — a linha que o ponteiro pegou, onde o ponteiro estava ao pegar e agora, e o topo dela ao pegar. Com isso a
188
+ // linha anda com o ponteiro (`translate`), e não só pula de casa em casa.
189
+ const mao = useRef(null);
190
+ // S-05: onde cada linha estava NA TELA antes da troca — a conta do "First, Last, Invert, Play": depois de a ordem nova
191
+ // entrar, cada vizinho parte de onde estava e desliza até o lugar novo.
192
+ const antes = useRef(null);
82
193
  const anuncia = (verbo, i) => setAviso(`${verbo}: ${i + 1} ${s.positionOf} ${count}`);
83
194
  const alcas = () => [...(ref.current?.querySelectorAll(handleSelector) ?? [])];
195
+ const linhas = () => [...(ref.current?.querySelectorAll(rowSelector) ?? [])];
196
+ // `offsetTop` não conta o `translate` nem a animação: é o lugar da linha na lista, e não onde ela aparece agora.
197
+ const seguir = (m) => { m.linha.style.translate = `0 ${m.y - m.y0 - (m.linha.offsetTop - m.topo0)}px`; };
84
198
  // Vai por efeito e não por `requestAnimationFrame`: o React precisa ter commitado a ordem nova
85
199
  // antes de procurar a alça.
86
200
  useEffect(() => { if (foco !== null) {
87
201
  alcas()[foco]?.focus();
88
202
  setFoco(null);
89
203
  } }, [foco, order]);
90
- const mover = (de, para) => { if (para >= 0 && para < count && de !== para)
91
- onReorder(de, para); };
204
+ // A ordem nova entrou: a linha pega volta para baixo do ponteiro, e os vizinhos deslizam de onde estavam.
205
+ useLayoutEffect(() => {
206
+ const m = mao.current;
207
+ if (m)
208
+ seguir(m);
209
+ const mapa = antes.current;
210
+ antes.current = null;
211
+ if (!mapa || !ref.current || reduzMovimento())
212
+ return;
213
+ const tempo = tempoDoToken(ref.current, "--duration-base", "--ease-emphasized");
214
+ if (!tempo)
215
+ return;
216
+ for (const l of linhas()) {
217
+ if (m && l === m.linha)
218
+ continue;
219
+ const de = mapa.get(l);
220
+ if (de == null || typeof l.animate !== "function")
221
+ continue;
222
+ for (const a of l.getAnimations?.() ?? [])
223
+ if (a.id === ANIMACAO)
224
+ a.cancel();
225
+ const d = de - l.getBoundingClientRect().top;
226
+ if (Math.abs(d) >= 1)
227
+ l.animate([{ transform: `translateY(${d}px)` }, { transform: "none" }], { ...tempo, id: ANIMACAO });
228
+ }
229
+ }, [order]);
230
+ const mover = (de, para) => {
231
+ if (para >= 0 && para < count && de !== para) {
232
+ antes.current = new Map(linhas().map(l => [l, l.getBoundingClientRect().top]));
233
+ onReorder(de, para);
234
+ }
235
+ };
92
236
  const teclado = (e, i) => {
93
237
  if (e.key === " " || e.key === "Enter") {
94
238
  e.preventDefault();
@@ -130,23 +274,56 @@ export function useReorder({ count, order, onReorder, rowSelector, handleSelecto
130
274
  }
131
275
  };
132
276
  // O alvo é a primeira linha cujo MEIO ainda está abaixo do ponteiro — a conta que faz o item
133
- // trocar de lugar ao passar da metade da linha vizinha, e não ao encostar nela.
277
+ // trocar de lugar ao passar da metade da linha vizinha, e não ao encostar nela. O meio é o do LUGAR da linha
278
+ // (`offsetTop`), e não o de onde ela aparece: a linha pega anda com o ponteiro e os vizinhos estão deslizando, e medir a
279
+ // caixa na tela faria o alvo tremer. A origem dos `offsetTop` na tela sai da própria lista.
134
280
  const ponteiroMove = (e) => {
135
281
  const de = arrasto.current;
136
282
  if (de === null)
137
283
  return;
138
- const linhas = [...(ref.current?.querySelectorAll(rowSelector) ?? [])];
139
- const achado = linhas.findIndex(l => { const r = l.getBoundingClientRect(); return e.clientY < r.top + r.height / 2; });
140
- const para = achado === -1 ? linhas.length - 1 : achado;
284
+ const m = mao.current;
285
+ if (m) {
286
+ m.y = e.clientY;
287
+ seguir(m);
288
+ }
289
+ const lista = ref.current;
290
+ if (!lista)
291
+ return;
292
+ const ls = linhas();
293
+ const caixa = lista.getBoundingClientRect();
294
+ const zero = ls[0]?.offsetParent === lista ? caixa.top + lista.clientTop - lista.scrollTop : caixa.top - lista.offsetTop;
295
+ const achado = ls.findIndex(l => e.clientY < zero + l.offsetTop + l.offsetHeight / 2);
296
+ const para = achado === -1 ? ls.length - 1 : achado;
141
297
  if (para !== de) {
142
298
  mover(de, para);
143
299
  arrasto.current = para;
144
300
  setPego(para);
145
301
  }
146
302
  };
303
+ // Ao soltar, a linha desliza do ponteiro até o lugar dela, e só então desce de volta para o nível das outras.
304
+ const pousa = () => {
305
+ const m = mao.current;
306
+ mao.current = null;
307
+ if (!m)
308
+ return;
309
+ const d = m.y - m.y0 - (m.linha.offsetTop - m.topo0);
310
+ m.linha.style.translate = "";
311
+ if (!m.linha.getAttribute("style"))
312
+ m.linha.removeAttribute("style");
313
+ const fim = () => m.linha.removeAttribute("data-dragging");
314
+ const tempo = ref.current && !reduzMovimento() && typeof m.linha.animate === "function" ? tempoDoToken(ref.current, "--duration-base", "--ease-emphasized") : null;
315
+ if (!tempo || Math.abs(d) < 1) {
316
+ fim();
317
+ return;
318
+ }
319
+ const a = m.linha.animate([{ transform: `translateY(${d}px)` }, { transform: "none" }], { ...tempo, id: ANIMACAO });
320
+ a.onfinish = fim;
321
+ a.oncancel = fim;
322
+ };
147
323
  const ponteiroSolta = () => {
148
324
  if (arrasto.current === null)
149
325
  return;
326
+ pousa();
150
327
  anuncia(s.sortableDropped, arrasto.current);
151
328
  arrasto.current = null;
152
329
  setPego(null);
@@ -157,6 +334,14 @@ export function useReorder({ count, order, onReorder, rowSelector, handleSelecto
157
334
  e.currentTarget.setPointerCapture(e.pointerId);
158
335
  arrasto.current = i;
159
336
  setPego(i);
337
+ const linha = e.currentTarget.closest(rowSelector);
338
+ if (linha) {
339
+ for (const a of linha.getAnimations?.() ?? [])
340
+ if (a.id === ANIMACAO)
341
+ a.finish();
342
+ linha.setAttribute("data-dragging", "");
343
+ mao.current = { linha, y0: e.clientY, topo0: linha.offsetTop, y: e.clientY };
344
+ }
160
345
  };
161
346
  return { ref, pego, aviso, teclado, ponteiroBaixo, ponteiroMove, ponteiroSolta };
162
347
  }
@@ -3,7 +3,7 @@ import { type Orientation, type Responsive } from "./pure.js";
3
3
  import { type IconButtonProps } from "./actions.js";
4
4
  import { type HeaderVariant, type HeaderTab, type SidebarVariant, type SidebarScrollbar, type SidebarEntry, type SidebarCollapsible, type SidebarIndicator } from "./navigation.js";
5
5
  declare function AppShellSidebarToggle(props: Omit<IconButtonProps, "icon" | "label" | "onClick" | "variant">): React.JSX.Element;
6
- export declare function AppShell({ brand, navigation, navItems, currentNavId, navLabel, topbar, topbarVariant, topbarDivider, topbarTabs, topbarCurrentTab, topbarTabsLabel, topbarTabsEnd, topbarActions, topbarBlurred, layout, sidebarVariant, sidebarScrollbar, sidebarFooter, sidebarCollapsed, sidebarCollapsible, sidebarToggle, sidebarShortcut, sidebarIndicator, sidebarPanel, sidebarContrast, defaultSidebarCollapsed, onSidebarCollapsedChange, contentVariant, aside, asideLabel, asideVariant, children, className, ...props }: HTMLAttributes<HTMLDivElement> & RefAttributes<HTMLDivElement> & {
6
+ export declare function AppShell({ brand, navigation, navItems, currentNavId, navLabel, topbar, topbarVariant, topbarDivider, topbarTabs, topbarCurrentTab, topbarTabsLabel, topbarTabsEnd, topbarActions, topbarBlurred, layout, sidebarVariant, sidebarScrollbar, sidebarFooter, sidebarCollapsed, sidebarCollapsible, sidebarToggle, sidebarShortcut, sidebarIndicator, sidebarPanel, sidebarContrast, defaultSidebarCollapsed, onSidebarCollapsedChange, contentVariant, highContrast, textSize, aside, asideLabel, asideVariant, children, className, ...props }: HTMLAttributes<HTMLDivElement> & RefAttributes<HTMLDivElement> & {
7
7
  brand: ReactNode;
8
8
  navigation?: ReactNode;
9
9
  navItems?: SidebarEntry[];
@@ -74,6 +74,13 @@ export declare function AppShell({ brand, navigation, navItems, currentNavId, na
74
74
  /** AN-01: avisa cada troca — pelo botão, ou pela largura cruzando 1280. Guardar a preferência é do app. */
75
75
  onSidebarCollapsedChange?: (collapsed: boolean) => void;
76
76
  contentVariant?: "surface" | "plain";
77
+ /** O alto contraste no app inteiro (o MNT-30, ADR-0090): contorno nos campos, nos cartões e nos botões, o texto de apoio
78
+ * mais perto do texto e o link sempre sublinhado. Marca o `AppShell` e a raiz da página (`data-contrast="high"`), para
79
+ * os menus e as janelas, que abrem fora dele, saírem iguais. */
80
+ highContrast?: boolean;
81
+ /** O texto grande no app inteiro (o MNT-30, ADR-0090): cada letra 2 acima e o controle médio a 44. `data-text="lg"` no
82
+ * `AppShell` e na raiz da página, pela mesma razão do `highContrast`. */
83
+ textSize?: "md" | "lg";
77
84
  }): React.JSX.Element;
78
85
  export declare namespace AppShell {
79
86
  export { AppShellSidebarToggle as SidebarToggle };
@@ -3,7 +3,7 @@ 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 { useEffect, useRef, useState, useSyncExternalStore } from "react";
6
+ import React, { useEffect, useRef, useState, useSyncExternalStore } from "react";
7
7
  import { Separator as BaseSeparator } from "@base-ui/react/separator";
8
8
  import { useDirection } from "@base-ui/react/direction-provider";
9
9
  import { cx, useAureaStrings } from "./internal.js";
@@ -167,8 +167,25 @@ function ProvedorDaLateral({ ligado, children, ...p }) {
167
167
  return ligado ? _jsx(Sidebar.Provider, { id: SHELL_NAV_ID, ...p, children: children }) : _jsx(_Fragment, { children: children });
168
168
  }
169
169
  function AppShellSidebarToggle(props) { return _jsx(Sidebar.Toggle, { ...props }); }
170
- export function AppShell({ brand, navigation, navItems, currentNavId, navLabel, topbar, topbarVariant = "floating", topbarDivider, topbarTabs, topbarCurrentTab, topbarTabsLabel, topbarTabsEnd, topbarActions, topbarBlurred, layout = "app", sidebarVariant, sidebarScrollbar, sidebarFooter, sidebarCollapsed, sidebarCollapsible, sidebarToggle = "edge", sidebarShortcut = true, sidebarIndicator, sidebarPanel, sidebarContrast, defaultSidebarCollapsed, onSidebarCollapsedChange, contentVariant = "surface", aside, asideLabel, asideVariant, children, className, ...props }) {
170
+ // O modo de leitura (o alto contraste, o texto grande) também na RAIZ da página (ADR-0090): menus, janelas e avisos abrem
171
+ // fora do `AppShell`, presos ao fim do documento, e sem isto sairiam sem o modo. Devolve a raiz como estava ao sair.
172
+ function useModoNaRaiz(atributo, valor) {
173
+ React.useEffect(() => {
174
+ if (valor === undefined || typeof document === "undefined")
175
+ return;
176
+ const raiz = document.documentElement;
177
+ const antes = raiz.getAttribute(atributo);
178
+ raiz.setAttribute(atributo, valor);
179
+ return () => { if (antes === null)
180
+ raiz.removeAttribute(atributo);
181
+ else
182
+ raiz.setAttribute(atributo, antes); };
183
+ }, [atributo, valor]);
184
+ }
185
+ export function AppShell({ brand, navigation, navItems, currentNavId, navLabel, topbar, topbarVariant = "floating", topbarDivider, topbarTabs, topbarCurrentTab, topbarTabsLabel, topbarTabsEnd, topbarActions, topbarBlurred, layout = "app", sidebarVariant, sidebarScrollbar, sidebarFooter, sidebarCollapsed, sidebarCollapsible, sidebarToggle = "edge", sidebarShortcut = true, sidebarIndicator, sidebarPanel, sidebarContrast, defaultSidebarCollapsed, onSidebarCollapsedChange, contentVariant = "surface", highContrast, textSize, aside, asideLabel, asideVariant, children, className, ...props }) {
171
186
  const s = useAureaStrings();
187
+ useModoNaRaiz("data-contrast", highContrast ? "high" : undefined);
188
+ useModoNaRaiz("data-text", textSize === "lg" ? "lg" : undefined);
172
189
  const docs = layout === "docs", lateral = layout === "sidebar";
173
190
  useGavetaFechaNoDesktop(docs);
174
191
  const controlado = sidebarCollapsed !== undefined;
@@ -224,7 +241,7 @@ export function AppShell({ brand, navigation, navItems, currentNavId, navLabel,
224
241
  // No arranjo, o topo é o pequeno (`size="sm"`), e o botão também: com o tamanho de sempre ele ficava 4 maior que os
225
242
  // vizinhos e fora da linha deles (medido a 375, sem toque; com toque, todos sobem para 44).
226
243
  const botaoDaGaveta = _jsx(IconButton, { className: "nav-toggle", icon: "list", label: s.navigationToggle, popoverTarget: SHELL_NAV_ID, ...(docs ? { size: "sm" } : {}) });
227
- return _jsx("div", { className: cx("app-shell", layout === "docs" && "app-shell-docs", lateral && "app-shell-lateral", !lateral && topbarVariant === "flush" && "app-shell-flush", !lateral && topbarVariant === "pill" && "app-shell-pill", recolhe && "app-shell-collapsible", aside != null && "app-shell-with-aside", className), ...props, children: _jsxs(ProvedorDaLateral, { ligado: recolhe, collapsed: efetiva, onCollapsedChange: definir, collapsible: forma, shortcut: sidebarShortcut, children: [_jsx("a", { className: "skip-link", href: `#${SHELL_MAIN_ID}`, children: s.skipToContent }), !lateral && _jsxs(Header, { variant: topbarVariant, divider: topbarDivider, tabs: topbarTabs, currentTab: topbarCurrentTab, tabsLabel: topbarTabsLabel, tabsEnd: topbarTabsEnd, ...(docs ? { maxWidth: "docs", ...(topbarVariant === "pill" ? {} : { size: "sm" }) } : topbarVariant === "pill" ? { maxWidth: "full" } : {}), blurred: topbarBlurred, brand: docs ? brand : _jsxs(_Fragment, { children: [botaoDaGaveta, brand] }), actions: topbarActions != null ? _jsxs(_Fragment, { children: [topbarActions, docs && botaoDaGaveta] }) : undefined, children: [topbar, docs && topbarActions == null && botaoDaGaveta] }), recolhe && !lateral && sidebarToggle !== "none" && _jsx("div", { className: "sidebar-toggle-slot", children: _jsx(Tooltip, { content: rotulo, side: "right", children: _jsx(IconButton, { className: "sidebar-toggle", size: "xs", icon: paraOComeco === direita ? "caret-right" : "caret-left", label: rotulo, "aria-expanded": !efetiva, "aria-controls": SHELL_NAV_ID, onClick: alternar }) }) }), _jsx(Sidebar, { id: SHELL_NAV_ID, popover: "auto", variant: sidebarVariant, scrollbar: sidebarScrollbar, collapsed: efetiva, collapsible: forma, indicator: sidebarIndicator, panel: sidebarPanel, contrast: sidebarContrast, items: navItems, current: currentNavId, label: navLabel, onToggle: focoDaGaveta, header: lateral ? brand : undefined, footer: sidebarFooter, children: navigation }), docs && aside != null && _jsx("div", { className: "docs-toc-bar", children: _jsxs("button", { type: "button", className: "docs-toc-toggle", popoverTarget: SHELL_TOC_ID, children: [_jsx(Icon, { name: "list-bullets" }), _jsx("span", { className: "docs-toc-current", "data-toc-current": "", children: asideLabel ?? s.tocLabel }), _jsx(Icon, { name: "caret-down" })] }) }), lateral
244
+ return _jsx("div", { "data-contrast": highContrast ? "high" : undefined, "data-text": textSize === "lg" ? "lg" : undefined, className: cx("app-shell", layout === "docs" && "app-shell-docs", lateral && "app-shell-lateral", !lateral && topbarVariant === "flush" && "app-shell-flush", !lateral && topbarVariant === "pill" && "app-shell-pill", recolhe && "app-shell-collapsible", aside != null && "app-shell-with-aside", className), ...props, children: _jsxs(ProvedorDaLateral, { ligado: recolhe, collapsed: efetiva, onCollapsedChange: definir, collapsible: forma, shortcut: sidebarShortcut, children: [_jsx("a", { className: "skip-link", href: `#${SHELL_MAIN_ID}`, children: s.skipToContent }), !lateral && _jsxs(Header, { variant: topbarVariant, divider: topbarDivider, tabs: topbarTabs, currentTab: topbarCurrentTab, tabsLabel: topbarTabsLabel, tabsEnd: topbarTabsEnd, ...(docs ? { maxWidth: "docs", ...(topbarVariant === "pill" ? {} : { size: "sm" }) } : topbarVariant === "pill" ? { maxWidth: "full" } : {}), blurred: topbarBlurred, brand: docs ? brand : _jsxs(_Fragment, { children: [botaoDaGaveta, brand] }), actions: topbarActions != null ? _jsxs(_Fragment, { children: [topbarActions, docs && botaoDaGaveta] }) : undefined, children: [topbar, docs && topbarActions == null && botaoDaGaveta] }), recolhe && !lateral && sidebarToggle !== "none" && _jsx("div", { className: "sidebar-toggle-slot", children: _jsx(Tooltip, { content: rotulo, side: "right", children: _jsx(IconButton, { className: "sidebar-toggle", size: "xs", icon: paraOComeco === direita ? "caret-right" : "caret-left", label: rotulo, "aria-expanded": !efetiva, "aria-controls": SHELL_NAV_ID, onClick: alternar }) }) }), _jsx(Sidebar, { id: SHELL_NAV_ID, popover: "auto", variant: sidebarVariant, scrollbar: sidebarScrollbar, collapsed: efetiva, collapsible: forma, indicator: sidebarIndicator, panel: sidebarPanel, contrast: sidebarContrast, items: navItems, current: currentNavId, label: navLabel, onToggle: focoDaGaveta, header: lateral ? brand : undefined, footer: sidebarFooter, children: navigation }), docs && aside != null && _jsx("div", { className: "docs-toc-bar", children: _jsxs("button", { type: "button", className: "docs-toc-toggle", popoverTarget: SHELL_TOC_ID, children: [_jsx(Icon, { name: "list-bullets" }), _jsx("span", { className: "docs-toc-current", "data-toc-current": "", children: asideLabel ?? s.tocLabel }), _jsx(Icon, { name: "caret-down" })] }) }), lateral
228
245
  ? _jsxs("div", { className: "app-shell-column", children: [topbar != null || topbarActions != null
229
246
  ? _jsx(Header, { variant: "flush", size: "sm", divider: topbarDivider, brand: _jsxs(_Fragment, { children: [botaoDaGaveta, sidebarToggle !== "none" && _jsx(AppShellSidebarToggle, {})] }), actions: topbarActions, children: topbar })
230
247
  : _jsxs("div", { className: "app-shell-narrow-bar", children: [botaoDaGaveta, brand] }), _jsx("main", { id: SHELL_MAIN_ID, tabIndex: -1, className: cx("content", contentVariant === "plain" && "content-plain"), children: children })] })
package/dist/markup.d.ts CHANGED
@@ -200,7 +200,8 @@ export interface KPIProps extends HTMLAttributes<HTMLDivElement>, RefAttributes<
200
200
  /** As barras embaixo do número (o site B, 0.54.0): cada uma é um botão de opção nativo; a escolhida fica branca no
201
201
  * `brand` (na cor do texto, fora dele) e a leitura diz o nome e o valor dela. */
202
202
  bars?: KPIBar[];
203
- /** A barra que começa escolhida (o índice). Padrão: a última. */
203
+ /** A barra que começa escolhida (o índice). Padrão: a última. Dois `KPI` com o mesmo título E as mesmas barras na
204
+ * mesma página precisam de `id` diferentes, para cada um guardar a sua barra. */
204
205
  defaultBar?: number;
205
206
  }
206
207
  export declare function KPI({ label, value, trend, direction, tone, directionLabel, variant, bars, defaultBar, className, ...props }: KPIProps): React.JSX.Element;
package/dist/markup.js CHANGED
@@ -119,6 +119,10 @@ export function Section({ surface = "background", spacing = "md", theme, variant
119
119
  }
120
120
  const TOM_DA_DIRECAO = { up: "success", down: "danger", flat: "neutral" };
121
121
  const PALAVRA_DA_DIRECAO = { up: "Up", down: "Down", flat: "No change" };
122
+ // Uma assinatura curta e determinística de um texto (djb2, em base 36): o mesmo texto dá o mesmo nome no servidor e no
123
+ // navegador, sem hook.
124
+ const assinatura = (texto) => { let h = 5381; for (let i = 0; i < texto.length; i++)
125
+ h = ((h << 5) + h + texto.charCodeAt(i)) | 0; return (h >>> 0).toString(36); };
122
126
  export function KPI({ label, value, trend, direction, tone, directionLabel, variant = "card", bars, defaultBar, className, ...props }) {
123
127
  const tom = direction ? (tone ?? TOM_DA_DIRECAO[direction]) : undefined;
124
128
  const miolo = _jsxs(_Fragment, { children: [_jsx("span", { className: "muted", children: label }), _jsx("strong", { children: value }), (trend || trend === 0 || direction) && _jsxs("small", { className: direction ? cx("kpi-trend", `kpi-trend-${direction}`, `kpi-tone-${tom}`) : undefined, children: [direction && _jsx("span", { className: "kpi-trend-icon", "aria-hidden": "true" }), direction && _jsx("span", { className: "sr-only", children: directionLabel ?? PALAVRA_DA_DIRECAO[direction] }), trend] })] });
@@ -127,8 +131,13 @@ export function KPI({ label, value, trend, direction, tone, directionLabel, vari
127
131
  const leituraDe = (b) => `${b.label} · ${typeof b.value === "string" || typeof b.value === "number" ? b.value : ""}`;
128
132
  // As barras são botões de opção NATIVOS (o KPI é marcação pura, roda no servidor): o grupo é um `fieldset` com o rótulo
129
133
  // do KPI, cada barra o `label` de um rádio com a leitura dela; o CSS mostra a leitura da escolhida e a da que está sob o
130
- // mouse. O `name` vem do rótulo quando é texto.
131
- const nomeDoGrupo = `kpi-bars-${typeof label === "string" ? label.toLowerCase().replace(/[^a-z0-9]+/g, "-") : "x"}`;
134
+ // mouse.
135
+ // O `name` do grupo (auditoria externa de 05/11/2026): vinha SÓ do rótulo, e dois KPI com o mesmo título — ou com o
136
+ // título em JSX, que virava "x" — caíam no mesmo grupo de rádios; medido no Chromium e no Firefox, o primeiro já
137
+ // perdia a barra escolhida ao carregar. Marcação pura não chama `useId` (a mesma regra do `menuId` do `Header`), então
138
+ // o nome junta o `id` do KPI, quando o app passa um, o rótulo e uma assinatura das próprias barras: dois KPI com dados
139
+ // diferentes nunca colidem, e dois idênticos se separam pelo `id`.
140
+ const nomeDoGrupo = bars && bars.length > 0 ? `kpi-bars-${props.id ? `${props.id}-` : ""}${typeof label === "string" ? label.toLowerCase().replace(/[^a-z0-9]+/g, "-") : "x"}-${assinatura(bars.map(b => `${b.label}:${b.amount}`).join("|"))}` : "";
132
141
  const barras = bars && bars.length > 0 && _jsxs("fieldset", { className: "kpi-bars-wrap", children: [typeof label === "string" && _jsx("legend", { className: "sr-only", children: label }), _jsx("div", { className: "kpi-bars", style: { "--kpi-bars": bars.length }, children: bars.map((b, i) => _jsxs("label", { className: "kpi-bar", children: [_jsx("input", { type: "radio", className: "kpi-bar-input", name: nomeDoGrupo, value: String(i), defaultChecked: i === escolhida, "aria-label": leituraDe(b) }), _jsx("span", { className: "kpi-bar-fill", style: { "--kpi-bar-h": `${Math.round(b.amount / maior * 100)}%` } }), _jsx("span", { className: "kpi-bar-reading", "aria-hidden": "true", children: leituraDe(b) })] }, i)) }), _jsx("div", { className: "kpi-bars-x", "aria-hidden": "true", style: { "--kpi-bars": bars.length }, children: bars.map((b, i) => _jsx("span", { children: b.label }, i)) })] });
133
142
  return variant === "plain" ? _jsxs("div", { className: cx("kpi", className), ...props, children: [miolo, barras] })
134
143
  : _jsxs(Card, { className: cx("kpi", variant === "brand" && "kpi-brand", className), ...props, children: [miolo, barras] });
@@ -10,7 +10,7 @@ import { Autocomplete as BaseAutocomplete } from "@base-ui/react/autocomplete";
10
10
  import { Dialog as BaseDialog } from "@base-ui/react/dialog";
11
11
  import { Popover as BasePopover } from "@base-ui/react/popover";
12
12
  import { useValorResponsivo } from "./responsivo-runtime.js";
13
- import { cx, fundirRender, useAureaStrings, usePortalContainer } from "./internal.js";
13
+ import { cx, fundirRender, useAureaStrings, usePortalContainer, useDeslize } from "./internal.js";
14
14
  import { Kbd, ProgressCircle } from "./markup.js";
15
15
  import { Icon } from "./system.js";
16
16
  import { Button, IconButton } from "./actions.js";
@@ -37,7 +37,7 @@ export function Stepper({ items, label, variant = "default", startContent, endCo
37
37
  return lista;
38
38
  }
39
39
  export function Breadcrumb({ items, label }) { const s = useAureaStrings(); return _jsx("nav", { className: "breadcrumb", "aria-label": label ?? s.breadcrumbLabel, children: items.map((i, n) => _jsxs(React.Fragment, { children: [n > 0 && _jsx(Icon, { name: "caret-right", size: "sm" }), " ", i.render ? fundirRender(i.render, { children: i.label }, "a") : i.href ? _jsx("a", { href: i.href, children: i.label }) : _jsx("strong", { "aria-current": "page", children: i.label })] }, n)) }); }
40
- export function Tabs({ tabs, value, onChange, label, orientation, activateOnFocus = true, loopFocus, panel = "card", variant = "primary" }) { const s = useAureaStrings(); const ancora = useRef(null); const resolvida = useValorResponsivo(orientation, "horizontal", ancora); return _jsxs(BaseTabs.Root, { ref: ancora, value: value, onValueChange: v => onChange(String(v)), orientation: resolvida, className: "tabs-root", children: [_jsx(BaseTabs.List, { className: cx("tabs", variant === "secondary" && "tabs-secondary"), "aria-label": label ?? s.tabsLabel, activateOnFocus: activateOnFocus, loopFocus: loopFocus, children: tabs.map(t => _jsx(BaseTabs.Tab, { value: t.id, className: "tab", children: t.label }, t.id)) }), tabs.map(t => _jsx(BaseTabs.Panel, { value: t.id, className: panel === "plain" ? "tabs-panel" : "card card-inset", tabIndex: 0, children: t.content }, t.id))] }); }
40
+ export function Tabs({ tabs, value, onChange, label, orientation, activateOnFocus = true, loopFocus, panel = "card", variant = "primary" }) { const s = useAureaStrings(); const ancora = useRef(null); const resolvida = useValorResponsivo(orientation, "horizontal", ancora); const lista = useRef(null); useDeslize(lista, ".tab[data-active]", value); return _jsxs(BaseTabs.Root, { ref: ancora, value: value, onValueChange: v => onChange(String(v)), orientation: resolvida, className: "tabs-root", children: [_jsx(BaseTabs.List, { ref: lista, className: cx("tabs", variant === "secondary" && "tabs-secondary"), "aria-label": label ?? s.tabsLabel, activateOnFocus: activateOnFocus, loopFocus: loopFocus, children: tabs.map(t => _jsx(BaseTabs.Tab, { value: t.id, className: "tab", children: t.label }, t.id)) }), tabs.map(t => _jsx(BaseTabs.Panel, { value: t.id, className: panel === "plain" ? "tabs-panel" : "card card-inset", tabIndex: 0, children: t.content }, t.id))] }); }
41
41
  function paginasVisiveis(page, total, siblings) {
42
42
  if (total <= 5 + siblings * 2)
43
43
  return Array.from({ length: total }, (_, i) => i + 1);
@@ -76,6 +76,10 @@ interface MenuItemBase {
76
76
  label: ReactNode;
77
77
  disabled?: boolean;
78
78
  leadingIcon?: IconName;
79
+ /** 0.55.0: a explicação embaixo do nome, na letra pequena (a anatomia do item da referência). */
80
+ description?: ReactNode;
81
+ /** 0.55.0: o ícone na ponta do item — por exemplo, o "abre fora" de um link que abre outro site. */
82
+ trailingIcon?: IconName;
79
83
  }
80
84
  /** Item de ação — a forma de sempre. `kind` é opcional para não quebrar quem já usa. */
81
85
  export interface MenuActionDef extends MenuItemBase {
@@ -93,6 +97,8 @@ export interface MenuLinkDef {
93
97
  kind: "link";
94
98
  label: ReactNode;
95
99
  leadingIcon?: IconName;
100
+ description?: ReactNode;
101
+ trailingIcon?: IconName;
96
102
  href: string;
97
103
  target?: string;
98
104
  rel?: string;
@@ -137,10 +143,12 @@ export type MenuEntry = MenuActionDef | MenuLinkDef | MenuCheckboxDef | MenuRadi
137
143
  /** @deprecated desde 28/08/2026 — use `MenuEntry`. Mantido porque `MenuActionDef` É a forma
138
144
  * antiga: quem tipava com `MenuItemDef` continua compilando. */
139
145
  export type MenuItemDef = MenuActionDef;
140
- export declare function DropdownMenu({ trigger, items, side, label }: {
146
+ export declare function DropdownMenu({ trigger, items, side, align, label }: {
141
147
  trigger: ReactElement;
142
148
  items: MenuEntry[];
143
149
  side?: OverlaySide;
150
+ /** 0.55.0: por onde o menu se alinha ao gatilho — `"end"` encosta a ponta dele na ponta do botão (a referência, no botão dividido). */
151
+ align?: "start" | "center" | "end";
144
152
  label?: string;
145
153
  }): React.JSX.Element;
146
154
  export declare function ContextMenu({ children, items, label, className }: {
package/dist/overlays.js CHANGED
@@ -150,26 +150,31 @@ const marca = (children) => _jsx("span", { className: "menu-mark", "aria-hidden"
150
150
  // container configurado pelo provider e o submenu escaparia para o `document.body`. Duas
151
151
  // camadas do mesmo menu em containers diferentes é o tipo de divergência que só aparece quando
152
152
  // alguém monta a Aurea dentro de um shadow root ou de um diálogo nativo.
153
- function renderMenuItems(items, portal) {
153
+ // O item com `description` (0.55.0) se anuncia como o da referência: o NOME é só o rótulo e a explicação é a DESCRIÇÃO
154
+ // (`aria-labelledby` + `aria-describedby`). Sem isso o leitor de tela lia os dois colados ("View as MarkdownView page as…").
155
+ // `ids` é o `useId` de quem monta o menu, mais a posição do item: estável no servidor e no navegador.
156
+ const nomeDoItem = (x, id) => x.description != null ? { "aria-labelledby": `${id}-l`, "aria-describedby": `${id}-d` } : {};
157
+ function renderMenuItems(items, portal, ids = "") {
154
158
  return items.map((it, i) => {
155
159
  if (it === "separator")
156
160
  return _jsx(BaseMenu.Separator, { className: "menu-sep" }, i);
157
161
  const kind = it.kind ?? "item";
158
- const dentro = (x) => _jsxs(_Fragment, { children: [x.leadingIcon && _jsx(Icon, { name: x.leadingIcon }), x.label] });
162
+ const id = `${ids}-${i}`;
163
+ const dentro = (x, xid = id) => _jsxs(_Fragment, { children: [x.leadingIcon && _jsx(Icon, { name: x.leadingIcon }), x.description != null ? _jsxs("span", { className: "menu-item-text", children: [_jsx("span", { id: `${xid}-l`, children: x.label }), _jsx("span", { id: `${xid}-d`, className: "menu-item-description", children: x.description })] }) : x.label, x.trailingIcon && _jsx(Icon, { name: x.trailingIcon, className: "menu-item-trailing" })] });
159
164
  switch (kind) {
160
165
  case "group": {
161
166
  const g = it;
162
- return _jsxs(BaseMenu.Group, { children: [g.label && _jsx(BaseMenu.GroupLabel, { className: "menu-label", children: g.label }), renderMenuItems(g.items, portal)] }, i);
167
+ return _jsxs(BaseMenu.Group, { children: [g.label && _jsx(BaseMenu.GroupLabel, { className: "menu-label", children: g.label }), renderMenuItems(g.items, portal, id)] }, i);
163
168
  }
164
169
  case "radiogroup": {
165
170
  const g = it;
166
- return _jsxs(BaseMenu.RadioGroup, { value: g.value, defaultValue: g.defaultValue, onValueChange: v => g.onValueChange?.(String(v)), children: [g.label && _jsx(BaseMenu.GroupLabel, { className: "menu-label", children: g.label }), g.items.map((r, j) => _jsxs(BaseMenu.RadioItem, { className: "menu-item", value: r.value, disabled: r.disabled, children: [marca(_jsx(BaseMenu.RadioItemIndicator, { className: "menu-ponto" })), dentro(r)] }, j))] }, i);
171
+ return _jsxs(BaseMenu.RadioGroup, { value: g.value, defaultValue: g.defaultValue, onValueChange: v => g.onValueChange?.(String(v)), children: [g.label && _jsx(BaseMenu.GroupLabel, { className: "menu-label", children: g.label }), g.items.map((r, j) => _jsxs(BaseMenu.RadioItem, { className: "menu-item", value: r.value, disabled: r.disabled, ...nomeDoItem(r, `${id}-${j}`), children: [marca(_jsx(BaseMenu.RadioItemIndicator, { className: "menu-ponto" })), dentro(r, `${id}-${j}`)] }, j))] }, i);
167
172
  }
168
173
  case "submenu": {
169
174
  const sm = it;
170
175
  // O submenu é uma RAIZ própria com gatilho próprio — não é um item que "abre outro menu".
171
176
  // É o que dá `aria-haspopup`, a seta para a direita e o fechamento em cascata de graça.
172
- return _jsxs(BaseMenu.SubmenuRoot, { children: [_jsxs(BaseMenu.SubmenuTrigger, { className: "menu-item", disabled: sm.disabled, children: [dentro(sm), _jsx(Icon, { name: "caret-right", size: "sm", className: "menu-seta" })] }), _jsx(BaseMenu.Portal, { container: portal, children: _jsx(BaseMenu.Positioner, { className: "positioner", side: "inline-end", sideOffset: 4, children: _jsx(BaseMenu.Popup, { className: "menu", children: renderMenuItems(sm.items, portal) }) }) })] }, i);
177
+ return _jsxs(BaseMenu.SubmenuRoot, { children: [_jsxs(BaseMenu.SubmenuTrigger, { className: "menu-item", disabled: sm.disabled, ...nomeDoItem(sm, id), children: [dentro(sm), _jsx(Icon, { name: "caret-right", size: "sm", className: "menu-seta" })] }), _jsx(BaseMenu.Portal, { container: portal, children: _jsx(BaseMenu.Positioner, { className: "positioner", side: "inline-end", sideOffset: 4, children: _jsx(BaseMenu.Popup, { className: "menu", children: renderMenuItems(sm.items, portal, id) }) }) })] }, i);
173
178
  }
174
179
  case "checkbox": {
175
180
  const c = it;
@@ -178,26 +183,26 @@ function renderMenuItems(items, portal) {
178
183
  // assinatura pública da Aurea mudar junto com uma versão do motor. Mesma decisão do
179
184
  // `onValueChange` do grupo de rádio. O teste pegou: sem o embrulho, o `vi.fn()` recebia um
180
185
  // `PointerEvent` de brinde.
181
- return _jsxs(BaseMenu.CheckboxItem, { className: "menu-item", disabled: c.disabled, checked: c.checked, defaultChecked: c.defaultChecked, onCheckedChange: v => c.onCheckedChange?.(v), children: [marca(_jsx(BaseMenu.CheckboxItemIndicator, { children: _jsx(Icon, { name: "check", size: "sm" }) })), dentro(c)] }, i);
186
+ return _jsxs(BaseMenu.CheckboxItem, { className: "menu-item", disabled: c.disabled, checked: c.checked, defaultChecked: c.defaultChecked, onCheckedChange: v => c.onCheckedChange?.(v), ...nomeDoItem(c, id), children: [marca(_jsx(BaseMenu.CheckboxItemIndicator, { children: _jsx(Icon, { name: "check", size: "sm" }) })), dentro(c)] }, i);
182
187
  }
183
188
  case "link": {
184
189
  const l = it;
185
- return _jsx(BaseMenu.LinkItem, { className: "menu-item", href: l.href, target: l.target, rel: l.rel, closeOnClick: l.closeOnClick ?? true, children: dentro(l) }, i);
190
+ return _jsx(BaseMenu.LinkItem, { className: "menu-item", href: l.href, target: l.target, rel: l.rel, closeOnClick: l.closeOnClick ?? true, ...nomeDoItem(l, id), children: dentro(l) }, i);
186
191
  }
187
192
  default: {
188
193
  const a = it;
189
- return _jsx(BaseMenu.Item, { className: "menu-item", disabled: a.disabled, onClick: a.onClick, render: a.render, children: dentro(a) }, i);
194
+ return _jsx(BaseMenu.Item, { className: "menu-item", disabled: a.disabled, onClick: a.onClick, render: a.render, ...nomeDoItem(a, id), children: dentro(a) }, i);
190
195
  }
191
196
  }
192
197
  });
193
198
  }
194
- export function DropdownMenu({ trigger, items, side = "bottom", label }) { const portal = usePortalContainer(); return _jsxs(BaseMenu.Root, { children: [_jsx(BaseMenu.Trigger, { render: trigger }), _jsx(BaseMenu.Portal, { container: portal, children: _jsx(BaseMenu.Positioner, { className: "positioner", side: side, sideOffset: 6, children: _jsx(BaseMenu.Popup, { className: "menu", "aria-label": label, children: renderMenuItems(items, portal) }) }) })] }); }
199
+ export function DropdownMenu({ trigger, items, side = "bottom", align = "center", label }) { const portal = usePortalContainer(); const ids = React.useId(); return _jsxs(BaseMenu.Root, { children: [_jsx(BaseMenu.Trigger, { render: trigger }), _jsx(BaseMenu.Portal, { container: portal, children: _jsx(BaseMenu.Positioner, { className: "positioner", side: side, align: align, sideOffset: 6, children: _jsx(BaseMenu.Popup, { className: "menu", "aria-label": label, children: renderMenuItems(items, portal, ids) }) }) })] }); }
195
200
  // ContextMenu: abre no botão direito e — por teclado — em Shift+F10 / tecla Menu,
196
201
  // que o browser só dispara (como evento contextmenu) sobre um elemento FOCADO. Por
197
202
  // isso o gatilho é focável (tabIndex 0) e tem nome acessível (auditoria 18/07/2026,
198
203
  // MÉDIO 3); sem isso o teclado não alcança o menu. Consumidor pode sobrescrever
199
204
  // tabIndex via children se o próprio já for focável.
200
- export function ContextMenu({ children, items, label, className }) { const portal = usePortalContainer(); return _jsxs(BaseContextMenu.Root, { children: [_jsx(BaseContextMenu.Trigger, { className: className, tabIndex: 0, "aria-label": label, "aria-haspopup": "menu", children: children }), _jsx(BaseContextMenu.Portal, { container: portal, children: _jsx(BaseContextMenu.Positioner, { className: "positioner", children: _jsx(BaseContextMenu.Popup, { className: "menu", "aria-label": label, children: renderMenuItems(items, portal) }) }) })] }); }
205
+ export function ContextMenu({ children, items, label, className }) { const portal = usePortalContainer(); const ids = React.useId(); return _jsxs(BaseContextMenu.Root, { children: [_jsx(BaseContextMenu.Trigger, { className: className, tabIndex: 0, "aria-label": label, "aria-haspopup": "menu", children: children }), _jsx(BaseContextMenu.Portal, { container: portal, children: _jsx(BaseContextMenu.Positioner, { className: "positioner", children: _jsx(BaseContextMenu.Popup, { className: "menu", "aria-label": label, children: renderMenuItems(items, portal, ids) }) }) })] }); }
201
206
  // G-AXIS-06 — o valor responsivo RESOLVIDO é a única fonte de verdade: ele vai para o motor, o
202
207
  // motor publica `data-orientation`/`aria-orientation` e ajusta o teclado, e a pele reage ao
203
208
  // atributo publicado. Ver `decisions/0047-css-first-para-apresentacao-runtime-para-semantica.md`.
@@ -206,5 +211,6 @@ export function Menubar({ menus, label, modal, disabled, loopFocus, orientation,
206
211
  const ancora = useRef(null);
207
212
  const resolvida = useValorResponsivo(orientation, "horizontal", ancora);
208
213
  const vertical = resolvida === "vertical";
209
- return _jsx(BaseMenubar, { ref: ancora, modal: modal, disabled: disabled, loopFocus: loopFocus, orientation: resolvida, "aria-label": label, className: cx("menubar", className), children: menus.map((m, i) => _jsxs(BaseMenu.Root, { disabled: m.disabled, children: [_jsx(BaseMenu.Trigger, { className: "menubar-trigger", children: m.label }), _jsx(BaseMenu.Portal, { container: portal, children: _jsx(BaseMenu.Positioner, { className: "positioner", side: vertical ? "inline-end" : "bottom", align: "start", sideOffset: 4, children: _jsx(BaseMenu.Popup, { className: "menu", children: renderMenuItems(m.items) }) }) })] }, i)) });
214
+ const ids = React.useId();
215
+ return _jsx(BaseMenubar, { ref: ancora, modal: modal, disabled: disabled, loopFocus: loopFocus, orientation: resolvida, "aria-label": label, className: cx("menubar", className), children: menus.map((m, i) => _jsxs(BaseMenu.Root, { disabled: m.disabled, children: [_jsx(BaseMenu.Trigger, { className: "menubar-trigger", children: m.label }), _jsx(BaseMenu.Portal, { container: portal, children: _jsx(BaseMenu.Positioner, { className: "positioner", side: vertical ? "inline-end" : "bottom", align: "start", sideOffset: 4, children: _jsx(BaseMenu.Popup, { className: "menu", children: renderMenuItems(m.items, portal, `${ids}-${i}`) }) }) })] }, i)) });
210
216
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aurea-uds/react",
3
- "version": "0.54.1",
3
+ "version": "0.59.0",
4
4
  "description": "Os componentes React da Aurea UDS. Importe @aurea-uds/fonts/css e @aurea-uds/core/css junto.",
5
5
  "keywords": [
6
6
  "aurea",