@aurea-uds/native 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +135 -3
- package/dist/busca.d.ts +119 -0
- package/dist/busca.js +338 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +21 -0
- package/dist/inputs.d.ts +31 -1
- package/dist/inputs.js +19 -2
- package/dist/midia.d.ts +98 -0
- package/dist/midia.js +181 -0
- package/dist/numero.d.ts +100 -0
- package/dist/numero.js +291 -0
- package/dist/strings.d.ts +26 -0
- package/dist/strings.js +16 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,9 +6,18 @@ Pacote **irmão** do `@aurea-uds/react`, não um wrapper dele: componentes próp
|
|
|
6
6
|
`View`/`Text`/`Pressable`, consumindo `@aurea-uds/tokens`. Só os tokens atravessam — decisão de
|
|
7
7
|
18/07/2026, registrada no `ROADMAP.md` Fase 7.
|
|
8
8
|
|
|
9
|
-
> **Estado:
|
|
10
|
-
>
|
|
11
|
-
>
|
|
9
|
+
> **Estado: 51 componentes.** O **Lote 7** entrou em 11/09/2026 e é o primeiro que **não sai do
|
|
10
|
+
> plano** — ele saiu do APP. O consumidor mediu três lacunas na primeira tela que a pessoa toca, e
|
|
11
|
+
> elas viraram cinco componentes: **`Combobox`, `SearchField`, `NumberField`, `Image` e
|
|
12
|
+
> `Gallery`**, mais a prop `formatOnBlur` no `Input`. **Zero dependência nova.**
|
|
13
|
+
> Ver a seção **Buscar, número e mídia** abaixo, e o `NATIVE.md` §8.
|
|
14
|
+
>
|
|
15
|
+
> ⚠ **O `Combobox` DIVERGE da web de propósito** — ele aceita busca REMOTA em seleção única, que
|
|
16
|
+
> lá não existe ([ADR-0043](../../decisions/0043-o-combobox-nativo-diverge-da-web-e-a-presenca-da-prop-e-a-chave.md)).
|
|
17
|
+
> ⚠ **Os cinco do Lote 7 ainda não rodaram em aparelho** — só em teste.
|
|
18
|
+
>
|
|
19
|
+
> **O PLANO fechou no Lote 6**, em 09/09/2026 (`Timeline`, `DataList`, `Table`), e o **`Chart`** —
|
|
20
|
+
> a única peça que tinha ficado de fora — entrou no mesmo dia pela
|
|
12
21
|
> [ADR-0041](../../decisions/0041-o-motor-de-grafico-do-nativo-e-nosso-sobre-react-native-svg.md).
|
|
13
22
|
>
|
|
14
23
|
> ⚠ **O `Chart` é o único componente deste pacote cuja GEOMETRIA é nossa.** Na web ele é 79 linhas
|
|
@@ -219,6 +228,129 @@ leitor anuncia.
|
|
|
219
228
|
O `behavior` muda com a plataforma — `padding` no iOS, `height` no Android — e a Aurea escolhe
|
|
220
229
|
por você. Um só nos dois lados deixa metade dos aparelhos com o botão de salvar sob o teclado.
|
|
221
230
|
|
|
231
|
+
## Buscar, número e mídia
|
|
232
|
+
|
|
233
|
+
O **Lote 7** (11/09/2026). Três lacunas que o app mediu, e não o plano — `NATIVE.md` §8.
|
|
234
|
+
|
|
235
|
+
### `Combobox` — digitar para achar um item
|
|
236
|
+
|
|
237
|
+
⚠ **Não confunda com o `Select`.** A escolha entre os dois é de TAMANHO DE LISTA, e a linha é
|
|
238
|
+
dura: o `Select` monta todos os itens num `ScrollView` — serve para unidades, estados e tipos.
|
|
239
|
+
**Um catálogo de milhares de linhas ali trava.** O `Combobox` usa `FlatList` e aceita busca remota.
|
|
240
|
+
|
|
241
|
+
```tsx
|
|
242
|
+
import {Combobox, Field} from "@aurea-uds/native";
|
|
243
|
+
|
|
244
|
+
// ① catálogo REMOTO — a Aurea não filtra, o app busca
|
|
245
|
+
<Field label="Item">
|
|
246
|
+
<Combobox
|
|
247
|
+
items={resultados} // a lista JÁ BUSCADA
|
|
248
|
+
value={escolhido} // o ITEM inteiro, não o id
|
|
249
|
+
onValueChange={setEscolhido}
|
|
250
|
+
onSearchChange={buscar} // ← a presença desta prop DESLIGA o filtro da Aurea
|
|
251
|
+
loading={buscando}
|
|
252
|
+
onEndReached={proximaPagina}
|
|
253
|
+
/>
|
|
254
|
+
</Field>
|
|
255
|
+
|
|
256
|
+
// ② lista NA MÃO — a Aurea filtra, ignorando acento e caixa
|
|
257
|
+
<Combobox items={unidades} value={unidade} onValueChange={setUnidade} />
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
**A regra em uma linha: `onSearchChange` presente = a Aurea não filtra.** É presença de prop, não
|
|
261
|
+
configuração — e ela existe porque filtrar de novo o que o servidor devolveu esconde resultado.
|
|
262
|
+
|
|
263
|
+
| | |
|
|
264
|
+
|---|---|
|
|
265
|
+
| espera antes de chamar `onSearchChange` | **250 ms**. `searchDelay={0}` desliga |
|
|
266
|
+
| onde se digita | **dentro da folha**, não no gatilho — o teclado ocupa metade da tela |
|
|
267
|
+
| `value` | o **item inteiro**: numa busca remota a lista some debaixo da escolha |
|
|
268
|
+
| ícones a registrar | `chevron--down`, `search`, `close` |
|
|
269
|
+
|
|
270
|
+
### `SearchField` — filtrar o que já está na tela
|
|
271
|
+
|
|
272
|
+
Outro papel: o `Combobox` **escolhe** de um catálogo e devolve a escolha; este **filtra** e
|
|
273
|
+
devolve texto.
|
|
274
|
+
|
|
275
|
+
```tsx
|
|
276
|
+
<SearchField placeholder="Filtrar" onSearchChange={setFiltro} />
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### `NumberField` — moeda, medida e contador
|
|
280
|
+
|
|
281
|
+
```tsx
|
|
282
|
+
<Field label="Valor">
|
|
283
|
+
<NumberField value={valor} onValueChange={setValor}
|
|
284
|
+
format={{style: "currency", currency: "BRL"}} locale="pt-BR" />
|
|
285
|
+
</Field>
|
|
286
|
+
|
|
287
|
+
<Field label="Litros">
|
|
288
|
+
<NumberField value={litros} onValueChange={setLitros}
|
|
289
|
+
format={{maximumFractionDigits: 1}} locale="pt-BR" min={0} />
|
|
290
|
+
</Field>
|
|
291
|
+
|
|
292
|
+
<Field label="Quantidade">
|
|
293
|
+
<NumberField value={qtd} onValueChange={setQtd} min={0} step={1} />
|
|
294
|
+
</Field>
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
⚠ **O que sai por `onValueChange` é `number`, sempre** — `1234.5`, nunca `"R$ 1.234,50"`.
|
|
298
|
+
|
|
299
|
+
⚠ **Formata no BLUR, e só no blur** ([ADR-0024](../../decisions/0024-mascara-de-campo-e-o-momento-nao-o-formato.md)
|
|
300
|
+
e [ADR-0042](../../decisions/0042-o-intl-e-o-formatador-do-nativo-e-a-volta-e-nossa.md)). Enquanto
|
|
301
|
+
o campo tem foco, ele mostra exatamente o que foi digitado. Máscara ao vivo é o defeito que o
|
|
302
|
+
USWDS publicou com reprovação WCAG registrada e que o MUI abandonou na v6.
|
|
303
|
+
|
|
304
|
+
⚠ **`notation: "compact"` é recusado** — quebrado no motor JS do RN nos dois sistemas. Sai aviso em
|
|
305
|
+
`__DEV__` e a opção é ignorada; o resto do formato sobrevive.
|
|
306
|
+
|
|
307
|
+
Os três auxiliares são públicos, porque o app tem o mesmo problema fora do campo:
|
|
308
|
+
|
|
309
|
+
```tsx
|
|
310
|
+
import {formatarNumero, lerNumero, separadoresDoLocale} from "@aurea-uds/native";
|
|
311
|
+
|
|
312
|
+
formatarNumero(1234.5, "pt-BR", {style: "currency", currency: "BRL"}); // "R$ 1.234,50"
|
|
313
|
+
lerNumero("R$ 1.234,50", "pt-BR"); // 1234.5
|
|
314
|
+
lerNumero("", "pt-BR"); // null ← não é 0
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Para **texto** (placa, documento, telefone) o momento é o mesmo e o formato é seu:
|
|
318
|
+
|
|
319
|
+
```tsx
|
|
320
|
+
<Input value={placa} onChangeText={setPlaca} formatOnBlur={(v) => v.toUpperCase()} />
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
⚠ `formatOnBlur` exige o campo **controlado** — o valor formatado sai por `onChangeText`.
|
|
324
|
+
|
|
325
|
+
### `Image` e `Gallery` — mostrar foto
|
|
326
|
+
|
|
327
|
+
⚠ **`Image` NÃO é o `Image` do React Native com outra pele.** Ele faz as duas coisas que o
|
|
328
|
+
primitivo cru não faz: **reserva a caixa** antes dos bytes (sem isso o layout salta) e **cai para
|
|
329
|
+
um substituto** quando eles não vêm (sem isso uma URL quebrada deixa buraco).
|
|
330
|
+
|
|
331
|
+
```tsx
|
|
332
|
+
<Image source={foto.uri} alt="Frente do item" ratio="4/3" />
|
|
333
|
+
|
|
334
|
+
<Gallery items={fotos} zoom selected={atual} onSelect={setAtual} />
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
| | |
|
|
338
|
+
|---|---|
|
|
339
|
+
| `ratio` | aceita `4/3`, `"4/3"`, `"16:9"` ou o número. A web usa string; o RN quer número |
|
|
340
|
+
| `alt` | **obrigatório**. Decorativa de verdade se escreve `alt=""`, explícito |
|
|
341
|
+
| ampliar | é o **`Dialog`** que já existe, não uma superfície nova |
|
|
342
|
+
| legenda | com legenda, o nome vai para o LADRILHO e a imagem sai da árvore de acessibilidade |
|
|
343
|
+
| ícone a registrar | `image` (o substituto) |
|
|
344
|
+
|
|
345
|
+
Quem já usa `expo-image` por cache de disco troca o elemento sem perder a pele:
|
|
346
|
+
|
|
347
|
+
```tsx
|
|
348
|
+
<Image render={<ExpoImage contentFit="cover" transition={150} />} source={u} alt="…" />
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
⚠ `fit` **não** é traduzido para o elemento passado — `resizeMode` é do `Image` do RN e o
|
|
352
|
+
`expo-image` chama a mesma coisa de `contentFit`. Quem passa o elemento escreve a prop dele.
|
|
353
|
+
|
|
222
354
|
## Data e câmera
|
|
223
355
|
|
|
224
356
|
Estes dois **não saem do barril** — são o único ponto do pacote com importação própria:
|
package/dist/busca.d.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { type StyleProp, type ViewStyle } from "react-native";
|
|
3
|
+
import { type IconName } from "./icon.js";
|
|
4
|
+
import { type AureaFieldSize } from "./inputs.js";
|
|
5
|
+
/** Uma linha do catálogo. **Mesma forma da web** (`ComboboxOption`), mais o `disabled` que o `Select` daqui já tinha. */
|
|
6
|
+
export interface AureaComboboxItem {
|
|
7
|
+
value: string;
|
|
8
|
+
label: string;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface ComboboxProps {
|
|
12
|
+
/**
|
|
13
|
+
* O que a folha mostra. **Com `onSearchChange`, é a lista JÁ BUSCADA** — a Aurea não filtra e
|
|
14
|
+
* não guarda; ela desenha o que chegou.
|
|
15
|
+
*/
|
|
16
|
+
items: AureaComboboxItem[];
|
|
17
|
+
/**
|
|
18
|
+
* O escolhido, como ITEM inteiro e não como `value` — mesma escolha da web, e ela tem razão
|
|
19
|
+
* prática aqui: numa busca remota o item escolhido pode não estar mais em `items` (a pessoa
|
|
20
|
+
* digitou outra coisa depois de escolher), e o gatilho ainda precisa saber que rótulo mostrar.
|
|
21
|
+
* Guardar só o `value` deixaria o campo em branco no instante seguinte à escolha.
|
|
22
|
+
*/
|
|
23
|
+
value?: AureaComboboxItem | null;
|
|
24
|
+
onValueChange?: (v: AureaComboboxItem | null) => void;
|
|
25
|
+
/**
|
|
26
|
+
* O que foi digitado, já com espera. **A presença desta prop DESLIGA o filtro da Aurea** — é a
|
|
27
|
+
* chave entre catálogo remoto e lista na mão, e é presença, não configuração.
|
|
28
|
+
*/
|
|
29
|
+
onSearchChange?: (query: string) => void;
|
|
30
|
+
/**
|
|
31
|
+
* A espera antes de chamar `onSearchChange`, em ms. Padrão **250**.
|
|
32
|
+
*
|
|
33
|
+
* Não é enfeite de performance: sem ela, "cadeira" são sete chamadas ao servidor e seis
|
|
34
|
+
* respostas descartadas — e a última a chegar pode não ser a última pedida. `0` desliga.
|
|
35
|
+
*/
|
|
36
|
+
searchDelay?: number;
|
|
37
|
+
/** Mostra o anel no lugar da lista. Use enquanto a busca remota não voltou. */
|
|
38
|
+
loading?: boolean;
|
|
39
|
+
/** Fim da lista alcançado — é por aqui que o catálogo remoto pagina. */
|
|
40
|
+
onEndReached?: () => void;
|
|
41
|
+
/** O que o gatilho mostra sem escolha. */
|
|
42
|
+
placeholder?: string;
|
|
43
|
+
/** O que o campo da folha mostra vazio. Sem ele, a frase `comboboxSearch` do provider. */
|
|
44
|
+
searchPlaceholder?: string;
|
|
45
|
+
/** O que a folha mostra sem resultado. Sem ele, a frase `comboboxEmpty` do provider. */
|
|
46
|
+
empty?: React.ReactNode;
|
|
47
|
+
/** Mostra o botão de limpar no gatilho quando há escolha. Padrão **true**. */
|
|
48
|
+
clearable?: boolean;
|
|
49
|
+
disabled?: boolean;
|
|
50
|
+
size?: AureaFieldSize;
|
|
51
|
+
/** O glifo da seta. Registre-o, ou passe `false`. */
|
|
52
|
+
chevron?: IconName | false;
|
|
53
|
+
/** O glifo da lupa no campo da folha. Registre-o, ou passe `false`. */
|
|
54
|
+
searchIcon?: IconName | false;
|
|
55
|
+
style?: StyleProp<ViewStyle>;
|
|
56
|
+
testID?: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* O campo em que se DIGITA para achar um item.
|
|
60
|
+
*
|
|
61
|
+
* ```tsx
|
|
62
|
+
* // catálogo remoto — a Aurea não filtra, o app busca
|
|
63
|
+
* <Field label="Item">
|
|
64
|
+
* <Combobox
|
|
65
|
+
* items={resultados}
|
|
66
|
+
* value={escolhido}
|
|
67
|
+
* onValueChange={setEscolhido}
|
|
68
|
+
* onSearchChange={buscarNoServidor}
|
|
69
|
+
* loading={buscando}
|
|
70
|
+
* onEndReached={proximaPagina}
|
|
71
|
+
* />
|
|
72
|
+
* </Field>
|
|
73
|
+
*
|
|
74
|
+
* // lista na mão — a Aurea filtra
|
|
75
|
+
* <Combobox items={unidades} value={unidade} onValueChange={setUnidade} />
|
|
76
|
+
* ```
|
|
77
|
+
*
|
|
78
|
+
* ⚠ **O gatilho é `button`, não `combobox`** — e desta vez a razão é medida, não herdada do
|
|
79
|
+
* `Select`: `role="combobox"` mapeia no Android (`roleDescription`) e **não mapeia no iOS**; e o
|
|
80
|
+
* gatilho, de qualquer forma, não aceita digitação. Quem digita é o campo da folha, que leva
|
|
81
|
+
* `role="search"` — esse mapeia nos dois. A tabela com arquivo e linha está no topo deste módulo.
|
|
82
|
+
*
|
|
83
|
+
* ⚠ **Ele não guarda a escolha nem o texto buscado.** `value` e o que se digita são do app, como
|
|
84
|
+
* na web e pela mesma razão do achado I1 da auditoria: um componente com a sua própria cópia do
|
|
85
|
+
* estado é a segunda fonte de verdade que ninguém sabe que existe.
|
|
86
|
+
*/
|
|
87
|
+
export declare function Combobox({ items, value, onValueChange, onSearchChange, searchDelay, loading, onEndReached, placeholder, searchPlaceholder, empty, clearable, disabled, size, chevron, searchIcon, style, testID, }: ComboboxProps): React.JSX.Element;
|
|
88
|
+
export interface SearchFieldProps {
|
|
89
|
+
value?: string;
|
|
90
|
+
onChangeText?: (v: string) => void;
|
|
91
|
+
/** Chamado com o texto já esperado. Mesma espera do `Combobox`, e mesmo padrão de 250 ms. */
|
|
92
|
+
onSearchChange?: (query: string) => void;
|
|
93
|
+
searchDelay?: number;
|
|
94
|
+
placeholder?: string;
|
|
95
|
+
disabled?: boolean;
|
|
96
|
+
size?: AureaFieldSize;
|
|
97
|
+
/** O glifo da lupa. Padrão `search`; `false` tira. */
|
|
98
|
+
icon?: IconName | false;
|
|
99
|
+
/** Mostra o botão de limpar quando há texto. Padrão **true**. */
|
|
100
|
+
clearable?: boolean;
|
|
101
|
+
onSubmit?: () => void;
|
|
102
|
+
style?: StyleProp<ViewStyle>;
|
|
103
|
+
testID?: string;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* A lupa + o campo, numa peça só.
|
|
107
|
+
*
|
|
108
|
+
* ⚠ **Não é o mesmo papel do `Combobox`, e confundir os dois é o erro comum:** o `Combobox`
|
|
109
|
+
* ESCOLHE um item de um catálogo e devolve a escolha; este FILTRA o que já está na tela e devolve
|
|
110
|
+
* texto. Um cadastro pede o primeiro; uma lista com muitas linhas pede o segundo.
|
|
111
|
+
*
|
|
112
|
+
* ⚠ **`role="search"`, e ele mapeia nos dois sistemas** — conferido no fonte do RN 0.87.1, com
|
|
113
|
+
* arquivo e linha no topo deste módulo. É a diferença entre este componente e o `Combobox`, cujo
|
|
114
|
+
* gatilho não pôde levar `combobox` porque o iOS não tem o trait.
|
|
115
|
+
*
|
|
116
|
+
* ⚠ **A borda mora no GRUPO, não no campo** — é o `.input-group` da web (`aurea.css:741`), e é o
|
|
117
|
+
* que deixa lupa, campo e botão de limpar dentro de uma caixa só em vez de três.
|
|
118
|
+
*/
|
|
119
|
+
export declare function SearchField({ value, onChangeText, onSearchChange, searchDelay, placeholder, disabled, size, icon, clearable, onSubmit, style, testID, }: SearchFieldProps): React.JSX.Element;
|
package/dist/busca.js
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Aurea nativo — a BUSCA: `Combobox` e `SearchField`.
|
|
3
|
+
//
|
|
4
|
+
// Lote 7 do `NATIVE.md` §8 — o primeiro lote que nasce de DEMANDA MEDIDA e não do plano: o alvo
|
|
5
|
+
// nativo fechou em 46 componentes em 11/09/2026, e o consumidor bateu em três lacunas na primeira
|
|
6
|
+
// tela que a pessoa toca. Esta é a primeira delas, e é a que bloqueia o cadastro.
|
|
7
|
+
//
|
|
8
|
+
// ── O QUE FALTAVA, LITERALMENTE ──────────────────────────────────────────────────────────────
|
|
9
|
+
// O `Select` (`inputs.tsx:576`) escreve a própria lacuna: *"O papel é `button` e não `combobox`.
|
|
10
|
+
// `combobox` promete um campo em que se DIGITA para filtrar; isto só abre uma lista."* Era
|
|
11
|
+
// honesto, e continua sendo — o `Select` não mudou. O que faltava era o componente que CUMPRE a
|
|
12
|
+
// promessa, e é este.
|
|
13
|
+
//
|
|
14
|
+
// E havia um agravante que a demanda não citava, medido aqui: a folha do `Select` usa
|
|
15
|
+
// `ScrollView` (`inputs.tsx:632`), que **monta todos os itens**. Um catálogo de milhares de
|
|
16
|
+
// linhas ali não fica lento — ele trava. Este usa `FlatList`, como o `DataList` do Lote 6 já
|
|
17
|
+
// fazia (`data.tsx:191`).
|
|
18
|
+
//
|
|
19
|
+
// ── A DIVERGÊNCIA DA WEB, E ELA É DELIBERADA ─────────────────────────────────────────────────
|
|
20
|
+
// **A demanda é busca REMOTA de seleção única, e essa combinação não existe na web.** Medido em
|
|
21
|
+
// `inputs-client.tsx`:
|
|
22
|
+
//
|
|
23
|
+
// Combobox (seleção única) -> `items` e filtro do Base UI, do lado do cliente. Sem `onInputChange`.
|
|
24
|
+
// MultiCombobox (múltipla) -> tem `onInputChange` + `loading`, e desliga o filtro (`filter={null}`)
|
|
25
|
+
//
|
|
26
|
+
// Ou seja: quem quer buscar no servidor E escolher UM item não tem componente na web. Copiar o
|
|
27
|
+
// contrato de lá deixaria o consumidor com um filtro de cliente sobre milhares de linhas que ele
|
|
28
|
+
// nem baixou.
|
|
29
|
+
//
|
|
30
|
+
// Então este componente soma `onSearchChange` + `loading` + `onEndReached` ao contrato de seleção
|
|
31
|
+
// única, e **a regra é uma só e vale para os dois lados**: se `onSearchChange` existe, a Aurea
|
|
32
|
+
// NÃO filtra — a lista que chega é a lista que aparece, e quem busca é o app. Sem ele, a Aurea
|
|
33
|
+
// filtra o que recebeu. É a mesma chave do `filter={null}` do `MultiCombobox`, escrita como
|
|
34
|
+
// presença de prop em vez de configuração de motor.
|
|
35
|
+
//
|
|
36
|
+
// É a mesma classe de divergência que a `Table` do Lote 6 assumiu (`columns`+`rows` em vez de
|
|
37
|
+
// `children`): quando a plataforma muda o problema, o contrato muda com ele — declarado, não
|
|
38
|
+
// escondido.
|
|
39
|
+
//
|
|
40
|
+
// ── ONDE SE DIGITA, E POR QUE NÃO É NO GATILHO ───────────────────────────────────────────────
|
|
41
|
+
// Na web o `<input>` É o combobox: digita-se nele e a lista desce ancorada. Aqui **o gatilho
|
|
42
|
+
// continua sendo um botão, e o campo de digitar mora DENTRO da folha**. Três razões medidas, não
|
|
43
|
+
// preferidas:
|
|
44
|
+
//
|
|
45
|
+
// 1. o teclado do telefone ocupa metade da tela. Um campo ancorado no meio de um formulário
|
|
46
|
+
// abre o teclado por cima da própria lista — a folha sobe do rodapé e fica acima dele;
|
|
47
|
+
// 2. o gatilho precisa mostrar o ESCOLHIDO com a geometria do `.input` (mesma altura, mesma
|
|
48
|
+
// borda, a mesma seta) — é o que faz o campo parecer um campo no formulário. Um campo de
|
|
49
|
+
// texto ali mostraria o que se digitou, não o que se escolheu;
|
|
50
|
+
// 3. é o idioma dos seletores do próprio sistema, nos dois lados.
|
|
51
|
+
//
|
|
52
|
+
// ── A ACESSIBILIDADE, MEDIDA NO FONTE DO RN 0.87.1 (e não presumida) ─────────────────────────
|
|
53
|
+
// O Lote 5 ensinou que o RN ACEITA papel que não mapeia (`dialog` compila e vira `null`). Então
|
|
54
|
+
// os dois papéis daqui foram conferidos no fonte do `react-native@0.87.1` antes de usados:
|
|
55
|
+
//
|
|
56
|
+
// `role="search"` -> `accessibilityPropsConversions.h:64-65` traduz para
|
|
57
|
+
// `AccessibilityTraits::SearchField`, que o `RCTConversions.h:120-121`
|
|
58
|
+
// vira `UIAccessibilityTraitSearchField` no iOS; no Android o
|
|
59
|
+
// `ReactAccessibilityDelegate.kt:461` dá a `className`
|
|
60
|
+
// `android.widget.EditText`. **Mapeia nos dois.**
|
|
61
|
+
// `role="combobox"` -> no Android o `ReactAccessibilityDelegate.kt:680-682` põe
|
|
62
|
+
// `roleDescription` de `R.string.combobox_description` (o TalkBack diz
|
|
63
|
+
// "caixa de combinação"); **no iOS não há trait equivalente** — não está
|
|
64
|
+
// na tabela do `RCTConversions.h`. Mapeia em UM lado.
|
|
65
|
+
//
|
|
66
|
+
// Por isso o gatilho é `button` com `expanded`, como o do `Select`: ele abre uma folha e não
|
|
67
|
+
// aceita digitação, então `button` é o que ele É nos dois sistemas. Quem digita é o campo da
|
|
68
|
+
// folha, e ele leva `search` — que mapeia nos dois.
|
|
69
|
+
import * as React from "react";
|
|
70
|
+
import { FlatList, Modal, Pressable, TextInput, View, } from "react-native";
|
|
71
|
+
import { IconButton } from "./actions.js";
|
|
72
|
+
import { criarFolha } from "./estilos.js";
|
|
73
|
+
import { Spinner } from "./feedback.js";
|
|
74
|
+
import { Icon } from "./icon.js";
|
|
75
|
+
import { useCampo } from "./inputs.js";
|
|
76
|
+
import { Text } from "./text.js";
|
|
77
|
+
import { useAureaStrings, useAureaTokens } from "./theme.js";
|
|
78
|
+
// As mesmas contas do `inputs.tsx`, e elas são repetidas AQUI de propósito: exportá-las de lá
|
|
79
|
+
// tornaria pública uma conta interna do outro módulo, e um `import` de função privada entre
|
|
80
|
+
// módulos irmãos é o começo de um acoplamento que ninguém declara. São três linhas.
|
|
81
|
+
const alturaDoTamanho = (t, s) => s === "sm" ? t.size.controlHSm : s === "lg" ? t.size.controlHLg : t.size.controlHMd;
|
|
82
|
+
const fonteDoTamanho = (t, s) => s === "sm" ? t.size.textXs : s === "lg" ? t.size.textBase : t.size.textMd;
|
|
83
|
+
const respiroDoTamanho = (t, s) => s === "sm" ? t.size.space3 : s === "lg" ? t.size.space4 : 13;
|
|
84
|
+
const folha = criarFolha((t) => ({
|
|
85
|
+
// O gatilho é o `.input` da web (`aurea.css:685`), igual ao do `Select` — mesma altura, mesma
|
|
86
|
+
// borda, o mesmo raio de controle.
|
|
87
|
+
caixa: {
|
|
88
|
+
width: "100%", minWidth: 0,
|
|
89
|
+
borderWidth: t.size.borderWidth, borderColor: t.color.borderStrong,
|
|
90
|
+
borderRadius: t.size.radiusControl, backgroundColor: t.color.fieldBg,
|
|
91
|
+
flexDirection: "row", alignItems: "center",
|
|
92
|
+
},
|
|
93
|
+
invalido: { borderColor: t.color.danger400 ?? t.color.destructive },
|
|
94
|
+
desabilitado: { opacity: 0.5 },
|
|
95
|
+
// As ações do gatilho — limpar e a seta. O `.combobox-actions` da web (`aurea.css:1673`) é
|
|
96
|
+
// absoluto sobre o campo; aqui é um irmão na linha, porque não há campo por baixo.
|
|
97
|
+
acoes: { flexDirection: "row", alignItems: "center", gap: 2 },
|
|
98
|
+
// A folha, com a mesma anatomia da do `Select` (`inputs.tsx:98-113`) — e é de propósito que
|
|
99
|
+
// sejam iguais: são o mesmo gesto, e duas folhas diferentes para o mesmo gesto é como uma
|
|
100
|
+
// biblioteca deixa de ter linguagem.
|
|
101
|
+
fundoDaLista: { flex: 1, justifyContent: "flex-end", backgroundColor: "#00000080" },
|
|
102
|
+
fundoDeToque: { position: "absolute", top: 0, right: 0, bottom: 0, left: 0 },
|
|
103
|
+
lista: {
|
|
104
|
+
// `maxHeight` MAIOR que a do `Select` (60%), e a razão é o teclado: aqui ele sobe junto, e
|
|
105
|
+
// uma folha de 60% com teclado aberto deixa três linhas visíveis. O `KeyboardAvoidingView`
|
|
106
|
+
// não entra — dentro de um `Modal` do RN ele não recebe as métricas do teclado no Android.
|
|
107
|
+
maxHeight: "85%", minHeight: "45%",
|
|
108
|
+
borderTopLeftRadius: t.size.radiusCard, borderTopRightRadius: t.size.radiusCard,
|
|
109
|
+
backgroundColor: t.color.popover, borderWidth: t.size.borderWidth, borderColor: t.color.border,
|
|
110
|
+
},
|
|
111
|
+
// O campo de digitar, no topo da folha. É o `.input-group` da web (`aurea.css:741`): a borda
|
|
112
|
+
// mora no GRUPO e o campo dentro dele é transparente e sem borda.
|
|
113
|
+
grupo: {
|
|
114
|
+
flexDirection: "row", alignItems: "center", gap: t.size.space1,
|
|
115
|
+
margin: t.size.space3,
|
|
116
|
+
paddingHorizontal: t.size.space3,
|
|
117
|
+
borderWidth: t.size.borderWidth, borderColor: t.color.borderStrong,
|
|
118
|
+
borderRadius: t.size.radiusControl, backgroundColor: t.color.fieldBg,
|
|
119
|
+
},
|
|
120
|
+
// ⚠ `paddingVertical: 0` e `textAlignVertical: "center"` — a MESMA correção que o `Input` do
|
|
121
|
+
// Lote 4 levou depois do vidro (`inputs.tsx:280-296`): sem elas o Android injeta o padding do
|
|
122
|
+
// tema dele no nó do Yoga e o texto sai cortado dentro de uma caixa de altura fixa. A regra do
|
|
123
|
+
// `CLAUDE.md` ("correção local é proibida sem responder quem mais tem esse problema") é o que
|
|
124
|
+
// traz as duas linhas para cá, e este campo é exatamente "quem mais".
|
|
125
|
+
campoDeTexto: { flex: 1, minWidth: 0, paddingVertical: 0, textAlignVertical: "center" },
|
|
126
|
+
opcao: {
|
|
127
|
+
minHeight: t.size.controlHLg, justifyContent: "center",
|
|
128
|
+
paddingHorizontal: t.size.space4, paddingVertical: t.size.space2,
|
|
129
|
+
},
|
|
130
|
+
opcaoEscolhida: { backgroundColor: t.color.surface3 ?? t.color.muted },
|
|
131
|
+
vazio: { paddingHorizontal: t.size.space4, paddingVertical: t.size.space5, alignItems: "center" },
|
|
132
|
+
carregando: { paddingVertical: t.size.space4, alignItems: "center" },
|
|
133
|
+
}));
|
|
134
|
+
/**
|
|
135
|
+
* O campo em que se DIGITA para achar um item.
|
|
136
|
+
*
|
|
137
|
+
* ```tsx
|
|
138
|
+
* // catálogo remoto — a Aurea não filtra, o app busca
|
|
139
|
+
* <Field label="Item">
|
|
140
|
+
* <Combobox
|
|
141
|
+
* items={resultados}
|
|
142
|
+
* value={escolhido}
|
|
143
|
+
* onValueChange={setEscolhido}
|
|
144
|
+
* onSearchChange={buscarNoServidor}
|
|
145
|
+
* loading={buscando}
|
|
146
|
+
* onEndReached={proximaPagina}
|
|
147
|
+
* />
|
|
148
|
+
* </Field>
|
|
149
|
+
*
|
|
150
|
+
* // lista na mão — a Aurea filtra
|
|
151
|
+
* <Combobox items={unidades} value={unidade} onValueChange={setUnidade} />
|
|
152
|
+
* ```
|
|
153
|
+
*
|
|
154
|
+
* ⚠ **O gatilho é `button`, não `combobox`** — e desta vez a razão é medida, não herdada do
|
|
155
|
+
* `Select`: `role="combobox"` mapeia no Android (`roleDescription`) e **não mapeia no iOS**; e o
|
|
156
|
+
* gatilho, de qualquer forma, não aceita digitação. Quem digita é o campo da folha, que leva
|
|
157
|
+
* `role="search"` — esse mapeia nos dois. A tabela com arquivo e linha está no topo deste módulo.
|
|
158
|
+
*
|
|
159
|
+
* ⚠ **Ele não guarda a escolha nem o texto buscado.** `value` e o que se digita são do app, como
|
|
160
|
+
* na web e pela mesma razão do achado I1 da auditoria: um componente com a sua própria cópia do
|
|
161
|
+
* estado é a segunda fonte de verdade que ninguém sabe que existe.
|
|
162
|
+
*/
|
|
163
|
+
export function Combobox({ items, value, onValueChange, onSearchChange, searchDelay = 250, loading, onEndReached, placeholder, searchPlaceholder, empty, clearable = true, disabled, size, chevron = "chevron--down", searchIcon = "search", style, testID, }) {
|
|
164
|
+
const t = useAureaTokens();
|
|
165
|
+
const s = folha(t);
|
|
166
|
+
const strings = useAureaStrings();
|
|
167
|
+
const campo = useCampo();
|
|
168
|
+
const tam = size ?? campo?.size ?? "md";
|
|
169
|
+
const inativo = disabled ?? campo?.disabled;
|
|
170
|
+
const [aberto, setAberto] = React.useState(false);
|
|
171
|
+
const [texto, setTexto] = React.useState("");
|
|
172
|
+
// ── A ESPERA ────────────────────────────────────────────────────────────────────────────────
|
|
173
|
+
// O timer vive num `ref` e é limpo em três lugares: à digitação seguinte, ao fechar a folha e
|
|
174
|
+
// ao desmontar. O terceiro é o que importa e o que mais se esquece — sem ele, sair da tela com
|
|
175
|
+
// uma busca pendente dispara `onSearchChange` sobre um componente que já não existe, e o app
|
|
176
|
+
// leva um `setState` em árvore desmontada.
|
|
177
|
+
const relogio = React.useRef(null);
|
|
178
|
+
const cancelar = React.useCallback(() => {
|
|
179
|
+
if (relogio.current != null) {
|
|
180
|
+
clearTimeout(relogio.current);
|
|
181
|
+
relogio.current = null;
|
|
182
|
+
}
|
|
183
|
+
}, []);
|
|
184
|
+
React.useEffect(() => cancelar, [cancelar]);
|
|
185
|
+
// A referência mais recente da função de busca, para o timer não disparar a de um render
|
|
186
|
+
// anterior. É o padrão de `ref` para callback — e existe porque o app costuma passar uma
|
|
187
|
+
// arrow nova a cada render.
|
|
188
|
+
const buscaAtual = React.useRef(onSearchChange);
|
|
189
|
+
React.useEffect(() => { buscaAtual.current = onSearchChange; }, [onSearchChange]);
|
|
190
|
+
const digitar = React.useCallback((v) => {
|
|
191
|
+
setTexto(v);
|
|
192
|
+
if (!buscaAtual.current)
|
|
193
|
+
return;
|
|
194
|
+
cancelar();
|
|
195
|
+
if (searchDelay <= 0) {
|
|
196
|
+
buscaAtual.current(v);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
relogio.current = setTimeout(() => {
|
|
200
|
+
relogio.current = null;
|
|
201
|
+
buscaAtual.current?.(v);
|
|
202
|
+
}, searchDelay);
|
|
203
|
+
}, [cancelar, searchDelay]);
|
|
204
|
+
const fechar = React.useCallback(() => {
|
|
205
|
+
cancelar();
|
|
206
|
+
setAberto(false);
|
|
207
|
+
setTexto("");
|
|
208
|
+
}, [cancelar]);
|
|
209
|
+
// ── O FILTRO, QUANDO ELE É NOSSO ────────────────────────────────────────────────────────────
|
|
210
|
+
const filtrados = React.useMemo(() => {
|
|
211
|
+
if (onSearchChange)
|
|
212
|
+
return items; // busca remota: a lista que chegou é a lista.
|
|
213
|
+
const alvo = dobrar(texto.trim());
|
|
214
|
+
if (!alvo)
|
|
215
|
+
return items;
|
|
216
|
+
return items.filter((i) => dobrar(i.label).includes(alvo));
|
|
217
|
+
}, [items, texto, onSearchChange]);
|
|
218
|
+
const nada = !loading && filtrados.length === 0;
|
|
219
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Pressable, { testID: testID, onPress: inativo ? undefined : () => setAberto(true), disabled: inativo, accessibilityRole: "button", accessibilityLabel: campo?.label, accessibilityHint: campo?.hint, accessibilityValue: { text: value?.label }, accessibilityState: { disabled: !!inativo, expanded: aberto }, style: [
|
|
220
|
+
s.caixa,
|
|
221
|
+
{ height: alturaDoTamanho(t, tam), paddingHorizontal: respiroDoTamanho(t, tam) },
|
|
222
|
+
campo?.invalido && s.invalido,
|
|
223
|
+
inativo && s.desabilitado,
|
|
224
|
+
style,
|
|
225
|
+
], children: [_jsx(Text, { size: tam === "sm" ? "xs" : tam === "lg" ? "base" : "md", tone: value ? "default" : "subtle", numberOfLines: 1, style: { flex: 1, minWidth: 0 }, children: value?.label ?? placeholder ?? "" }), _jsxs(View, { style: s.acoes, children: [clearable && value != null && !inativo && (_jsx(IconButton, { name: "close", label: strings.comboboxClear, appearance: "ghost", size: "sm", onPress: () => onValueChange?.(null), testID: testID ? `${testID}-limpar` : undefined })), chevron && _jsx(Icon, { name: chevron, size: "sm", color: t.color.subtleForeground })] })] }), _jsx(Modal, { visible: aberto, transparent: true, animationType: "slide", statusBarTranslucent: true, navigationBarTranslucent: true, onRequestClose: fechar, children: _jsxs(View, { style: s.fundoDaLista, children: [_jsx(Pressable, { style: s.fundoDeToque, onPress: fechar, accessible: false, testID: testID ? `${testID}-fundo` : undefined }), _jsxs(View, { style: s.lista, onStartShouldSetResponder: () => true, children: [_jsxs(View, { style: [s.grupo, { height: alturaDoTamanho(t, tam) }], children: [searchIcon && _jsx(Icon, { name: searchIcon, size: "sm", color: t.color.subtleForeground }), _jsx(TextInput, { testID: testID ? `${testID}-busca` : undefined, value: texto, onChangeText: digitar, placeholder: searchPlaceholder ?? strings.comboboxSearch, placeholderTextColor: t.color.subtleForeground,
|
|
226
|
+
// ⚠ `autoFocus` é o que faz a folha valer a pena: abrir um campo de busca e
|
|
227
|
+
// exigir um segundo toque para o teclado subir é um toque a mais em cada
|
|
228
|
+
// cadastro. O foco entra com a folha; o teclado vem junto.
|
|
229
|
+
autoFocus: true, autoCorrect: false, autoCapitalize: "none",
|
|
230
|
+
// Mapeia nos dois sistemas — conferido no fonte do RN, tabela no topo do módulo.
|
|
231
|
+
accessibilityRole: "search", accessibilityLabel: searchPlaceholder ?? strings.comboboxSearch,
|
|
232
|
+
// `returnKeyType="search"` troca o "enter" do teclado pela lupa. É pista de
|
|
233
|
+
// plataforma, não decoração: diz à pessoa que aquele campo é de busca antes de
|
|
234
|
+
// ela digitar a primeira letra.
|
|
235
|
+
returnKeyType: "search", style: [
|
|
236
|
+
s.campoDeTexto,
|
|
237
|
+
{ fontSize: fonteDoTamanho(t, tam), fontFamily: t.font.ui[400],
|
|
238
|
+
color: t.color.foreground },
|
|
239
|
+
] }), texto.length > 0 && (_jsx(IconButton, { name: "close", label: strings.searchClear, appearance: "ghost", size: "sm", onPress: () => digitar(""), testID: testID ? `${testID}-busca-limpar` : undefined }))] }), loading && (_jsx(View, { style: s.carregando, children: _jsx(Spinner, { label: strings.comboboxLoading }) })), nada && (_jsx(View, { style: s.vazio, children: typeof empty === "string" || empty == null
|
|
240
|
+
? _jsx(Text, { size: "sm", tone: "muted", children: empty ?? strings.comboboxEmpty })
|
|
241
|
+
: empty })), _jsx(FlatList, { data: filtrados, keyExtractor: (i) => i.value, keyboardShouldPersistTaps: "handled", onEndReached: onEndReached, onEndReachedThreshold: 0.5, renderItem: ({ item }) => (_jsx(Pressable, { disabled: item.disabled, onPress: () => { onValueChange?.(item); fechar(); }, accessibilityRole: "menuitem", accessibilityState: {
|
|
242
|
+
selected: item.value === value?.value, disabled: !!item.disabled,
|
|
243
|
+
}, style: [
|
|
244
|
+
s.opcao,
|
|
245
|
+
item.value === value?.value && s.opcaoEscolhida,
|
|
246
|
+
item.disabled && s.desabilitado,
|
|
247
|
+
], children: _jsx(Text, { size: "md", weight: item.value === value?.value ? 600 : 400, children: item.label }) })) })] })] }) })] }));
|
|
248
|
+
}
|
|
249
|
+
// ── A DOBRA DE ACENTO, e por que ela é sondada em vez de presumida ───────────────────────────
|
|
250
|
+
// Buscar "acucar" tem de achar "açúcar" — num catálogo em português, exigir o acento certo é
|
|
251
|
+
// exigir que a pessoa saiba escrever o que está procurando. A dobra é `NFD` + remoção dos
|
|
252
|
+
// diacríticos combinantes, que é a forma padrão.
|
|
253
|
+
//
|
|
254
|
+
// **A sonda existe porque o motor é o motor, não um navegador.** `String.prototype.normalize` é
|
|
255
|
+
// ES2015 e está no motor, mas este pacote roda em qualquer motor que o consumidor escolha — e
|
|
256
|
+
// um `normalize` ausente derrubaria a busca inteira com `TypeError` em vez de degradá-la. A
|
|
257
|
+
// sonda roda UMA vez, no carregamento do módulo, e custa uma string de dois caracteres.
|
|
258
|
+
const TEM_NORMALIZE = (() => {
|
|
259
|
+
try {
|
|
260
|
+
return "é".normalize("NFD").length === 2;
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
})();
|
|
266
|
+
const dobrar = (v) => {
|
|
267
|
+
const minusculo = v.toLowerCase();
|
|
268
|
+
return TEM_NORMALIZE
|
|
269
|
+
? minusculo.normalize("NFD").replace(/[̀-ͯ]/g, "")
|
|
270
|
+
: minusculo;
|
|
271
|
+
};
|
|
272
|
+
/**
|
|
273
|
+
* A lupa + o campo, numa peça só.
|
|
274
|
+
*
|
|
275
|
+
* ⚠ **Não é o mesmo papel do `Combobox`, e confundir os dois é o erro comum:** o `Combobox`
|
|
276
|
+
* ESCOLHE um item de um catálogo e devolve a escolha; este FILTRA o que já está na tela e devolve
|
|
277
|
+
* texto. Um cadastro pede o primeiro; uma lista com muitas linhas pede o segundo.
|
|
278
|
+
*
|
|
279
|
+
* ⚠ **`role="search"`, e ele mapeia nos dois sistemas** — conferido no fonte do RN 0.87.1, com
|
|
280
|
+
* arquivo e linha no topo deste módulo. É a diferença entre este componente e o `Combobox`, cujo
|
|
281
|
+
* gatilho não pôde levar `combobox` porque o iOS não tem o trait.
|
|
282
|
+
*
|
|
283
|
+
* ⚠ **A borda mora no GRUPO, não no campo** — é o `.input-group` da web (`aurea.css:741`), e é o
|
|
284
|
+
* que deixa lupa, campo e botão de limpar dentro de uma caixa só em vez de três.
|
|
285
|
+
*/
|
|
286
|
+
export function SearchField({ value, onChangeText, onSearchChange, searchDelay = 250, placeholder, disabled, size, icon = "search", clearable = true, onSubmit, style, testID, }) {
|
|
287
|
+
const t = useAureaTokens();
|
|
288
|
+
const s = folha(t);
|
|
289
|
+
const strings = useAureaStrings();
|
|
290
|
+
const campo = useCampo();
|
|
291
|
+
const tam = size ?? campo?.size ?? "md";
|
|
292
|
+
const inativo = disabled ?? campo?.disabled;
|
|
293
|
+
const [focado, setFocado] = React.useState(false);
|
|
294
|
+
// O campo pode ser controlado (`value`) ou não; o texto interno cobre o segundo caso, e é o que
|
|
295
|
+
// o botão de limpar precisa para saber se tem o que limpar.
|
|
296
|
+
const [interno, setInterno] = React.useState("");
|
|
297
|
+
const texto = value ?? interno;
|
|
298
|
+
const relogio = React.useRef(null);
|
|
299
|
+
const cancelar = React.useCallback(() => {
|
|
300
|
+
if (relogio.current != null) {
|
|
301
|
+
clearTimeout(relogio.current);
|
|
302
|
+
relogio.current = null;
|
|
303
|
+
}
|
|
304
|
+
}, []);
|
|
305
|
+
React.useEffect(() => cancelar, [cancelar]);
|
|
306
|
+
const buscaAtual = React.useRef(onSearchChange);
|
|
307
|
+
React.useEffect(() => { buscaAtual.current = onSearchChange; }, [onSearchChange]);
|
|
308
|
+
const digitar = React.useCallback((v) => {
|
|
309
|
+
setInterno(v);
|
|
310
|
+
onChangeText?.(v);
|
|
311
|
+
if (!buscaAtual.current)
|
|
312
|
+
return;
|
|
313
|
+
cancelar();
|
|
314
|
+
if (searchDelay <= 0) {
|
|
315
|
+
buscaAtual.current(v);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
relogio.current = setTimeout(() => {
|
|
319
|
+
relogio.current = null;
|
|
320
|
+
buscaAtual.current?.(v);
|
|
321
|
+
}, searchDelay);
|
|
322
|
+
}, [cancelar, onChangeText, searchDelay]);
|
|
323
|
+
return (_jsxs(View, { testID: testID, style: [
|
|
324
|
+
s.caixa,
|
|
325
|
+
{ height: alturaDoTamanho(t, tam), paddingHorizontal: respiroDoTamanho(t, tam),
|
|
326
|
+
gap: t.size.space1 },
|
|
327
|
+
campo?.invalido && s.invalido,
|
|
328
|
+
// O foco é a única pista de campo ativo que sobra no telefone — mesma decisão do `Input`
|
|
329
|
+
// do Lote 4, e ela vale igual aqui.
|
|
330
|
+
focado && { borderColor: t.color.focusStrong },
|
|
331
|
+
inativo && s.desabilitado,
|
|
332
|
+
style,
|
|
333
|
+
], children: [icon && _jsx(Icon, { name: icon, size: "sm", color: t.color.subtleForeground }), _jsx(TextInput, { testID: testID ? `${testID}-campo` : undefined, value: value, onChangeText: digitar, placeholder: placeholder, placeholderTextColor: t.color.subtleForeground, editable: !inativo, autoCorrect: false, autoCapitalize: "none", returnKeyType: "search", onSubmitEditing: onSubmit, onFocus: () => setFocado(true), onBlur: () => setFocado(false), accessibilityRole: "search", accessibilityLabel: campo?.label ?? placeholder, accessibilityHint: campo?.hint, accessibilityState: { disabled: !!inativo }, style: [
|
|
334
|
+
s.campoDeTexto,
|
|
335
|
+
{ fontSize: fonteDoTamanho(t, tam), fontFamily: t.font.ui[400],
|
|
336
|
+
color: t.color.foreground },
|
|
337
|
+
] }), clearable && texto.length > 0 && !inativo && (_jsx(IconButton, { name: "close", label: strings.searchClear, appearance: "ghost", size: "sm", onPress: () => digitar(""), testID: testID ? `${testID}-limpar` : undefined }))] }));
|
|
338
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -33,3 +33,9 @@ export { Timeline, DataList, Table } from "./data.js";
|
|
|
33
33
|
export type { TimelineProps, DataListProps, TableProps, AureaColumn, AureaTimelineItem, AureaDataListItem, } from "./data.js";
|
|
34
34
|
export { Chart, ChartLegend } from "./chart.js";
|
|
35
35
|
export type { ChartProps, ChartLegendProps, AureaSeries, AureaChartMark } from "./chart.js";
|
|
36
|
+
export { Combobox, SearchField } from "./busca.js";
|
|
37
|
+
export type { ComboboxProps, SearchFieldProps, AureaComboboxItem } from "./busca.js";
|
|
38
|
+
export { NumberField, formatarNumero, lerNumero, separadoresDoLocale } from "./numero.js";
|
|
39
|
+
export type { NumberFieldProps, AureaSeparadores } from "./numero.js";
|
|
40
|
+
export { Image, Gallery } from "./midia.js";
|
|
41
|
+
export type { ImageProps, GalleryProps, AureaGalleryItem, AureaImageSource, } from "./midia.js";
|
package/dist/index.js
CHANGED
|
@@ -86,3 +86,24 @@ export { Timeline, DataList, Table } from "./data.js";
|
|
|
86
86
|
// ⚠ **Com duas ou mais séries a legenda NÃO desliga**: a paleta `--chart-*` é uma rampa de um
|
|
87
87
|
// azul só, e duas séries vizinhas nela são indistinguíveis (ΔE 5.9, medido).
|
|
88
88
|
export { Chart, ChartLegend } from "./chart.js";
|
|
89
|
+
// ── Lote 7 — as três lacunas que o CONSUMIDOR mediu, e não o plano ─────────────────────────
|
|
90
|
+
// ⚠ Este é o primeiro lote do alvo nativo que **não sai do `NATIVE.md` §5**: o plano fechou em
|
|
91
|
+
// 11/09/2026 com 46 componentes e "não sobrou nada". Sobrou — só que a fila passou a ser a do
|
|
92
|
+
// app, não a do documento. As três estão no §8, com a demanda de cada uma.
|
|
93
|
+
//
|
|
94
|
+
// ⚠ **O `Combobox` DIVERGE da web, e é deliberado:** ele soma `onSearchChange`, `loading` e
|
|
95
|
+
// `onEndReached` ao contrato de seleção única, porque busca remota + escolha de UM item não
|
|
96
|
+
// existe lá (o `Combobox` da web filtra no cliente; quem tem `onInputChange` é o
|
|
97
|
+
// `MultiCombobox`, que é de seleção múltipla). A regra: **`onSearchChange` presente = a Aurea
|
|
98
|
+
// não filtra.**
|
|
99
|
+
export { Combobox, SearchField } from "./busca.js";
|
|
100
|
+
// ⚠ O `NumberField` formata no BLUR, nunca enquanto se digita — a [ADR-0024] atravessa inteira.
|
|
101
|
+
// O que muda no nativo é quem chama o `Intl` (não há Base UI) e a VOLTA, que a web não precisou
|
|
102
|
+
// escrever: `lerNumero` converte de novo o que a pessoa digitou. Os dois auxiliares são públicos
|
|
103
|
+
// porque o app tem o mesmo problema em toda tela de lançamento.
|
|
104
|
+
export { NumberField, formatarNumero, lerNumero, separadoresDoLocale } from "./numero.js";
|
|
105
|
+
// ⚠ O `Image` NÃO é o `Image` do React Native com outra pele: ele **reserva a caixa** antes dos
|
|
106
|
+
// bytes e **cai para um substituto** quando eles não vêm — as duas coisas que a ficha da web
|
|
107
|
+
// promete e que o primitivo cru não faz. Foi por não fazê-las que o `Avatar` precisou de
|
|
108
|
+
// conserto em 31/07/2026.
|
|
109
|
+
export { Image, Gallery } from "./midia.js";
|