@aurea-uds/react 0.31.0 → 0.33.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.
@@ -11,16 +11,230 @@ import { Form as BaseForm } from "@base-ui/react/form";
11
11
  import { Field as BaseField } from "@base-ui/react/field";
12
12
  import { NumberField as BaseNumberField } from "@base-ui/react/number-field";
13
13
  import { OTPField as BaseOTPField } from "@base-ui/react/otp-field";
14
+ import { Slider as BaseSlider } from "@base-ui/react/slider";
14
15
  import { RadioGroup as BaseRadioGroup } from "@base-ui/react/radio-group";
15
16
  import { Radio as BaseRadio } from "@base-ui/react/radio";
16
- import { cx, useAureaStrings, usePortalContainer, useReorder } from "./internal.js";
17
+ import { useDirection } from "@base-ui/react/direction-provider";
18
+ import { cx, useAureaStrings, usePortalContainer, useReorder, controlesRotulaveis } from "./internal.js";
19
+ import { COLOR_CHANNELS, areaBackground, channelRange, colorName, corToHex, formatChannel, getChannel, hsbToRgb, parseColor, pureHue, setChannel, spaceOf, trackBackground } from "./color.js";
20
+ import { Popover as BasePopover } from "@base-ui/react/popover";
17
21
  // Os seis controles abaixo saíram deste arquivo no item O1 e moram no `markup.tsx`, sem a
18
22
  // diretiva — são marcação, e quem escuta `onChange` é o consumidor. Voltam IMPORTADOS e não só
19
23
  // reexportados porque o `ehControle` do `Field` compara por IDENTIDADE de referência: dois
20
24
  // objetos diferentes com o mesmo nome quebrariam o rótulo do campo em silêncio.
21
- import { Textarea, Checkbox, Radio, Switch, Range, InputGroup, InputGroupAddon } from "./markup.js";
25
+ import { Textarea, Checkbox, Radio, Switch, Range, InputGroup, InputGroupAddon, ColorSwatch, Kbd } from "./markup.js";
22
26
  import { Icon } from "./system.js";
23
27
  import { IconButton } from "./actions.js";
28
+ // A FAMÍLIA DE COR (0.32.0, ADR-0069) — as peças INTERNAS moram aqui, no topo, antes do primeiro export (auxiliar
29
+ // entre exports entra na fatia de outro componente — regra do check 22). As públicas (`ColorArea`, `Slider` com
30
+ // `channel`, `ColorField`, `ColorPicker`) ficam com as irmãs delas, lá embaixo.
31
+ //
32
+ // A COR DE DENTRO é HSB com alfa (`Cor`, do `color.ts`), e não hex: o hex perde o matiz no cinza e no preto —
33
+ // puxar a saturação a 0 não pode trocar a cor do fundo da área. Os três espaços (RGB, HSL, HSB) e o alfa entram por
34
+ // ela, com as faixas de cada canal iguais às da biblioteca de acessibilidade em que a referência se apoia.
35
+ //
36
+ // A ÁREA (`AreaDeCor`) é o DESENHO acessível da referência, lido no código da biblioteca dela: um grupo com DOIS
37
+ // `input type="range"` escondidos, um por eixo, anunciados como controle 2D, e o valor dito com o NOME da cor. Só um
38
+ // dos dois fica na ordem do Tab (o do eixo que mexeu por último); o outro sai da árvore até a pessoa mexer nele pelo
39
+ // teclado. As teclas são as dela: setas (Shift: o salto do canal), Page Up/Down no eixo de cima, Home/End no de
40
+ // lado, invertidas na escrita da direita para a esquerda.
41
+ const no01 = (n) => Math.min(1, Math.max(0, n));
42
+ // COMO A PESSOA CHEGOU AQUI: a última interação foi teclado ou ponteiro? O anel de foco da área é só do teclado, e o
43
+ // foco que chega por código (a janela do `ColorPicker` o põe na área ao abrir; o arrasto o põe no `range`) não diz
44
+ // nada por si. É o jeito da biblioteca de acessibilidade em que a referência se apoia: ouvir o documento inteiro, uma
45
+ // vez, e lembrar a última modalidade. Medido em 13/10/2026: abrindo o seletor com o mouse, o anel acendia.
46
+ let modalidade = "teclado";
47
+ let ouvindoModalidade = false;
48
+ function ouvirModalidade() {
49
+ if (ouvindoModalidade || typeof document === "undefined")
50
+ return;
51
+ ouvindoModalidade = true;
52
+ document.addEventListener("keydown", (e) => { if (!e.metaKey && !e.ctrlKey && !e.altKey)
53
+ modalidade = "teclado"; }, true);
54
+ document.addEventListener("pointerdown", () => { modalidade = "ponteiro"; }, true);
55
+ }
56
+ /** O texto de entrada → a cor de dentro. Hex e `rgb()` não dizem o matiz do cinza: aí vale o matiz (e a saturação)
57
+ * da cor de antes. `hsl()`/`hsb()` dizem, e o que veio no texto vale. */
58
+ function corDoTexto(texto, anterior) {
59
+ const nova = texto ? parseColor(texto) : null;
60
+ if (!nova)
61
+ return anterior ?? { h: 0, s: 0, b: 1, a: 1 };
62
+ if (anterior && (nova.s === 0 || nova.b === 0) && !/^\s*hs[lb]/i.test(texto))
63
+ return { ...nova, h: anterior.h, s: nova.b === 0 ? anterior.s : nova.s };
64
+ return nova;
65
+ }
66
+ /** A cor que o `ColorPicker` reparte com as peças de dentro, quando o conteúdo da janela é montado por quem usa
67
+ * ("com campos", "com trilhas" — os exemplos da referência). */
68
+ const CorDoSeletor = React.createContext(null);
69
+ /** O estado de cor das peças: a cor de dentro por dentro, hex por fora. Controlado, se o hex que chega é o que a peça
70
+ * acabou de mandar, a cor de dentro vale (com o matiz); se é outro, vira cor guardando o matiz de antes no cinza.
71
+ * Dentro de um `ColorPicker`, a peça sem valor próprio usa a cor DELE. */
72
+ function useCor(value, defaultValue, onValueChange, onValueCommitted) {
73
+ const doSeletor = React.useContext(CorDoSeletor);
74
+ const [interno, setInterno] = React.useState(() => corDoTexto(defaultValue));
75
+ const ultimo = React.useRef(interno);
76
+ let cor = interno;
77
+ if (value !== undefined) {
78
+ const chegou = parseColor(value);
79
+ const mesmo = chegou !== null && corToHex(chegou) === corToHex(ultimo.current);
80
+ cor = mesmo ? ultimo.current : corDoTexto(value, ultimo.current);
81
+ }
82
+ ultimo.current = cor;
83
+ const avisar = (novo, fim) => { const h = corToHex(novo); onValueChange?.(h); if (fim)
84
+ onValueCommitted?.(h); };
85
+ if (doSeletor && value === undefined && defaultValue === undefined)
86
+ return [doSeletor.cor, (novo, fim) => { doSeletor.mudar(novo, fim); avisar(novo, fim); }];
87
+ const mudar = (novo, fim) => {
88
+ ultimo.current = novo;
89
+ // SEMPRE, e não só sem controle: no cinza o hex não muda com o matiz, o app devolve o MESMO valor e nada
90
+ // redesenharia — a trilha mexia e a área não (o teste do `ColorPicker` pegou). Este estado é o que redesenha;
91
+ // com controle, quem decide continua sendo o `value` (acima).
92
+ setInterno(novo);
93
+ avisar(novo, fim);
94
+ };
95
+ return [cor, mudar];
96
+ }
97
+ /** O espaço de uma peça de cor: o pedido, ou o que os canais exigem (vermelho/verde/azul no RGB, luminosidade no
98
+ * HSL, brilho no HSB). */
99
+ function espacoDosCanais(pedido, ...canais) {
100
+ if (pedido)
101
+ return pedido;
102
+ for (const ch of canais) {
103
+ const e = spaceOf(ch);
104
+ if (e !== "hsb" || ch === "brightness")
105
+ return e;
106
+ }
107
+ return "hsb";
108
+ }
109
+ function AreaDeCor({ cor, onCor, space, x: canalX, y: canalY, showDots, disabled, name, id, className, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-describedby": ariaDescribedby }) {
110
+ const s = useAureaStrings();
111
+ const rtl = useDirection() === "rtl";
112
+ const caixa = React.useRef(null);
113
+ const inX = React.useRef(null), inY = React.useRef(null);
114
+ const [arrastando, setArrastando] = React.useState(false);
115
+ const [focado, setFocado] = React.useState(null);
116
+ const [pelaTecla, setPelaTecla] = React.useState(false);
117
+ // O anel de foco é do TECLADO, como na referência: o foco que o arrasto põe no `range` não acende o anel.
118
+ const [peloPonteiro, setPeloPonteiro] = React.useState(false);
119
+ React.useEffect(ouvirModalidade, []);
120
+ const fx = channelRange(canalX), fy = channelRange(canalY);
121
+ const canalZ = COLOR_CHANNELS[space].find((ch) => ch !== canalX && ch !== canalY);
122
+ const vx = getChannel(cor, canalX, space), vy = getChannel(cor, canalY, space);
123
+ const px = (vx - fx.min) / (fx.max - fx.min), py = (vy - fy.min) / (fy.max - fy.min);
124
+ const hex = corToHex({ ...cor, a: 1 }), nome = colorName(hsbToRgb(cor), s);
125
+ // Antes de mexer, o leitor de tela ouve os três canais e o nome; depois de mexer pelo teclado, só o canal que
126
+ // mudou e o nome — como na referência (o resto ele já ouviu).
127
+ const fala = (ch, v) => `${s.colorChannel[ch]} ${formatChannel(ch, v)}`;
128
+ const textoDo = (eixo) => {
129
+ const a = fala(canalX, vx), b = fala(canalY, vy), c = fala(canalZ, getChannel(cor, canalZ, space));
130
+ const partes = pelaTecla ? [eixo === "x" ? a : b] : eixo === "x" ? [a, b, c] : [b, a, c];
131
+ return `${partes.join(", ")}, ${nome}`;
132
+ };
133
+ const rotulo = ariaLabel ? `${ariaLabel}, ${s.colorPicker}` : ariaLabelledby ? undefined : s.colorPicker;
134
+ // Na grade do passo do canal: somar passos em ponto flutuante acumula erro.
135
+ const naGrade = (v, f) => Math.round(Math.min(f.max, Math.max(f.min, v)) / f.step) * f.step;
136
+ const com = (nx, ny) => setChannel(setChannel(cor, canalX, nx, space), canalY, ny, space);
137
+ const pelaPosicao = (cx, cy, fim) => {
138
+ const r = caixa.current.getBoundingClientRect();
139
+ if (!r.width || !r.height)
140
+ return;
141
+ const x = no01((cx - r.left) / r.width), y = no01((cy - r.top) / r.height);
142
+ onCor(com(fx.min + (rtl ? 1 - x : x) * (fx.max - fx.min), fy.min + (1 - y) * (fy.max - fy.min)), fim);
143
+ };
144
+ const focar = (eixo) => { (eixo === "x" ? inX : inY).current?.focus({ preventScroll: true }); setFocado(eixo); };
145
+ const aoApertar = (e) => {
146
+ if (disabled || e.button !== 0)
147
+ return;
148
+ e.preventDefault();
149
+ e.currentTarget.setPointerCapture?.(e.pointerId);
150
+ setArrastando(true);
151
+ setPelaTecla(false);
152
+ setPeloPonteiro(true);
153
+ focar("x");
154
+ pelaPosicao(e.clientX, e.clientY, false);
155
+ };
156
+ const aoMover = (e) => { if (arrastando)
157
+ pelaPosicao(e.clientX, e.clientY, false); };
158
+ const aoSoltar = (e) => { if (!arrastando)
159
+ return; setArrastando(false); pelaPosicao(e.clientX, e.clientY, true); };
160
+ const aoTeclar = (e) => {
161
+ if (disabled)
162
+ return;
163
+ const frente = rtl ? -1 : 1, sx = e.shiftKey ? fx.page : fx.step, sy = e.shiftKey ? fy.page : fy.step;
164
+ let nx = vx, ny = vy, eixo;
165
+ switch (e.key) {
166
+ case "ArrowRight":
167
+ nx += sx * frente;
168
+ eixo = "x";
169
+ break;
170
+ case "ArrowLeft":
171
+ nx -= sx * frente;
172
+ eixo = "x";
173
+ break;
174
+ case "ArrowUp":
175
+ ny += sy;
176
+ eixo = "y";
177
+ break;
178
+ case "ArrowDown":
179
+ ny -= sy;
180
+ eixo = "y";
181
+ break;
182
+ case "PageUp":
183
+ ny += fy.page;
184
+ eixo = "y";
185
+ break;
186
+ case "PageDown":
187
+ ny -= fy.page;
188
+ eixo = "y";
189
+ break;
190
+ case "Home":
191
+ nx -= fx.page * frente;
192
+ eixo = "x";
193
+ break;
194
+ case "End":
195
+ nx += fx.page * frente;
196
+ eixo = "x";
197
+ break;
198
+ default: return;
199
+ }
200
+ e.preventDefault();
201
+ setPelaTecla(true);
202
+ setPeloPonteiro(false);
203
+ focar(eixo);
204
+ onCor(com(naGrade(nx, fx), naGrade(ny, fy)), true);
205
+ };
206
+ // O motor do navegador também muda o `range` (o leitor de tela pede "aumentar"): o valor chega pelo `onChange`.
207
+ const aoMudar = (eixo) => (e) => {
208
+ setPelaTecla(true);
209
+ onCor(setChannel(cor, eixo === "x" ? canalX : canalY, Number(e.target.value), space), true);
210
+ };
211
+ const escondido = (eixo) => focado === null ? eixo === "y" : focado !== eixo;
212
+ const fundo = areaBackground(cor, space, canalX, canalY, rtl);
213
+ return _jsxs("div", { ref: caixa, role: "group", id: id, "aria-label": ariaLabel ? rotulo : undefined, "aria-labelledby": ariaLabelledby, "aria-disabled": disabled || undefined, "data-disabled": disabled ? "" : undefined, "data-dragging": arrastando ? "" : undefined, className: cx("color-area", showDots && "color-area-dots", className), style: { "--color-area-fill": fundo.background, "--color-area-blend": fundo.blend ?? "normal", "--color-area-thumb": hex }, onPointerDown: aoApertar, onPointerMove: aoMover, onPointerUp: aoSoltar, onPointerCancel: aoSoltar, onKeyDown: aoTeclar, children: [_jsx("div", { className: "color-area-thumb", "data-focus-visible": focado && !peloPonteiro ? "" : undefined, style: { insetInlineStart: `${px * 100}%`, insetBlockStart: `${(1 - py) * 100}%` }, children: ["x", "y"].map((eixo) => {
214
+ const f = eixo === "x" ? fx : fy;
215
+ return _jsx("input", { ref: eixo === "x" ? inX : inY, type: "range", className: "color-area-input", min: f.min, max: f.max, step: f.step, value: naGrade(eixo === "x" ? vx : vy, f), disabled: disabled, "aria-label": rotulo, "aria-labelledby": rotulo ? undefined : ariaLabelledby, "aria-describedby": ariaDescribedby, "aria-roledescription": s.colorArea2D, "aria-valuetext": textoDo(eixo), "aria-orientation": eixo === "x" ? "horizontal" : "vertical", tabIndex: escondido(eixo) ? -1 : undefined, "aria-hidden": escondido(eixo) && !pelaTecla ? true : undefined, onChange: aoMudar(eixo), onFocus: () => { setFocado(eixo); setPeloPonteiro(modalidade === "ponteiro"); }, onBlur: (e) => { if (!caixa.current?.contains(e.relatedTarget)) {
216
+ setFocado(null);
217
+ setPeloPonteiro(false);
218
+ } } }, eixo);
219
+ }) }), name && _jsx("input", { type: "hidden", name: name, value: corToHex(cor) })] });
220
+ }
221
+ function TrilhaDeCor({ cor, onCor, channel, colorSpace, label, labelHidden, showValue = true, orientation = "horizontal", disabled, name, className, id }) {
222
+ const s = useAureaStrings();
223
+ const rtl = useDirection() === "rtl";
224
+ const space = spaceOf(channel, colorSpace);
225
+ const { min, max, step, page } = channelRange(channel);
226
+ const v = getChannel(cor, channel, space);
227
+ const idDoRotulo = React.useId();
228
+ const fundo = trackBackground(cor, channel, space, orientation === "vertical" ? "to top" : rtl ? "to left" : "to right");
229
+ const pino = channel === "hue" ? pureHue(v) : channel === "alpha" ? corToHex(cor) : corToHex({ ...cor, a: 1 });
230
+ const falado = (n) => {
231
+ const base = `${s.colorChannel[channel]} ${formatChannel(channel, n)}`;
232
+ if (channel === "alpha")
233
+ return base;
234
+ return `${base}, ${colorName(hsbToRgb(channel === "hue" ? { h: n, s: 1, b: 1 } : setChannel(cor, channel, n, space)), s)}`;
235
+ };
236
+ return _jsxs(BaseSlider.Root, { id: id, "aria-labelledby": idDoRotulo, "aria-disabled": disabled || undefined, value: v, onValueChange: (n) => onCor(setChannel(cor, channel, n, space), false), onValueCommitted: (n) => onCor(setChannel(cor, channel, n, space), true), min: min, max: max, step: step, largeStep: page, orientation: orientation, disabled: disabled, className: cx("slider", "slider-color", channel === "alpha" && "slider-alpha", className), style: { "--slider-track-fill": fundo, "--slider-thumb-color": pino }, children: [_jsx("div", { id: idDoRotulo, className: labelHidden ? "sr-only" : "slider-label", children: label }), showValue && _jsx(BaseSlider.Value, { className: "slider-output", children: (_, valores) => formatChannel(channel, valores[0] ?? v) }), _jsx(BaseSlider.Control, { className: "slider-control", children: _jsx(BaseSlider.Track, { className: "slider-track", children: _jsx(BaseSlider.Thumb, { className: "slider-thumb", "aria-labelledby": idDoRotulo, getAriaValueText: (_f, n) => falado(n) }) }) }), name && _jsx("input", { type: "hidden", name: name, value: corToHex(cor) })] });
237
+ }
24
238
  // FileInput mora em arquivo próprio (132 linhas — upload real, aborto, progresso) e é público
25
239
  // por aqui: a categoria dele é Inputs.
26
240
  export { FileInput, matchesAccept } from "./file-input.js";
@@ -77,9 +291,11 @@ function elementoRotulavel(tipo) {
77
291
  // reprovava no axe com "Form elements must have labels". Fora desta lista o Field vira GRUPO e
78
292
  // não pendura `htmlFor`, então o <input> de dentro ficava sem nome. Ele qualifica pela mesma
79
293
  // regra que os outros: recebe `id` e espalha `...props` no `Input`, que é o nó focal.
80
- return [Input, Select, Textarea, SearchField, PasswordField, Checkbox, Radio, Switch, Range, Combobox, MultiCombobox].includes(tipo);
294
+ return [Input, Select, Textarea, SearchField, PasswordField, Checkbox, Radio, Switch, Range, Combobox, MultiCombobox, ColorField].includes(tipo)
295
+ // os que moram fora do pacote principal e se registraram (o `DatePicker`, 0.32.0)
296
+ || (typeof tipo === "function" && controlesRotulaveis.has(tipo));
81
297
  }
82
- export function Field({ label, hint, description, error, children, className, id, name, orientation, labelWidth, style, ...props }) {
298
+ export function Field({ label, hint, description, error, children, className, id, name, orientation, labelWidth, style, required, ...props }) {
83
299
  const auto = React.useId();
84
300
  // Fragment não cria nó no DOM. Um Fragment com um só controle é só sintaxe; com vários,
85
301
  // Field precisa voltar para grupo em vez de pendurar `htmlFor` num id que nunca existirá.
@@ -116,6 +332,10 @@ export function Field({ label, hint, description, error, children, className, id
116
332
  const invalidoFinal = error ? true : unico?.props["aria-invalid"];
117
333
  if (invalidoFinal !== undefined)
118
334
  injecao["aria-invalid"] = invalidoFinal;
335
+ // o `required` do controle é o que anuncia; o do filho, se ele já disse, vence
336
+ if (required && unico?.props.required === undefined)
337
+ injecao.required = true;
338
+ const asterisco = required ? _jsx("span", { className: "label-required", "aria-hidden": "true", children: "*" }) : null;
119
339
  const controle = rotulavel ? React.cloneElement(unico, injecao) : children;
120
340
  // a descrição mora no nó FOCAL quando existe um; sem filho único, no grupo — que é o que sobra
121
341
  // para anunciar. Nos dois lugares seria anunciar duas vezes.
@@ -125,7 +345,7 @@ export function Field({ label, hint, description, error, children, className, id
125
345
  // árvore de acessibilidade enquanto a frase de erro aparece na tela. Pior que não mostrar.
126
346
  const controleFinal = name ? _jsx(BaseField.Control, { render: controle }) : controle;
127
347
  const estilo = labelWidth ? { ...style, "--field-label-width": labelWidth === "auto" ? "max-content" : labelWidth } : style;
128
- const corpo = _jsxs("div", { className: cx("field", peleDoEixo("field", orientation, "vertical", "field"), className), ...(nomeAqui ? {} : { role: "group", "aria-labelledby": idRotulo, ...(rotulavel ? {} : { "aria-describedby": descrito }) }), style: estilo, ...props, children: [_jsxs("span", { className: "label", children: [nomeAqui ? _jsx("label", { htmlFor: idControle, children: label }) : _jsx("span", { id: idRotulo, children: label }), hint && _jsx("span", { className: "hint", id: idHint, children: hint })] }), controleFinal, description && _jsx("span", { className: "field-description", id: idDescricao, children: description }), error && _jsx("span", { className: "field-error", id: idErro, children: error }), name && _jsx(BaseField.Error, { className: "field-error" })] });
348
+ const corpo = _jsxs("div", { className: cx("field", peleDoEixo("field", orientation, "vertical", "field"), className), ...(nomeAqui ? {} : { role: "group", "aria-labelledby": idRotulo, ...(rotulavel ? {} : { "aria-describedby": descrito }) }), style: estilo, ...props, children: [_jsxs("span", { className: "label", children: [nomeAqui ? _jsxs("label", { htmlFor: idControle, id: idRotulo, children: [label, asterisco] }) : _jsxs("span", { id: idRotulo, children: [label, asterisco] }), hint && _jsx("span", { className: "hint", id: idHint, children: hint })] }), controleFinal, description && _jsx("span", { className: "field-description", id: idDescricao, children: description }), error && _jsx("span", { className: "field-error", id: idErro, children: error }), name && _jsx(BaseField.Error, { className: "field-error" })] });
129
349
  // `Field.Root` do motor entra SÓ quando há `name`: é ele que liga o campo à chave de `errors`
130
350
  // do formulário. Ele não desenha nada — é contexto — então a pele não muda.
131
351
  return name ? _jsx(BaseField.Root, { name: name, invalid: error ? true : undefined, render: corpo }) : corpo;
@@ -146,8 +366,8 @@ export function Field({ label, hint, description, error, children, className, id
146
366
  export function Form({ errors, onSubmit, children, className, ...props }) {
147
367
  return _jsx(BaseForm, { errors: errors, onFormSubmit: onSubmit ? (v) => onSubmit(v) : undefined, className: cx("form", className), ...props, children: children });
148
368
  }
149
- export const Input = forwardRef(function Input({ className, formatOnBlur, size, onBlur, onChange, ...props }, ref) {
150
- return _jsx("input", { ref: ref, className: cx("input", peleDoEixo("input", size), className), onChange: onChange, onBlur: e => {
369
+ export const Input = forwardRef(function Input({ className, formatOnBlur, size, variant, onBlur, onChange, ...props }, ref) {
370
+ return _jsx("input", { ref: ref, className: cx("input", peleDoEixo("input", size), variant === "secondary" && "input-secondary", className), onChange: onChange, onBlur: e => {
151
371
  if (formatOnBlur) {
152
372
  const formatado = formatOnBlur(e.currentTarget.value);
153
373
  if (formatado !== e.currentTarget.value) {
@@ -163,7 +383,8 @@ export const Input = forwardRef(function Input({ className, formatOnBlur, size,
163
383
  // `.input-wrap-end` para o botão do PasswordField —, e o grupo é a terceira vez virando uma. O
164
384
  // merge das duas linhagens tinha trazido de volta a versão com `.input-wrap`, contra um core
165
385
  // que já não a pinta.
166
- export function SearchField({ icon = "magnifying-glass", className, size, width, ...props }) { return _jsxs(InputGroup, { className: className, width: width, children: [_jsx(InputGroupAddon, { children: _jsx(Icon, { name: icon, size: valorBase(size) === "sm" ? "sm" : undefined }) }), _jsx(Input, { type: "search", size: size, ...props })] }); }
386
+ // `search-field` (15/10/2026): a busca tem a medida própria da referência — 12, a lupa, 8 (ver o core).
387
+ export function SearchField({ icon = "magnifying-glass", className, size, width, ...props }) { return _jsxs(InputGroup, { className: cx("search-field", className), width: width, children: [_jsx(InputGroupAddon, { children: _jsx(Icon, { name: icon, size: valorBase(size) === "sm" ? "sm" : undefined }) }), _jsx(Input, { type: "search", size: size, ...props })] }); }
167
388
  // labelHidden: coluna de seleção de tabela precisa do rótulo POR LINHA para o leitor de
168
389
  // tela ("Select Analyst"), mas mostrá-lo engorda a coluna. O rótulo continua no DOM, só
169
390
  // sai da tela (.sr-only) — nunca trocar por aria-label solto num <label> visível vazio.
@@ -186,7 +407,310 @@ export function SearchField({ icon = "magnifying-glass", className, size, width,
186
407
  // é a trava do item ("o valor mascarado continua no DOM") entregue pelo motor, e o teste a cobra.
187
408
  export function NumberField({ value, defaultValue, onValueChange, min, max, step, disabled, readOnly, required, label, id, name, format, locale, className, scrubbable, scrubDirection }) {
188
409
  const s = useAureaStrings();
189
- return _jsx(BaseNumberField.Root, { id: id, name: name, value: value, defaultValue: defaultValue, onValueChange: soOValor(onValueChange), min: min, max: max, step: step, format: format, locale: locale, disabled: disabled, readOnly: readOnly, required: required, className: cx("number-field", className), children: _jsxs(BaseNumberField.Group, { className: "number-field-group", children: [scrubbable && _jsxs(BaseNumberField.ScrubArea, { className: cx("number-field-scrub", scrubDirection === "vertical" && "number-field-scrub-vertical"), direction: scrubDirection, "aria-hidden": "true", children: [_jsx(Icon, { name: "dots-six-vertical", "aria-hidden": true }), _jsx(BaseNumberField.ScrubAreaCursor, { className: "number-field-scrub-cursor", children: _jsx(Icon, { name: "dots-six-vertical", "aria-hidden": true }) })] }), _jsx(BaseNumberField.Decrement, { className: "btn btn-ghost btn-icon", "aria-label": s.decrement, children: _jsx(Icon, { name: "minus" }) }), _jsx(BaseNumberField.Input, { className: "input number-field-input", "aria-label": label }), _jsx(BaseNumberField.Increment, { className: "btn btn-ghost btn-icon", "aria-label": s.increment, children: _jsx(Icon, { name: "plus" }) })] }) });
410
+ return _jsx(BaseNumberField.Root, { id: id, name: name, value: value, defaultValue: defaultValue, onValueChange: soOValor(onValueChange), min: min, max: max, step: step, format: format, locale: locale, disabled: disabled, readOnly: readOnly, required: required, className: cx("number-field", className), children: _jsxs(BaseNumberField.Group, { className: "number-field-group", children: [scrubbable && _jsxs(BaseNumberField.ScrubArea, { className: cx("number-field-scrub", scrubDirection === "vertical" && "number-field-scrub-vertical"), direction: scrubDirection, "aria-hidden": "true", children: [_jsx(Icon, { name: "dots-six-vertical", "aria-hidden": true }), _jsx(BaseNumberField.ScrubAreaCursor, { className: "number-field-scrub-cursor", children: _jsx(Icon, { name: "dots-six-vertical", "aria-hidden": true }) })] }), _jsx(BaseNumberField.Decrement, { className: "number-field-button number-field-decrement", "aria-label": s.decrement, children: _jsx(Icon, { name: "minus" }) }), _jsx(BaseNumberField.Input, { className: "input number-field-input", "aria-label": label }), _jsx(BaseNumberField.Increment, { className: "number-field-button number-field-increment", "aria-label": s.increment, children: _jsx(Icon, { name: "plus" }) })] }) });
411
+ }
412
+ export function CheckboxGroup({ label, description, error, value, defaultValue, onValueChange, name, disabled, required, children, className, ...props }) {
413
+ const auto = React.useId();
414
+ const [proprio, setProprio] = React.useState(defaultValue ?? []);
415
+ const marcados = value ?? proprio;
416
+ const trocar = (v, ligado) => {
417
+ const novo = ligado ? [...marcados.filter(x => x !== v), v] : marcados.filter(x => x !== v);
418
+ if (value === undefined)
419
+ setProprio(novo);
420
+ onValueChange?.(novo);
421
+ };
422
+ const idRotulo = `${auto}-label`, idDescricao = description ? `${auto}-description` : undefined, idErro = error ? `${auto}-error` : undefined;
423
+ const caixas = React.Children.map(children, filho => {
424
+ if (!React.isValidElement(filho))
425
+ return filho;
426
+ const p = filho.props;
427
+ if (typeof p.value !== "string")
428
+ return filho;
429
+ const v = p.value;
430
+ return React.cloneElement(filho, { checked: marcados.includes(v), name: name ?? filho.props.name,
431
+ disabled: p.disabled ?? disabled, ...(error ? { "aria-invalid": true } : null),
432
+ onChange: (e) => { p.onChange?.(e); trocar(v, e.currentTarget.checked); } });
433
+ });
434
+ return _jsxs("div", { role: "group", "aria-labelledby": idRotulo, "aria-describedby": [idDescricao, idErro].filter(Boolean).join(" ") || undefined, className: cx("checkbox-group", className), ...props, children: [_jsx("span", { className: "label", id: idRotulo, children: _jsxs("span", { children: [label, required ? _jsx("span", { className: "label-required", "aria-hidden": "true", children: "*" }) : null] }) }), description && _jsx("span", { className: "field-description", id: idDescricao, children: description }), caixas, error && _jsx("span", { className: "field-error", id: idErro, children: error })] });
435
+ }
436
+ export function RadioGroup({ label, labelHidden, description, error, value, defaultValue, onValueChange, name, disabled, invalid, orientation = "vertical", required, children, className, ...props }) {
437
+ const auto = React.useId();
438
+ const [proprio, setProprio] = React.useState(defaultValue);
439
+ const escolhido = value ?? proprio;
440
+ const idRotulo = `${auto}-label`, idDescricao = description ? `${auto}-description` : undefined, idErro = error ? `${auto}-error` : undefined;
441
+ const opcoes = React.Children.map(children, filho => {
442
+ if (!React.isValidElement(filho))
443
+ return filho;
444
+ const p = filho.props;
445
+ if (typeof p.value !== "string")
446
+ return filho;
447
+ const v = p.value;
448
+ return React.cloneElement(filho, { checked: escolhido === v, name: name ?? auto, required,
449
+ disabled: p.disabled ?? disabled, ...(error || invalid ? { "aria-invalid": true } : null),
450
+ onChange: (e) => {
451
+ p.onChange?.(e);
452
+ if (!e.currentTarget.checked)
453
+ return;
454
+ if (value === undefined)
455
+ setProprio(v);
456
+ onValueChange?.(v);
457
+ } });
458
+ });
459
+ return _jsxs("div", { role: "radiogroup", "aria-labelledby": idRotulo, "aria-describedby": [idDescricao, idErro].filter(Boolean).join(" ") || undefined, "aria-orientation": orientation, "aria-required": required || undefined, "aria-invalid": error || invalid ? true : undefined, className: cx("radio-group", orientation === "horizontal" && "radio-group-horizontal", labelHidden && "radio-group-bare", className), ...props, children: [_jsx("span", { className: cx("label", labelHidden && "sr-only"), id: idRotulo, children: _jsxs("span", { children: [label, required ? _jsx("span", { className: "label-required", "aria-hidden": "true", children: "*" }) : null] }) }), description && _jsx("span", { className: "field-description", id: idDescricao, children: description }), orientation === "horizontal" ? _jsx("div", { className: "radio-group-items", children: opcoes }) : opcoes, error && _jsx("span", { className: "field-error", id: idErro, children: error })] });
460
+ }
461
+ /** A opção do grupo: o `Radio`, como o `RadioGroup.Item` do nativo. */
462
+ RadioGroup.Item = Radio;
463
+ const ehSecao = (x) => "items" in x;
464
+ const textoDoItem = (it) => it.textValue ?? (typeof it.label === "string" ? it.label : "");
465
+ export function ListBox({ label, items, selectionMode = "single", value, defaultValue, onValueChange, onAction, className, ...props }) {
466
+ const auto = React.useId();
467
+ const [proprio, setProprio] = React.useState(defaultValue ?? []);
468
+ const escolhidos = value ?? proprio;
469
+ const todos = React.useMemo(() => items.flatMap(x => ehSecao(x) ? x.items : [x]), [items]);
470
+ const habilitados = todos.filter(it => !it.disabled);
471
+ const [ativo, setAtivo] = React.useState(undefined);
472
+ // O item que recebe o Tab: o ativo; senão o primeiro escolhido; senão o primeiro habilitado.
473
+ const alvo = ativo ?? habilitados.find(it => escolhidos.includes(it.id))?.id ?? habilitados[0]?.id;
474
+ const refs = React.useRef(new Map());
475
+ const focar = (id) => { if (!id)
476
+ return; setAtivo(id); refs.current.get(id)?.focus(); };
477
+ const escolher = (it) => {
478
+ if (it.disabled)
479
+ return;
480
+ if (selectionMode === "none") {
481
+ onAction?.(it.id);
482
+ return;
483
+ }
484
+ const ja = escolhidos.includes(it.id);
485
+ const novo = selectionMode === "multiple" ? (ja ? escolhidos.filter(x => x !== it.id) : [...escolhidos, it.id]) : [it.id];
486
+ if (value === undefined)
487
+ setProprio(novo);
488
+ onValueChange?.(novo);
489
+ onAction?.(it.id);
490
+ };
491
+ const aoTeclar = (e, it) => {
492
+ const i = habilitados.findIndex(x => x.id === it.id);
493
+ if (e.key === "ArrowDown") {
494
+ e.preventDefault();
495
+ focar(habilitados[Math.min(i + 1, habilitados.length - 1)]?.id);
496
+ }
497
+ else if (e.key === "ArrowUp") {
498
+ e.preventDefault();
499
+ focar(habilitados[Math.max(i - 1, 0)]?.id);
500
+ }
501
+ else if (e.key === "Home") {
502
+ e.preventDefault();
503
+ focar(habilitados[0]?.id);
504
+ }
505
+ else if (e.key === "End") {
506
+ e.preventDefault();
507
+ focar(habilitados[habilitados.length - 1]?.id);
508
+ }
509
+ else if (e.key === " " || e.key === "Enter") {
510
+ e.preventDefault();
511
+ escolher(it);
512
+ }
513
+ else if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
514
+ // a primeira letra: o próximo item, depois do atual, que começa com ela (e dá a volta)
515
+ const letra = e.key.toLocaleLowerCase();
516
+ const ordem = [...habilitados.slice(i + 1), ...habilitados.slice(0, i + 1)];
517
+ const achou = ordem.find(x => textoDoItem(x).toLocaleLowerCase().startsWith(letra));
518
+ if (achou) {
519
+ e.preventDefault();
520
+ focar(achou.id);
521
+ }
522
+ }
523
+ };
524
+ const item = (it) => {
525
+ const escolhido = selectionMode !== "none" && escolhidos.includes(it.id);
526
+ const idDesc = it.description ? `${auto}-${it.id}-d` : undefined;
527
+ return _jsxs("div", { ref: n => { if (n)
528
+ refs.current.set(it.id, n);
529
+ else
530
+ refs.current.delete(it.id); }, role: "option", "aria-selected": selectionMode === "none" ? undefined : escolhido, "aria-disabled": it.disabled || undefined, "aria-describedby": idDesc, tabIndex: it.id === alvo ? 0 : -1, className: cx("list-box-item", it.danger && "list-box-item-danger"), onClick: () => { if (!it.disabled) {
531
+ setAtivo(it.id);
532
+ escolher(it);
533
+ } }, onKeyDown: e => aoTeclar(e, it), onFocus: () => setAtivo(it.id), children: [it.icon && _jsx(Icon, { name: it.icon }), _jsxs("span", { className: "list-box-item-text", children: [_jsx("span", { className: "list-box-item-label", children: it.label }), it.description && _jsx("span", { className: "list-box-item-description", id: idDesc, children: it.description })] }), it.kbd && _jsx(Kbd, { className: "list-box-item-kbd", children: it.kbd }), escolhido && _jsx(Icon, { name: "check", className: "list-box-item-indicator" })] }, it.id);
534
+ };
535
+ return _jsx("div", { role: "listbox", "aria-label": label, "aria-multiselectable": selectionMode === "multiple" || undefined, className: cx("list-box", className), ...props, children: items.map((x, i) => ehSecao(x)
536
+ ? _jsxs("div", { role: "group", "aria-labelledby": `${auto}-s${i}`, className: "list-box-section", children: [_jsx("span", { className: "list-box-section-title", id: `${auto}-s${i}`, role: "presentation", children: x.title }), x.items.map(item)] }, `s${i}`)
537
+ : item(x)) });
538
+ }
539
+ const textoDaEtiqueta = (it) => it.textValue ?? (typeof it.label === "string" ? it.label : "");
540
+ export function TagGroup({ label, labelHidden, description, errorMessage, items, selectionMode = "none", selectedKeys, defaultSelectedKeys, onSelectionChange, onRemove, size = "md", variant = "default", className, id }) {
541
+ const s = useAureaStrings();
542
+ const auto = React.useId();
543
+ const [proprio, setProprio] = React.useState(defaultSelectedKeys ?? []);
544
+ const escolhidos = selectedKeys ?? proprio;
545
+ const escolhe = selectionMode !== "none";
546
+ const habilitadas = items.filter(it => !it.disabled);
547
+ const [ativa, setAtiva] = React.useState(undefined);
548
+ const alvo = (ativa && habilitadas.some(it => it.id === ativa) ? ativa : undefined) ?? habilitadas[0]?.id;
549
+ const refs = React.useRef(new Map());
550
+ const focar = (id) => { if (!id)
551
+ return; setAtiva(id); refs.current.get(id)?.focus(); };
552
+ const escolher = (it) => {
553
+ if (!escolhe || it.disabled)
554
+ return;
555
+ const ja = escolhidos.includes(it.id);
556
+ const novo = selectionMode === "multiple" ? (ja ? escolhidos.filter(x => x !== it.id) : [...escolhidos, it.id]) : (ja ? [] : [it.id]);
557
+ if (selectedKeys === undefined)
558
+ setProprio(novo);
559
+ onSelectionChange?.(novo);
560
+ };
561
+ const tirar = (it) => {
562
+ if (!onRemove || it.disabled)
563
+ return;
564
+ // o foco vai para a vizinha (a seguinte; na última, a anterior), e não se perde no documento
565
+ const i = habilitadas.findIndex(x => x.id === it.id);
566
+ const vizinha = habilitadas[i + 1] ?? habilitadas[i - 1];
567
+ onRemove([it.id]);
568
+ if (vizinha)
569
+ requestAnimationFrame(() => focar(vizinha.id));
570
+ };
571
+ const aoTeclar = (e, it) => {
572
+ const i = habilitadas.findIndex(x => x.id === it.id);
573
+ const rtl = getComputedStyle(e.currentTarget).direction === "rtl";
574
+ const frente = rtl ? "ArrowLeft" : "ArrowRight", tras = rtl ? "ArrowRight" : "ArrowLeft";
575
+ if (e.key === frente || e.key === "ArrowDown") {
576
+ e.preventDefault();
577
+ focar(habilitadas[Math.min(i + 1, habilitadas.length - 1)]?.id);
578
+ }
579
+ else if (e.key === tras || e.key === "ArrowUp") {
580
+ e.preventDefault();
581
+ focar(habilitadas[Math.max(i - 1, 0)]?.id);
582
+ }
583
+ else if (e.key === "Home") {
584
+ e.preventDefault();
585
+ focar(habilitadas[0]?.id);
586
+ }
587
+ else if (e.key === "End") {
588
+ e.preventDefault();
589
+ focar(habilitadas[habilitadas.length - 1]?.id);
590
+ }
591
+ else if ((e.key === "Delete" || e.key === "Backspace") && onRemove) {
592
+ e.preventDefault();
593
+ tirar(it);
594
+ }
595
+ };
596
+ const guardar = (itId) => (n) => { if (n)
597
+ refs.current.set(itId, n);
598
+ else
599
+ refs.current.delete(itId); };
600
+ const idRotulo = `${auto}-l`, idDesc = description ? `${auto}-d` : undefined, idErro = errorMessage ? `${auto}-e` : undefined;
601
+ return _jsxs("div", { className: cx("tag-group", className), id: id, children: [_jsx("span", { className: labelHidden ? "sr-only" : "label", id: idRotulo, children: label }), _jsx("div", { role: escolhe ? "group" : "list", "aria-labelledby": idRotulo, "aria-describedby": [idDesc, idErro].filter(Boolean).join(" ") || undefined, className: "tag-group-list", children: items.map(it => {
602
+ const escolhida = escolhe && escolhidos.includes(it.id);
603
+ const conteudo = _jsxs(_Fragment, { children: [it.icon && _jsx(Icon, { name: it.icon }), _jsx("span", { className: "tag-label", children: it.label })] });
604
+ const x = onRemove && _jsx("button", { type: "button", className: "tag-remove", disabled: it.disabled, tabIndex: escolhe ? -1 : it.id === alvo ? 0 : -1, ref: escolhe ? undefined : guardar(it.id), "aria-label": `${s.comboboxRemove} ${textoDaEtiqueta(it)}`.trim(), onClick: () => tirar(it), onKeyDown: escolhe ? undefined : e => aoTeclar(e, it), onFocus: escolhe ? undefined : () => setAtiva(it.id), children: _jsx(Icon, { name: "x", weight: "bold" }) });
605
+ return _jsxs("span", { role: escolhe ? undefined : "listitem", className: cx("tag", size !== "md" && `tag-${size}`, variant !== "default" && `tag-${variant}`), "data-selected": escolhida || undefined, "data-disabled": it.disabled || undefined, children: [escolhe
606
+ ? _jsx("button", { type: "button", className: "tag-main", ref: guardar(it.id), "aria-pressed": escolhida, disabled: it.disabled, tabIndex: it.id === alvo ? 0 : -1, onClick: () => { setAtiva(it.id); escolher(it); }, onKeyDown: e => aoTeclar(e, it), onFocus: () => setAtiva(it.id), children: conteudo })
607
+ : _jsx("span", { className: "tag-main", children: conteudo }), x] }, it.id);
608
+ }) }), description && _jsx("span", { className: "field-description", id: idDesc, children: description }), errorMessage && _jsx("span", { className: "field-error", id: idErro, children: errorMessage })] });
609
+ }
610
+ export function Slider(props) {
611
+ return props.channel ? _jsx(SliderDeCor, { ...props }) : _jsx(SliderDeNumero, { ...props });
612
+ }
613
+ function SliderDeCor({ value, defaultValue, onValueChange, onValueCommitted, ...resto }) {
614
+ const [cor, mudar] = useCor(value, defaultValue, onValueChange, onValueCommitted);
615
+ return _jsx(TrilhaDeCor, { ...resto, cor: cor, onCor: mudar });
616
+ }
617
+ function SliderDeNumero({ label, labelHidden, value, defaultValue, onValueChange, onValueCommitted, min, max, step, largeStep, orientation = "horizontal", disabled, name, format, locale, showValue = true, thumbLabels, className, id }) {
618
+ // Quantas alças: tantas quantos valores houver (a faixa é uma lista).
619
+ const atual = value ?? defaultValue;
620
+ const alcas = Array.isArray(atual) ? atual.length : 1;
621
+ // O NOME DA ALÇA SAI NA MARCAÇÃO. O motor liga o `<input>` de cada alça ao rótulo por um id que ele
622
+ // só conhece DEPOIS de montar — no HTML do servidor (e no catálogo, que é HTML parado) as alças saíam
623
+ // sem nome, e o axe do catálogo acusou as cinco. O `Slider.Label` do motor não aceita id, então o
624
+ // rótulo é um `<div>` nosso, com id, que nomeia o grupo e cada alça — o nome já vem no HTML. E o desligado vai também para o grupo (`aria-disabled`): o motor só marca o `<input>`, e a
625
+ // letra esmaecida do rótulo e do valor era lida como texto de contraste baixo, e não como peça desligada.
626
+ const idDoRotulo = React.useId();
627
+ return _jsxs(BaseSlider.Root, { id: id, name: name, "aria-labelledby": idDoRotulo, "aria-disabled": disabled || undefined, value: value, defaultValue: defaultValue, onValueChange: onValueChange ? (v) => onValueChange(v) : undefined, onValueCommitted: onValueCommitted ? (v) => onValueCommitted(v) : undefined, min: min, max: max, step: step, largeStep: largeStep, orientation: orientation, disabled: disabled, format: format, locale: locale, className: cx("slider", className), children: [_jsx("div", { id: idDoRotulo, className: labelHidden ? "sr-only" : "slider-label", children: label }), showValue && _jsx(BaseSlider.Value, { className: "slider-output" }), _jsx(BaseSlider.Control, { className: "slider-control", children: _jsxs(BaseSlider.Track, { className: "slider-track", children: [_jsx(BaseSlider.Indicator, { className: "slider-fill" }), Array.from({ length: alcas }, (_, i) => _jsx(BaseSlider.Thumb, { index: i, className: "slider-thumb", getAriaLabel: thumbLabels ? () => thumbLabels[i] ?? "" : undefined, "aria-labelledby": thumbLabels ? undefined : idDoRotulo }, i))] }) })] });
628
+ }
629
+ const EIXOS_PADRAO = { hsb: ["saturation", "brightness"], hsl: ["saturation", "lightness"], rgb: ["red", "green"] };
630
+ export function ColorArea({ value, defaultValue, onValueChange, onValueCommitted, colorSpace, xChannel, yChannel, ...resto }) {
631
+ const [cor, mudar] = useCor(value, defaultValue, onValueChange, onValueCommitted);
632
+ const space = espacoDosCanais(colorSpace, ...[xChannel, yChannel].filter((c) => !!c));
633
+ const [x, y] = [xChannel ?? EIXOS_PADRAO[space][0], yChannel ?? EIXOS_PADRAO[space][1]];
634
+ return _jsx(AreaDeCor, { ...resto, cor: cor, onCor: mudar, space: space, x: x, y: y });
635
+ }
636
+ export function ColorField({ value, defaultValue, onValueChange, onValueCommitted, channel, colorSpace, swatch, size, variant, width, disabled, readOnly, required, name, placeholder, id, className, ...aria }) {
637
+ const s = useAureaStrings();
638
+ const doSeletor = React.useContext(CorDoSeletor);
639
+ const [cor, mudar] = useCor(value === "" ? undefined : value, defaultValue, onValueChange, onValueCommitted);
640
+ // Vazio: o valor "" de fora, ou sem valor nenhum e ainda não escrito (o campo da referência começa vazio).
641
+ const [escrito, setEscrito] = React.useState(defaultValue !== undefined);
642
+ const vazio = !doSeletor && (value !== undefined ? value === "" : !escrito);
643
+ const [rascunho, setRascunho] = React.useState(null);
644
+ const canal = channel;
645
+ const space = canal ? spaceOf(canal, colorSpace) : "hsb";
646
+ const faixa = canal ? channelRange(canal) : null;
647
+ const v = canal ? getChannel(cor, canal, space) : 0;
648
+ const mostrado = vazio ? "" : canal ? formatChannel(canal, v) : corToHex(cor);
649
+ const confirmar = () => {
650
+ if (rascunho === null)
651
+ return;
652
+ const t = rascunho.trim();
653
+ setRascunho(null);
654
+ if (t === "") {
655
+ if (canal)
656
+ return;
657
+ setEscrito(false);
658
+ onValueChange?.("");
659
+ onValueCommitted?.("");
660
+ return;
661
+ }
662
+ if (canal) {
663
+ const n = Number(t.replace(/[°%\s]/g, "").replace(",", "."));
664
+ if (!Number.isFinite(n))
665
+ return;
666
+ mudar(setChannel(cor, canal, canal === "alpha" && (n > 1 || /%/.test(t)) ? n / 100 : n, space), true);
667
+ return;
668
+ }
669
+ const nova = parseColor(t);
670
+ if (!nova)
671
+ return;
672
+ setEscrito(true);
673
+ mudar(corDoTexto(t, cor), true);
674
+ };
675
+ const aoTeclar = (e) => {
676
+ if (e.key === "Enter") {
677
+ e.preventDefault();
678
+ confirmar();
679
+ return;
680
+ }
681
+ if (!canal || !faixa || disabled || readOnly)
682
+ return;
683
+ const passo = e.key === "ArrowUp" ? faixa.step : e.key === "ArrowDown" ? -faixa.step : e.key === "PageUp" ? faixa.page : e.key === "PageDown" ? -faixa.page : 0;
684
+ const alvo = e.key === "Home" ? faixa.min : e.key === "End" ? faixa.max : passo ? v + passo : null;
685
+ if (alvo === null)
686
+ return;
687
+ e.preventDefault();
688
+ setRascunho(null);
689
+ mudar(setChannel(cor, canal, Math.round(alvo / faixa.step) * faixa.step, space), true);
690
+ };
691
+ const comAmostra = swatch ?? !canal;
692
+ return _jsxs(InputGroup, { className: className, width: width, children: [comAmostra && _jsx(InputGroupAddon, { children: _jsx(ColorSwatch, { color: vazio ? "transparent" : corToHex(cor), size: "sm", "aria-hidden": "true" }) }), _jsx(Input, { id: id, size: size, variant: variant, value: rascunho ?? mostrado, placeholder: placeholder, disabled: disabled, readOnly: readOnly, required: required, spellCheck: false, autoComplete: "off", inputMode: canal ? "decimal" : undefined, role: canal ? "spinbutton" : undefined, "aria-valuenow": canal ? v : undefined, "aria-valuemin": faixa?.min, "aria-valuemax": faixa?.max, "aria-valuetext": canal ? `${formatChannel(canal, v)}, ${colorName(hsbToRgb(cor), s)}` : undefined, ...aria, onChange: (e) => setRascunho(e.target.value), onBlur: confirmar, onKeyDown: aoTeclar }), name && _jsx("input", { type: "hidden", name: name, value: vazio ? "" : corToHex(cor) })] });
693
+ }
694
+ export function ColorPicker({ label, value, defaultValue, onValueChange, onValueCommitted, swatches, variant = "full", children, disabled, name, id, className }) {
695
+ const s = useAureaStrings();
696
+ const portal = usePortalContainer();
697
+ const idGrupo = React.useId();
698
+ const [cor, mudar] = useCor(value, defaultValue, onValueChange, onValueCommitted);
699
+ const hex = corToHex(cor), nome = colorName(hsbToRgb(cor), s);
700
+ const amostras = swatches?.map((o) => {
701
+ const op = typeof o === "string" ? { color: o } : o;
702
+ const c = parseColor(op.color);
703
+ return { color: op.color, label: op.label ?? (c ? colorName(hsbToRgb(c), s) : op.color) };
704
+ });
705
+ const escolhida = amostras?.find((o) => { const c = parseColor(o.color); return c !== null && corToHex(c) === hex; })?.color ?? "";
706
+ const contexto = React.useMemo(() => ({ cor, mudar }), [cor, mudar]);
707
+ // O seletor escuta o teclado e o ponteiro desde que MONTA: a área só monta ao abrir, DEPOIS do clique que abriu, e
708
+ // a primeira abertura com o mouse acendia o anel (medido na bancada em 14/10/2026; o teste abria pelo teclado antes).
709
+ React.useEffect(ouvirModalidade, []);
710
+ // O foco fica PRESO na janela (`trap-focus`), como na referência: com quatro peças dentro, o Tab que saía depois
711
+ // do código fechava a janela (medido no navegador). O motor pede um botão de fechar dentro, para o leitor de tela
712
+ // por toque sair; ele fica invisível e fora do Tab — quem usa teclado tem o Esc, e o foco nunca some na tela.
713
+ return _jsxs(BasePopover.Root, { modal: "trap-focus", children: [_jsxs(BasePopover.Trigger, { disabled: disabled, id: id, "aria-label": `${label}, ${nome}`, className: cx("color-picker-trigger", className), children: [_jsx(ColorSwatch, { color: hex, "aria-hidden": "true" }), _jsx("span", { children: label })] }), name && _jsx("input", { type: "hidden", name: name, value: hex }), _jsx(BasePopover.Portal, { container: portal, children: _jsx(BasePopover.Positioner, { className: "positioner", side: "bottom", align: "start", sideOffset: 6, children: _jsxs(BasePopover.Popup, { className: "popover color-picker-popover", "aria-label": label, children: [_jsx(CorDoSeletor.Provider, { value: contexto, children: children ?? _jsxs(_Fragment, { children: [variant !== "swatches" && _jsxs(_Fragment, { children: [_jsx(AreaDeCor, { cor: cor, onCor: mudar, space: "hsb", x: "saturation", y: "brightness", "aria-label": label }), _jsx(TrilhaDeCor, { cor: cor, onCor: mudar, channel: "hue", label: s.colorChannel.hue, labelHidden: true, showValue: false })] }), amostras && _jsx(ColorSwatch, { options: amostras, name: `${idGrupo}-amostras`, label: s.colorSwatches, value: escolhida, onChange: (e) => mudar(corDoTexto(e.target.value, cor), true) }), _jsx(ColorField, { "aria-label": s.colorHex })] }) }), _jsx(BasePopover.Close, { className: "sr-only", tabIndex: -1, children: s.close })] }) }) })] });
190
714
  }
191
715
  // OTPField (Lote 1 do BUILDING.md). Um campo por dígito, com o comportamento que ninguém acerta
192
716
  // à mão: colar o código inteiro distribui pelos campos, Backspace volta um, e o
@@ -199,9 +723,12 @@ export function NumberField({ value, defaultValue, onValueChange, min, max, step
199
723
  // formulário anônimos. Um leitor de tela anunciaria "editar texto" seis vezes seguidas sem
200
724
  // dizer qual é qual. O grupo continua nomeado (`role="group"`), que é o que diz para que serve
201
725
  // o código; o nome de cada caixa é o que diz onde você está.
202
- export function OTPField({ length, value, defaultValue, onValueChange, mask, disabled, required, label, id, className }) {
726
+ // 15/10/2026 (o Victor: "A, B, C, pode"), como o campo de código da referência: `groupSize` põe o TRACINHO a cada N
727
+ // caixas (o 3 + 3 da vitrine dela; o `Separator` do motor, de 6 × 2), e `variant="secondary"` dá às caixas o fundo
728
+ // neutro do `Input` secundário. Sem os dois, a marcação é a de antes.
729
+ export function OTPField({ length, value, defaultValue, onValueChange, mask, disabled, required, label, id, className, groupSize, variant }) {
203
730
  const s = useAureaStrings();
204
- return _jsx(BaseOTPField.Root, { id: id, length: length, value: value, defaultValue: defaultValue, onValueChange: soOValor(onValueChange), mask: mask, disabled: disabled, required: required, role: "group", "aria-label": label, className: cx("otp-field", className), children: Array.from({ length }, (_, i) => _jsx(BaseOTPField.Input, { render: _jsx("input", { "aria-label": `${s.otpDigit} ${i + 1}` }), className: "input otp-slot" }, i)) });
731
+ return _jsx(BaseOTPField.Root, { id: id, length: length, value: value, defaultValue: defaultValue, onValueChange: soOValor(onValueChange), mask: mask, disabled: disabled, required: required, role: "group", "aria-label": label, className: cx("otp-field", className), children: Array.from({ length }, (_, i) => _jsxs(React.Fragment, { children: [_jsx(BaseOTPField.Input, { render: _jsx("input", { "aria-label": `${s.otpDigit} ${i + 1}` }), className: cx("input otp-slot", variant === "secondary" && "input-secondary") }), groupSize && groupSize > 0 && (i + 1) % groupSize === 0 && i < length - 1 ? _jsx(BaseOTPField.Separator, { className: "otp-separator" }) : null] }, i)) });
205
732
  }
206
733
  // SegmentedControl — achado M20, fechado na Parte C do PLANO-1.0 em 07/08/2026.
207
734
  //
@@ -263,7 +790,7 @@ export function Combobox({ items, value, onValueChange, placeholder, label, empt
263
790
  const portal = usePortalContainer();
264
791
  const autoId = React.useId();
265
792
  const inputId = id ?? autoId;
266
- return _jsxs(BaseCombobox.Root, { items: items, value: value, onValueChange: soOValor(onValueChange), disabled: disabled, itemToStringLabel: (i) => i.label, children: [_jsxs("div", { className: cx("field", className), children: [label && _jsx("label", { className: "label", htmlFor: inputId, children: label }), _jsxs(BaseCombobox.InputGroup, { className: "combobox-group", children: [_jsx(BaseCombobox.Input, { id: inputId, placeholder: placeholder, className: cx("input", peleDoEixo("input", size)), "aria-describedby": descrito, "aria-invalid": invalido, "aria-label": rotulo }), _jsxs("span", { className: "combobox-actions", children: [_jsx(BaseCombobox.Clear, { render: _jsx(IconButton, { variant: "ghost", size: "sm", icon: "x", label: s.comboboxClear }) }), _jsx(BaseCombobox.Trigger, { render: _jsx(IconButton, { variant: "ghost", size: "sm", icon: "caret-down", label: s.comboboxOpen }) })] })] })] }), _jsx(BaseCombobox.Portal, { container: portal, children: _jsx(BaseCombobox.Positioner, { sideOffset: 6, children: _jsxs(BaseCombobox.Popup, { className: "menu combobox-popup", children: [_jsx(BaseCombobox.Empty, { className: "combobox-empty", children: empty ?? s.comboboxEmpty }), listaCombobox(items)] }) }) })] });
793
+ return _jsxs(BaseCombobox.Root, { items: items, value: value, onValueChange: soOValor(onValueChange), disabled: disabled, itemToStringLabel: (i) => i.label, children: [_jsxs("div", { className: cx("field", className), children: [label && _jsx("label", { className: "label", htmlFor: inputId, children: label }), _jsxs(BaseCombobox.InputGroup, { className: "combobox-group", children: [_jsx(BaseCombobox.Input, { id: inputId, placeholder: placeholder, className: cx("input", peleDoEixo("input", size)), "aria-describedby": descrito, "aria-invalid": invalido, "aria-label": rotulo }), _jsxs("span", { className: "combobox-actions", children: [_jsx(BaseCombobox.Clear, { render: _jsx(IconButton, { variant: "ghost", size: "sm", icon: "x", label: s.comboboxClear }) }), _jsx(BaseCombobox.Trigger, { render: _jsx(IconButton, { variant: "ghost", size: "sm", icon: "caret-down", label: s.comboboxOpen }) })] })] })] }), _jsx(BaseCombobox.Portal, { container: portal, children: _jsx(BaseCombobox.Positioner, { className: "positioner", sideOffset: 6, children: _jsxs(BaseCombobox.Popup, { className: "menu combobox-popup", children: [_jsx(BaseCombobox.Empty, { className: "combobox-empty", children: empty ?? s.comboboxEmpty }), listaCombobox(items)] }) }) })] });
267
794
  }
268
795
  // MultiCombobox (Fase 4): multi-seleção com chips, opções agrupadas e filtro
269
796
  // externo (async). Reusa .combobox-popup/-item/-empty do básico.
@@ -278,7 +805,7 @@ export function MultiCombobox({ items, value, onValueChange, onInputChange, load
278
805
  const autoId = React.useId();
279
806
  const inputId = id ?? autoId;
280
807
  return _jsxs(BaseCombobox.Root, { multiple: true, items: items, value: value, onValueChange: soOValor(onValueChange), disabled: disabled, itemToStringLabel: (i) => i.label, filter: onInputChange ? null : undefined, onInputValueChange: onInputChange ? ((v, d) => { if (d.reason !== "item-press")
281
- onInputChange(v); }) : undefined, children: [_jsxs("div", { className: cx("field", className), children: [label && _jsx("label", { className: "label", htmlFor: inputId, children: label }), _jsxs(BaseCombobox.InputGroup, { className: "combobox-multi", children: [_jsx(BaseCombobox.Chips, { className: "combobox-chips", children: _jsx(BaseCombobox.Value, { children: (selected) => _jsxs(_Fragment, { children: [selected.map((item) => _jsxs(BaseCombobox.Chip, { className: "combobox-chip", children: [item.label, _jsx(BaseCombobox.ChipRemove, { className: "combobox-chip-remove", "aria-label": `${s.comboboxRemove} ${item.label}`, children: _jsx(Icon, { name: "x", size: "sm" }) })] }, item.value)), _jsx(BaseCombobox.Input, { id: inputId, placeholder: selected.length ? undefined : placeholder, className: "combobox-chip-input", "aria-describedby": descrito, "aria-invalid": invalido, "aria-label": rotulo })] }) }) }), _jsxs("span", { className: "combobox-actions", children: [_jsx(BaseCombobox.Clear, { render: _jsx(IconButton, { variant: "ghost", size: "sm", icon: "x", label: s.comboboxClear }) }), _jsx(BaseCombobox.Trigger, { render: _jsx(IconButton, { variant: "ghost", size: "sm", icon: "caret-down", label: s.comboboxOpen }) })] })] })] }), _jsx(BaseCombobox.Portal, { container: portal, children: _jsx(BaseCombobox.Positioner, { sideOffset: 6, children: _jsxs(BaseCombobox.Popup, { className: "menu combobox-popup", children: [_jsx(BaseCombobox.Empty, { className: "combobox-empty", children: loading ? s.comboboxLoading : (empty ?? s.comboboxEmpty) }), listaCombobox(items)] }) }) })] });
808
+ onInputChange(v); }) : undefined, children: [_jsxs("div", { className: cx("field", className), children: [label && _jsx("label", { className: "label", htmlFor: inputId, children: label }), _jsxs(BaseCombobox.InputGroup, { className: "combobox-multi", children: [_jsx(BaseCombobox.Chips, { className: "combobox-chips", children: _jsx(BaseCombobox.Value, { children: (selected) => _jsxs(_Fragment, { children: [selected.map((item) => _jsxs(BaseCombobox.Chip, { className: "combobox-chip", children: [item.label, _jsx(BaseCombobox.ChipRemove, { className: "combobox-chip-remove", "aria-label": `${s.comboboxRemove} ${item.label}`, children: _jsx(Icon, { name: "x", size: "sm" }) })] }, item.value)), _jsx(BaseCombobox.Input, { id: inputId, placeholder: selected.length ? undefined : placeholder, className: "combobox-chip-input", "aria-describedby": descrito, "aria-invalid": invalido, "aria-label": rotulo })] }) }) }), _jsxs("span", { className: "combobox-actions", children: [_jsx(BaseCombobox.Clear, { render: _jsx(IconButton, { variant: "ghost", size: "sm", icon: "x", label: s.comboboxClear }) }), _jsx(BaseCombobox.Trigger, { render: _jsx(IconButton, { variant: "ghost", size: "sm", icon: "caret-down", label: s.comboboxOpen }) })] })] })] }), _jsx(BaseCombobox.Portal, { container: portal, children: _jsx(BaseCombobox.Positioner, { className: "positioner", sideOffset: 6, children: _jsxs(BaseCombobox.Popup, { className: "menu combobox-popup", children: [_jsx(BaseCombobox.Empty, { className: "combobox-empty", children: loading ? s.comboboxLoading : (empty ?? s.comboboxEmpty) }), listaCombobox(items)] }) }) })] });
282
809
  }
283
810
  const ehGrupoDoSelect = (i) => typeof i === "object" && i != null && "items" in i;
284
811
  // Lê `<option>`/`<optgroup>` como o navegador lê: `value` ausente vale o TEXTO da opção.
@@ -308,7 +835,7 @@ export const Select = forwardRef(function Select({ items, children, value, defau
308
835
  onValueChange?.(valor);
309
836
  onChange?.({ target: { value: valor, name }, currentTarget: { value: valor, name } });
310
837
  };
311
- return _jsxs(BaseSelect.Root, { items: planas.map(o => ({ value: o.value, label: o.label })), ...(value !== undefined ? { value } : { defaultValue: inicial }), onValueChange: v => mudar(v), name: name, required: required, disabled: disabled, children: [_jsx(BaseSelect.Trigger, { ref: ref, className: cx("select", "select-trigger", peleDoEixo("select", size), className), ...props, children: _jsx(BaseSelect.Value, { placeholder: placeholder, className: "select-value" }) }), _jsx(BaseSelect.Portal, { container: portal, children: _jsx(BaseSelect.Positioner, { sideOffset: 6, alignItemWithTrigger: false, children: _jsx(BaseSelect.Popup, { className: "menu combobox-popup", children: _jsx(BaseSelect.List, { children: lista.map((i, n) => ehGrupoDoSelect(i)
838
+ return _jsxs(BaseSelect.Root, { items: planas.map(o => ({ value: o.value, label: o.label })), ...(value !== undefined ? { value } : { defaultValue: inicial }), onValueChange: v => mudar(v), name: name, required: required, disabled: disabled, children: [_jsx(BaseSelect.Trigger, { ref: ref, className: cx("select", "select-trigger", peleDoEixo("select", size), className), ...props, children: _jsx(BaseSelect.Value, { placeholder: placeholder, className: "select-value" }) }), _jsx(BaseSelect.Portal, { container: portal, children: _jsx(BaseSelect.Positioner, { className: "positioner", sideOffset: 6, alignItemWithTrigger: false, children: _jsx(BaseSelect.Popup, { className: "menu combobox-popup", children: _jsx(BaseSelect.List, { children: lista.map((i, n) => ehGrupoDoSelect(i)
312
839
  ? _jsxs(BaseSelect.Group, { className: "combobox-section", children: [_jsx(BaseSelect.GroupLabel, { className: "combobox-group-label", children: i.label }), i.items.map(itemDoSelect)] }, n)
313
840
  : itemDoSelect(i)) }) }) }) })] });
314
841
  });