@aurea-uds/react 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/layout.js CHANGED
@@ -2,4 +2,6 @@
2
2
  // de propósito: a diretiva contamina o módulo inteiro e faria a marcação pura chegar como cliente
3
3
  // por vizinhança (ADR-0026; o check 26b reprova quem a puser de volta aqui).
4
4
  export * from "./layout-client.js";
5
- export { Card, Stack, Cluster, Grid, Separator } from "./markup.js";
5
+ export { Card, Stack, Cluster, Grid } from "./markup.js";
6
+ // PORTADO no merge de 28/08/2026 — sai do markup (servidor), como os demais de marcação.
7
+ export { AspectRatio } from "./markup.js";
package/dist/markup.d.ts CHANGED
@@ -1,14 +1,19 @@
1
- import React, { type HTMLAttributes, type InputHTMLAttributes, type ReactNode, type RefAttributes, type SelectHTMLAttributes, type TextareaHTMLAttributes } from "react";
1
+ import React, { type HTMLAttributes, type InputHTMLAttributes, type ReactElement, type ReactNode, type RefAttributes, type SelectHTMLAttributes, type TextareaHTMLAttributes } from "react";
2
+ import { type Responsive } from "./pure.js";
3
+ import type { ComponentSize } from "./actions.js";
2
4
  export type BadgeVariant = "neutral" | "primary" | "info" | "success" | "warning" | "danger" | "running" | "paused" | "offline" | "review";
3
5
  export type AvatarSize = "sm" | "md" | "lg";
4
6
  export type TopbarVariant = "floating" | "flush" | "pill";
5
- export declare function Card({ variant, className, ...props }: HTMLAttributes<HTMLDivElement> & RefAttributes<HTMLDivElement> & {
6
- variant?: "base" | "raised" | "interactive" | "inset" | "selected" | "danger";
7
- }): React.JSX.Element;
7
+ type CardBase = HTMLAttributes<HTMLDivElement> & RefAttributes<HTMLDivElement>;
8
+ export type CardProps = CardBase & ({
9
+ variant?: "base" | "raised" | "inset" | "selected" | "danger";
10
+ render?: ReactElement;
11
+ } | {
12
+ variant: "interactive";
13
+ render: ReactElement;
14
+ });
15
+ export declare function Card({ variant, className, render, ...props }: CardProps): ReactElement<unknown, string | React.JSXElementConstructor<any>>;
8
16
  export declare function Stack({ className, ...props }: HTMLAttributes<HTMLDivElement> & RefAttributes<HTMLDivElement>): React.JSX.Element;
9
- export declare function Separator({ orientation, className, ...props }: HTMLAttributes<HTMLHRElement> & RefAttributes<HTMLHRElement> & {
10
- orientation?: "horizontal" | "vertical";
11
- }): React.JSX.Element;
12
17
  export declare function Cluster({ className, ...props }: HTMLAttributes<HTMLDivElement> & RefAttributes<HTMLDivElement>): React.JSX.Element;
13
18
  export declare function Grid({ className, ...props }: HTMLAttributes<HTMLDivElement> & RefAttributes<HTMLDivElement>): React.JSX.Element;
14
19
  export declare function KPI({ label, value, trend, className, ...props }: HTMLAttributes<HTMLDivElement> & RefAttributes<HTMLDivElement> & {
@@ -71,7 +76,7 @@ export declare function AvatarGroup({ children, max, total, label, size, classNa
71
76
  max?: number;
72
77
  total?: number;
73
78
  label?: string;
74
- size?: AvatarSize;
79
+ size?: Responsive<AvatarSize>;
75
80
  className?: string;
76
81
  }): React.JSX.Element;
77
82
  export declare function LogStream({ lines }: {
@@ -87,17 +92,44 @@ export declare function Topbar({ variant, brand, children, className, ...props }
87
92
  brand?: ReactNode;
88
93
  }): React.JSX.Element;
89
94
  export declare function Kbd({ children, className, ...props }: HTMLAttributes<HTMLElement> & RefAttributes<HTMLElement>): React.JSX.Element;
90
- export declare const Select: React.ForwardRefExoticComponent<Omit<SelectHTMLAttributes<HTMLSelectElement> & RefAttributes<HTMLSelectElement>, "ref"> & RefAttributes<HTMLSelectElement>>;
91
- export declare const Textarea: React.ForwardRefExoticComponent<Omit<TextareaHTMLAttributes<HTMLTextAreaElement> & RefAttributes<HTMLTextAreaElement>, "ref"> & RefAttributes<HTMLTextAreaElement>>;
92
- export declare function Checkbox({ label, labelHidden, description, className, ...props }: InputHTMLAttributes<HTMLInputElement> & RefAttributes<HTMLInputElement> & {
95
+ export interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, "size">, RefAttributes<HTMLSelectElement> {
96
+ size?: Responsive<FieldSize>;
97
+ }
98
+ export declare const Select: React.ForwardRefExoticComponent<Omit<SelectProps, "ref"> & RefAttributes<HTMLSelectElement>>;
99
+ export interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement>, RefAttributes<HTMLTextAreaElement> {
100
+ size?: Responsive<FieldSize>;
101
+ }
102
+ export declare const Textarea: React.ForwardRefExoticComponent<Omit<TextareaProps, "ref"> & RefAttributes<HTMLTextAreaElement>>;
103
+ export declare function Checkbox({ label, labelHidden, description, size, className, ...props }: Omit<InputHTMLAttributes<HTMLInputElement>, "size"> & RefAttributes<HTMLInputElement> & {
93
104
  label: ReactNode;
94
105
  labelHidden?: boolean;
95
106
  description?: ReactNode;
107
+ size?: Responsive<FieldSize>;
96
108
  }): React.JSX.Element;
97
- export declare function Radio({ label, className, ...props }: InputHTMLAttributes<HTMLInputElement> & RefAttributes<HTMLInputElement> & {
109
+ export declare function Radio({ label, size, className, ...props }: Omit<InputHTMLAttributes<HTMLInputElement>, "size"> & RefAttributes<HTMLInputElement> & {
98
110
  label: ReactNode;
111
+ size?: Responsive<FieldSize>;
99
112
  }): React.JSX.Element;
100
- export declare function Switch({ label, className, ...props }: InputHTMLAttributes<HTMLInputElement> & RefAttributes<HTMLInputElement> & {
113
+ export declare function Switch({ label, size, className, ...props }: Omit<InputHTMLAttributes<HTMLInputElement>, "size"> & RefAttributes<HTMLInputElement> & {
101
114
  label: ReactNode;
115
+ size?: Responsive<FieldSize>;
116
+ }): React.JSX.Element;
117
+ export type RangeOrientation = "horizontal" | "vertical";
118
+ export declare function Range({ orientation, className, ...props }: InputHTMLAttributes<HTMLInputElement> & RefAttributes<HTMLInputElement> & {
119
+ orientation?: Responsive<RangeOrientation>;
120
+ }): React.JSX.Element;
121
+ export type FieldSize = Extract<ComponentSize, "sm" | "md" | "lg">;
122
+ export declare function Label({ htmlFor, className, children, ...props }: HTMLAttributes<HTMLLabelElement> & RefAttributes<HTMLLabelElement> & {
123
+ htmlFor?: string;
124
+ }): React.JSX.Element;
125
+ export type AddonSide = "start" | "end";
126
+ export type AddonLayout = "inline" | "block";
127
+ export declare function InputGroup({ className, children, ...props }: HTMLAttributes<HTMLDivElement> & RefAttributes<HTMLDivElement>): React.JSX.Element;
128
+ export declare function InputGroupAddon({ side, layout, className, children, ...props }: HTMLAttributes<HTMLSpanElement> & RefAttributes<HTMLSpanElement> & {
129
+ side?: AddonSide;
130
+ layout?: Responsive<AddonLayout>;
131
+ }): React.JSX.Element;
132
+ export declare function AspectRatio({ ratio, className, style, ...props }: HTMLAttributes<HTMLDivElement> & RefAttributes<HTMLDivElement> & {
133
+ ratio?: number;
102
134
  }): React.JSX.Element;
103
- export declare function Range(props: InputHTMLAttributes<HTMLInputElement> & RefAttributes<HTMLInputElement>): React.JSX.Element;
135
+ export {};
package/dist/markup.js CHANGED
@@ -36,9 +36,10 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
36
36
  // PARA QUEM VIER: componente novo só entra aqui se não chamar hook, não prender manipulador e não
37
37
  // tocar motor. Na dúvida, ele NÃO entra — cliente a mais é lento, servidor a mais é quebrado.
38
38
  import React, { forwardRef } from "react";
39
- import { cx } from "./pure.js";
40
- // ── Layout ───────────────────────────────────────────────────────────────────────────────────
41
- export function Card({ variant = "base", className, ...props }) { return _jsx("div", { className: cx("card", variant !== "base" && `card-${variant}`, className), ...props }); }
39
+ import { cx, fundirRender, peleDoEixo } from "./pure.js";
40
+ export function Card({ variant = "base", className, render, ...props }) {
41
+ return fundirRender(render, { className: cx("card", variant !== "base" && `card-${variant}`, className), ...props });
42
+ }
42
43
  export function Stack({ className, ...props }) { return _jsx("div", { className: cx("stack", className), ...props }); }
43
44
  // SEPARATOR — a régua entre coisas. Faltava: a Aurea só tinha `.prose hr` (preso à prosa) e o
44
45
  // `ToolbarSeparator` (preso ao Toolbar). Untitled UI precisa dela para a variante de seções da
@@ -60,7 +61,13 @@ export function Stack({ className, ...props }) { return _jsx("div", { className:
60
61
  // separador COM RÓTULO no meio ("or", uma data numa conversa), que eles têm em
61
62
  // `separator__container` + `__line` + `__content`: é outra estrutura (o `<hr>` não carrega texto),
62
63
  // ninguém pediu ainda, e fica registrado aqui como o próximo degrau se alguém precisar.
63
- export function Separator({ orientation = "horizontal", className, ...props }) { return _jsx("hr", { "aria-orientation": orientation === "vertical" ? "vertical" : undefined, className: cx("separator", orientation === "vertical" && "separator-vertical", className), ...props }); }
64
+ // O `Separator` SAIU daqui em 29/08/2026, e a razão é o gate que provou a decisão errada.
65
+ // Ele era um <hr> com orientação estática, e o `comportamental.multi-motor.spec.ts` — a prova
66
+ // das cinco capacidades que o Victor especificou no G-AXIS-06 — cobra dele `data-orientation`
67
+ // e `aria-orientation` RESOLVIDOS por largura de contêiner. Isso é eixo COMPORTAMENTAL: não
68
+ // sai de classe CSS, precisa de runtime, e runtime precisa de hook. Um componente que precisa
69
+ // de hook é de cliente, e é isso que a ADR-0026 manda — ela separa quem PRECISA do cliente de
70
+ // quem só caiu num módulo com a diretiva. Mora no `layout-client.tsx`.
64
71
  export function Cluster({ className, ...props }) { return _jsx("div", { className: cx("cluster", className), ...props }); }
65
72
  export function Grid({ className, ...props }) { return _jsx("div", { className: cx("grid", className), ...props }); }
66
73
  // ── Data Display ─────────────────────────────────────────────────────────────────────────────
@@ -95,14 +102,22 @@ export function Progress({ value, label }) { const pct = Math.max(0, Math.min(10
95
102
  export function Skeleton({ className, ...props }) { return _jsx("div", { className: cx("skeleton", className), "aria-hidden": "true", ...props }); }
96
103
  // ── Identity ─────────────────────────────────────────────────────────────────────────────────
97
104
  // `React.Children` não é hook — é utilitário de leitura de `children`, e roda no servidor.
98
- export function AvatarGroup({ children, max, total, label, size = "md", className }) {
105
+ export function AvatarGroup({ children, max, total, label, size, className }) {
99
106
  const itens = React.Children.toArray(children);
100
107
  const mostra = max ? itens.slice(0, max) : itens;
101
108
  const resto = (total ?? itens.length) - mostra.length;
102
- return _jsxs("div", { role: "group", "aria-label": label, className: cx("avatar-group", className), children: [mostra, resto > 0 && _jsxs("span", { className: cx("avatar", "avatar-count", size !== "md" && `avatar-${size}`), "aria-hidden": "true", children: ["+", resto] })] });
109
+ return _jsxs("div", { role: "group", "aria-label": label, className: cx("avatar-group", className), children: [mostra, resto > 0 && _jsxs("span", { className: cx("avatar", "avatar-count", peleDoEixo("avatar", size)), "aria-hidden": "true", children: ["+", resto] })] });
103
110
  }
104
111
  // ── Code ─────────────────────────────────────────────────────────────────────────────────────
105
- export function LogStream({ lines }) { return _jsx("div", { className: "log-stream", role: "log", "aria-live": "polite", children: lines.map((l, n) => _jsxs("div", { className: cx("log-line", l.level && `log-${l.level}`), children: [_jsx("time", { className: "log-time", children: l.time }), _jsx("span", { className: "log-level", children: l.level }), _jsx("span", { children: l.text })] }, n)) }); }
112
+ // O NÍVEL É MODIFICADOR DA CÉLULA, não classe na linha. O core pinta `.log-level.warn`, e a
113
+ // regra descendente que pintava a partir de uma classe no container (`.log-warn .log-level`)
114
+ // saiu do CSS de propósito — o comentário dela dizia, com todas as letras, que servia a um
115
+ // arquivo que não existe mais. O merge das duas linhagens ficou com o React da versão antiga
116
+ // contra o CSS da nova: `log-warn` era emitido e ninguém o pintava, e `.log-level` saía sem
117
+ // modificador nenhum. Os dois testes do `grade-do-core.test.tsx` pegaram os dois lados.
118
+ //
119
+ // `<time>` fica: é a semântica certa para a hora, e o core casa pela CLASSE, não pela tag.
120
+ export function LogStream({ lines }) { return _jsx("div", { className: "log-stream", role: "log", "aria-live": "polite", children: lines.map((l, n) => _jsxs("div", { className: "log-line", children: [_jsx("time", { className: "log-time", children: l.time }), _jsx("span", { className: cx("log-level", l.level && l.level.toLowerCase()), children: l.level }), _jsx("span", { children: l.text })] }, n)) }); }
106
121
  // ── Media ────────────────────────────────────────────────────────────────────────────────────
107
122
  export function MediaPlayerShell({ children, className, ...props }) { return _jsx("div", { className: cx("media-player", className), ...props, children: children }); }
108
123
  // ── Navigation ───────────────────────────────────────────────────────────────────────────────
@@ -112,13 +127,48 @@ export function Topbar({ variant = "floating", brand, children, className, ...pr
112
127
  // morava fora da casa dele (no `internal.tsx`, por causa do `Button`); agora mora aqui, pelo
113
128
  // mesmo motivo elevado a regra.
114
129
  export function Kbd({ children, className, ...props }) { return _jsx("kbd", { className: cx("kbd", className), ...props, children: children }); }
115
- // ── Inputs ───────────────────────────────────────────────────────────────────────────────────
116
- // Os seis controles abaixo NÃO prendem manipulador nenhum: quem escuta `onChange` é o consumidor,
117
- // e por isso eles são marcação. O `Field`, o `Input` e o `Combobox` ficaram no `inputs.tsx`
118
- // aqueles chamam hook de verdade.
119
- export const Select = forwardRef(function Select({ className, ...props }, ref) { return _jsx("select", { ref: ref, className: cx("select", className), ...props }); });
120
- export const Textarea = forwardRef(function Textarea({ className, ...props }, ref) { return _jsx("textarea", { ref: ref, className: cx("textarea", className), ...props }); });
121
- export function Checkbox({ label, labelHidden, description, className, ...props }) { return _jsxs("label", { className: cx("checkbox", labelHidden && "checkbox-bare", className), children: [_jsx("input", { type: "checkbox", ...props }), _jsx("span", { className: "control-mark" }), _jsxs("span", { className: labelHidden ? "sr-only" : undefined, children: [_jsx("strong", { children: label }), description && _jsxs(_Fragment, { children: [_jsx("br", {}), _jsx("span", { className: "muted", children: description })] })] })] }); }
122
- export function Radio({ label, className, ...props }) { return _jsxs("label", { className: cx("radio", className), children: [_jsx("input", { type: "radio", ...props }), _jsx("span", { className: "control-mark" }), label] }); }
123
- export function Switch({ label, className, ...props }) { return _jsxs("label", { className: cx("switch", className), children: [_jsx("input", { type: "checkbox", role: "switch", ...props }), _jsx("span", { className: "switch-track" }), _jsx("span", { children: label })] }); }
124
- export function Range(props) { return _jsx("input", { className: cx("range", props.className), type: "range", ...props }); }
130
+ export const Select = forwardRef(function Select({ className, size, ...props }, ref) { return _jsx("select", { ref: ref, className: cx("select", peleDoEixo("select", size), className), ...props }); });
131
+ export const Textarea = forwardRef(function Textarea({ className, size, ...props }, ref) { return _jsx("textarea", { ref: ref, className: cx("textarea", peleDoEixo("textarea", size), className), ...props }); });
132
+ // `size` nos três marcáveis pela mesma razão do campo: uma linha de formulário com um campo `sm`
133
+ // e um checkbox de tamanho fixo ao lado desalinha. O degrau vai na MARCA, não no rótulo.
134
+ export function Checkbox({ label, labelHidden, description, size, className, ...props }) { return _jsxs("label", { className: cx("checkbox", labelHidden && "checkbox-bare", className), children: [_jsx("input", { type: "checkbox", ...props }), _jsx("span", { className: cx("control-mark", peleDoEixo("control-mark", size)) }), _jsxs("span", { className: labelHidden ? "sr-only" : undefined, children: [_jsx("strong", { children: label }), description && _jsxs(_Fragment, { children: [_jsx("br", {}), _jsx("span", { className: "muted", children: description })] })] })] }); }
135
+ export function Radio({ label, size, className, ...props }) { return _jsxs("label", { className: cx("radio", className), children: [_jsx("input", { type: "radio", ...props }), _jsx("span", { className: cx("control-mark", peleDoEixo("control-mark", size)) }), label] }); }
136
+ export function Switch({ label, size, className, ...props }) { return _jsxs("label", { className: cx("switch", className), children: [_jsx("input", { type: "checkbox", role: "switch", ...props }), _jsx("span", { className: cx("switch-track", peleDoEixo("switch-track", size)) }), _jsx("span", { children: label })] }); }
137
+ export function Range({ orientation, className, ...props }) { return _jsx("input", { className: cx("range", peleDoEixo("range", orientation, "horizontal", "range"), className), type: "range", ...props }); }
138
+ // `md` é a classe base não existe `.input-md`, como não existe `.btn-md`. O prefixo é escrito
139
+ // literal em cada chamada (`input-${size}`, e não `${base}-${size}`) porque é assim que o check 15
140
+ // enxerga uma classe montada em tempo de execução; com prefixo dinâmico ele para de proteger a
141
+ // fronteira do core justamente nas classes novas.
142
+ // RÓTULO avulso. O `Field` já traz o dele, e é o caminho normal; este existe para quem monta o
143
+ // próprio campo — uma linha de tabela editável, um filtro que não é um `Field`. Sem ele, quem sai
144
+ // do `Field` escreve um `<label>` cru e perde a pele.
145
+ export function Label({ htmlFor, className, children, ...props }) { return _jsx("label", { htmlFor: htmlFor, className: cx("label", className), ...props, children: children }); }
146
+ function ladoDoAdorno(no) {
147
+ if (!React.isValidElement(no))
148
+ return null;
149
+ const p = no.props;
150
+ return no.type === InputGroupAddon ? (p.side ?? "start") : null;
151
+ }
152
+ export function InputGroup({ className, children, ...props }) {
153
+ const filhos = React.Children.toArray(children);
154
+ const inicio = filhos.filter(f => ladoDoAdorno(f) === "start");
155
+ const fim = filhos.filter(f => ladoDoAdorno(f) === "end");
156
+ const meio = filhos.filter(f => ladoDoAdorno(f) === null);
157
+ return _jsx("div", { className: cx("input-group", className), ...props, children: [...inicio, ...meio, ...fim] });
158
+ }
159
+ // `layout` é responsivo, e a triagem que o autorizou está no `G-AXIS-07`: com o adorno em linha,
160
+ // a 200px de caixa sobram 48% da largura para o campo; com ele em faixa, 99%. Não é preferência
161
+ // de desenho — é o campo deixar de caber.
162
+ //
163
+ // `padrao: ""` porque as duas geometrias têm regra própria: um `inline` que não emitisse classe
164
+ // ficaria sem o recuo, que mora na regra composta `.…-inline.…-start`.
165
+ export function InputGroupAddon({ side = "start", layout = "inline", className, children, ...props }) {
166
+ return _jsx("span", { className: cx("input-group-addon", `input-group-addon-${side}`, peleDoEixo("input-group-addon", layout, "", "input-group-addon"), className), ...props, children: children });
167
+ }
168
+ // PORTADO no merge de 28/08/2026: não existia na `main`, e a pele (`.aspect-ratio`) veio junto
169
+ // no core. Marcação pura — uma linha de CSS —, então é servidor.
170
+ // PROPORÇÃO FIXA. Quatro referências têm. Hoje é uma linha de CSS — `aspect-ratio` é suportado em
171
+ // todo navegador que a Aurea alcança —, e ainda assim é componente: sem ele cada consumidor
172
+ // escreve o `padding-top: 56.25%` de 2015, ou esquece o `min-width: 0` que impede a caixa de
173
+ // estourar dentro de um grid.
174
+ export function AspectRatio({ ratio = 1, className, style, ...props }) { return _jsx("div", { className: cx("aspect-ratio", className), style: { aspectRatio: ratio, ...style }, ...props }); }
@@ -1,4 +1,5 @@
1
1
  import React, { type HTMLAttributes, type RefAttributes, type ReactNode, type ReactElement } from "react";
2
+ import { type Responsive } from "./pure.js";
2
3
  import { type IconName } from "./system.js";
3
4
  export type StepState = "default" | "active" | "done" | "error";
4
5
  export interface StepItem {
@@ -19,7 +20,8 @@ export declare function Breadcrumb({ items, label }: {
19
20
  }>;
20
21
  label?: string;
21
22
  }): React.JSX.Element;
22
- export declare function Tabs({ tabs, value, onChange, label }: {
23
+ export type TabsOrientation = "horizontal" | "vertical";
24
+ export declare function Tabs({ tabs, value, onChange, label, orientation, activateOnFocus, loopFocus }: {
23
25
  tabs: Array<{
24
26
  id: string;
25
27
  label: ReactNode;
@@ -28,6 +30,9 @@ export declare function Tabs({ tabs, value, onChange, label }: {
28
30
  value: string;
29
31
  onChange: (id: string) => void;
30
32
  label?: string;
33
+ orientation?: Responsive<TabsOrientation>;
34
+ activateOnFocus?: boolean;
35
+ loopFocus?: boolean;
31
36
  }): React.JSX.Element;
32
37
  export declare function Pagination({ page, total, onPageChange }: {
33
38
  page: number;
@@ -78,10 +83,11 @@ export type BottomNavVariant = "floating" | "edge";
78
83
  export type BottomNavIndicator = "none" | "subtle" | "pill" | "circle" | "circle-raised" | "circle-bold" | "circle-outline";
79
84
  /** @deprecated Os quatro nomes de 17/08/2026. Use `variant` + `indicator`. */
80
85
  export type BottomNavVariantLegacy = "flat" | "surface" | "pill" | "dock";
86
+ export type BottomNavVariantAny = BottomNavVariant | BottomNavVariantLegacy;
81
87
  export declare function BottomNav({ items, current, variant, indicator, label, className, ...props }: HTMLAttributes<HTMLElement> & RefAttributes<HTMLElement> & {
82
88
  items: SidebarItem[];
83
89
  current?: string;
84
- variant?: BottomNavVariant | BottomNavVariantLegacy;
90
+ variant?: BottomNavVariantAny;
85
91
  indicator?: BottomNavIndicator;
86
92
  label?: string;
87
93
  }): React.JSX.Element;
@@ -3,9 +3,10 @@ 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 React from "react";
6
+ import React, { useRef } from "react";
7
7
  import { Tabs as BaseTabs } from "@base-ui/react/tabs";
8
8
  import { Autocomplete as BaseAutocomplete } from "@base-ui/react/autocomplete";
9
+ import { useValorResponsivo } from "./responsivo-runtime.js";
9
10
  import { cx, useAureaStrings, usePortalContainer } from "./internal.js";
10
11
  import { Kbd } from "./markup.js";
11
12
  import { Icon } from "./system.js";
@@ -26,11 +27,67 @@ export function Stepper({ items, label, className }) {
26
27
  }) });
27
28
  }
28
29
  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: "chevron--right", size: "sm" }), " ", i.href ? _jsx("a", { href: i.href, children: i.label }) : _jsx("strong", { "aria-current": "page", children: i.label })] }, n)) }); }
29
- export function Tabs({ tabs, value, onChange, label }) { const s = useAureaStrings(); return _jsxs(BaseTabs.Root, { value: value, onValueChange: v => onChange(String(v)), children: [_jsx(BaseTabs.List, { className: "tabs", "aria-label": label ?? s.tabsLabel, activateOnFocus: true, 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: "card card-inset", tabIndex: 0, children: t.content }, t.id))] }); }
30
+ export function Tabs({ tabs, value, onChange, label, orientation, activateOnFocus = true, loopFocus }) { 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: "tabs", "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: "card card-inset", tabIndex: 0, children: t.content }, t.id))] }); }
30
31
  export function Pagination({ page, total, onPageChange }) { const s = useAureaStrings(); return _jsxs("nav", { className: "pagination", "aria-label": s.paginationLabel, children: [_jsx(Button, { variant: "ghost", size: "sm", disabled: page <= 1, onClick: () => onPageChange(page - 1), children: s.previous }), _jsxs(Badge, { variant: "primary", children: [page, " / ", total] }), _jsx(Button, { variant: "ghost", size: "sm", disabled: page >= total, onClick: () => onPageChange(page + 1), children: s.next })] }); }
31
32
  export function TableOfContents({ items, current, label, className, ...props }) {
32
33
  const s = useAureaStrings();
33
- return _jsxs("nav", { className: cx("toc", className), "aria-label": label ?? s.tocLabel, ...props, children: [_jsx("p", { className: "toc-label", children: label ?? s.tocLabel }), items.map(i => _jsx("a", { href: `#${i.id}`, className: cx(i.sub && "toc-sub"), ...(i.id === current ? { "aria-current": "true" } : {}), children: i.label }, i.id))] });
34
+ const meu = React.useRef(null);
35
+ const observado = useSecaoEmVista(items, current === undefined, meu);
36
+ const atual = current ?? observado;
37
+ // O `ref` do consumidor continua chegando: em React 19 ele é prop comum, então espalhá-lo por
38
+ // `...props` depois do nosso o sobrescreveria em silêncio. Os dois são atendidos aqui.
39
+ const refDele = props.ref;
40
+ const { ref: _ignorado, ...resto } = props;
41
+ return _jsxs("nav", { ref: n => { meu.current = n; if (typeof refDele === "function")
42
+ refDele(n);
43
+ else if (refDele)
44
+ refDele.current = n; }, className: cx("toc", className), "aria-label": label ?? s.tocLabel, ...resto, children: [_jsx("p", { className: "toc-label", children: label ?? s.tocLabel }), items.map(i => _jsx("a", { href: `#${i.id}`, className: cx(i.sub && "toc-sub"), ...(i.id === atual ? { "aria-current": "true" } : {}), children: i.label }, i.id))] });
45
+ }
46
+ /** Qual dos `items` está na faixa de leitura, observando o documento. Devolve `undefined` até o
47
+ * primeiro cálculo — e em SSR, onde não há documento para observar.
48
+ *
49
+ * A afinação NÃO é escolha nova: é a mesma do `tocSpy` do `aurea.js`, que já estava provada em
50
+ * produção. Uma faixa estreita no alto da janela (`-75%` embaixo), e vence o ÚLTIMO que a cruza,
51
+ * não o primeiro — seção e subseção cruzam juntas, e o específico é o que interessa. Nada
52
+ * cruzando quer dizer topo da página, e aí o atual é o primeiro item. Reimplementar com outra
53
+ * régua seria criar a terceira versão divergente do mesmo comportamento, que é o defeito que
54
+ * este componente existe para encerrar. */
55
+ function useSecaoEmVista(items, ligado, nav) {
56
+ const [atual, setAtual] = React.useState(undefined);
57
+ // os ids como string estável: sem isto o efeito re-roda a cada render, porque `items` é um
58
+ // array novo toda vez que o pai renderiza.
59
+ const ids = items.map(i => i.id).join(" ");
60
+ React.useEffect(() => {
61
+ if (!ligado)
62
+ return;
63
+ if (typeof IntersectionObserver === "undefined")
64
+ return; // navegador antigo: fica lista de links
65
+ const lista = ids ? ids.split(" ") : [];
66
+ const alvos = lista.map(id => document.getElementById(id)).filter((e) => e !== null);
67
+ if (!alvos.length)
68
+ return;
69
+ // O acordo com o runtime vanilla, e ele mora no EFEITO e não no render por um motivo medido:
70
+ // o catálogo é gerado ESTÁTICO a partir deste mesmo componente. Marcar no render poria o
71
+ // atributo nas páginas todas, onde não há React vivo para observar — e o `tocSpy` do
72
+ // `aurea.js`, que é quem de fato marca lá, se afastaria de um substituto inexistente. Efeito
73
+ // só roda onde há React de verdade, que é exatamente o que a marca precisa afirmar.
74
+ nav.current?.setAttribute("data-toc-spy", "react");
75
+ setAtual(alvos[0].id);
76
+ const visivel = new Map();
77
+ const obs = new IntersectionObserver(entradas => {
78
+ for (const e of entradas)
79
+ visivel.set(e.target.id, e.isIntersecting);
80
+ let achado = alvos[0].id;
81
+ for (const alvo of alvos)
82
+ if (visivel.get(alvo.id))
83
+ achado = alvo.id;
84
+ setAtual(achado);
85
+ }, { rootMargin: "0px 0px -75% 0px" });
86
+ for (const alvo of alvos)
87
+ obs.observe(alvo);
88
+ return () => obs.disconnect();
89
+ }, [ids, ligado]);
90
+ return ligado ? atual : undefined;
34
91
  }
35
92
  function flattenVisible(nodes, expanded, level = 1, parentId, acc = []) {
36
93
  for (const node of nodes) {
@@ -1,4 +1,5 @@
1
1
  import React, { type ReactNode, type ReactElement } from "react";
2
+ import { type Responsive } from "./pure.js";
2
3
  import { type IconName } from "./system.js";
3
4
  export declare function Dialog({ open, title, children, footer, onClose }: {
4
5
  open: boolean;
@@ -54,21 +55,95 @@ export declare function HoverCard({ trigger, children, side }: {
54
55
  children: ReactNode;
55
56
  side?: OverlaySide;
56
57
  }): React.JSX.Element;
57
- export interface MenuItemDef {
58
+ interface MenuItemBase {
58
59
  label: ReactNode;
59
- onClick?: () => void;
60
60
  disabled?: boolean;
61
61
  leadingIcon?: IconName;
62
62
  }
63
+ /** Item de ação — a forma de sempre. `kind` é opcional para não quebrar quem já usa. */
64
+ export interface MenuActionDef extends MenuItemBase {
65
+ kind?: "item";
66
+ onClick?: () => void;
67
+ /** O elemento que MATERIALIZA o item, no idioma `render` da casa (o mesmo do `Card`). Para
68
+ * navegação prefira `href`, que usa o `LinkItem` do motor. */
69
+ render?: ReactElement;
70
+ }
71
+ /** Item que é um link de verdade: `<a href>`, com o `LinkItem` do motor por trás.
72
+ * NÃO estende `MenuItemBase`, e a ausência é a informação: **link não tem `disabled`** — o HTML
73
+ * não desabilita âncora, e o motor não aceita a prop. Um destino indisponível é um item de ação
74
+ * desabilitado, não um link apagado. */
75
+ export interface MenuLinkDef {
76
+ kind: "link";
77
+ label: ReactNode;
78
+ leadingIcon?: IconName;
79
+ href: string;
80
+ target?: string;
81
+ rel?: string;
82
+ /** Fechar o menu ao clicar. O padrão do motor é `false` (a navegação desmonta tudo de qualquer
83
+ * jeito); aqui o padrão é `true`, porque numa aplicação de página única o clique NÃO desmonta
84
+ * e o menu ficaria aberto por cima da tela nova. */
85
+ closeOnClick?: boolean;
86
+ }
87
+ /** Item que ALTERNA. Controlado (`checked`) ou não (`defaultChecked`), como todo par da casa. */
88
+ export interface MenuCheckboxDef extends MenuItemBase {
89
+ kind: "checkbox";
90
+ checked?: boolean;
91
+ defaultChecked?: boolean;
92
+ onCheckedChange?: (checked: boolean) => void;
93
+ }
94
+ /** Opções MUTUAMENTE EXCLUSIVAS. O grupo é a unidade — um rádio solto não tem sentido. */
95
+ export interface MenuRadioGroupDef {
96
+ kind: "radiogroup";
97
+ label?: ReactNode;
98
+ value?: string;
99
+ defaultValue?: string;
100
+ onValueChange?: (value: string) => void;
101
+ items: Array<{
102
+ value: string;
103
+ label: ReactNode;
104
+ disabled?: boolean;
105
+ leadingIcon?: IconName;
106
+ }>;
107
+ }
108
+ /** Submenu. `items` aninha a mesma união — um submenu pode ter submenu. */
109
+ export interface MenuSubmenuDef extends MenuItemBase {
110
+ kind: "submenu";
111
+ items: MenuEntry[];
112
+ }
113
+ /** Seção ROTULADA. O rótulo é do grupo, não um item: `GroupLabel` não é focável nem clicável. */
114
+ export interface MenuGroupDef {
115
+ kind: "group";
116
+ label?: ReactNode;
117
+ items: MenuEntry[];
118
+ }
119
+ export type MenuEntry = MenuActionDef | MenuLinkDef | MenuCheckboxDef | MenuRadioGroupDef | MenuSubmenuDef | MenuGroupDef | "separator";
120
+ /** @deprecated desde 28/08/2026 — use `MenuEntry`. Mantido porque `MenuActionDef` É a forma
121
+ * antiga: quem tipava com `MenuItemDef` continua compilando. */
122
+ export type MenuItemDef = MenuActionDef;
63
123
  export declare function DropdownMenu({ trigger, items, side, label }: {
64
124
  trigger: ReactElement;
65
- items: Array<MenuItemDef | "separator">;
125
+ items: MenuEntry[];
66
126
  side?: OverlaySide;
67
127
  label?: string;
68
128
  }): React.JSX.Element;
69
129
  export declare function ContextMenu({ children, items, label, className }: {
70
130
  children: ReactNode;
71
- items: Array<MenuItemDef | "separator">;
131
+ items: MenuEntry[];
72
132
  label?: string;
73
133
  className?: string;
74
134
  }): React.JSX.Element;
135
+ export type MenubarOrientation = "horizontal" | "vertical";
136
+ export declare function Menubar({ menus, label, modal, disabled, loopFocus, orientation, className }: {
137
+ menus: Array<{
138
+ label: ReactNode;
139
+ items: MenuEntry[];
140
+ disabled?: boolean;
141
+ }>;
142
+ label?: string;
143
+ modal?: boolean;
144
+ disabled?: boolean;
145
+ loopFocus?: boolean;
146
+ orientation?: Responsive<MenubarOrientation>;
147
+ className?: string;
148
+ }): React.JSX.Element;
149
+ export {};
package/dist/overlays.js CHANGED
@@ -3,15 +3,17 @@ 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 React from "react";
6
+ import React, { useRef } from "react";
7
7
  import { Dialog as BaseDialog } from "@base-ui/react/dialog";
8
8
  import { AlertDialog as BaseAlertDialog } from "@base-ui/react/alert-dialog";
9
9
  import { Popover as BasePopover } from "@base-ui/react/popover";
10
10
  import { Tooltip as BaseTooltip } from "@base-ui/react/tooltip";
11
11
  import { Menu as BaseMenu } from "@base-ui/react/menu";
12
+ import { Menubar as BaseMenubar } from "@base-ui/react/menubar";
12
13
  import { ContextMenu as BaseContextMenu } from "@base-ui/react/context-menu";
13
14
  import { PreviewCard as BasePreviewCard } from "@base-ui/react/preview-card";
14
15
  import { cx, useAureaStrings, usePortalContainer } from "./internal.js";
16
+ import { useValorResponsivo } from "./responsivo-runtime.js";
15
17
  import { Icon } from "./system.js";
16
18
  import { Button } from "./actions.js";
17
19
  export function Dialog({ open, title, children, footer, onClose }) { const s = useAureaStrings(); const portal = usePortalContainer(); return _jsx(BaseDialog.Root, { open: open, onOpenChange: o => { if (!o)
@@ -78,15 +80,72 @@ export function Popover({ trigger, title, children, side = "bottom" }) { const p
78
80
  // Por depender de repouso do ponteiro, NÃO serve para informação essencial — quem navega só por
79
81
  // teclado ou toque não abre um hover card. Conteúdo obrigatório vai em Popover.
80
82
  export function HoverCard({ trigger, children, side = "bottom" }) { const portal = usePortalContainer(); return _jsxs(BasePreviewCard.Root, { children: [_jsx(BasePreviewCard.Trigger, { render: trigger }), _jsx(BasePreviewCard.Portal, { container: portal, children: _jsx(BasePreviewCard.Positioner, { side: side, sideOffset: 8, children: _jsx(BasePreviewCard.Popup, { className: "popover hover-card", children: children }) }) })] }); }
83
+ // A MARCA de um item que alterna fica num slot de largura FIXA, e não condicional: sem ele, um
84
+ // menu com um item marcado e outro não desalinharia os rótulos a cada clique — o texto andaria
85
+ // para o lado sozinho. É a mesma razão de o `leadingIcon` ter lugar próprio.
86
+ const marca = (children) => _jsx("span", { className: "menu-mark", "aria-hidden": "true", children: children });
81
87
  // ContextMenu.Item/.Separator/.Popup são os MESMOS componentes de Menu.* no Base UI,
82
88
  // então os itens renderizam igual nos dois menus.
83
- const renderMenuItems = (items) => items.map((it, i) => it === "separator"
84
- ? _jsx(BaseMenu.Separator, { className: "menu-sep" }, i)
85
- : _jsxs(BaseMenu.Item, { className: "menu-item", disabled: it.disabled, onClick: it.onClick, children: [it.leadingIcon && _jsx(Icon, { name: it.leadingIcon }), it.label] }, i));
86
- 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, { side: side, sideOffset: 6, children: _jsx(BaseMenu.Popup, { className: "menu", "aria-label": label, children: renderMenuItems(items) }) }) })] }); }
89
+ // `portal` viaja como PARÂMETRO e não por hook: este renderizador não é componente, e o
90
+ // submenu tem portal PRÓPRIO — sem passá-lo adiante, o menu de primeiro nível respeitaria o
91
+ // container configurado pelo provider e o submenu escaparia para o `document.body`. Duas
92
+ // camadas do mesmo menu em containers diferentes é o tipo de divergência que aparece quando
93
+ // alguém monta a Aurea dentro de um shadow root ou de um diálogo nativo.
94
+ function renderMenuItems(items, portal) {
95
+ return items.map((it, i) => {
96
+ if (it === "separator")
97
+ return _jsx(BaseMenu.Separator, { className: "menu-sep" }, i);
98
+ const kind = it.kind ?? "item";
99
+ const dentro = (x) => _jsxs(_Fragment, { children: [x.leadingIcon && _jsx(Icon, { name: x.leadingIcon }), x.label] });
100
+ switch (kind) {
101
+ case "group": {
102
+ const g = it;
103
+ return _jsxs(BaseMenu.Group, { children: [g.label && _jsx(BaseMenu.GroupLabel, { className: "menu-label", children: g.label }), renderMenuItems(g.items, portal)] }, i);
104
+ }
105
+ case "radiogroup": {
106
+ const g = it;
107
+ 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);
108
+ }
109
+ case "submenu": {
110
+ const sm = it;
111
+ // O submenu é uma RAIZ própria com gatilho próprio — não é um item que "abre outro menu".
112
+ // É o que dá `aria-haspopup`, a seta para a direita e o fechamento em cascata de graça.
113
+ return _jsxs(BaseMenu.SubmenuRoot, { children: [_jsxs(BaseMenu.SubmenuTrigger, { className: "menu-item", disabled: sm.disabled, children: [dentro(sm), _jsx(Icon, { name: "chevron--right", size: "sm", className: "menu-seta" })] }), _jsx(BaseMenu.Portal, { container: portal, children: _jsx(BaseMenu.Positioner, { side: "inline-end", sideOffset: 4, children: _jsx(BaseMenu.Popup, { className: "menu", children: renderMenuItems(sm.items, portal) }) }) })] }, i);
114
+ }
115
+ case "checkbox": {
116
+ const c = it;
117
+ // O motor chama `onCheckedChange` com `(checked, detalhes)`. A Aurea passa SÓ o `checked`,
118
+ // e isso é CONTRATO: o segundo argumento é a forma interna do Base UI, e repassá-lo faria a
119
+ // assinatura pública da Aurea mudar junto com uma versão do motor. Mesma decisão do
120
+ // `onValueChange` do grupo de rádio. O teste pegou: sem o embrulho, o `vi.fn()` recebia um
121
+ // `PointerEvent` de brinde.
122
+ 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: "checkmark", size: "sm" }) })), dentro(c)] }, i);
123
+ }
124
+ case "link": {
125
+ const l = it;
126
+ return _jsx(BaseMenu.LinkItem, { className: "menu-item", href: l.href, target: l.target, rel: l.rel, closeOnClick: l.closeOnClick ?? true, children: dentro(l) }, i);
127
+ }
128
+ default: {
129
+ const a = it;
130
+ return _jsx(BaseMenu.Item, { className: "menu-item", disabled: a.disabled, onClick: a.onClick, render: a.render, children: dentro(a) }, i);
131
+ }
132
+ }
133
+ });
134
+ }
135
+ 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, { side: side, sideOffset: 6, children: _jsx(BaseMenu.Popup, { className: "menu", "aria-label": label, children: renderMenuItems(items, portal) }) }) })] }); }
87
136
  // ContextMenu: abre no botão direito e — por teclado — em Shift+F10 / tecla Menu,
88
137
  // que o browser só dispara (como evento contextmenu) sobre um elemento FOCADO. Por
89
138
  // isso o gatilho é focável (tabIndex 0) e tem nome acessível (auditoria 18/07/2026,
90
139
  // MÉDIO 3); sem isso o teclado não alcança o menu. Consumidor pode sobrescrever
91
140
  // tabIndex via children se o próprio já for focável.
92
- 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, { children: _jsx(BaseContextMenu.Popup, { className: "menu", "aria-label": label, children: renderMenuItems(items) }) }) })] }); }
141
+ 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, { children: _jsx(BaseContextMenu.Popup, { className: "menu", "aria-label": label, children: renderMenuItems(items, portal) }) }) })] }); }
142
+ // G-AXIS-06 — o valor responsivo RESOLVIDO é a única fonte de verdade: ele vai para o motor, o
143
+ // motor publica `data-orientation`/`aria-orientation` e ajusta o teclado, e a pele reage ao
144
+ // atributo publicado. Ver `decisions/0019-css-first-para-apresentacao-runtime-para-semantica.md`.
145
+ export function Menubar({ menus, label, modal, disabled, loopFocus, orientation, className }) {
146
+ const portal = usePortalContainer();
147
+ const ancora = useRef(null);
148
+ const resolvida = useValorResponsivo(orientation, "horizontal", ancora);
149
+ const vertical = resolvida === "vertical";
150
+ 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, { side: vertical ? "inline-end" : "bottom", align: "start", sideOffset: 4, children: _jsx(BaseMenu.Popup, { className: "menu", children: renderMenuItems(m.items) }) }) })] }, i)) });
151
+ }