@aurea-uds/native 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.
@@ -0,0 +1,466 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Aurea nativo — três peças de ESCOLHER (0.32.0, passo 9b): `Slider`, `OTPField` e `TagGroup`.
3
+ //
4
+ // Mesmo nome e mesma API da web (`inputs-client.tsx`): as mesmas props, com os mesmos tipos de
5
+ // valor. Saem as que só existem no navegador (`className`, `id`, `name`, o `largeStep` do teclado,
6
+ // o `required` de formulário); entram `style` e `testID`, como no resto do pacote.
7
+ //
8
+ // O desenho, o comportamento e as medidas são os do pacote nativo da referência, lidos no código
9
+ // publicado dela e escritos com os tokens que dão o mesmo número. A cor, o canto, a fonte e os
10
+ // ícones são os da Aurea. Sem dependência nova: o gesto é o `PanResponder`, a animação é o
11
+ // `Animated` e o campo do código é um `TextInput`, todos do próprio React Native — a mesma escolha
12
+ // do `BottomSheet` (`overlays.tsx`).
13
+ import * as React from "react";
14
+ import { Animated, PanResponder, Platform, Pressable, TextInput, View, } from "react-native";
15
+ import { canto, criarFolha, estadoAcessivel } from "./estilos.js";
16
+ import { Icon } from "./icon.js";
17
+ import { Label, useCampo } from "./inputs.js";
18
+ import { driverNativo, useReduceMotion } from "./movimento.js";
19
+ import { formatarNumero } from "./numero.js";
20
+ import { Text } from "./text.js";
21
+ import { useAureaStrings, useAureaTokens, usePeleSobreAMarca } from "./theme.js";
22
+ const folha = criarFolha((t) => ({
23
+ desabilitado: { opacity: t.size.opacityDisabled },
24
+ // ── Slider ──────────────────────────────────────────────────────────────────────────────────
25
+ // A referência: 8 entre as partes; o trilho de 20, a alça em cápsula de 28 × 20 com a bolinha de
26
+ // dentro a 2 da borda. O canto é a cápsula da Aurea (lá é o `radius-xl`, 12, que no trilho de 20
27
+ // já quase fecha a cápsula).
28
+ deslizante: { gap: t.size.space2 },
29
+ // Na vertical a referência ocupa a altura do pai e centraliza; a ordem das partes é a da web
30
+ // (valor em cima, trilho, rótulo embaixo).
31
+ deslizanteVertical: { height: "100%", alignItems: "center" },
32
+ // A linha de cima: o rótulo e o valor nas pontas, no meio da altura um do outro.
33
+ cabeca: { alignItems: "center" },
34
+ soValor: { flexDirection: "row", justifyContent: "flex-end" },
35
+ trilho: { width: "100%", height: t.size.space5, justifyContent: "center",
36
+ ...canto(t.size.radiusFull), backgroundColor: t.color.secondary },
37
+ // ⚠ Sem altura no pai, a vertical da referência some (altura zero). Aqui ela nunca fica menor que
38
+ // a da web: o `--range-vertical-size` de lá é `10rem`.
39
+ trilhoVertical: { width: t.size.space5, height: undefined, flex: 1, minHeight: 10 * t.remInDp,
40
+ justifyContent: "flex-start", alignItems: "center" },
41
+ preenchimento: { position: "absolute", ...canto(t.size.radiusFull), backgroundColor: t.color.controlSelected },
42
+ preenchimentoDeitado: { top: 0, bottom: 0 },
43
+ preenchimentoEmPe: { left: 0, right: 0 },
44
+ alca: { position: "absolute", padding: t.size.space05, ...canto(t.size.radiusFull),
45
+ backgroundColor: t.color.controlSelected },
46
+ alcaDeitada: { width: t.size.space7, height: t.size.space5 },
47
+ alcaEmPe: { width: t.size.space5, height: t.size.space7 },
48
+ bolinha: { flex: 1, ...canto(t.size.radiusFull), backgroundColor: t.color.controlSelectedForeground,
49
+ boxShadow: [...t.shadowLayers.shadowSm] },
50
+ // ── OTPField ────────────────────────────────────────────────────────────────────────────────
51
+ // A referência: casas de 44 × 48 com 8 entre elas. A altura é a do campo (`touchControlHMd`, a
52
+ // escala de 48 do `Input`); a pele é a do `Input` — borda, fundo e a cápsula.
53
+ codigo: { alignSelf: "flex-start" },
54
+ casas: { flexDirection: "row", alignItems: "center", gap: t.size.space2 },
55
+ casa: {
56
+ width: t.size.space10 + t.size.space1, height: t.size.touchControlHMd,
57
+ alignItems: "center", justifyContent: "center",
58
+ borderWidth: t.size.borderWidth, borderColor: t.color.borderStrong,
59
+ ...canto(t.size.radiusControl), backgroundColor: t.color.fieldBg,
60
+ },
61
+ // A casa ativa: a borda da cor do foco, com a espessura do anel (o contorno de 2 da referência).
62
+ casaAtiva: { borderColor: t.color.focusStrong, borderWidth: t.size.focusWidth },
63
+ casaInvalida: { borderColor: t.color.danger400 ?? t.color.destructive },
64
+ // O dígito da referência: 18 em seminegrito, na linha de 28 do `text-lg`.
65
+ digito: { fontSize: t.size.textLg, lineHeight: t.size.textLg * t.size.textLgLeading,
66
+ fontFamily: t.font.ui[600], color: t.color.foreground },
67
+ // O ponto da máscara: o mesmo ponto de 10 do `RadioGroup`.
68
+ ponto: { width: t.size.space2 + t.size.space05, height: t.size.space2 + t.size.space05,
69
+ ...canto(t.size.radiusFull), backgroundColor: t.color.foreground },
70
+ // O cursor da referência: 2 de largura, da altura da letra.
71
+ cursor: { width: t.size.space05, height: t.size.textLg, ...canto(t.size.radiusFull),
72
+ backgroundColor: t.color.mutedForeground },
73
+ campoEscondido: { position: "absolute", top: 0, right: 0, bottom: 0, left: 0 },
74
+ // ── TagGroup ────────────────────────────────────────────────────────────────────────────────
75
+ // A referência: 12 entre rótulo, lista e descrição; 8 entre as etiquetas, numa fila que quebra.
76
+ grupo: { gap: t.size.space3 },
77
+ lista: { width: "100%", flexDirection: "row", flexWrap: "wrap", alignItems: "flex-start", gap: t.size.space2 },
78
+ etiqueta: { flexDirection: "row", alignItems: "center", ...canto(t.size.radiusFull) },
79
+ // A parte principal fica POR CIMA do X: a área de toque estendida do X não pode roubar o toque de
80
+ // quem quer escolher a etiqueta (ver o `TagGroup`).
81
+ principal: { flexDirection: "row", alignItems: "center", zIndex: 1 },
82
+ remover: { alignItems: "center", justifyContent: "center", ...canto(t.size.radiusFull) },
83
+ }));
84
+ /** Prende `v` entre `a` e `b`. */
85
+ const limitar = (v, a, b) => Math.min(Math.max(v, a), b);
86
+ /** Quantas casas decimais um número tem — para o passo de 0,1 não virar 0,30000000000000004. */
87
+ function casasDecimais(n) {
88
+ const s = String(n);
89
+ const exp = /e-(\d+)$/.exec(s);
90
+ if (exp)
91
+ return Number(exp[1]);
92
+ const i = s.indexOf(".");
93
+ return i < 0 ? 0 : s.length - i - 1;
94
+ }
95
+ /** O valor no passo mais perto, contado a partir do `min` (como na referência e no motor da web). */
96
+ function noPasso(v, min, max, passo) {
97
+ if (!(passo > 0))
98
+ return limitar(v, min, max);
99
+ const bruto = min + Math.round((v - min) / passo) * passo;
100
+ return limitar(Number(bruto.toFixed(Math.max(casasDecimais(passo), casasDecimais(min)))), min, max);
101
+ }
102
+ const ehLista = (v) => Array.isArray(v);
103
+ /**
104
+ * O controle deslizante da referência: o trilho, o preenchimento e a alça em cápsula, com o rótulo
105
+ * e o valor na linha de cima.
106
+ *
107
+ * <Slider label="Volume" defaultValue={30} />
108
+ * <Slider label="Preço" value={faixa} onValueChange={setFaixa} min={0} max={1000} step={10}
109
+ * format={{style: "currency", currency: "BRL"}} locale="pt-BR" thumbLabels={["Mínimo", "Máximo"]} />
110
+ *
111
+ * Arrastar a alça move a partir de onde ela está; tocar no trilho leva a alça mais perto até ali (e
112
+ * continua arrastando, como na web). Na faixa, uma alça não passa da outra — para nela, como na
113
+ * referência. Cada alça é um elemento `adjustable`: o leitor de tela anuncia o valor formatado e
114
+ * desliza um passo para cima ou para baixo (`increment`/`decrement`).
115
+ */
116
+ export function Slider({ label, labelHidden, value, defaultValue, onValueChange, onValueCommitted, min = 0, max = 100, step = 1, orientation = "horizontal", disabled, format, locale, showValue = true, thumbLabels, style, testID, }) {
117
+ const t = useAureaTokens();
118
+ const s = folha(t);
119
+ const strings = useAureaStrings();
120
+ const campo = useCampo();
121
+ const inativo = disabled ?? campo?.disabled ?? false;
122
+ const vertical = orientation === "vertical";
123
+ // A faixa é decidida pela forma do valor, como na web: lista entra, lista sai.
124
+ const faixa = ehLista(value ?? defaultValue);
125
+ const paraLista = (v) => (v === undefined ? [min] : ehLista(v) ? [...v] : [v]).map((n) => limitar(n, min, max));
126
+ const [proprio, setProprio] = React.useState(() => paraLista(defaultValue));
127
+ const valores = value !== undefined ? paraLista(value) : proprio;
128
+ const [trilho, setTrilho] = React.useState(0);
129
+ const [arrastando, setArrastando] = React.useState(null);
130
+ // A alça mede 28 no eixo do movimento; o curso dela é o trilho menos ela — a alça nunca sai do
131
+ // trilho (a conta da referência).
132
+ const alca = t.size.space7;
133
+ const curso = Math.max(0, trilho - alca);
134
+ const fracao = (v) => (max === min ? 0 : (v - min) / (max - min));
135
+ const texto = (v) => formatarNumero(v, locale, format);
136
+ // O valor vivo do gesto. Entre dois movimentos o React pode não ter desenhado ainda, então quem
137
+ // arrasta lê e escreve aqui; a cada desenho ele volta a ser o que a tela mostra.
138
+ const vivos = React.useRef(valores);
139
+ vivos.current = valores;
140
+ const gesto = React.useRef(null);
141
+ const saida = (lista) => (faixa ? lista : lista[0]);
142
+ const mudar = (i, bruto) => {
143
+ const lista = [...vivos.current];
144
+ // A alça para na vizinha (a referência não deixa uma passar da outra).
145
+ const piso = i > 0 ? lista[i - 1] : min;
146
+ const teto = i < lista.length - 1 ? lista[i + 1] : max;
147
+ const v = limitar(noPasso(bruto, min, max, step), piso, teto);
148
+ if (v === lista[i])
149
+ return;
150
+ lista[i] = v;
151
+ vivos.current = lista;
152
+ if (value === undefined)
153
+ setProprio(lista);
154
+ onValueChange?.(saida(lista));
155
+ };
156
+ const comprometer = () => onValueCommitted?.(saida(vivos.current));
157
+ const ctl = React.useRef(null);
158
+ ctl.current = {
159
+ inativo,
160
+ pegar: (i) => {
161
+ const atual = vivos.current;
162
+ // Duas alças no mesmo ponto: quem anda é decidido pelo primeiro movimento (para a esquerda, a
163
+ // de baixo; para a direita, a de cima). Sem isso a alça de cima, que é a que recebe o toque,
164
+ // ficaria presa na de baixo e a faixa não abriria para trás.
165
+ gesto.current = { i, inicio: atual[i], decidir: atual.filter((v) => v === atual[i]).length > 1 };
166
+ setArrastando(i);
167
+ },
168
+ pegarNoTrilho: (e) => {
169
+ if (curso <= 0)
170
+ return;
171
+ const bruto = vertical ? e.nativeEvent.locationY : e.nativeEvent.locationX;
172
+ // A distância desde o começo do trilho — na vertical, o começo é embaixo.
173
+ const p = vertical ? trilho - bruto : bruto;
174
+ const alvo = min + limitar((p - alca / 2) / curso, 0, 1) * (max - min);
175
+ // A alça mais perto do toque; no empate, a que fica do lado do toque.
176
+ let i = 0;
177
+ let melhor = Infinity;
178
+ vivos.current.forEach((v, k) => {
179
+ const d = Math.abs(v - alvo);
180
+ if (d < melhor || (d === melhor && alvo > v)) {
181
+ melhor = d;
182
+ i = k;
183
+ }
184
+ });
185
+ gesto.current = { i, inicio: alvo, decidir: false };
186
+ setArrastando(i);
187
+ mudar(i, alvo);
188
+ },
189
+ mover: (g) => {
190
+ const ag = gesto.current;
191
+ if (!ag || curso <= 0)
192
+ return;
193
+ const delta = vertical ? -g.dy : g.dx;
194
+ if (ag.decidir && delta !== 0) {
195
+ const iguais = vivos.current.flatMap((v, k) => (v === vivos.current[ag.i] ? [k] : []));
196
+ ag.i = delta < 0 ? iguais[0] : iguais[iguais.length - 1];
197
+ ag.decidir = false;
198
+ setArrastando(ag.i);
199
+ }
200
+ mudar(ag.i, ag.inicio + (delta / curso) * (max - min));
201
+ },
202
+ soltar: () => {
203
+ if (!gesto.current)
204
+ return;
205
+ gesto.current = null;
206
+ setArrastando(null);
207
+ comprometer();
208
+ },
209
+ passo: (i, sentido) => {
210
+ if (inativo)
211
+ return;
212
+ const antes = vivos.current[i];
213
+ mudar(i, antes + sentido * step);
214
+ if (vivos.current[i] !== antes)
215
+ comprometer();
216
+ },
217
+ };
218
+ // O toque no trilho. A alça tem o gesto dela (abaixo) e ganha o toque quando o dedo cai nela; o
219
+ // preenchimento não recebe toque, então a posição do dedo é sempre contada a partir do trilho.
220
+ const gestoDoTrilho = React.useMemo(() => PanResponder.create({
221
+ onStartShouldSetPanResponder: () => !ctl.current.inativo,
222
+ onMoveShouldSetPanResponder: () => !ctl.current.inativo,
223
+ // Arrastando, a rolagem da tela não toma o dedo.
224
+ onPanResponderTerminationRequest: () => false,
225
+ onPanResponderGrant: (e) => ctl.current.pegarNoTrilho(e),
226
+ onPanResponderMove: (_e, g) => ctl.current.mover(g),
227
+ onPanResponderRelease: () => ctl.current.soltar(),
228
+ onPanResponderTerminate: () => ctl.current.soltar(),
229
+ }), []);
230
+ const inicioF = valores.length > 1 ? fracao(valores[0]) : 0;
231
+ const fimF = fracao(valores[valores.length - 1]);
232
+ // O preenchimento cobre a alça inteira (a conta da referência): começa no começo da primeira alça
233
+ // (ou do trilho) e acaba no fim da última.
234
+ const extensao = Math.max(alca, (fimF - inicioF) * curso + alca);
235
+ const preenchido = vertical
236
+ ? { bottom: inicioF * curso, height: extensao }
237
+ : { left: inicioF * curso, width: extensao };
238
+ // A área de toque chega aos 44 no eixo de fora (o trilho tem 20).
239
+ const folga = (t.size.targetMin - t.size.space5) / 2;
240
+ const nomeDoGrupo = typeof label === "string" ? label : campo?.label;
241
+ const valorEscrito = showValue ? (_jsx(Text, { size: "xs", weight: 500, numeric: true, children: valores.map(texto).join(" – ") })) : null;
242
+ const trilhoDesenhado = (_jsxs(View, { ...gestoDoTrilho.panHandlers, onLayout: (e) => {
243
+ const { width, height } = e.nativeEvent.layout;
244
+ setTrilho(vertical ? height : width);
245
+ }, hitSlop: vertical ? { left: folga, right: folga } : { top: folga, bottom: folga }, testID: testID ? `${testID}-trilho` : undefined, style: [s.trilho, vertical && s.trilhoVertical], children: [_jsx(View, { pointerEvents: "none", style: [s.preenchimento, vertical ? s.preenchimentoEmPe : s.preenchimentoDeitado, preenchido] }), valores.map((v, i) => (_jsx(AlcaDoDeslizante, { indice: i, ctl: ctl, vertical: vertical, deslocamento: fracao(v) * curso, arrastando: arrastando === i, inativo: inativo, nome: thumbLabels?.[i] ?? nomeDoGrupo, valorEscrito: texto(v), porcento: Math.round(fracao(v) * 100), rotulosDasAcoes: [strings.increment, strings.decrement], testID: testID ? `${testID}-alca-${i}` : undefined }, i)))] }));
246
+ return (_jsx(View, { testID: testID, style: [s.deslizante, vertical && s.deslizanteVertical, inativo && s.desabilitado, style], children: vertical ? (_jsxs(_Fragment, { children: [valorEscrito, trilhoDesenhado, !labelHidden && (typeof label === "string" ? _jsx(Text, { size: "sm", weight: 500, children: label }) : label)] })) : (_jsxs(_Fragment, { children: [!labelHidden
247
+ ? _jsx(Label, { trailing: valorEscrito, style: s.cabeca, children: label })
248
+ : valorEscrito && _jsx(View, { style: s.soValor, children: valorEscrito }), trilhoDesenhado] })) }));
249
+ }
250
+ function AlcaDoDeslizante({ indice, ctl, vertical, deslocamento, arrastando, inativo, nome, valorEscrito, porcento, rotulosDasAcoes, testID, }) {
251
+ const t = useAureaTokens();
252
+ const s = folha(t);
253
+ const reduzir = useReduceMotion();
254
+ // A bolinha encolhe a 90% enquanto o dedo arrasta — a referência e o `.slider-thumb` da web. Com
255
+ // "menos movimento" ela salta, sem deslizar.
256
+ const escala = React.useRef(new Animated.Value(1)).current;
257
+ React.useEffect(() => {
258
+ const destino = arrastando ? 0.9 : 1;
259
+ if (reduzir !== false) {
260
+ escala.setValue(destino);
261
+ return;
262
+ }
263
+ const a = Animated.timing(escala, { toValue: destino, duration: t.size.durationFast, useNativeDriver: driverNativo() });
264
+ a.start();
265
+ return () => a.stop();
266
+ }, [arrastando, reduzir, escala, t.size.durationFast]);
267
+ const gesto = React.useMemo(() => PanResponder.create({
268
+ onStartShouldSetPanResponder: () => !ctl.current.inativo,
269
+ onMoveShouldSetPanResponder: () => !ctl.current.inativo,
270
+ onPanResponderTerminationRequest: () => false,
271
+ onPanResponderGrant: () => ctl.current.pegar(indice),
272
+ onPanResponderMove: (_e, g) => ctl.current.mover(g),
273
+ onPanResponderRelease: () => ctl.current.soltar(),
274
+ onPanResponderTerminate: () => ctl.current.soltar(),
275
+ }), [ctl, indice]);
276
+ // A área de toque da alça chega aos 44 nos dois eixos (ela tem 28 × 20).
277
+ const noEixo = (t.size.targetMin - t.size.space7) / 2;
278
+ const fora = (t.size.targetMin - t.size.space5) / 2;
279
+ return (_jsx(View, { ...gesto.panHandlers, testID: testID, hitSlop: vertical ? { top: noEixo, bottom: noEixo, left: fora, right: fora }
280
+ : { top: fora, bottom: fora, left: noEixo, right: noEixo }, accessible: true, accessibilityRole: "adjustable", accessibilityLabel: nome,
281
+ // O motor do aparelho converte min, max e now para INTEIRO, e um valor fracionário (0,75)
282
+ // derruba a ponte — por isso a escala de 0 a 100, como na referência; quem fala o valor é o
283
+ // `text`, já formatado.
284
+ accessibilityValue: { min: 0, max: 100, now: porcento, text: valorEscrito }, accessibilityActions: [{ name: "increment", label: rotulosDasAcoes[0] }, { name: "decrement", label: rotulosDasAcoes[1] }], onAccessibilityAction: (e) => {
285
+ const nomeDaAcao = e.nativeEvent.actionName;
286
+ if (nomeDaAcao === "increment")
287
+ ctl.current.passo(indice, 1);
288
+ else if (nomeDaAcao === "decrement")
289
+ ctl.current.passo(indice, -1);
290
+ }, ...estadoAcessivel({ disabled: inativo }), style: [s.alca, vertical ? s.alcaEmPe : s.alcaDeitada, vertical ? { bottom: deslocamento } : { left: deslocamento }], children: _jsx(Animated.View, { style: [s.bolinha, { transform: [{ scale: escala }] }] }) }));
291
+ }
292
+ const soDigitos = (s) => s.replace(/\D/g, "");
293
+ /**
294
+ * O próximo valor a partir do texto do campo escondido. Só dígitos (o `numeric` da web), cortado
295
+ * no tamanho. Quando chegam vários dígitos de uma vez — colar, ou o código do SMS que o sistema
296
+ * preenche — e eles formam um código inteiro, o código colado vence o que já estava escrito.
297
+ */
298
+ function proximoCodigo(anterior, texto, tamanho) {
299
+ const novo = soDigitos(texto);
300
+ if (novo.length > anterior.length + 1) {
301
+ const colado = texto.startsWith(anterior) ? soDigitos(texto.slice(anterior.length)) : novo;
302
+ if (colado.length >= tamanho)
303
+ return colado.slice(0, tamanho);
304
+ }
305
+ return novo.slice(0, tamanho);
306
+ }
307
+ /**
308
+ * O campo de código de uso único — a anatomia da referência: UM campo de texto invisível por cima
309
+ * das casas, que só desenham. É ele que recebe o teclado, o colar e o código do SMS
310
+ * (`textContentType="oneTimeCode"` no iPhone, `autoComplete="sms-otp"` no Android).
311
+ *
312
+ * <Field label="Código enviado por SMS"><OTPField length={6} value={codigo} onValueChange={setCodigo} /></Field>
313
+ *
314
+ * A casa da vez ganha a borda do foco e o cursor que pisca (parado com "menos movimento"). Só
315
+ * dígitos entram, como na web. Para o leitor de tela é um campo só, com o nome do `label` (ou do
316
+ * `Field`); as casas ficam fora da árvore, para ele não ler cada dígito duas vezes.
317
+ */
318
+ export function OTPField({ length, value, defaultValue, onValueChange, mask, disabled, label, style, testID }) {
319
+ const t = useAureaTokens();
320
+ const s = folha(t);
321
+ const strings = useAureaStrings();
322
+ const campo = useCampo();
323
+ const pele = usePeleSobreAMarca();
324
+ const inativo = disabled ?? campo?.disabled ?? false;
325
+ const invalido = campo?.invalido ?? false;
326
+ const [proprio, setProprio] = React.useState(() => soDigitos(defaultValue ?? "").slice(0, length));
327
+ const valor = value !== undefined ? soDigitos(value).slice(0, length) : proprio;
328
+ const [focado, setFocado] = React.useState(false);
329
+ const campoRef = React.useRef(null);
330
+ const aoDigitar = (texto) => {
331
+ const novo = proximoCodigo(valor, texto, length);
332
+ // Letra rejeitada: o valor não muda, e o campo controlado volta sozinho ao que era.
333
+ if (novo === valor)
334
+ return;
335
+ if (value === undefined)
336
+ setProprio(novo);
337
+ onValueChange?.(novo);
338
+ };
339
+ return (_jsxs(Pressable, { onPress: () => campoRef.current?.focus(), disabled: inativo, accessible: false, testID: testID, style: [s.codigo, inativo && s.desabilitado, style], children: [_jsx(View, { accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", style: s.casas, children: Array.from({ length }, (_, i) => {
340
+ const digito = valor[i];
341
+ // A da vez é a primeira vazia, só com o campo em foco; cheio, nenhuma (a referência).
342
+ const ativa = focado && i === valor.length;
343
+ return (_jsx(View, { testID: testID ? `${testID}-casa-${i}` : undefined, style: [
344
+ s.casa,
345
+ ativa && s.casaAtiva,
346
+ invalido && s.casaInvalida,
347
+ // Sobre a marca vale a tinta, inclusive no lugar do vermelho — a regra do `Input`.
348
+ pele && { borderColor: pele.borderColor, backgroundColor: pele.backgroundColor },
349
+ ], children: digito != null
350
+ ? (mask
351
+ ? _jsx(View, { style: [s.ponto, pele && { backgroundColor: pele.color }] })
352
+ : _jsx(Text, { style: [s.digito, pele && { color: pele.color }], children: digito }))
353
+ : ativa ? _jsx(CursorDoCodigo, { cor: pele?.color }) : null }, i));
354
+ }) }), _jsx(TextInput, { ref: campoRef, testID: testID ? `${testID}-campo` : undefined, value: valor, onChangeText: aoDigitar, onFocus: () => setFocado(true), onBlur: () => setFocado(false), editable: !inativo, inputMode: "numeric", textContentType: "oneTimeCode", autoComplete: Platform.OS === "android" ? "sms-otp" : "one-time-code", autoCorrect: false, autoCapitalize: "none",
355
+ // Sem `maxLength`: um limite no campo cortaria o texto colado ANTES de a Aurea achar o
356
+ // código dentro dele ("Seu código é 123456").
357
+ // No iPhone, com opacidade zero o campo não aceita colar — então 0,02 e a letra
358
+ // transparente, e o cursor do sistema escondido (o desenhado é o da casa).
359
+ caretHidden: Platform.OS === "ios", accessibilityLabel: label ?? campo?.label, accessibilityHint: campo?.hint, accessibilityValue: { text: `${valor.length} ${strings.positionOf} ${length}` }, ...estadoAcessivel({ disabled: inativo }), "aria-invalid": invalido || undefined, style: [s.campoEscondido, Platform.select({ ios: { opacity: 0.02, color: "transparent" }, default: { opacity: 0 } })] })] }));
360
+ }
361
+ function CursorDoCodigo({ cor }) {
362
+ const t = useAureaTokens();
363
+ const s = folha(t);
364
+ const reduzir = useReduceMotion();
365
+ const opacidade = React.useRef(new Animated.Value(1)).current;
366
+ React.useEffect(() => {
367
+ // Parado e aceso com "menos movimento" (e enquanto o sistema não respondeu).
368
+ if (reduzir !== false) {
369
+ opacidade.setValue(1);
370
+ return;
371
+ }
372
+ // O piscar da referência: 500 ms para apagar, 500 para acender.
373
+ const laco = Animated.loop(Animated.sequence([
374
+ Animated.timing(opacidade, { toValue: 0, duration: 500, useNativeDriver: driverNativo() }),
375
+ Animated.timing(opacidade, { toValue: 1, duration: 500, useNativeDriver: driverNativo() }),
376
+ ]));
377
+ laco.start();
378
+ return () => laco.stop();
379
+ }, [opacidade, reduzir]);
380
+ return _jsx(Animated.View, { pointerEvents: "none", style: [s.cursor, cor != null && { backgroundColor: cor }, { opacity: opacidade }] });
381
+ }
382
+ /**
383
+ * As medidas de cada tamanho, as da referência: em cima e embaixo 2 · 4 · 6, dos lados 8 · 10 · 12,
384
+ * vão 4 · 4 · 6, letra 12 · 14 · 16 na linha de 16 · 20 · 24.
385
+ */
386
+ const medidasDaEtiqueta = (t, tamanho) => tamanho === "sm"
387
+ ? { vertical: t.size.space05, lados: t.size.space2, vao: t.size.space1, letra: t.size.textXs, linha: t.size.space4 }
388
+ : tamanho === "lg"
389
+ ? { vertical: t.size.space1 + t.size.space05, lados: t.size.space3, vao: t.size.space1 + t.size.space05,
390
+ letra: t.size.textBase, linha: t.size.space6 }
391
+ : { vertical: t.size.space1, lados: t.size.space2 + t.size.space05, vao: t.size.space1, letra: t.size.textSm,
392
+ linha: t.size.space5 };
393
+ /**
394
+ * A mistura de duas cores opacas, `pct` da primeira — o `color-mix()` da web, que o React Native
395
+ * não tem. ⚠ A conta é em sRGB; a web mistura em OKLab. Com 10% a diferença não passa de um ponto
396
+ * por canal. Cor que não for hex de seis cai na segunda, para nunca entregar uma cor que o motor
397
+ * descarte.
398
+ */
399
+ function misturar(a, b, pct) {
400
+ const hex = /^#[0-9a-fA-F]{6}$/;
401
+ if (!hex.test(a) || !hex.test(b))
402
+ return b;
403
+ const ca = parseInt(a.slice(1), 16);
404
+ const cb = parseInt(b.slice(1), 16);
405
+ return "#" + [16, 8, 0]
406
+ .map((d) => Math.round(((ca >> d) & 255) * pct + ((cb >> d) & 255) * (1 - pct)).toString(16).padStart(2, "0"))
407
+ .join("");
408
+ }
409
+ const textoDaEtiqueta = (it) => it.textValue ?? (typeof it.label === "string" ? it.label : "");
410
+ /**
411
+ * O grupo de etiquetas da referência: as etiquetas numa fila que quebra, com rótulo, descrição e
412
+ * erro; escolha de nenhuma, uma ou várias; e o X de tirar.
413
+ *
414
+ * <TagGroup label="Assuntos" selectionMode="multiple" items={assuntos}
415
+ * selectedKeys={escolhidos} onSelectionChange={setEscolhidos} onRemove={tirar} />
416
+ *
417
+ * ⚠ **O X é IRMÃO da etiqueta, e não filho**, ao contrário da referência (lá o botão de tirar mora
418
+ * dentro do item tocável). No iPhone, um tocável dentro de outro some para o VoiceOver (`check
419
+ * 43`); aqui a etiqueta é uma moldura com duas partes lado a lado — a que escolhe e a que tira —,
420
+ * como o `.tag-main` e o `.tag-remove` da web.
421
+ *
422
+ * Com escolha, cada etiqueta é um botão com o estado `selected`; sem escolha, é só texto. A
423
+ * escolhida ganha o fundo com 10% da marca e a letra no destaque (`primaryEmphasis`), a pintura
424
+ * da web.
425
+ */
426
+ export function TagGroup({ label, labelHidden, description, errorMessage, items, selectionMode = "none", selectedKeys, defaultSelectedKeys, onSelectionChange, onRemove, size = "md", variant = "default", icons, style, testID, }) {
427
+ const t = useAureaTokens();
428
+ const s = folha(t);
429
+ const strings = useAureaStrings();
430
+ const [proprio, setProprio] = React.useState(defaultSelectedKeys ?? []);
431
+ const escolhidos = selectedKeys ?? proprio;
432
+ const escolhe = selectionMode !== "none";
433
+ const m = medidasDaEtiqueta(t, size);
434
+ const escolher = (it) => {
435
+ if (!escolhe || it.disabled)
436
+ return;
437
+ const ja = escolhidos.includes(it.id);
438
+ const novo = selectionMode === "multiple"
439
+ ? (ja ? escolhidos.filter((x) => x !== it.id) : [...escolhidos, it.id])
440
+ : (ja ? [] : [it.id]);
441
+ if (selectedKeys === undefined)
442
+ setProprio(novo);
443
+ onSelectionChange?.(novo);
444
+ };
445
+ // As áreas de toque chegam aos 44: a parte que escolhe, pela altura da linha; o X (12), pelos
446
+ // quatro lados — e o pedaço do X que cai em cima do texto fica com o texto (`principal`, acima).
447
+ const folgaDoTexto = Math.max(0, (t.size.targetMin - m.linha) / 2);
448
+ const folgaDoX = (t.size.targetMin - t.size.space3) / 2;
449
+ const fundo = variant === "surface" ? t.color.card : t.color.secondary;
450
+ const letra = variant === "surface" ? t.color.cardForeground : t.color.secondaryForeground;
451
+ return (_jsxs(View, { testID: testID, style: [s.grupo, style], children: [!labelHidden && _jsx(Label, { children: label }), _jsx(View, { accessibilityRole: "list", accessibilityLabel: typeof label === "string" ? label : undefined, style: s.lista, children: items.map((it) => {
452
+ const escolhida = escolhe && escolhidos.includes(it.id);
453
+ const cor = escolhida ? t.color.primaryEmphasis : letra;
454
+ const conteudo = (_jsxs(_Fragment, { children: [it.icon != null && _jsx(Icon, { name: it.icon, size: "sm", color: cor, icons: icons }), _jsx(Text, { weight: 500, numberOfLines: 1, style: { fontSize: m.letra, lineHeight: m.linha, color: cor }, children: it.label })] }));
455
+ const estiloPrincipal = [s.principal, { gap: m.vao }];
456
+ return (_jsxs(View, { testID: testID ? `${testID}-${it.id}` : undefined, style: [
457
+ s.etiqueta,
458
+ { gap: m.vao, paddingVertical: m.vertical, paddingHorizontal: m.lados,
459
+ // O fundo existe desde a montagem; escolher só troca a cor (a regra do raio no Android).
460
+ backgroundColor: escolhida ? misturar(t.color.primary, t.color.secondary, 0.1) : fundo },
461
+ it.disabled && s.desabilitado,
462
+ ], children: [escolhe ? (_jsx(Pressable, { onPress: it.disabled ? undefined : () => escolher(it), disabled: it.disabled, accessibilityRole: "button", accessibilityLabel: textoDaEtiqueta(it) || undefined, ...estadoAcessivel({ selected: escolhida, disabled: !!it.disabled }), hitSlop: { top: folgaDoTexto, bottom: folgaDoTexto, left: m.lados, right: onRemove ? 0 : m.lados }, testID: testID ? `${testID}-${it.id}-escolher` : undefined, style: estiloPrincipal, children: conteudo })) : (_jsx(View, { style: estiloPrincipal, children: conteudo })), onRemove && (_jsx(Pressable, { onPress: it.disabled ? undefined : () => onRemove([it.id]), disabled: it.disabled, accessibilityRole: "button", accessibilityLabel: `${strings.fileRemove} ${textoDaEtiqueta(it)}`.trim(), ...estadoAcessivel({ disabled: !!it.disabled }), hitSlop: folgaDoX, testID: testID ? `${testID}-${it.id}-remover` : undefined, style: s.remover, children: _jsx(Icon, { name: "x", weight: "bold", size: t.size.space3, color: cor, icons: icons }) }))] }, it.id));
463
+ }) }), description != null && (typeof description === "string"
464
+ ? _jsx(Text, { size: "xs", tone: "muted", children: description }) : description), errorMessage != null && (typeof errorMessage === "string"
465
+ ? _jsx(Text, { size: "xs", tone: "danger", children: errorMessage }) : errorMessage)] }));
466
+ }
@@ -0,0 +1,188 @@
1
+ import * as React from "react";
2
+ import { type PressableProps, type StyleProp, type ViewStyle } from "react-native";
3
+ import { Skeleton, type AureaSkeletonAnimation } from "./feedback.js";
4
+ /** `surface` põe tudo num cartão. Os mesmos nomes da web. */
5
+ export type AureaAccordionVariant = "default" | "surface";
6
+ /** Um item — o MESMO modelo do `Accordion` da web (`AccordionItem`). */
7
+ export interface AureaAccordionItem {
8
+ id: string;
9
+ title: React.ReactNode;
10
+ content: React.ReactNode;
11
+ /** O item que não abre: anunciado como desligado, apagado, e o toque não faz nada. */
12
+ disabled?: boolean;
13
+ }
14
+ export interface AccordionProps {
15
+ items: AureaAccordionItem[];
16
+ variant?: AureaAccordionVariant;
17
+ /** Deixa vários itens abertos ao mesmo tempo. Sem isto, abrir um fecha o outro. */
18
+ multiple?: boolean;
19
+ /** Os itens que começam abertos (o `id` de cada um). */
20
+ defaultValue?: string[];
21
+ /**
22
+ * Os itens abertos, do lado de quem usa — o `value` da referência, que a web não tem (lá o
23
+ * `<details>` guarda o próprio estado). Com ele, quem abre e fecha é o `onValueChange`.
24
+ */
25
+ value?: string[];
26
+ /** Chamado com a lista nova de abertos a cada toque. */
27
+ onValueChange?: (value: string[]) => void;
28
+ style?: StyleProp<ViewStyle>;
29
+ testID?: string;
30
+ }
31
+ /**
32
+ * Seções que abrem e fecham — o accordion da referência, com a API da web (`items`, `variant`,
33
+ * `multiple`, `defaultValue`).
34
+ *
35
+ * Um de cada vez por padrão, e o aberto fecha no toque (o `isCollapsible` dela, ligado). O
36
+ * gatilho é um botão que diz se está aberto (`expanded`) dentro de um título (`header`), como o
37
+ * dela; a resposta fechada não está na árvore, e o leitor de tela não a lê.
38
+ */
39
+ export declare function Accordion({ items, variant, multiple, defaultValue, value, onValueChange, style, testID, }: AccordionProps): React.JSX.Element;
40
+ export type AureaScrollShadowOrientation = "horizontal" | "vertical";
41
+ /**
42
+ * Em que ponta o fio pode aparecer. `auto` e `both` são as duas pontas — e são o mesmo, como no
43
+ * código dela (lá o `both` cai na mesma conta do `auto`). `top`/`left` só a do começo,
44
+ * `bottom`/`right` só a do fim, `none` nenhuma.
45
+ */
46
+ export type AureaScrollShadowVisibility = "auto" | "top" | "bottom" | "left" | "right" | "both" | "none";
47
+ /**
48
+ * `surface` (o padrão): a lista numa superfície da Aurea, com o canto 22 do cartão, como o `Accordion`
49
+ * no cartão. `plain`: sem caixa — para a lista que já mora dentro de um cartão (caixa dentro de caixa é
50
+ * o que o `KPI variant="plain"` evita).
51
+ */
52
+ export type AureaScrollShadowVariant = "surface" | "plain";
53
+ export interface ScrollShadowProps {
54
+ /** O rolador: um `ScrollView`, um `FlatList`… Um elemento só. */
55
+ children: React.ReactElement;
56
+ /** Sem isto, sai do `horizontal` do rolador, como na referência. */
57
+ orientation?: AureaScrollShadowOrientation;
58
+ visibility?: AureaScrollShadowVisibility;
59
+ /** `false` tira os fios. */
60
+ isEnabled?: boolean;
61
+ /** Padrão: `surface`. */
62
+ variant?: AureaScrollShadowVariant;
63
+ style?: StyleProp<ViewStyle>;
64
+ testID?: string;
65
+ }
66
+ /**
67
+ * "Tem mais para lá" — o scroll-shadow da referência, com um FIO no lugar do degradê.
68
+ *
69
+ * 🔴 **Decisão do Victor:** a referência esmaece a ponta com um degradê de 50; a Aurea não tem
70
+ * degradê (CLAUDE.md §5). Aqui é uma linha de 1 (a borda da Aurea, na cor de borda do tema), com as
71
+ * pontas em cápsula, na ponta onde há mais conteúdo escondido, e SÓ lá: no topo da lista não há fio
72
+ * em cima; no fim, não há embaixo. Na horizontal, à esquerda e à direita.
73
+ *
74
+ * 🔴 **E a lista mora numa superfície da Aurea** (Victor, 12/10/2026: *"o ScrollShadow não é [arredondado]
75
+ * como todos da Aurea"*, apontando o `Accordion` no cartão): por padrão, fundo do cartão, canto 22 e a
76
+ * sombra de superfície — a referência não tem caixa nenhuma. `variant="plain"` tira a caixa, para dentro
77
+ * de um cartão; aí o fio recua 22 dos lados, para não fazer quina.
78
+ *
79
+ * Ele mede como o dela, pelos três avisos do rolador (`onLayout`, `onContentSizeChange` e
80
+ * `onScroll`), e entrega ao rolador os avisos que o app já tinha posto, depois dos dele. O
81
+ * `inverted` (a conversa, de baixo para cima) troca as pontas. O fio entra e sai esmaecendo em
82
+ * 200 ms; com "menos movimento", aparece e some de uma vez.
83
+ *
84
+ * ⚠ Um `onScroll` que não é função — o `Animated.event` com o motor nativo — não é repassado: o
85
+ * aviso dele vive no motor, e não há como chamá-lo daqui.
86
+ */
87
+ export declare function ScrollShadow({ children, orientation, visibility, isEnabled, variant, style, testID, }: ScrollShadowProps): React.JSX.Element | null;
88
+ export interface SkeletonGroupProps {
89
+ children?: React.ReactNode;
90
+ /** `false` troca todos os esqueletos de dentro pelo conteúdo deles. Padrão: `true`. */
91
+ isLoading?: boolean;
92
+ /**
93
+ * Some o grupo inteiro quando `isLoading` é `false` — para o grupo feito só de esqueletos, cujas
94
+ * caixas de arrumação ficariam vazias ocupando lugar. Padrão: `false`.
95
+ */
96
+ isSkeletonOnly?: boolean;
97
+ /** `"none"` para o pulso de todos. Padrão: `"pulse"`. */
98
+ animation?: AureaSkeletonAnimation;
99
+ style?: StyleProp<ViewStyle>;
100
+ testID?: string;
101
+ }
102
+ /**
103
+ * Um comando para vários esqueletos — o skeleton-group da referência. O `isLoading` e a `animation`
104
+ * descem para todo `Skeleton` de dentro (a qualquer profundidade), e cada um pode trocar o seu.
105
+ *
106
+ * O PULSO é um só, do grupo: as caixas sobem e descem juntas, em vez de cada uma no tempo em que
107
+ * montou. `SkeletonGroup.Item` é o próprio `Skeleton` — o nome existe porque a referência o tem.
108
+ */
109
+ declare function SkeletonGroupRaiz({ children, isLoading, isSkeletonOnly, animation, style, testID, }: SkeletonGroupProps): React.JSX.Element | null;
110
+ export declare const SkeletonGroup: typeof SkeletonGroupRaiz & {
111
+ Item: typeof Skeleton;
112
+ };
113
+ export interface PressableFeedbackProps extends Omit<PressableProps, "children" | "style" | "disabled"> {
114
+ children?: React.ReactNode;
115
+ /** Desliga o toque: anunciado como desligado e apagado (a `REACAO_AO_TOQUE` inerte da Aurea). */
116
+ isDisabled?: boolean;
117
+ /**
118
+ * `false` tira a escala da raiz — para pôr a `PressableFeedback.Scale` num filho. `"disable-all"`
119
+ * desliga também o realce, a onda e a escala de dentro. Padrão: `true`.
120
+ */
121
+ animation?: boolean | "disable-all";
122
+ /** A escala com o dedo em cima, numa peça de 300 de largura. Padrão: 0,985, a da referência. */
123
+ scale?: number;
124
+ style?: StyleProp<ViewStyle>;
125
+ }
126
+ /**
127
+ * Um alvo de toque que responde — o pressable-feedback da referência. Por padrão a peça inteira
128
+ * encolhe um pouco com o dedo em cima (0,985 numa peça de 300, proporcional à largura, em 300 ms).
129
+ * Dentro dele, três partes que se combinam: `PressableFeedback.Highlight` (a camada que acende),
130
+ * `PressableFeedback.Ripple` (a onda que nasce no dedo) e `PressableFeedback.Scale` (a escala num
131
+ * filho só).
132
+ *
133
+ * É um botão para o leitor de tela (`accessibilityRole` troca). Com "menos movimento" a escala e o
134
+ * realce SALTAM em vez de deslizar — como o `Button` da Aurea, que salta para 0,97 —, e a onda vira
135
+ * uma camada parada.
136
+ *
137
+ * ⚠ Ele é UM elemento para o leitor de tela: não ponha botão dentro (no iPhone, o de dentro some —
138
+ * o `check 43`). O que vai dentro é conteúdo.
139
+ */
140
+ declare function PressableFeedbackRaiz({ children, isDisabled, animation, scale, style, onLayout, onPressIn, onPressOut, accessibilityRole, accessibilityState, ...resto }: PressableFeedbackProps): React.JSX.Element;
141
+ export interface PressableFeedbackScaleProps {
142
+ children?: React.ReactNode;
143
+ /** A escala com o dedo em cima, pela largura da RAIZ. Padrão: 0,985. */
144
+ scale?: number;
145
+ /** `false` para esta escala. */
146
+ animation?: boolean;
147
+ style?: StyleProp<ViewStyle>;
148
+ testID?: string;
149
+ }
150
+ /** A escala num filho só — com `animation={false}` na raiz, para não encolher duas vezes. */
151
+ declare function EscalaDoToque({ children, scale, animation, style, testID }: PressableFeedbackScaleProps): React.JSX.Element;
152
+ export interface PressableFeedbackHighlightProps {
153
+ /** Padrão: a cor do texto do tema (a referência usa um cinza claro no escuro e um escuro no claro). */
154
+ color?: string;
155
+ /** A opacidade com o dedo em cima. Padrão: 0,1. */
156
+ opacity?: number;
157
+ /** `false` tira o realce. */
158
+ animation?: boolean;
159
+ style?: StyleProp<ViewStyle>;
160
+ testID?: string;
161
+ }
162
+ /** A camada que acende com o dedo em cima (o toque do iPhone): cobre a peça e vai a 10% em 200 ms. */
163
+ declare function RealceDoToque({ color, opacity, animation, style, testID }: PressableFeedbackHighlightProps): React.JSX.Element | null;
164
+ export interface PressableFeedbackRippleProps {
165
+ /** Padrão: a cor do texto do tema. */
166
+ color?: string;
167
+ /** A opacidade no pico da onda. Padrão: 0,1. */
168
+ opacity?: number;
169
+ /** `false` tira a onda. */
170
+ animation?: boolean;
171
+ style?: StyleProp<ViewStyle>;
172
+ testID?: string;
173
+ }
174
+ /**
175
+ * A onda que nasce no dedo (o toque do Android): um círculo de 1,25 × a diagonal da peça, que
176
+ * cresce de 0 até cobrir tudo e some ao soltar. Duas camadas que se revezam, como na referência: o
177
+ * toque novo começa numa camada limpa enquanto a anterior termina de sumir, e a onda não pisca.
178
+ *
179
+ * Círculo cheio, de UMA cor — a dela é um degradê do centro para fora, e a Aurea não tem degradê.
180
+ * Com "menos movimento" ela não cresce: acende inteira ao tocar e apaga ao soltar.
181
+ */
182
+ declare function OndaDoToque({ color, opacity, animation, style, testID }: PressableFeedbackRippleProps): React.JSX.Element | null;
183
+ export declare const PressableFeedback: typeof PressableFeedbackRaiz & {
184
+ Scale: typeof EscalaDoToque;
185
+ Highlight: typeof RealceDoToque;
186
+ Ripple: typeof OndaDoToque;
187
+ };
188
+ export {};